Examples
Manually editing quantum states
../examples/manual_quantum_states.py
1"""Example for manually working with the quantum objects classes.
2
3While manually manipulating quantum states is definitely not the focus of this
4simulation package (there are better tools for that), it is nonetheless a good
5starting point. Knowing how the quantum objects work is also essential for
6creating custom events.
7"""
8import numpy as np
9from requsim.world import World
10from requsim.quantum_objects import Qubit, Pair
11import requsim.libs.matrix as mat
12from requsim.tools.noise_channels import z_noise_channel
13
14world = World()
15
16# without specifying any specific setup, make a Pair object with a specific
17# density matrix
18qubit1 = Qubit(world=world)
19qubit2 = Qubit(world=world)
20initial_state = mat.phiplus @ mat.H(mat.phiplus) # Bell state
21pair = Pair(world=world, qubits=[qubit1, qubit2], initial_state=initial_state)
22# Tip: you can use world.print_status() to show the state of world in a
23# human-readable format
24
25# now apply some map to the state. In quantum repeater context, this is most
26# often a noise channel
27# a) manually change the state with matrix operations
28epsilon = 0.01
29noise_operator = mat.tensor(mat.Z, mat.I(2)) # z_noise on qubit1
30pair.state = (1 - epsilon) * pair.state + epsilon * noise_operator @ pair.state @ mat.H(
31 noise_operator
32)
33
34# b) use appropriate NoiseChannel object and Qubit methods
35qubit3 = Qubit(world=world)
36qubit4 = Qubit(world=world)
37initial_state = mat.phiplus @ mat.H(mat.phiplus) # Bell state
38pair2 = Pair(world=world, qubits=[qubit3, qubit4], initial_state=initial_state)
39# z_noise_channel imported from requsim.tools.noise_channels
40qubit3.apply_noise(z_noise_channel, epsilon=0.01)
41assert np.allclose(pair.state, pair2.state)
42
43# removing objects from the world works via the destroy methods
44pair.destroy()
45qubit1.destroy()
46qubit2.destroy()
47pair2.destroy()
48qubit3.destroy()
49qubit4.destroy()
Manually running a simple quantum repeater protocol
../examples/manual_simple_repeater.py
1"""A simple repeater protcol performed manually with requsim objects.
2
3Obviously, this is not the recommended way to do this, but it gives insight
4into the low-level steps that need to be performed by the simulation.
5
6This Protocol distributes a Bell pair between two Stations A and B via a
7repeater station in-between that performs entanglement swapping.
8Note that this implementation does not concern itself with timing and
9failed trials, but of course this could be added manually as well.
10"""
11import numpy as np
12from requsim.quantum_objects import Station, Source, Pair
13from requsim.world import World
14import requsim.libs.matrix as mat
15
16total_length = 2000 # meters
17initial_state = mat.phiplus @ mat.H(mat.phiplus) # perfect Bell state
18
19# Step 1: Perform world setup
20world = World()
21station_a = Station(world=world, position=0, label="Alice")
22station_b = Station(world=world, position=total_length, label="Bob")
23station_central = Station(world=world, position=total_length / 2, label="Repeater")
24# Sources generate an entangled pair and place them into storage at two stations
25# here they are positioned at the central station and send qubits out to the
26# outer stations
27source_a = Source(
28 world=world,
29 position=station_central.position,
30 target_stations=[station_a, station_central],
31)
32source_b = Source(
33 world=world,
34 position=station_central.position,
35 target_stations=[station_central, station_b],
36)
37if __name__ == "__main__":
38 print("World status after Step 1: Perform world setup")
39 world.print_status()
40
41# Step 2: Distribute pairs
42pair_a = source_a.generate_pair(initial_state=initial_state)
43pair_b = source_b.generate_pair(initial_state=initial_state)
44if __name__ == "__main__":
45 print("\n\nWorld status after Step 2: Distribute pairs")
46 world.print_status()
47
48# Step 3: perform entanglement swapping
49four_qubit_state = mat.tensor(pair_a.state, pair_b.state)
50operator = mat.tensor(mat.I(2), mat.H(mat.phiplus), mat.I(2))
51state_after_swapping = operator @ four_qubit_state @ mat.H(operator)
52state_after_swapping = state_after_swapping / np.trace(state_after_swapping)
53# remove outdated objects
54pair_a.destroy() # destroying a Pair object does not remove the associated qubits
55pair_b.destroy()
56for qubit in station_central.qubits:
57 qubit.destroy()
58new_pair = Pair(
59 world=world,
60 qubits=[station_a.qubits[0], station_b.qubits[0]],
61 initial_state=state_after_swapping,
62)
63if __name__ == "__main__":
64 print("\n\nWorld status after Step 3: Perform entanglement swapping")
65 world.print_status()
66
67# at this point you would probably collect information about the long distance
68# state before removing it from the world
Using the event system with a global protocol
../examples/event_protocol_simple_repeater.py
1"""Using the event system to run a simple repeater protocol.
2
3This variant accurately uses timing data and collects information about the
4generated long-distance states. The protocol decisions are made at a global
5level, with the protocol looking at the whole state of the world and deciding
6what to do next.
7
8The world setup and protocol can be described as follows:
9
10Stations A and B want to establish entangled pairs via a repeater station at
11the middle point between them.
12The middle station has a mechanism to create entangled pairs and a quantum
13memory (can hold one qubit for each side). The middle station will continously
14try to establish pairs between itself and both A and B. After it has established
15a pair with both A and B, an entanglement swapping operation is performed to
16connect A and B. Repeat until the desired number of pairs has been established.
17
18Error model: The entangled pairs are considered to be perfect upon generation.
19The quantum memories introduce a time-dependent dephasing noise while a qubit
20sits in memory. The Bell measurement to perform the entanglement swapping is
21modeled to always succeed, but to introduce some additional depolarizing noise
22in the process.
23
24For a detailed explanation of this type of protocol see e.g.
25D. Luong, L. Jiang, J. Kim, N. Lütkenhaus; Applied Physics B 122, 96 (2016)
26Preprint: https://arxiv.org/abs/1508.02811
27The protocol in this example corresponds to the 'simultaneous' variant of the
28protocol discussed there, but with a simpler error model.
29"""
30import numpy as np
31import pandas as pd
32from requsim.tools.protocol import TwoLinkProtocol
33from requsim.tools.noise_channels import z_noise_channel
34from requsim.tools.evaluation import standard_bipartite_evaluation
35from requsim.libs.aux_functions import distance
36import requsim.libs.matrix as mat
37from requsim.events import EntanglementSwappingEvent
38from requsim.world import World
39from requsim.noise import NoiseChannel, NoiseModel
40from requsim.quantum_objects import Station, SchedulingSource
41
42
43def construct_dephasing_noise_channel(dephasing_time):
44 def lambda_dp(t):
45 return (1 - np.exp(-t / dephasing_time)) / 2
46
47 def dephasing_noise_func(rho, t):
48 return z_noise_channel(rho=rho, epsilon=lambda_dp(t))
49
50 return NoiseChannel(n_qubits=1, channel_function=dephasing_noise_func)
51
52
53class SimpleProtocol(TwoLinkProtocol):
54 # TwoLinkProtocol is a helpful abstract class that provides useful
55 # attributes and methods for dealing with and evaluating repeater protocols
56 # consisting of two links (supports multi-mode memories as well).
57 # It needs to be initialized via its `setup` method after world setup
58 # is complete, in order to find all the relevant objects in the world.
59 def check(self, message=None):
60 """Check current status and schedule new events.
61
62 This looks globally at the status of the whole `world` and decides
63 which events should be scheduled because of it. It is intended to be
64 called after every change in the `world`.
65
66 Parameters
67 ----------
68 message : dict or None
69 Optional additional information for the Protocol to consider.
70 This particular protocol does not use it. Default is None.
71
72 Returns
73 -------
74 None
75
76 """
77 left_pairs = self._get_left_pairs()
78 num_left_pairs = len(left_pairs)
79 num_left_pairs_scheduled = len(self._left_pairs_scheduled())
80 right_pairs = self._get_right_pairs()
81 num_right_pairs = len(right_pairs)
82 num_right_pairs_scheduled = len(self._right_pairs_scheduled())
83 long_distance_pairs = self._get_long_range_pairs()
84
85 # STEP 1: For each link, if there are no pairs established and
86 # no pairs scheduled: Schedule a pair.
87 if num_left_pairs + num_left_pairs_scheduled == 0:
88 self.source_A.schedule_event()
89 if num_right_pairs + num_right_pairs_scheduled == 0:
90 self.source_B.schedule_event()
91
92 # STEP 2: If both links are present, do entanglement swapping.
93 if num_left_pairs == 1 and num_right_pairs == 1:
94 left_pair = left_pairs[0]
95 right_pair = right_pairs[0]
96 ent_swap_event = EntanglementSwappingEvent(
97 time=self.world.event_queue.current_time,
98 pairs=[left_pair, right_pair],
99 station=self.station_central,
100 )
101 self.world.event_queue.add_event(ent_swap_event)
102
103 # STEP 3: If a long range pair is present, save its data and delete
104 # the associated objects.
105 if long_distance_pairs:
106 for pair in long_distance_pairs:
107 self._eval_pair(pair)
108 for qubit in pair.qubits:
109 qubit.destroy()
110 pair.destroy()
111
112
113def run(length, max_iter, params):
114 C = params["COMMUNICATION_SPEED"]
115 P_LINK = params["P_LINK"]
116 T_DP = params["T_DP"]
117 LAMBDA_BSM = params["LAMBDA_BSM"]
118 L_ATT = 22e3 # attenuation length
119
120 # define functions for link generation behavior
121 def state_generation(source):
122 # this returns the density matrix of a successful trial
123 # this particular function already assumes some things that are only
124 # appropriate for this particular setup, e.g. the source is at one
125 # of the stations and the end stations do not decohere
126 state = mat.phiplus @ mat.H(mat.phiplus)
127 comm_distance = max(
128 distance(source, source.target_stations[0]),
129 distance(source, source.target_stations[1]),
130 )
131 storage_time = 2 * comm_distance / C
132 for idx, station in enumerate(source.target_stations):
133 if station.memory_noise is not None:
134 state = station.memory_noise.apply_to(
135 rho=state, qubit_indices=[idx], t=storage_time
136 )
137 return state
138
139 def time_distribution(source):
140 comm_distance = max(
141 distance(source, source.target_stations[0]),
142 distance(source, source.target_stations[1]),
143 )
144 trial_time = 2 * comm_distance / C
145 eta = P_LINK * np.exp(-comm_distance / L_ATT)
146 num_trials = np.random.geometric(eta)
147 time_taken = num_trials * trial_time
148 return time_taken
149
150 def BSM_error_func(rho):
151 return LAMBDA_BSM * rho + (1 - LAMBDA_BSM) * mat.I(4) / 4
152
153 BSM_noise_channel = NoiseChannel(n_qubits=2, channel_function=BSM_error_func)
154 BSM_noise_model = NoiseModel(channel_before=BSM_noise_channel)
155
156 # perform the world setup
157 world = World()
158 station_A = Station(world=world, position=0)
159 station_central = Station(
160 world=world,
161 position=length / 2,
162 memory_noise=construct_dephasing_noise_channel(T_DP),
163 BSM_noise_model=BSM_noise_model,
164 )
165 station_B = Station(world=world, position=length)
166 source_A = SchedulingSource(
167 world=world,
168 position=station_central.position,
169 target_stations=[station_A, station_central],
170 time_distribution=time_distribution,
171 state_generation=state_generation,
172 )
173 source_B = SchedulingSource(
174 world=world,
175 position=station_central.position,
176 target_stations=[station_central, station_B],
177 time_distribution=time_distribution,
178 state_generation=state_generation,
179 )
180 protocol = SimpleProtocol()
181 protocol.setup(world=world, communication_speed=C)
182
183 current_message = None
184 while len(protocol.time_list) < max_iter:
185 protocol.check(message=current_message)
186 current_message = world.event_queue.resolve_next_event()
187 return protocol
188
189
190if __name__ == "__main__":
191 params = {
192 "P_LINK": 0.80, # link generation probability
193 "T_DP": 100e-3, # dephasing time
194 "LAMBDA_BSM": 0.99, # Bell-State-Measurement ideality parameter
195 "COMMUNICATION_SPEED": 2e8, # speed of light in optical fibre
196 }
197 length_list = np.linspace(20e3, 200e3, num=8)
198 max_iter = 1000
199 raw_data = [
200 run(length=length, max_iter=max_iter, params=params).data
201 for length in length_list
202 ]
203 result_list = [standard_bipartite_evaluation(data_frame=df) for df in raw_data]
204 results = pd.DataFrame(
205 data=result_list,
206 index=length_list,
207 columns=[
208 "raw_rate",
209 "fidelity",
210 "fidelity_std_err",
211 "key_per_time",
212 "key_per_time_std_err",
213 ],
214 )
215 print(results)
216 # plotting key_per_time vs. length is usually what you want to do with these
Using the event system with callbacks
../examples/event_protocol_with_callbacks.py
1"""An alternative way to use the event system.
2
3This implements the same protocol as event_protocol_simple_repeater.py with
4a different mechanism: Instead of globally examining the whole status of the
5simulation and deciding from there what events to add to the event_queue, the
6next thing to do after a event is resolved is already attached to the event via
7a callback.
8
9While this breaks the neat interpretation of the protocol deciding what to do
10next purely from the current state of the simulation, this way can save time
11especially for complex setups where the current state may be hard to analyze.
12"""
13import numpy as np
14import pandas as pd
15from requsim.tools.protocol import TwoLinkProtocol
16from requsim.tools.noise_channels import z_noise_channel
17from requsim.tools.evaluation import standard_bipartite_evaluation
18from requsim.libs.aux_functions import distance
19import requsim.libs.matrix as mat
20from requsim.events import EntanglementSwappingEvent
21from requsim.world import World
22from requsim.noise import NoiseChannel, NoiseModel
23from requsim.quantum_objects import Station, SchedulingSource
24
25
26def construct_dephasing_noise_channel(dephasing_time):
27 def lambda_dp(t):
28 return (1 - np.exp(-t / dephasing_time)) / 2
29
30 def dephasing_noise_func(rho, t):
31 return z_noise_channel(rho=rho, epsilon=lambda_dp(t))
32
33 return NoiseChannel(n_qubits=1, channel_function=dephasing_noise_func)
34
35
36class CallbackProtocol(TwoLinkProtocol):
37 def _check_for_swapping_A(self, event_dict):
38 right_pairs = self._get_right_pairs()
39 if not right_pairs:
40 return
41 left_pair = event_dict["output_pair"]
42 right_pair = right_pairs[0]
43 ent_swap_event = EntanglementSwappingEvent(
44 time=self.world.event_queue.current_time,
45 pairs=[left_pair, right_pair],
46 station=self.station_central,
47 )
48 ent_swap_event.add_callback(self._after_swapping)
49 self.world.event_queue.add_event(ent_swap_event)
50
51 def _check_for_swapping_B(self, event_dict):
52 left_pairs = self._get_left_pairs()
53 if not left_pairs:
54 return
55 left_pair = left_pairs[0]
56 right_pair = event_dict["output_pair"]
57 ent_swap_event = EntanglementSwappingEvent(
58 time=self.world.event_queue.current_time,
59 pairs=[left_pair, right_pair],
60 station=self.station_central,
61 )
62 self.world.event_queue.add_event(ent_swap_event)
63 ent_swap_event.add_callback(self._after_swapping)
64
65 def _after_swapping(self, event_dict):
66 long_distance_pairs = self._get_long_range_pairs()
67 if long_distance_pairs:
68 for pair in long_distance_pairs:
69 self._eval_pair(pair)
70 for qubit in pair.qubits:
71 qubit.destroy()
72 pair.destroy()
73 self.start()
74
75 def start(self):
76 """Start the event chain.
77
78 This needs to be called once to schedule the initial events.
79 """
80 initial_event_A = self.source_A.schedule_event()
81 initial_event_A.add_callback(self._check_for_swapping_A)
82 initial_event_B = self.source_B.schedule_event()
83 initial_event_B.add_callback(self._check_for_swapping_B)
84
85 def check(self, message=None):
86 pass
87
88
89def run(length, max_iter, params):
90 C = params["COMMUNICATION_SPEED"]
91 P_LINK = params["P_LINK"]
92 T_DP = params["T_DP"]
93 LAMBDA_BSM = params["LAMBDA_BSM"]
94 L_ATT = 22e3 # attenuation length
95
96 # define functions for link generation behavior
97 def state_generation(source):
98 # this returns the density matrix of a successful trial
99 # this particular function already assumes some things that are only
100 # appropriate for this particular setup, e.g. the source is at one
101 # of the stations and the end stations do not decohere
102 state = mat.phiplus @ mat.H(mat.phiplus)
103 comm_distance = max(
104 distance(source, source.target_stations[0]),
105 distance(source, source.target_stations[1]),
106 )
107 storage_time = 2 * comm_distance / C
108 for idx, station in enumerate(source.target_stations):
109 if station.memory_noise is not None:
110 state = station.memory_noise.apply_to(
111 rho=state, qubit_indices=[idx], t=storage_time
112 )
113 return state
114
115 def time_distribution(source):
116 comm_distance = max(
117 distance(source, source.target_stations[0]),
118 distance(source, source.target_stations[1]),
119 )
120 trial_time = 2 * comm_distance / C
121 eta = P_LINK * np.exp(-comm_distance / L_ATT)
122 num_trials = np.random.geometric(eta)
123 time_taken = num_trials * trial_time
124 return time_taken
125
126 def BSM_error_func(rho):
127 return LAMBDA_BSM * rho + (1 - LAMBDA_BSM) * mat.I(4) / 4
128
129 BSM_noise_channel = NoiseChannel(n_qubits=2, channel_function=BSM_error_func)
130 BSM_noise_model = NoiseModel(channel_before=BSM_noise_channel)
131
132 # perform the world setup
133 world = World()
134 station_A = Station(world=world, position=0)
135 station_central = Station(
136 world=world,
137 position=length / 2,
138 memory_noise=construct_dephasing_noise_channel(T_DP),
139 BSM_noise_model=BSM_noise_model,
140 )
141 station_B = Station(world=world, position=length)
142 source_A = SchedulingSource(
143 world=world,
144 position=station_central.position,
145 target_stations=[station_A, station_central],
146 time_distribution=time_distribution,
147 state_generation=state_generation,
148 )
149 source_B = SchedulingSource(
150 world=world,
151 position=station_central.position,
152 target_stations=[station_central, station_B],
153 time_distribution=time_distribution,
154 state_generation=state_generation,
155 )
156 protocol = CallbackProtocol()
157 protocol.setup(world=world, communication_speed=C)
158 protocol.start()
159
160 while len(protocol.time_list) < max_iter:
161 world.event_queue.resolve_next_event()
162 return protocol
163
164
165if __name__ == "__main__":
166 params = {
167 "P_LINK": 0.80, # link generation probability
168 "T_DP": 100e-3, # dephasing time
169 "LAMBDA_BSM": 0.99, # Bell-State-Measurement ideality parameter
170 "COMMUNICATION_SPEED": 2e8, # speed of light in optical fibre
171 }
172 length_list = np.linspace(20e3, 200e3, num=8)
173 max_iter = 1000
174 raw_data = [
175 run(length=length, max_iter=max_iter, params=params).data
176 for length in length_list
177 ]
178 result_list = [standard_bipartite_evaluation(data_frame=df) for df in raw_data]
179 results = pd.DataFrame(
180 data=result_list,
181 index=length_list,
182 columns=[
183 "raw_rate",
184 "fidelity",
185 "fidelity_std_err",
186 "key_per_time",
187 "key_per_time_std_err",
188 ],
189 )
190 print(results)
191 # plotting key_per_time vs. length is usually what you want to do with these