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:

examples/tutorial/4.1_YAML/basic_example.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.

examples/tutorial/4.1_YAML/run.py
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)
Output of pprint of basic example
{'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:

examples/tutorial/4.1_YAML/advanced_example.yaml
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
Output of pprint of advanced example
{'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:

examples/tutorial/4.2_network-configuration/1_perfect.yaml
# 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.

examples/tutorial/4.2_network-configuration/2_generic_qdevice.yaml
# 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.

examples/tutorial/4.2_network-configuration/3_nv_qdevice.yaml
# 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.

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.

examples/tutorial/4.3_multi-node/config.yaml
# 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.

examples/tutorial/4.3_multi-node/application.py AliceProgram
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.

examples/tutorial/4.3_parameter-sweeping/run_simulation.py
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:

examples/tutorial/4.3_parameter-sweeping/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:

../_images/output_error_vs_fid.png