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