diff --git a/sequence/components/bsm.py b/sequence/components/bsm.py index d2694a82..e8d06c17 100644 --- a/sequence/components/bsm.py +++ b/sequence/components/bsm.py @@ -606,3 +606,101 @@ def trigger(self, detector: Detector, info: Dict[str, Any]): res = detector_num info = {'entity': 'BSM', 'info_type': 'BSM_res', 'res': res, 'time': time} self.notify(info) + + + +class SingleHeraldedBSM(BSM): + """Class modeling an abstract/simplified BSM device for single-heralded entanglement generation protocols. + + We assume that in the single-heralded entanglement generation protocols, + two memories each emit one photon entangled with memory state, + EG is successful only if both photons arrive at the BSM, + and conditioned on both arrivals there is 1/2 probability (assuming linear optics) + that the BSM can give distinguishable output, + in the end whether successful EG is heralded still depends on detection (efficiency / dark counts). + + In this relatively simplified model, we do not perform explicit measurement and communicate explicit outcome, + but assume that local correction based on classical feedforward is a ``free'' operation, + and successfully generated EPR pair is in Phi+ form. + This is to be aligned with analytical formulae, and note that the 4 BDS elements are in I, Z, X, Y order. + The device manages entanglement of associated memories. + + Attributes: + name (str): label for BSM instance. + timeline (Timeline): timeline for simulation. + detectors (List[Detector]): list of attached photon detection devices. + resolution (int): maximum time resolution achievable with attached detectors. + """ + + def __init__(self, name, timeline, phase_error=0, detectors=None, success_rate: float = 0.5): + """Constructor for the single atom BSM class. + + Args: + name (str): name of the BSM instance. + timeline (Timeline): simulation timeline. + phase_error (float): phase error applied to polarization qubits (unused) (default 0). + detectors (List[Dict]): list of parameters for attached detectors, in dictionary format; must be of length 2 + (default is None for default parameters). + """ + + if detectors is None: + detectors = [{}, {}] + super().__init__(name, timeline, phase_error, detectors) + self.encoding = "single_heralded" + assert len(self.detectors) == 2 + self.success_rate = success_rate + + def get(self, photon, **kwargs): + """See base class. + + This method adds additional side effects not present in the base class. + This implementation specifically is based on expectation that if both photons arrive at the BSM simultaneously, + they will trigger both detectors simultaneously as well, if both succeed given detector efficiency, + and then we can record both detection events in bsm_res of entanglement generation protocol, + when update_memory is invoked at future_start_time both detector triggers should have been recorded. + + Side Effects: + May call get method of one or more attached detector(s). + May alter the quantum state of memories corresponding to the photons. + """ + + super().get(photon) + log.logger.debug(self.name + " received photon") + + # assumed simultaneous arrival of both photons + if len(self.photons) == 2: + # at most 1/2 probability of success according to LO assumption + if self.get_generator().random() > self.success_rate: + log.logger.debug(f'{self.name}: photonic BSM failed') + else: + p0, p1 = self.photons + # if both memory successfully emit the photon in this round (consider memory emission inefficiency) + if self.get_generator().random() > p0.loss and self.get_generator().random() > p1.loss: + for idx, photon in enumerate(self.photons): + detector = self.detectors[idx] + detector.get(photon) + else: + log.logger.debug(f'{self.name}: photon lost (memory or optical fiber)') + + def trigger(self, detector: Detector, info: Dict[str, Any]): + """See base class. + + This method adds additional side effects not present in the base class. + + We assume that the single-heralded EG requires both incoming photons be detected, + thus two detector triggers are needed. + We will thus store the first trigger and see if there will be a second trigger. + Only when a trigger happens and there has been a trigger existing do we notify (bsm_update) the EG protocol. + TODO: verify that in this way we can record if dark count has happened. + + Side Effects: + May send a further message to any attached entities. + """ + + detector_num = self.detectors.index(detector) + time = info["time"] + + res = detector_num + info = {'entity': 'BSM', 'info_type': 'BSM_res', 'res': res, 'time': time} + self.notify(info) + diff --git a/sequence/components/memory.py b/sequence/components/memory.py index 50d0a40f..4c2b8743 100644 --- a/sequence/components/memory.py +++ b/sequence/components/memory.py @@ -8,7 +8,7 @@ from copy import copy from math import inf from typing import Any, List, TYPE_CHECKING, Dict, Callable, Union - +from numpy import exp, array from scipy import stats if TYPE_CHECKING: @@ -19,7 +19,9 @@ from ..kernel.entity import Entity from ..kernel.event import Event from ..kernel.process import Process -from ..utils.encoding import single_atom +from ..utils.encoding import single_atom, single_heralded +from ..constants import EPSILON +from ..utils import log def const(t): @@ -40,7 +42,8 @@ class MemoryArray(Entity): """ def __init__(self, name: str, timeline: "Timeline", num_memories=10, - fidelity=0.85, frequency=80e6, efficiency=1, coherence_time=-1, wavelength=500): + fidelity=0.85, frequency=80e6, efficiency=1, coherence_time=-1, wavelength=500, + decoherence_errors: List[float] = None, cutoff_ratio = 1): """Constructor for the Memory Array class. Args: @@ -52,6 +55,8 @@ def __init__(self, name: str, timeline: "Timeline", num_memories=10, efficiency (float): efficiency of memories (default 1). coherence_time (float): average time (in s) that memory state is valid (default -1 -> inf). wavelength (int): wavelength (in nm) of photons emitted by memories (default 500). + decoherence_errors (List[int]): pauli decoherence errors. Passed to memory object. + cutoff_ratio (float): the ratio between cutoff time and memory coherence time (default 1, should be between 0 and 1). """ Entity.__init__(self, name, timeline) @@ -61,7 +66,7 @@ def __init__(self, name: str, timeline: "Timeline", num_memories=10, for i in range(num_memories): memory_name = self.name + f"[{i}]" self.memory_name_to_index[memory_name] = i - memory = Memory(memory_name, timeline, fidelity, frequency, efficiency, coherence_time, wavelength) + memory = Memory(memory_name, timeline, fidelity, frequency, efficiency, coherence_time, wavelength, decoherence_errors, cutoff_ratio) memory.attach(self) self.memories.append(memory) memory.set_memory_array(self) @@ -107,6 +112,27 @@ def get_memory_by_name(self, name: str) -> "Memory": return self.memories[index] +# define helper functions for analytical BDS decoherence implementation, reference see recurrence protocol paper +def _p_id(x_rate, y_rate, z_rate, t): + val = (1 + exp(-2*(x_rate+y_rate)*t) + exp(-2*(x_rate+z_rate)*t) + exp(-2*(z_rate+y_rate)*t)) / 4 + return val + + +def _p_xerr(x_rate, y_rate, z_rate, t): + val = (1 - exp(-2*(x_rate+y_rate)*t) - exp(-2*(x_rate+z_rate)*t) + exp(-2*(z_rate+y_rate)*t)) / 4 + return val + + +def _p_yerr(x_rate, y_rate, z_rate, t): + val = (1 - exp(-2*(x_rate+y_rate)*t) + exp(-2*(x_rate+z_rate)*t) - exp(-2*(z_rate+y_rate)*t)) / 4 + return val + + +def _p_zerr(x_rate, y_rate, z_rate, t): + val = (1 + exp(-2*(x_rate+y_rate)*t) - exp(-2*(x_rate+z_rate)*t) - exp(-2*(z_rate+y_rate)*t)) / 4 + return val + + class Memory(Entity): """Individual single-atom memory. @@ -116,7 +142,8 @@ class Memory(Entity): Attributes: name (str): label for memory instance. timeline (Timeline): timeline for simulation. - fidelity (float): (current) fidelity of memory. + fidelity (float): (current) fidelity of memory. + raw_fidelity (float): (initial) fidelity of memory. frequency (float): maximum frequency at which memory can be excited. efficiency (float): probability of emitting a photon when excited. coherence_time (float): average usable lifetime of memory (in seconds). Negative value means infinite coherence time. @@ -124,20 +151,35 @@ class Memory(Entity): qstate_key (int): key for associated quantum state in timeline's quantum manager. memory_array (MemoryArray): memory array aggregating current memory. entangled_memory (Dict[str, Any]): tracks entanglement state of memory. + docoherence_errors (List[float]): assumeing the memory (qubit) decoherence channel being Pauli channel, + Probability distribution of X, Y, Z Pauli errors; + (default value is -1, meaning not using BDS or further density matrix representation) + Question: is it general enough? Dephasing/damping channel, multipartite entanglement? + cutoff_ratio (float): ratio between cutoff time and memory coherence time (default 1, should be between 0 and 1). + generation_time (float): time when the EPR is first generated (float or int depends on timeing unit) + (default -1 before generation or not used). Used only for logging + last_update_time (float): last time when the EPR pair is updated (usually when decoherence channel applied), + used to determine decoherence channel (default -1 before generation or not used) + is_in_application (bool): whether the quantum memory is involved in application after successful distribution of EPR pair """ def __init__(self, name: str, timeline: "Timeline", fidelity: float, frequency: float, - efficiency: float, coherence_time: float, wavelength: int): + efficiency: float, coherence_time: float, wavelength: int, decoherence_errors: List[float] = None, cutoff_ratio: float = 1): """Constructor for the Memory class. Args: name (str): name of the memory instance. timeline (Timeline): simulation timeline. - fidelity (float): fidelity of memory. + fidelity (float): initial fidelity of memory. frequency (float): maximum frequency of excitation for memory. efficiency (float): efficiency of memories. coherence_time (float): average time (in s) that memory state is valid. + decoherence_rate (float): rate of decoherence to implement time dependent decoherence. wavelength (int): wavelength (in nm) of photons emitted by memories. + decoherence_errors (List[float]): assuming the memory (qubit) decoherence channel being Pauli channel, + probability distribution of X, Y, Z Pauli errors + (default value is None, meaning not using BDS or further density matrix representation) + cutoff_ratio (float): the ratio between cutoff time and memory coherence time (default 1, should be between 0 and 1). """ super().__init__(name, timeline) @@ -149,14 +191,28 @@ def __init__(self, name: str, timeline: "Timeline", fidelity: float, frequency: self.frequency = frequency self.efficiency = efficiency self.coherence_time = coherence_time # coherence time in seconds + self.decoherence_rate = 1 / self.coherence_time # rate of decoherence to implement time dependent decoherence self.wavelength = wavelength self.qstate_key = timeline.quantum_manager.new() self.memory_array = None + self.decoherence_errors = decoherence_errors + if self.decoherence_errors is not None: + assert len(self.decoherence_errors) == 3 and abs(sum(self.decoherence_errors) - 1) < EPSILON, \ + "Decoherence errors refer to probabilities for each Pauli error to happen if an error happens, thus should be normalized." + self.cutoff_ratio = cutoff_ratio + assert 0 < self.cutoff_ratio <= 1, "Ratio of cutoff time and coherence time should be between 0 and 1" + self.generation_time = -1 + self.last_update_time = -1 + self.is_in_application = False + # for photons self.encoding = copy(single_atom) self.encoding["raw_fidelity"] = self.raw_fidelity + # for photons in general single-heralded EG protocols + self.encoding_sh = copy(single_heralded) + # keep track of previous BSM result (for entanglement generation) # -1 = no result, 0/1 give detector number self.previous_bsm = -1 @@ -176,13 +232,14 @@ def init(self): def set_memory_array(self, memory_array: MemoryArray): self.memory_array = memory_array - def excite(self, dst="") -> None: + def excite(self, dst="", protocol="bk") -> None: """Method to excite memory and potentially emit a photon. If it is possible to emit a photon, the photon may be marked as null based on the state of the memory. Args: dst (str): name of destination node for emitted photon (default ""). + protocol (str): Valid values are "bk" (for Barrett-Kok protocol) or "sh" (for single heralded) Side Effects: May modify quantum state of memory. @@ -194,8 +251,18 @@ def excite(self, dst="") -> None: return # create photon - photon = Photon("", self.timeline, wavelength=self.wavelength, location=self.name, encoding_type=self.encoding, - quantum_state=self.qstate_key, use_qm=True) + if protocol == "bk": + photon = Photon("", self.timeline, wavelength=self.wavelength, location=self.name, encoding_type=self.encoding, + quantum_state=self.qstate_key, use_qm=True) + elif protocol == "sh": + photon = Photon("", self.timeline, wavelength=self.wavelength, location=self.name, encoding_type=self.encoding_sh, + quantum_state=self.qstate_key, use_qm=True) + # keep track of memory initialization time + self.generation_time = self.timeline.now() + self.last_update_time = self.timeline.now() + else: + raise ValueError("Invalid protocol type {} specified for meomory.exite()".format(protocol)) + photon.timeline = None # facilitate cross-process exchange of photons photon.is_null = True photon.add_loss(1 - self.efficiency) @@ -213,17 +280,26 @@ def expire(self) -> None: Is scheduled automatically by the `set_plus` memory operation. + If the quantum memory has been explicitly involved in application after entanglement distribution, do not expire. + Some simplified applications do not necessarily need to modify the is_in_application attribute. + Some more complicated applications, such as probe state preparation for distributed quantum sensing, + may change is_in_application attribute to keep memory from expiring during study. + Side Effects: Will notify upper entities of expiration via the `pop` interface. Will modify the quantum state of the memory. """ - if self.excited_photon: - self.excited_photon.is_null = True + if self.is_in_application: + pass - self.reset() - # pop expiration message - self.notify(self) + else: + if self.excited_photon: + self.excited_photon.is_null = True + + self.reset() + # pop expiration message + self.notify(self) def reset(self) -> None: """Method to clear quantum memory. @@ -235,6 +311,8 @@ def reset(self) -> None: """ self.fidelity = 0 + self.generation_time = -1 + self.last_update_time = -1 self.timeline.quantum_manager.set([self.qstate_key], [complex(1), complex(0)]) self.entangled_memory = {'node_id': None, 'memo_id': None} @@ -261,11 +339,55 @@ def update_state(self, state: List[complex]) -> None: if self.coherence_time > 0: self._schedule_expiration() + def bds_decohere(self) -> None: + """Method to decohere stored BDS in quantum memory according to the single-qubit Pauli channels. + + During entanglement distribution (before application phase), + BDS decoherence can be treated analytically (see entanglement purification paper for explicit formulae). + + Side Effects: + Will modify BDS diagonal elements and last_update_time. + """ + + if self.decoherence_errors is None: + # if not considering time-dependent decoherence then do nothing + pass + + else: + time = (self.timeline.now() - self.last_update_time) * 1e-12 # duration of memory idling (in s) + if time > 0 and self.last_update_time > 0: # time > 0 means time has progressed, self.last_update_time > 0 means the memory has not been reset + + x_rate, y_rate, z_rate = self.decoherence_rate * self.decoherence_errors[0], \ + self.decoherence_rate * self.decoherence_errors[1], \ + self.decoherence_rate * self.decoherence_errors[2] + p_I, p_X, p_Y, p_Z = _p_id(x_rate, y_rate, z_rate, time), \ + _p_xerr(x_rate, y_rate, z_rate, time), \ + _p_yerr(x_rate, y_rate, z_rate, time), \ + _p_zerr(x_rate, y_rate, z_rate, time) + + state_now = self.timeline.quantum_manager.states[self.qstate_key].state # current diagonal elements + transform_mtx = array([[p_I, p_Z, p_X, p_Y], + [p_Z, p_I, p_Y, p_X], + [p_X, p_Y, p_I, p_Z], + [p_Y, p_X, p_Z, p_I]]) # transform matrix for diagonal elements + state_new = transform_mtx @ state_now # new diagonal elements after decoherence transformation + + log.logger.debug(f'{self.name}: before f={state_now[0]:.6f}, after f={state_new[0]:.6f}') + + # update the quantum state stored in quantum manager for self and entangled memory + keys = self.timeline.quantum_manager.states[self.qstate_key].keys + self.timeline.quantum_manager.set(keys, state_new) + + # update the last_update_time of self + # note that the attr of entangled memory should not be updated right now, + # because decoherence has not been applied there + self.last_update_time = self.timeline.now() + def _schedule_expiration(self) -> None: if self.expiration_event is not None: self.timeline.remove_event(self.expiration_event) - decay_time = self.timeline.now() + int(self.coherence_time * 1e12) + decay_time = self.timeline.now() + int(self.cutoff_ratio * self.coherence_time * 1e12) process = Process(self, "expire", []) event = Event(decay_time, process) self.timeline.schedule(event) @@ -301,6 +423,26 @@ def detach(self, observer: 'EntanglementProtocol'): # observer could be a Memor if observer in self._observers: self._observers.remove(observer) + def get_bds_state(self): + """Method to get state of memory in BDS formalism. + + Will automatically call the `bds_decohere` method. + """ + self.bds_decohere() + state_obj = self.timeline.quantum_manager.get(self.qstate_key) + state = state_obj.state + return state + + def get_bds_fidelity(self) -> float: + """Will get the fidelity from the BDS state + + Return: + (float): the fidelity of the BDS state + """ + state_obj = self.timeline.quantum_manager.get(self.qstate_key) + state = state_obj.state + return state[0] + class AbsorptiveMemory(Entity): """Atomic ensemble absorptive memory. diff --git a/sequence/components/optical_channel.py b/sequence/components/optical_channel.py index 95c41e30..003f4ab2 100644 --- a/sequence/components/optical_channel.py +++ b/sequence/components/optical_channel.py @@ -18,7 +18,7 @@ from ..kernel.event import Event from ..kernel.process import Process from ..utils import log -from ..constants import SPEED_OF_LIGHT +from ..constants import SPEED_OF_LIGHT, MICROSECOND class OpticalChannel(Entity): @@ -243,7 +243,7 @@ def __init__(self, name: str, timeline: "Timeline", distance: int, delay=-1): super().__init__(name, timeline, 0, distance, 0, SPEED_OF_LIGHT) if delay == -1: - self.delay = distance / self.light_speed + self.delay = distance / self.light_speed + 10*MICROSECOND else: self.delay = delay diff --git a/sequence/components/photon.py b/sequence/components/photon.py index 00f483ba..00277b42 100644 --- a/sequence/components/photon.py +++ b/sequence/components/photon.py @@ -177,5 +177,5 @@ def measure_multiple(basis, photons: List["Photon"], rng: "Generator"): def add_loss(self, loss: float): assert 0 <= loss <= 1 - assert self.encoding_type["name"] == "single_atom" + assert self.encoding_type.get("keep_photon", False) self.loss = 1 - (1 - self.loss) * (1 - loss) diff --git a/sequence/kernel/quantum_manager.py b/sequence/kernel/quantum_manager.py index 54249321..c81e2019 100644 --- a/sequence/kernel/quantum_manager.py +++ b/sequence/kernel/quantum_manager.py @@ -21,12 +21,13 @@ from scipy.sparse import csr_matrix from scipy.special import binom -from .quantum_state import KetState, DensityState +from .quantum_state import KetState, DensityState, BellDiagonalState from .quantum_utils import * KET_STATE_FORMALISM = "ket_vector" DENSITY_MATRIX_FORMALISM = "density_matrix" FOCK_DENSITY_MATRIX_FORMALISM = "fock_density" +BELL_DIAGONAL_STATE_FORMALISM = "bell_diagonal" class QuantumManager: @@ -703,3 +704,57 @@ def add_loss(self, key, loss_rate): output_state += kraus_op @ prepared_state @ kraus_op.conj().T self.set(all_keys, output_state) + + +class QuantumManagerBellDiagonal(QuantumManager): + """Class to track and manage quantum states with the bell diagonal formalism. + + To be aligned with analytical formulae, we have assumed that successfully generated EPR pair is in Phi+ form. + And note that the 4 BDS elements are in I, Z, X, Y order. + + * BDS is only used for entanglement distribution (generation, swapping, purification), assuming underlying errors being purely Pauli. + * All manipulation results can be tracked analytically, without explicit quantum gates / channels / measurements. + """ + + def __init__(self): + super().__init__(BELL_DIAGONAL_STATE_FORMALISM) + + def new(self, state=None) -> int: + """Generates new quantum state key for quantum manager. + + NOTE: since this generates only one state, there will be no corresponding entangled state stored. + The Bell diagonal state formalism assumes entangled states; + thus, attempting to call `get` will return an exception until entangled. + The purpose of this function is thus mainly to avoid state key collisions. + + Args: + state (Any): to conform to type definition (does nothing). + + Returns: + int: quantum state key corresponding to state. + """ + key = self._least_available + self._least_available += 1 + return key + + def get(self, key: int): + if key not in self.states: + raise Exception("Attempt to get Bell diagonal state before entanglement.") + + return super().get(key) + + def set(self, keys: List[int], diag_elems: List[float]) -> None: + super().set(keys, diag_elems) + # assert len(keys) == 2, "Bell diagonal states must have 2 keys." + if len(keys) != 2: + #raise Warning("bell diagonal quantum manager received invalid set request") # optional + for key in keys: + if key in self.states: + self.states.pop(key) + return + new_state = BellDiagonalState(diag_elems, keys) + for key in keys: + self.states[key] = new_state + + def set_to_noiseless(self, keys: List[int]): + self.set(keys, [float(1), float(0), float(0), float(0)]) diff --git a/sequence/kernel/quantum_state.py b/sequence/kernel/quantum_state.py index 9893ca86..81387e4f 100644 --- a/sequence/kernel/quantum_state.py +++ b/sequence/kernel/quantum_state.py @@ -378,3 +378,35 @@ def measure_multiple(basis, states, rng: Generator): state.entangled_photons = entangled_list return res + + +class BellDiagonalState(State): + """Class to represent a 2-qubit EPR pair as Bell diagonal state. + + Has 4 diagonal elements of density matrix in Bell basis. + + Attributes: + state (np.array): diagonal elements of 2-qubit density matrix in Bell bases. Should be of length 4. + keys (List[int]): list of keys (subsystems) associated with this state. Should be length 2. + """ + + def __init__(self, diag_elems: List[float], keys: List[int]): + """Constructor for Bell diagonal state class. + + Args: + diag_elems (List[float]): 4 diagonal elements of 2-qubit density matrix in Bell bases. + Default order: Phi+, Phi-, Psi+, Psi- (i.e. I, Z, X, Y errors). + keys (List[int]): list of keys to this state in quantum manager. Should be length 2. + """ + super().__init__() + + # check formatting + assert all([elem <= 1.001 and elem >= 0 for elem in diag_elems]), \ + "Illegal value with elem > 1 or elem < 0 in density matrix diagonal elements" + assert abs(sum([elem for elem in diag_elems]) - 1) < 1e-5, \ + "Density matrix diagonal elements do not sum to 1" + assert len(keys) == 2, "BellDiagonalState density matrix are only supported for 2-qubit entangled states." + + # note: density matrix diagonal elements are guaranteed to be real from Hermiticity + self.state = array(diag_elems, dtype=float) + self.keys = keys diff --git a/sequence/kernel/timeline.py b/sequence/kernel/timeline.py index c5e0ce4d..d86e4d25 100644 --- a/sequence/kernel/timeline.py +++ b/sequence/kernel/timeline.py @@ -22,9 +22,11 @@ from .quantum_manager import (QuantumManagerKet, QuantumManagerDensity, QuantumManagerDensityFock, + QuantumManagerBellDiagonal, KET_STATE_FORMALISM, DENSITY_MATRIX_FORMALISM, - FOCK_DENSITY_MATRIX_FORMALISM) + FOCK_DENSITY_MATRIX_FORMALISM, + BELL_DIAGONAL_STATE_FORMALISM) from ..constants import * @@ -70,13 +72,23 @@ def __init__(self, stop_time=inf, formalism=KET_STATE_FORMALISM, truncation=1): self.run_counter: int = 0 self.is_running: bool = False self.show_progress: bool = False - + self.set_quantum_manager(formalism, truncation) + + def set_quantum_manager(self, formalism: str, truncation: int = 1) -> None: + """Update the formalism + + Args: + formalism (str): the formalism. + truncation (int): truncation of Hilbert space (currently only for Fock representation). + """ if formalism == KET_STATE_FORMALISM: self.quantum_manager = QuantumManagerKet() elif formalism == DENSITY_MATRIX_FORMALISM: self.quantum_manager = QuantumManagerDensity() elif formalism == FOCK_DENSITY_MATRIX_FORMALISM: self.quantum_manager = QuantumManagerDensityFock(truncation=truncation) + elif formalism == BELL_DIAGONAL_STATE_FORMALISM: + self.quantum_manager = QuantumManagerBellDiagonal() else: raise ValueError(f"Invalid formalism {formalism}") diff --git a/sequence/network_management/reservation.py b/sequence/network_management/reservation.py index f51381bb..08bf50f8 100644 --- a/sequence/network_management/reservation.py +++ b/sequence/network_management/reservation.py @@ -93,6 +93,7 @@ def eg_rule_action2(memories_info: List["MemoryInfo"], args: Arguments) -> Tuple def eg_req_func(protocols: List["EntanglementProtocol"], args: Arguments) -> EntanglementGenerationA: """Function used by `eg_rule_action2` function for selecting generation protocols on the remote node + Args: protocols: the waiting protocols (wait for request) args: arguments from the node who sent the request @@ -143,6 +144,13 @@ def ep_rule_action2(memories_info: List["MemoryInfo"], args: Arguments) -> Tuple def ep_req_func1(protocols, args: Arguments) -> BBPSSW: """Function used by `ep_rule_action1` for selecting purification protocols on the remote node + Will 'combine two BBPSSW into one BBPSSW' + + Args: + protocols (list): a list of waiting protocols + args (dict): the arguments + Return: + the selected protocol """ remote0 = args["remote0"] remote1 = args["remote1"] @@ -176,11 +184,11 @@ def ep_rule_condition1(memory_info: "MemoryInfo", memory_manager: "MemoryManager """ memory_indices = args["memory_indices"] reservation = args["reservation"] - if (memory_info.index in memory_indices + if (memory_info.index in memory_indices # this memory (kept) and memory_info.state == "ENTANGLED" and memory_info.fidelity < reservation.fidelity): for info in memory_manager: - if (info != memory_info and info.index in memory_indices + if (info != memory_info and info.index in memory_indices # another memory (meas) and info.state == "ENTANGLED" and info.remote_node == memory_info.remote_node and info.fidelity == memory_info.fidelity): @@ -227,6 +235,12 @@ def es_rule_actionB(memories_info: List["MemoryInfo"], args: Arguments) -> Tuple def es_req_func(protocols: List["EntanglementProtocol"], args: Arguments) -> EntanglementSwappingB: """Function used by `es_rule_actionA` for selecting swapping protocols on the remote node + + Args: + protocols (list): a list of waiting protocols + args (dict): the arguments + Return: + the selected protocol """ target_memo = args["target_memo"] for protocol in protocols: diff --git a/sequence/resource_management/memory_manager.py b/sequence/resource_management/memory_manager.py index 0aeadae9..0f3e8c2f 100644 --- a/sequence/resource_management/memory_manager.py +++ b/sequence/resource_management/memory_manager.py @@ -126,6 +126,9 @@ def __init__(self, memory: "Memory", index: int, state="RAW"): self.expire_event = None self.entangle_time = -1 + def __str__(self) -> str: + return f'name={self.memory.name}, remote={self.remote_memo}, fidelity={self.fidelity:.6f}' + def to_raw(self) -> None: """Method to set memory to raw (unentangled) state.""" diff --git a/sequence/resource_management/rule_manager.py b/sequence/resource_management/rule_manager.py index 1c91ffc3..1af39d0e 100644 --- a/sequence/resource_management/rule_manager.py +++ b/sequence/resource_management/rule_manager.py @@ -122,8 +122,7 @@ class Rule: reservation (Reservation): associated reservation. """ - def __init__(self, priority: int, action: ActionFunc, condition: ConditionFunc, - action_args: Arguments, condition_args: Arguments): + def __init__(self, priority: int, action: ActionFunc, condition: ConditionFunc, action_args: Arguments, condition_args: Arguments): """Constructor for rule class.""" self.priority: int = priority diff --git a/sequence/topology/node.py b/sequence/topology/node.py index afc7de0d..71c43b1d 100644 --- a/sequence/topology/node.py +++ b/sequence/topology/node.py @@ -22,7 +22,7 @@ from ..kernel.entity import Entity from ..components.memory import MemoryArray -from ..components.bsm import SingleAtomBSM +from ..components.bsm import SingleAtomBSM, SingleHeraldedBSM from ..components.light_source import LightSource from ..components.detector import QSDetector, QSDetectorPolarization, QSDetectorTimeBin from ..qkd.BB84 import BB84 @@ -48,9 +48,11 @@ class Node(Entity): generator (np.random.Generator): random number generator used by node. components (Dict[str, Entity]): mapping of local component names to objects. first_component_name (str): name of component that first receives incoming qubits. + gate_fid (float): fidelity of multi-qubit gates (usually CNOT) that can be performed on the node. + meas_fid (float): fidelity of single-qubit measurements (usually Z measurement) that can be performed on the node. """ - def __init__(self, name: str, timeline: "Timeline", seed=None, component_templates=None): + def __init__(self, name: str, timeline: "Timeline", seed=None, gate_fid: float = 1, meas_fid: float = 1): """Constructor for node. name (str): name of node instance. @@ -68,6 +70,12 @@ def __init__(self, name: str, timeline: "Timeline", seed=None, component_templat self.components = {} self.first_component_name = None + # note that we are assuming homogeneous gates and measurements, + # i.e. every gate on one specific node has identical fidelity, and so is measurement. + self.gate_fid = gate_fid + self.meas_fid = meas_fid + assert 0 <= gate_fid <= 1 and 0 <= meas_fid <= 1, "Gate fidelity and measurement fidelity must be between 0 and 1." + def init(self) -> None: pass @@ -207,10 +215,19 @@ def __init__(self, name: str, timeline: "Timeline", other_nodes: List[str], if not component_templates: component_templates = {} + self.encoding_type = component_templates.get('encoding_type', 'single_atom') + # create BSM object with optional args bsm_name = name + ".BSM" - bsm_args = component_templates.get("SingleAtomBSM", {}) - bsm = SingleAtomBSM(bsm_name, timeline, **bsm_args) + if self.encoding_type == 'single_atom': + bsm_args = component_templates.get("SingleAtomBSM", {}) + bsm = SingleAtomBSM(bsm_name, timeline, **bsm_args) + elif self.encoding_type == 'single_heralded': + bsm_args = component_templates.get("SingleHeraldedBSM", {}) + bsm = SingleHeraldedBSM(bsm_name, timeline, **bsm_args) + else: + raise ValueError(f'Encoding type {self.encoding_type} not supported') + self.add_component(bsm) self.set_first_component(bsm_name) @@ -252,18 +269,26 @@ class QuantumRouter(Node): network_manager (NetworkManager): network management module. map_to_middle_node (Dict[str, str]): mapping of router names to intermediate bsm node names. app (any): application in use on node. + gate_fid (float): fidelity of multi-qubit gates (usually CNOT) that can be performed on the node. + meas_fid (float): fidelity of single-qubit measurements (usually Z measurement) that can be performed on the node. """ - def __init__(self, name, tl, memo_size=50, seed=None, component_templates=None): + def __init__(self, name, tl, memo_size=50, seed=None, component_templates=None, gate_fid: float = 1, meas_fid: float = 1): """Constructor for quantum router class. Args: name (str): label for node. tl (Timeline): timeline for simulation. memo_size (int): number of memories to add in the array (default 50). + seed (int): the random seed for the random number generator + compoment_templates (dict): parameters for the quantum router + gate_fid (float): fidelity of multi-qubit gates (usually CNOT) that can be performed on the node; + Default value is 1, meaning ideal gate. + meas_fid (float): fidelity of single-qubit measurements (usually Z measurement) that can be performed on the node; + Default value is 1, meaning ideal measurement. """ - super().__init__(name, tl, seed) + super().__init__(name, tl, seed, gate_fid, meas_fid) if not component_templates: component_templates = {} diff --git a/sequence/topology/router_net_topo.py b/sequence/topology/router_net_topo.py index 6ca41af5..60a83871 100644 --- a/sequence/topology/router_net_topo.py +++ b/sequence/topology/router_net_topo.py @@ -6,6 +6,7 @@ from ..kernel.timeline import Timeline from .node import BSMNode, QuantumRouter from ..constants import SPEED_OF_LIGHT +from ..kernel.quantum_manager import BELL_DIAGONAL_STATE_FORMALISM class RouterNetTopo(Topo): @@ -39,6 +40,7 @@ class RouterNetTopo(Topo): def __init__(self, conf_file_name: str): self.bsm_to_router_map = {} + self.encoding_type = None super().__init__(conf_file_name) def _load(self, filename: str): diff --git a/sequence/topology/topology.py b/sequence/topology/topology.py index 004e26cc..ca23634c 100644 --- a/sequence/topology/topology.py +++ b/sequence/topology/topology.py @@ -47,7 +47,10 @@ class Topology(ABC): TYPE = "type" ALL_TEMPLATES = "templates" TEMPLATE = "template" + GATE_FIDELITY = "gate_fidelity" + MEASUREMENT_FIDELITY = "measurement_fidelity" + def __init__(self, conf_file_name: str): """Constructor for topology class. diff --git a/sequence/utils/config_generator.py b/sequence/utils/config_generator.py index 4c867c59..e762991e 100644 --- a/sequence/utils/config_generator.py +++ b/sequence/utils/config_generator.py @@ -27,6 +27,8 @@ def add_default_args(parser): parser.add_argument('-s', '--stop', type=float, default=float('inf'), help='stop time (in s)') parser.add_argument('-p', '--parallel', nargs=4, help='optional parallel arguments: server ip, server port, num. processes, lookahead') parser.add_argument('-n', '--nodes', type=str, help='path to csv file to provide process for each node') + parser.add_argument('-gf', '--gate_fidelity', type=float, help='the fidelity of gate (CNOT)') + parser.add_argument('-mf', '--measurement_fidelity', type=float, help='the fidelity of measurment (Z measurement)') return parser @@ -57,7 +59,7 @@ def generate_node_procs(parallel, net_size, naming_func) -> dict: return node_procs -def generate_nodes(node_procs: dict, router_names: str, memo_size: int, template: str = None) -> list: +def generate_nodes(node_procs: dict, router_names: str, memo_size: int, template: str = None, gate_fidelity: float = None, measurement_fidelity: float = None) -> list: """generate a list of node configs""" nodes = [] for i, name in enumerate(router_names): @@ -67,7 +69,11 @@ def generate_nodes(node_procs: dict, router_names: str, memo_size: int, template RouterNetTopo.MEMO_ARRAY_SIZE: memo_size, RouterNetTopo.GROUP: node_procs[name]} if template: - config[RouterNetTopo.TEMPLATE] = template + config[Topology.TEMPLATE] = template + if gate_fidelity: + config[Topology.GATE_FIDELITY] = gate_fidelity + if measurement_fidelity: + config[Topology.MEASUREMENT_FIDELITY] = measurement_fidelity nodes.append(config) return nodes diff --git a/sequence/utils/encoding.py b/sequence/utils/encoding.py index edd33661..98983e5f 100644 --- a/sequence/utils/encoding.py +++ b/sequence/utils/encoding.py @@ -30,7 +30,8 @@ single_atom = \ {"name": "single_atom", "bases": [((complex(1), complex(0)), (complex(0), complex(1))), None], - "raw_fidelity": 1 + "raw_fidelity": 1, + "keep_photon": True } absorptive = \ @@ -42,3 +43,9 @@ {"name": "fock", "bases": None } + +single_heralded = \ + {"name": "single_heralded", + "bases": None, + "keep_photon": True + } diff --git a/sequence/utils/log.py b/sequence/utils/log.py index 04ef5e4f..33d9f272 100644 --- a/sequence/utils/log.py +++ b/sequence/utils/log.py @@ -20,7 +20,7 @@ def _init_logger(): return lg -logger = _init_logger() +logger = _init_logger() # global logger # LOG_FORMAT = '{asctime} {simtime:<20,} {levelname:7} {module:20} {message}' LOG_FORMAT = '{simtime:<20,} {levelname:7} {module:20} {message}' # no asctime _log_modules = [] @@ -40,6 +40,10 @@ def set_logger(name: str, timeline, logfile="out.log"): global logger logger = logging.getLogger(name) + # remove existing handleers + for h in logger.handlers: + logger.removeHandler(h) + handler = logging.FileHandler(logfile) fmt = logging.Formatter(LOG_FORMAT, style='{') f = ContextFilter(timeline) diff --git a/tests/components/test_photon.py b/tests/components/test_photon.py index 3b5aaad8..f564f398 100644 --- a/tests/components/test_photon.py +++ b/tests/components/test_photon.py @@ -3,6 +3,7 @@ from sequence.kernel.timeline import Timeline from sequence.components.photon import Photon +from sequence.utils.encoding import single_atom rng = np.random.default_rng() @@ -81,7 +82,7 @@ def test_measure_multiple(): def test_add_loss(): tl = Timeline() - photon = Photon("", tl, encoding_type={"name": "single_atom"}) + photon = Photon("", tl, encoding_type=single_atom) assert photon.loss == 0 photon.add_loss(0.5)