Advanced Usage and Features

In this demo we will be reviewing netQuil’s advanced features that make it a powerful and extensible platform for quantum networking simulation. Lets start with its built-in and custom devices.

Built-in and Custom Devices

A device is a codified, single piece of hardware in your network. In classical networking this would include devices such as a router, modem, repeater, and network switch. In quantum networking think fiber optics, intensity modulators, lasers or photon sources, and SNSPDs or detectors. Associated with each device in a quantum network is its physical effect on qubits and the time it takes qubits to pass through the device.

There are three types of devices in netQuil: source, transit, and target devices. Source and target devices are associated with an agent, while transit devices are associated with instances of QConnection. When a qubit leaves Alice and travels to Bob, the qubit originates from Alice’s source devices, travels through the transit devices attached to their QConnection and ends by arriving through Bob’s target devices. The order in which a qubit passes through each device corresponds to the order in which those devices were added to either the agent or connection.

NetQuil’s devices module allows you to choose from a number of built-in source, transit, and target devices.

Built-in Devices

Laser is a built-in noisy photon source that generates photons according to expected_photons and a poisson distribution. If network_monitor=True the laser will output its noise to signal ratio after each trial. You can create a custom get_results function that prints information about the device after each trial. The Laser will also return a delay equal to the photon pulse length.

Fiber is a built-in noisy fiber optical wire simulator and an example of a transit device. Fibers have an associated length in kilometers and an attenuation coefficient. The attenuation coefficient is proportional to the probability that a photon is lost while traveling within the fiber (i.e. the photon is measured, and the measured value is inaccessible by any agent). In netQuil, if a qubit is lost due to attenuation, the target will receive the negative index of the qubit lost. For example, if Alice sends qubit 3 and it is lost due to attenuation, Bob will receive -3, and neither Alice nor Bob will be able to operate with qubit 3. If the qubit lost is 0 then the value will be set to -inf. Remember that when we send qubits between agents we are sending the index of the qubit in the program and not the true qubit.

Here is an example of a very simple program using the Laser and Fiber devices.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Alice(Agent):
    def run(self):
        p = self.program
        for q in self.qubits:
            p += H(q)
            p += X(q)
            self.qsend('Bob', [q])

class Bob(Agent):
    def run(self):
        p = self.program
        for i in range(3):
            q = self.qrecv(alice.name)[0]

            # Check if qubit is lost
            if q >= 0:
                p += MEASURE(q, ro[q])

p = Program()
ro = p.declare('ro', 'BIT', 3)

alice = Alice(p, qubits=[0,1,2])
bob = Bob(p)

# Define source device
laser = Laser(rotation_prob_variance=.9)
alice.add_source_devices([laser])

# Define transit device and connection
fiber = Fiber(length=5, attenuation_coefficient=-.90)
QConnect(alice, bob, transit_devices=[fiber])

# Run simulation
Simulation(alice, bob).run()

# Run program
qvm = QVMConnection()
results = qvm.run(p)
print(results)

Custom Devices

Moreover, you can build your own custom devices by extending the Device class. All devices must have an apply function that is responsible for the device’s activity. The run function must return a dictionary that optionally contains the lost qubits and delay. The delay represents the time it took qubits to travel through the device. Remember, if qubits are lost while passing through the device, return an entry in the dictionary lost_qubits: [lost qubits].

Most devices can be arbitrarily complex in their design and can depend on environmental factors such as temperature, humidity, or pressure. Custom devices allow us to simulate these arbitrarily complex devices. As an example, we will create a simple custom fiber that changes the polarization of a photon by some random angle from a normal distribution.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Simple_Fiber(Device):
    def __init__(self, length, fiber_quality, rotation_std):
        self.fiber_quality = fiber_quality
        self.length = length
        self.rotation_std = rotation_std
        self.signal_speed = 2.998 * 10 ** 5 #speed of light in km/s

    def apply(self, program, qubits):
        for qubit in qubits:
            if np.random.rand() > self.fiber_quality:
                rotation_angle = np.random.normal(0, self.rotation_std)
                program += RX(rotation_angle, qubit)

        delay = self.length/self.signal_speed

        return {
            'delay': delay,
        }

class Alice(Agent):
    def run(self):
        p = self.program
        for q in self.qubits:
            p += H(q)
            p += X(q)
            self.qsend('Bob', [q])

class Bob(Agent):
    def run(self):
        p = self.program
        for _ in range(3):
            q = self.qrecv(alice.name)[0]
            p += MEASURE(q, ro[q])

p = Program()
ro = p.declare('ro', 'BIT', 3)

alice = Alice(p, qubits=[0,1,2])
bob = Bob(p)

# Define source device
laser = Laser(rotation_prob_variance=.9)
alice.add_source_devices([laser])

fiber = Simple_Fiber(length=10, fiber_quality=.6, rotation_std=5)
QConnect(alice, bob, transit_devices=[fiber])

Simulation(alice, bob).run(network_monitor=True)

qvm = QVMConnection()
results = qvm.run(p)
print(results)

NetQuil also has a built-in noise module for performing common qubit operations such as normal unitary rotations, depolarization, and bit and phase flips.

Trials and Time

In some situations, pyQuil programs generated between trials will be different depending on noise or the dynamic nature of your network. In order to accomodate this, Simulation().run() will always return a list of programs (i.e. one program per trial) that can be run on your qvm or qpu. Pass the number of trials you would like to run into Simulation().run(trials=5), as well as a list containing the class of each agent being run. Do NOT forget to pass agent_classes (Simulation(alice, bob).run(trials=5, agent_classes=[Alice, Bob]), since this is required in order to reset the agents between trials.

You can also pass network_monitor=True to run in order to see a list of transactions on the network, the time of each transaction, and information about your devices. In addition to individual agent clocks, a master clock is running throughout the network simulation that can be accessed through agent.get_master_time() on any agent. If you are implementing time-bin encoding or one of its variation, we encourage you to experiment with the master and agent clocks.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class Simple_Fiber(Device):
    def __init__(self, length, fiber_quality, rotation_std):
    self.fiber_quality = fiber_quality
    self.length = length
    self.rotation_std = rotation_std
    self.signal_speed = 2.998 * 10 ** 5 #speed of light in km/s

    def apply(self, program, qubits):
        for qubit in qubits:
            # Apply noise
            if np.random.rand() > self.fiber_quality:
                rotation_angle = np.random.normal(0, self.rotation_std)
                program += RX(rotation_angle, qubit)

        delay = self.length/self.signal_speed

        return {
            'delay': delay,
        }

class Alice(Agent):
    def run(self):
        p = self.program
        for q in self.qubits:
            p += H(q)
            p += X(q)
            self.qsend('Bob', [q])

class Bob(Agent):
    def run(self):
        p = self.program
        for _ in range(3):
            q = self.qrecv(alice.name)[0]

            # Check if qubit is lost
            if q >= 0:
                p += MEASURE(q, ro[q])

p = Program()
ro = p.declare('ro', 'BIT', 3)

alice = Alice(p, qubits=[0,1,2])
bob = Bob(p)

# Define source device
laser = Laser(rotation_prob_variance=.9)
alice.add_source_devices([laser])

# Define transit devices and connection
custom_fiber = Simple_Fiber(length=5, fiber_quality=.6, rotation_std=5)
fiber = Fiber(length=5, attenuation_coefficient=-.20)
QConnect(alice, bob, transit_devices=[fiber, custom_fiber])

# Run simulation
programs = Simulation(alice, bob).run(trials=5, agent_classes=[Alice, Bob])

# Run programs
qvm = QVMConnection()
for idx, program in enumerate(programs):
    results = qvm.run(program)
    print('Program {}: '.format(idx), results)

Looking Forward

In this demo we introduced netQuil’s built-in and custom devices, noise module, multiple trials, and network monitor. As you can see, netQuil is a powerful quantum networking simulator due to its extensibility, but it is also an active project. If you find a bug open an issue or, better yet, a PR!