Network configuration
In this section the network configuration file and its options will be introduced. We will also show how to edit and generate network configurations to do parameter sweeps. To support the network configuration tutorial, we start with a brief introduction of the YAML language.
YAML
YAML is a human-readable data-serialization language. It performs a similar function to XML and JSON. The main difference is that YAML relies more on indentation for nesting, thus using significantly less special characters and improving human readability.
We use the examples examples/tutorial/4.1_YAML for the YAML introduction.
It contains two YAML files and a python file to import the YAML files and print the imports.
The first YAML example shows the basics of YAML:
# Simple dictionary entries:
intro-text: "Hello world"
pi: 3.14
# Creates an entry that is itself a dictionary
settings:
alpha: 2.56
beta: 78.2
# Creates an entry that is a list using the - character for each list entry
famous-scientists:
- "Albert Einstein"
- "Isaac Newton"
- "Marie Curie"
- "Enrico Fermi"
The YAML files translate directly into python.
By using statements of the form: key: value, a python dictionary is created containing this key and value.
By using indents after a key, such as with settings: in the example, a nested dictionary is created.
It is also possible to create list instead of dictionaries, this is done by using the syntax: - value.
Using simple python script we can load the YAML files and print the contents of the file as interpreted by python.
from pprint import pprint
import yaml
with open("basic_example.yaml", "r") as f:
contents = yaml.load(f, yaml.Loader)
print("Basic Example:\n")
pprint(contents)
with open("advanced_example.yaml", "r") as f:
contents = yaml.load(f, yaml.Loader)
print("\n\nAdvanced example:\n")
pprint(contents)
{'famous-scientists': ['Albert Einstein',
'Isaac Newton',
'Marie Curie',
'Enrico Fermi'],
'intro-text': 'Hello world',
'pi': 3.14,
'settings': {'alpha': 2.56, 'beta': 78.2}}
We observe that the contents are a python dictionary with the settings key referring to a nested dictionary
and the famous-scientists key referring a nested list.
A more common example of a YAML file would use multiple levels and a mix of lists and dictionaries:
bob-owner: &bob
name: "Bob"
address: "Fermi street 10"
cars:
- type: "Audi"
condition:
vehicle-kilometers: 20_000
state: "good"
owner:
<<: *bob
- type: "Porsche"
condition:
vehicle-kilometers: 500
state: "bad"
owner:
<<: *bob
{'bob-owner': {'address': 'Fermi street 10', 'name': 'Bob'},
'cars': [{'condition': {'state': 'good', 'vehicle-kilometers': 20000},
'owner': {'address': 'Fermi street 10', 'name': 'Bob'},
'type': 'Audi'},
{'condition': {'state': 'bad', 'vehicle-kilometers': 500},
'owner': {'address': 'Fermi street 10', 'name': 'Bob'},
'type': 'Porsche'}]}
This example is focused on listing car objects, with associated information. By using the minus sign, a list of cars has been created. Each car has properties that define it, like condition, owner and type. Some of these properties, like condition, are in turn nested dictionaries.
One of the useful features of YAML, that will be used in the configuration files, is to copy and paste items.
This was done in this example with the bob-owner dictionary.
The bob-owner object is used to copy the name and address to the owner property of the cars.
By using placing the following statement &bob after an item, we have created an anchor with the tag: bob.
By using the statement <<: *bob we will copy and paste the values from that anchor.
This is useful to avoid duplication.
More advanced tutorials for YAML can be found easily online.
Configuration file
In this section we will explain the configuration file. We start with the simplest configuration file, one without any noise:
# Perfect 2 node network, no noise from either computation on the nodes or communication between nodes
stacks:
- name: Alice
qdevice_typ: generic
qdevice_cfg:
dummy: null
- name: Bob
qdevice_typ: generic
qdevice_cfg:
dummy: null
links:
- stack1: Alice
stack2: Bob
typ: perfect
cfg:
dummy: null
clinks:
- stack1: Alice
stack2: Bob
typ: instant
The network requires three types of objects to be specified: stacks, links and clinks.
Stacks are the end nodes of the network and run applications.
Each stack requires a name, this name will be used by the links and applications for reference.
The qdevice_typ field requires a string that will define the type of model used for the node.
The qdevice_cfg is where the various settings for the model can be defined.
The various stack types are discussed in Stack types.
Links connect the stacks with a way of generating EPR pairs between the two nodes.
A link requires references to the two stacks it is to connect.
This is done by registering the names of the stacks in the fields stack1 and stack2.
The model type of the link is specified using the typ field.
The various settings for the model are defined inside cfg.
Clinks are shorthand for classical links. These are similar to links, but for classical message communication.
A clink requires references to the two stacks it is to connect with a classical link.
This is done by registering the names of the stacks in the fields stack1 and stack2.
The model type of the clink is specified using the typ field.
The various settings for the model are defined inside cfg.
Stack types
Generic qdevice
The generic quantum device is an idealized model. It has models of noise, but lacks any peculiarities, such as certain operations and qubits experiencing more noise, that are found in most physical systems.
The generic qdevice has two broad sources of noise: decoherence over time and depolarisation due to gate operations.
The decoherence over time is modeled using T1, a energy or longitudinal relaxation time,
and T2, a dephasing or transverse relaxation time.
This noise is influenced by the time a qubit is kept in memory.
The gate execution times init_time, single_qubit_gate_time, two_qubit_gate_time and measure_time
contribute to this noise.
The init_time determines the time required for initializing a qubit.
All times are in nano seconds.
The gate operations noise is modeled using randomly applied pauli gates.
The single_qubit_gate_depolar_prob and two_qubit_gate_depolar_prob
control the chance that a random pauli gate is applied to the one or two qubits involved in the operation.
# Configuration with perfect link, a generic qdevice, but some noise on the generic device
qdevice_cfg: &qdevice_cfg
num_qubits: 2
# coherence times (same for each qubit)
T1: 10_000_000_000
T2: 1_000_000_000
# gate execution times
init_time: 10_000
single_qubit_gate_time: 1_000
two_qubit_gate_time: 100_000
measure_time: 10_000
# noise model
single_qubit_gate_depolar_prob: 0.01
two_qubit_gate_depolar_prob: 0.1
stacks:
- name: Alice
qdevice_typ: generic
qdevice_cfg:
<<: *qdevice_cfg
- name: Bob
qdevice_typ: generic
qdevice_cfg:
<<: *qdevice_cfg
links:
- stack1: Alice
stack2: Bob
typ: perfect
cfg:
dummy: null
clinks:
- stack1: Alice
stack2: Bob
typ: instant
Note
Depending on the NetSquid formalism slightly different noise models may be used. This originates from restrictions in using formalisms that scale better, but have more restrictions in what kind of quantum states may be described.
NV qdevice
The nitrogen-vacancy(NV) features a more advanced model. It describes a model with one electron qubit and one or more carbon qubits, with a topology that forbids direct carbon to carbon interactions.
Moreover the model has less native gates than the generic qdevice. For example, it does not have native Hadamard gates and qubit measurements can only be made on the electron qubit.
These effects do not demand different program code, so one is allowed to use a Hadamard gate and measure all qubits in the application code, but will cause differences in the simulation, as for example the absence of a native Hadamard gate, will result in the Hadamard being performed using two XY or YZ rotation gates. This in turn will apply gate depolarisation noise twice instead of once.
The NV qdevice always has one electron qubit and by increasing num_qubits multiple carbon qubits can be used.
All noise except for decoherence over time is modeled using application of a random pauli matrices.
electron_init_depolar_prob and carbon_init_depolar_prob determine the chance of noise during qubit initialization.
electron_single_qubit_depolar_prob and carbon_z_rot_depolar_prob control the chance of noise for single qubit operations.
ec_gate_depolar_prob determines the noise chance for any operations between the electron and a carbon qubit.
prob_error_0 and prob_error_1 simulate measurement errors using the electron.
prob_error_0 is the chance that instead of measuring a 0, a 1 is measured instead, prob_error_1 is the reverse.
The various gate execution times function similar to the generic qdevice, but there is more specification possible.
# Configuration with perfect link and NV qdevice with noise
qdevice_cfq: &qdevice_cfg
# number of qubits per NV
num_qubits: 2
# initialization error of the electron spin
electron_init_depolar_prob: 0.05
# error of the single-qubit gate
electron_single_qubit_depolar_prob: 0.01
# measurement errors (prob_error_X is the probability that outcome X is flipped to 1 - X)
# Chance of 0 being measured as 1
prob_error_0: 0.05
# Chance of 1 being measured as 0
prob_error_1: 0.005
# initialization error of the carbon nuclear spin
carbon_init_depolar_prob: 0.05
# error of the Z-rotation gate on the carbon nuclear spin
carbon_z_rot_depolar_prob: 0.001
# error of the native NV two-qubit gate
ec_gate_depolar_prob: 0.008
# coherence times
electron_T1: 1_000_000_000
electron_T2: 300_000_000
carbon_T1: 150_000_000_000
carbon_T2: 1_500_000_000
# gate execution times
carbon_init: 310_000
carbon_rot_x: 500_000
carbon_rot_y: 500_000
carbon_rot_z: 500_000
electron_init: 2_000
electron_rot_x: 5
electron_rot_y: 5
electron_rot_z: 5
ec_controlled_dir_x: 500_000
ec_controlled_dir_y: 500_000
measure: 3_700
stacks:
- name: Alice
qdevice_typ: nv
qdevice_cfg:
<<: *qdevice_cfg
- name: Bob
qdevice_typ: nv
qdevice_cfg:
<<: *qdevice_cfg
links:
- stack1: Alice
stack2: Bob
typ: perfect
clinks:
- stack1: Alice
stack2: Bob
typ: instant
Note
The decoherence models, using T1 and T2 are only applied to qubits that are idle in memory.
When a qubit is participating in an active operation, such as initialization, a gate or a measurement,
it is not subject to the decoherence model that is specified via T1 and T2.
The decoherence as well as all other noise sources during the operation
are described via the noise parameter for these operations.
Link types
Depolarise link
The depolarise link is a simple link model that simulates EPR pairs being generated with some noise.
The noise in the resulting EPR pairs,
is simulated via the fidelity parameter it controls how well entangled the successfully generated EPR pairs are.
The t_cycle controls how long the EPR generation takes, in nanoseconds, for a single attempt.
prob_success controls the likelihood of each attempt succeeding.
# A configuration with a perfect generic qdevice and a link with depolarizing noise model
stacks:
- name: Alice
qdevice_typ: generic
qdevice_cfg:
dummy: null
- name: Bob
qdevice_typ: generic
qdevice_cfg:
dummy: null
link_cfg: &link_cfg
# Fidelity between the EPR pair qubits
fidelity: 0.9
# Time in nanoseconds for an attempt to generated entanglement
t_cycle: 10.
# Chance for each attempt at entanglement to succeed
prob_success: 0.8
links:
- stack1: Alice
stack2: Bob
typ: depolarise
cfg:
<<: *link_cfg
clinks:
- stack1: Alice
stack2: Bob
typ: instant
Heralded link
The heralded link uses a model with both nodes connected by fiber to a midpoint station with a Bell-state measurement detector. The nodes repeatedly send out entangled photons and, on a successful measurement at the midpoint, the midpoint station will send out a signal to both nodes, heralding successful entanglement. The heralded link uses the double click model as developed and described by this paper.
# A configuration with a perfect generic qdevice and a link with a heralded model using a two click protocol
stacks:
- name: Alice
qdevice_typ: generic
qdevice_cfg:
dummy: null
- name: Bob
qdevice_typ: generic
qdevice_cfg:
dummy: null
link_cfg: &link_cfg
# total length [km] of heralded connection (i.e. sum of fibers on both sides on midpoint station).
length: 1.0
# probability that photons are lost when entering connection the connection on either side.
p_loss_init: 0
# attenuation coefficient [dB/km] of fiber on either side.
p_loss_length: 0.25
# speed of light [km/s] in fiber on either side.
speed_of_light: 200_000
# dark-count probability per detection
dark_count_probability: 0
# probability that the presence of a photon leads to a detection event
detector_efficiency: 1.0
# Hong-Ou-Mandel visibility of photons that are being interfered (measure of photon indistinguishability)
visibility: 1.0
# determines whether photon-number-resolving detectors are used for the Bell-state measurement
num_resolving: False
links:
- stack1: Alice
stack2: Bob
typ: heralded
cfg:
<<: *link_cfg
clinks:
- stack1: Alice
stack2: Bob
typ: instant
Clink types
instant clink
The instant clink is a classical link model where all classical communication is instant. It does not have any configuration options.
clinks:
- stack1: Alice
stack2: Bob
typ: instant
default clink
The default clink is a classical link model where classical communication is delayed by exactly the delay specified in the configuration.
clinks:
- stack1: Alice
stack2: Bob
typ: default
cfg:
delay: 20
Multiple nodes
SquidASM is capable of simulating networks that consist of more than two nodes. This extends straightforwardly from two node networks. To add an extra node, Charlie, the network configuration must be extended with a stack for Charlie and the desired connections must be added.
# 3 node network, all the sources of noise have been disabled for this example
qdevice_cfg: &qdevice_cfg
num_qubits: 2
# coherence times (same for each qubit)
T1: 0
T2: 0
# gate execution times
init_time: 10_000
single_qubit_gate_time: 1_000
two_qubit_gate_time: 100_000
measure_time: 10_000
# noise model
single_qubit_gate_depolar_prob: 0.
two_qubit_gate_depolar_prob: 0.
stacks:
- name: Alice
qdevice_typ: generic
qdevice_cfg:
<<: *qdevice_cfg
- name: Bob
qdevice_typ: generic
qdevice_cfg:
<<: *qdevice_cfg
- name: Charlie
qdevice_typ: generic
qdevice_cfg:
<<: *qdevice_cfg
link_cfg: &link_cfg
fidelity: 1
prob_success: 0.3
t_cycle: 1e5
links:
- stack1: Alice
stack2: Bob
typ: depolarise
cfg:
<<: *link_cfg
- stack1: Alice
stack2: Charlie
typ: depolarise
cfg:
<<: *link_cfg
- stack1: Bob
stack2: Charlie
typ: depolarise
cfg:
<<: *link_cfg
clinks:
- stack1: Alice
stack2: Bob
typ: default
cfg:
delay: 5e3
- stack1: Alice
stack2: Charlie
typ: default
cfg:
delay: 1e4
- stack1: Bob
stack2: Charlie
typ: default
cfg:
delay: 1e4
While the previous example has connected the Charlie stack to both Alice and Bob, this is not mandatory, as long as the application does not attempt to use a non-existent link. For larger networks it is useful to create the network configuration object via Util methods or create it programmatically yourself.
To write programs for networks with more than two nodes in the network, one must register each of the nodes for which a connection is used to the ProgramMeta object. An example of a program where we have three nodes in the network: Alice, Bob and Charlie, is shown below. In this example both Bob and Charlie generate a EPR pair with Alice. Alice will then perform a bell state measurement and send the corrections to Charlie. This will result in Bob and Charlie sharing an EPR pair.
class AliceProgram(Program):
PEER_BOB = "Bob"
PEER_CHARLIE = "Charlie"
@property
def meta(self) -> ProgramMeta:
return ProgramMeta(
name="tutorial_program",
csockets=[self.PEER_BOB, self.PEER_CHARLIE],
epr_sockets=[self.PEER_BOB, self.PEER_CHARLIE],
max_qubits=2,
)
def run(self, context: ProgramContext):
# get classical sockets
csocket_bob = context.csockets[self.PEER_BOB]
csocket_charlie = context.csockets[self.PEER_CHARLIE]
# get EPR sockets
epr_socket_bob = context.epr_sockets[self.PEER_BOB]
epr_socket_charlie = context.epr_sockets[self.PEER_CHARLIE]
# get connection to quantum network processing unit
connection = context.connection
# send a message to both nodes
msg = "Hello from Alice"
csocket_bob.send(msg)
csocket_charlie.send(msg)
print(f"{ns.sim_time()} ns: Alice sends: {msg} to Bob and Charlie")
# Generate EPR pairs with both Bob and Charlie
epr_qubit_bob = epr_socket_bob.create_keep()[0]
epr_qubit_charlie = epr_socket_charlie.create_keep()[0]
# Perform a bell state measurement on the qubit from Bob and the qubit from Charlie
epr_qubit_bob.cnot(epr_qubit_charlie)
epr_qubit_bob.H()
m2 = epr_qubit_bob.measure()
m1 = epr_qubit_charlie.measure()
yield from connection.flush()
print(
f"{ns.sim_time()} ns: Alice finished EPR generation and local quantum operations"
)
csocket_charlie.send(str(int(m2)))
csocket_charlie.send(str(int(m1)))
print(
f"{ns.sim_time()} ns: Alice sends corrections m1: {m1}, m2: {m2} to Charlie"
)
return {}
Parameter sweeping
Often it will be desired to simulate not a single network configuration, but a range of parameters.
In this section we will show how to modify an existing network configuration inside run_simulation.py
and how to import components of the network in order to support this modification.
In following example, we have a setup that is comparable to the earlier examples of examples/tutorial/3.1_output.
The application will generate EPR pairs and measure them after a Hadamard gate.
Our goal is to use run_simulation.py to modify the network of config.yaml,
replace its link with a depolarize link, perform multiple simulations with varying fidelity for the link
and generate an graph of the fidelity vs error rate.
import numpy as np
from application import AliceProgram, BobProgram
from matplotlib import pyplot
from squidasm.run.stack.config import (
DepolariseLinkConfig,
LinkConfig,
StackNetworkConfig,
)
from squidasm.run.stack.run import run
# import network configuration from file
cfg = StackNetworkConfig.from_file("config.yaml")
# Create a depolarise link in python
depolarise_config = DepolariseLinkConfig.from_file("depolarise_link_config.yaml")
link = LinkConfig(stack1="Alice", stack2="Bob", typ="depolarise", cfg=depolarise_config)
# Replace link from YAML file with new depolarise link
cfg.links = [link]
link_fidelity_list = np.arange(0.5, 1.0, step=0.05)
error_rate_result_list = []
for fidelity in link_fidelity_list:
# Set fidelity in depolarise link
depolarise_config.fidelity = fidelity
# Set a parameter, the number of epr rounds, for the programs
epr_rounds = 10
alice_program = AliceProgram(num_epr_rounds=epr_rounds)
bob_program = BobProgram(num_epr_rounds=epr_rounds)
# Run the simulation. Programs argument is a mapping of network node labels to programs to run on that node
# return from run method are the results per node
simulation_iterations = 20
results_alice, results_bob = run(
config=cfg,
programs={"Alice": alice_program, "Bob": bob_program},
num_times=simulation_iterations,
)
# results have List[Dict[]] structure. List contains the simulation iterations
results_alice = [
results_alice[i]["measurements"] for i in range(simulation_iterations)
]
results_bob = [results_bob[i]["measurements"] for i in range(simulation_iterations)]
# Create one large list of all EPR measurements
results_alice = np.concatenate(results_alice).flatten()
results_bob = np.concatenate(results_bob).flatten()
# Per EPR determine if results are identical
errors = [
result_alice != result_bob
for result_alice, result_bob in zip(results_alice, results_bob)
]
# Write out average error rate
error_percentage = sum(errors) / len(errors) * 100
error_rate_result_list.append(error_percentage)
pyplot.plot(link_fidelity_list, error_rate_result_list)
pyplot.xlabel("Fidelity")
pyplot.ylabel("Error percentage")
pyplot.savefig("output_error_vs_fid.png")
We still start with loading the original configuration:
cfg = StackNetworkConfig.from_file("config.yaml")
We then load in the configuration options for the depolarise link:
depolarise_config = DepolariseLinkConfig.from_file("depolarise_link_config.yaml")
This creates the DepolariseLinkConfig object based on the parameters in the file depolarise_link_config.yaml:
# Fidelity between the EPR pair qubits
fidelity: 0.9
# Time in nanoseconds for an attempt to generated entanglement
t_cycle: 10.
# Chance for each attempt at entanglement to succeed
prob_success: 0.8
Note
It is possible to create the DepolariseLinkConfig using:
DepolariseLinkConfig(fidelity=0.9, t_cycle=10., prob_success=0.8).
Nonetheless, it is advisable to store configurations in YAML files.
Afterwards a complete link object can be created with a depolarise model. This link object is used to replace the original links inside the configuration:
link = LinkConfig(stack1="Alice", stack2="Bob", typ="depolarise", cfg=depolarise_config)
# Replace link from YAML file with new depolarise link
cfg.links = [link]
Running the simulation will result in a basic graph inside the image with the name:
output_error_vs_fid.png being generated that looks similar to: