Repository: sequence-toolbox/SeQUeNCe (language: python)

Issue:
Hardware Standard
Update hardware interfaces in sequence. To be shortly followed up by memory updates.


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 909900f9..3cdf16c8 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,11 @@ SeQUeNCe is an open source, discrete-event simulator for quantum networks. As de
 These modules can be edited by users to define additional functionality and test protocol schemes, or may be used as-is to test network parameters and topologies.
 
 ## Installing
-SeQUeNCe requires an installation of Python 3.8 or later. This can be found at the [Python Website](https://www.python.org/downloads/). Then, simply download the package, navigate to its directory, and install with
+SeQUeNCe requires an installation of Python 3.8 or later. This can be found at the [Python Website](https://www.python.org/downloads/).
+The `mpi4py` package additionally requires an MPI installation on the local machine.
+For a few options listed by the MPI for python package, please see [this documentation page](https://mpi4py.readthedocs.io/en/stable/appendix.html#building-mpi).
+
+Then, simply download the package, navigate to its directory, and install with
 ```
 $ pip install .
 ```
@@ -48,6 +52,7 @@ Code for the experiments performed in our paper can be found in the file `starli
 The example folder contains several scripts that can be run with jupyter notebook for easy editing and visualization. These files require that the notebook package be installed:
 ```
 $ pip install notebook
+$ pip install ipywidgets
 ```
 To run each file, simply run
 ```
diff --git a/docs/source/references/components/circuit.rst b/docs/source/references/components/circuit.rst
new file mode 100644
index 00000000..f2748c1b
--- /dev/null
+++ b/docs/source/references/components/circuit.rst
@@ -0,0 +1,5 @@
+Circuit
+=======
+
+.. automodule:: src.components.circuit
+    :members:
\ No newline at end of file
diff --git a/docs/source/references/components/fiber_stretcher.rst b/docs/source/references/components/fiber_stretcher.rst
new file mode 100644
index 00000000..ec376707
--- /dev/null
+++ b/docs/source/references/components/fiber_stretcher.rst
@@ -0,0 +1,5 @@
+Fiber Stretcher
+===============
+
+.. automodule:: src.components.fiber_stretcher
+    :members:
\ No newline at end of file
diff --git a/docs/source/references/components/top.rst b/docs/source/references/components/top.rst
index 741a6854..23991992 100644
--- a/docs/source/references/components/top.rst
+++ b/docs/source/references/components/top.rst
@@ -8,7 +8,9 @@ The components module provides models for quantum hardware.
 
     beam_splitter
     bsm
+    circuit
     detector
+    fiber_stretcher
     interferometer
     light_source
     memory
diff --git a/docs/source/references/kernel/quantum_state.rst b/docs/source/references/kernel/quantum_state.rst
new file mode 100644
index 00000000..08a85281
--- /dev/null
+++ b/docs/source/references/kernel/quantum_state.rst
@@ -0,0 +1,5 @@
+Quantum State
+=============
+
+.. automodule:: src.kernel.quantum_state
+    :members:
\ No newline at end of file
diff --git a/docs/source/references/kernel/top.rst b/docs/source/references/kernel/top.rst
index 3be3a11e..a93e16a4 100644
--- a/docs/source/references/kernel/top.rst
+++ b/docs/source/references/kernel/top.rst
@@ -15,3 +15,4 @@ The Kernel module provides the discrete event simulator for SeQUeNCe.
     p_timeline
     quantum_manager_client
     quantum_manager_server
+    quantum_state
diff --git a/docs/source/references/misc/utils/quantum_state.rst b/docs/source/references/misc/utils/quantum_state.rst
deleted file mode 100644
index 9a33fdc5..00000000
--- a/docs/source/references/misc/utils/quantum_state.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Quantum State
-=============
-
-.. automodule:: src.utils.quantum_state
-    :members:
diff --git a/docs/source/references/misc/utils/top.rst b/docs/source/references/misc/utils/top.rst
index 29314e0a..4715a946 100644
--- a/docs/source/references/misc/utils/top.rst
+++ b/docs/source/references/misc/utils/top.rst
@@ -8,4 +8,3 @@ Utilities for SeQUeNCe.
 
     encoding
     log
-    quantum_state
diff --git a/docs/source/tutorial/chapter1/des_example1.py b/docs/source/tutorial/chapter1/des_example1.py
index c1a21a22..bb43241c 100644
--- a/docs/source/tutorial/chapter1/des_example1.py
+++ b/docs/source/tutorial/chapter1/des_example1.py
@@ -2,6 +2,7 @@
 from sequence.kernel.event import Event
 from sequence.kernel.process import Process
 
+
 class Store(object):
     def __init__(self, tl: Timeline):
         self.opening = False
diff --git a/docs/source/tutorial/chapter1/discrete-event-simulation.md b/docs/source/tutorial/chapter1/discrete-event-simulation.md
index 82f9646a..6bbf0eb7 100644
--- a/docs/source/tutorial/chapter1/discrete-event-simulation.md
+++ b/docs/source/tutorial/chapter1/discrete-event-simulation.md
@@ -217,7 +217,7 @@ with the above methods, we can observe the state of store after a specfic time.
 
 ```python
 for t in [15, 32, 52]:
-    tl = Timeline(32)
+    tl = Timeline(t)
     store = Store(tl)
     print(tl.now())
     
diff --git a/docs/source/tutorial/chapter2/example1.py b/docs/source/tutorial/chapter2/example1.py
index e271db61..8fa6e0ae 100644
--- a/docs/source/tutorial/chapter2/example1.py
+++ b/docs/source/tutorial/chapter2/example1.py
@@ -6,18 +6,14 @@
 from sequence.components.memory import Memory
 from sequence.components.optical_channel import QuantumChannel
 from sequence.components.detector import Detector
-from sequence.components.circuit import Circuit
 from sequence.topology.node import Node
 
 
 NUM_TRIALS = 1000
 FREQUENCY = 1e3
 
-_meas_circuit = Circuit(1)
-_meas_circuit.measure(0)
 
-
-class Counter():
+class Counter:
     def __init__(self):
         self.count = 0
 
@@ -25,32 +21,56 @@ def trigger(self, detector, info):
         self.count += 1
 
 
+class Sender:
+    def __init__(self, own, memory_name):
+        self.own = own
+        self.memory = own.components[memory_name]
+
+    def start(self, period):
+        process1 = Process(self.memory, "update_state", [[complex(math.sqrt(1/2)), complex(math.sqrt(1/2))]])
+        process2 = Process(self.memory, "excite", ["node2"])
+        for i in range(NUM_TRIALS):
+            event1 = Event(i * period, process1)
+            event2 = Event(i * period + (period / 2), process2)
+            self.own.timeline.schedule(event1)
+            self.own.timeline.schedule(event2)
+
+
 class SenderNode(Node):
     def __init__(self, name, timeline):
         super().__init__(name, timeline)
-        self.memory = Memory(name + ".memory", tl, fidelity=1, frequency=0,
-                             efficiency=1, coherence_time=0, wavelength=500)
-        self.memory.owner = self
+        memory_name = name + ".memory"
+        memory = Memory(memory_name, timeline, fidelity=1, frequency=0,
+                        efficiency=1, coherence_time=0, wavelength=500)
+        self.add_component(memory)
+        memory.add_receiver(self)
+
+        self.sender = Sender(self, memory_name)
+
+    def get(self, photon, **kwargs):
+        self.send_qubit(kwargs['dst'], photon)
 
 
 class ReceiverNode(Node):
     def __init__(self, name, timeline):
         super().__init__(name, timeline)
-        self.detector = Detector(name + ".detector", tl, efficiency=1)
-        self.detector.owner = self
+
+        detector_name = name + ".detector"
+        detector = Detector(detector_name, timeline, efficiency=1)
+        self.add_component(detector)
+        self.set_first_component(detector_name)
+        detector.owner = self
+
+        self.counter = Counter()
+        detector.attach(self.counter)
 
     def receive_qubit(self, src, qubit):
-        qm = self.timeline.quantum_manager
-        key = qubit.qstate_key
-        meas_res = qm.run_circuit(_meas_circuit, [key], self.get_generator().random())[key]
-        if meas_res:
-            self.detector.get()
+        self.components[self.first_component_name].get(qubit)
 
 
 if __name__ == "__main__":
     runtime = 10e12 
     tl = Timeline(runtime)
-    tl.show_progress = False
 
     # nodes and hardware
     node1 = SenderNode("node1", tl)
@@ -59,23 +79,12 @@ def receive_qubit(self, src, qubit):
     qc = QuantumChannel("qc", tl, attenuation=0, distance=1e3)
     qc.set_ends(node1, node2.name)
 
-    # counter
-    counter = Counter()
-    node2.detector.attach(counter)
+    tl.init()
 
     # schedule events
-    time_bin = int(1e12 / FREQUENCY)
-    
-    process1 = Process(node1.memory, "update_state", [[complex(math.sqrt(1/2)), complex(math.sqrt(1/2))]])
-    process2 = Process(node1.memory, "excite", ["node2"])
-    for i in range(NUM_TRIALS):
-        event1 = Event(i * time_bin, process1)
-        event2 = Event(i * time_bin + (time_bin / 2), process2)
-        tl.schedule(event1)
-        tl.schedule(event2)
+    period = int(1e12 / FREQUENCY)
+    node1.sender.start(period)
 
-    tl.init()
     tl.run()
 
-    print("percent measured: {}%".format(100 * counter.count / NUM_TRIALS))
-
+    print("percent measured: {}%".format(100 * node2.counter.count / NUM_TRIALS))
diff --git a/docs/source/tutorial/chapter2/hardware.md b/docs/source/tutorial/chapter2/hardware.md
index 0ffbc5ba..9e342737 100644
--- a/docs/source/tutorial/chapter2/hardware.md
+++ b/docs/source/tutorial/chapter2/hardware.md
@@ -5,7 +5,7 @@ In this tutorial we will show some of the tools available in the hardware module
 * Gain familiarity with the sequence `topology.node` module
 * See how networks may be built with sequence
 
-To achieve this, we will construct a couple simple networks and show their functionality. We will also create a custom “protocol” to measure detection events as well as custom node types to hold all of our hardware.
+To achieve this, we will construct a couple simple networks and show their functionality. We will also create custom “protocols” to control and monitor hardware, as well as custom node types to hold all of our components.
 
 
 ## Example: Optical Hardware
@@ -23,7 +23,7 @@ The hardware used in this tutorial is a single atom quantum memory and a single
 
 To begin, we create our custom node classes. We will make two types - a `SenderNode` to hold the memory and send photons and a `ReceiverNode` to receive and detect photons. Both of our node types will inherit from `Node`, the basic node class from sequence, and we invoke the parent constructor for `Node`. In general, entities (such as nodes and hardware) require a string `name` and a `Timeline` in their constructor, as well as necessary parameters (for more details on timelines, see Tutorial 1).
 
-Next, we create all of our hardware elements on each node. The detector is created easily, as no specific parameters are required (but we wish to set the `efficiency` to 1 to prevent errors). We’ll put the detector class on the receiver node.
+We will now create our hardware. The detector is created easily, as no specific parameters are required (but we wish to set the `efficiency` to 1 to prevent errors).
 
 The required parameters for memories are more numerous and are listed here:
 * `fidelity`: fidelity of entanglement. This is usually set to 0 when unentangled, but can be set to other values as it is usually replaced when entangled.
@@ -32,59 +32,69 @@ The required parameters for memories are more numerous and are listed here:
 * `coherence_time`: the time for which a memory state (other than down) is viable, given in seconds.
 * `wavelength`: the wavelength of emitted photons.
 
-We will add our memory to the sender node class.
+Next, we will add each component to the proper node using the `add_component` method.
+This method adds the component to the node's `components` dictionary, which maps component names to objects.
+It may be accessed by outside protocols to monitor components or get their current state.
+We'll put the detector on the receiver node, and the memory on the sender node.
 
 ```python
 from sequence.kernel.timeline import Timeline
 from sequence.topology.node import Node
 from sequence.components.memory import Memory
 from sequence.components.detector import Detector
-from sequence.components.circuit import Circuit
-
-_meas_circuit = Circuit(1)
-_meas_circuit.measure(0)
 
 class SenderNode(Node):
-    def __init__(self, name: str, timeline: Timeline):
+    def __init__(self, name, timeline):
         super().__init__(name, timeline)
-        from sequence.components.memory import Memory
-        self.memory = Memory(‘node1.memory’, tl, fidelity=0, frequency=0,
-                             efficiency=1, coherence_time=0, wavelength=500)
-        self.memory.owner = self
+        
+        memory_name = name + ".memory"
+        memory = Memory(memory_name, timeline, fidelity=1, frequency=0,
+                        efficiency=1, coherence_time=0, wavelength=500)
+        self.add_component(memory)
+        memory.add_receiver(self)
+
+    def get(self, photon, **kwargs):
+        self.send_qubit(kwargs['dst'], photon)
 
 class ReceiverNode(Node):
-    def __init__(self, name: str, timeline: Timeline):
+    def __init__(self, name, timeline):
         super().__init__(name, timeline)
-        self.detector = Detector(‘node2.detector’, tl, efficiency=1)
-        self.detector.owner = self
-
-    def receive_qubit(self, src: str, qubit) -> None:
-        qm = self.timeline.quantum_manager
-        key = qubit.qstate_key
-        meas_res = qm.run_circuit(_meas_circuit, [key], self.get_generator().random())[key]
-        if meas_res:
-            self.detector.get()
+
+        detector_name = name + ".detector"
+        detector = Detector(detector_name, timeline, efficiency=1)
+        self.add_component(detector)
+        self.set_first_component(detector_name)
+        detector.owner = self
+
+    def receive_qubit(self, src, qubit):
+        self.components[self.first_component_name].get()
 ```
 
-Notice that we also needed to change the `receive_qubit` method of the base `Node` class.
-This method is invoked by the quantum channel when transmitting photons, and by default is set to do nothing.
+You may notice that the initialization methods make use of a few additional functions, and methods have been added to the node classes.
+These are to establish the internal hardware connections on the node.
+The first of these methods is the `Entity.get` method.
+All physical simulation elements, including optical hardware and nodes, inherit from this class.
+The `get` method is used to receive photons from another entity which may be further processed.
+The `add_receiver` method similarly designates another component (or components) to receive photons from the current entity.
+In our case, we wish to set the memory's receiver as the `SenderNode`, so that the node may receive emitted photons and direct them to an inter-node quantum channel using `send_qubit`.
+This is performed in the `SenderNode.get` method.
+At the other end of the channel, the `receive_qubit` method is called by a quantum channel on the receiving node of a transmission.
 For this method, the `src` input specifies the name of the node sending the qubit.
 In our case, we don’t care about the source node, so we can ignore it.
-The `qubit` input is the transmitted photon.
-For single atom memories, the memory state heralded by the denotes the presence or absence of a photon.
-This corresponds to the up or down state of the memory.
-We will thus measure the memory state and record the photon accordingly (this will be done automatically in future updates).
-If we measure 0, we must ignore the photon and not record it.
-Otherwise, it is sent to the detector for recording.
-The detector uses the `get` method to receive photons, and this interface is shared by many other optical hardware elements.
+The `qubit` input is the transmitted photon; it is sent to the detector for measurement.
+This is done using the `first_component_name` attribute, which designates a component on a node to receive all incoming photons.
 
+### Step 2: Custom Counting Protocol
 
-### Step 2: Custom Protocol
-
-Next, we will create our custom protocol using a custom class. Let’s denote this class as `Counter`, as we will be counting photons detection. The initializing method is very simple, only setting the count to 0. We then proceed to the trigger function, which will handle information from the detector. Normally, the detector will pass two arguments through this function (a reference to the specific detector and info including the detection time), but we are not concerned with these. We only wish to increment our counter.
+Next, we will create our custom protocols using custom classes.
+Let’s denote the first class as `Counter`, as we will be counting photon detection.
+The initializing method is very simple, only setting the count to 0.
+We then proceed to the trigger function, which will handle information from the detector.
+Normally, the detector will pass two arguments through this function (a reference to the specific detector and info including the detection time), but we are not concerned with these.
+We only wish to increment our counter.
 
 ```python
-class Counter():
+class Counter:
     def __init__(self):
         self.count = 0
 
@@ -92,6 +102,28 @@ class Counter():
         self.count += 1
 ```
 
+We then add this counter protocol to our receiver node, by modifying the initialization method.
+To have the counter monitor the detector, we invoke the `Entity.attach` method.
+This method ensures that any updates (such as detection events) from an entity are passed to the object specified in the method arguments.
+
+```python
+class ReceiverNode(Node):
+    def __init__(self, name, timeline):
+        super().__init__(name, timeline)
+
+        detector_name = name + ".detector"
+        detector = Detector(detector_name, timeline, efficiency=1)
+        self.add_component(detector)
+        self.set_first_component(detector_name)
+        detector.owner = self
+
+        self.counter = Counter()
+        detector.attach(self.counter)
+```
+
+The second protocol class is slightly more complicated, and will be activating the quantum memory.
+We will define it later in the tutorial.
+
 ### Step 3: Build the Network
 
 We are now ready to start writing the main function of our script. The first step is to create the simulation timeline. We will use a 10 second run time, but more or less time may be needed depending on hardware parameters. Note that the runtime is given in **picoseconds**.
@@ -99,7 +131,6 @@ We are now ready to start writing the main function of our script. The first ste
 ```python
 from sequence.kernel.timeline import Timeline
 tl = Timeline(10e12)
-tl.show_progress = False
 ```
 
 We can then create our two network nodes using our custom node class. We only need to specify a name for each node and the timeline it belongs to:
@@ -119,28 +150,28 @@ qc = QuantumChannel("qc", tl, attenuation=0, distance=1e3)
 qc.set_ends(node1, node2.name)
 ```
 
-Lastly, we’ll create the counter for our detector. We only need to define an instance, and attach it to the detector. When the detector properly detects a photon, it will call the `trigger` method of all attached objects, including our counter.
-
-```python
-counter = Counter()
-node2.detector.attach(counter)
-```
-
 ### Step 4: Measure Memory Once
 
-With the network built, we are ready to schedule simulation events and run our experiment. The details on scheduling events are covered in Tutorial 1, so we will not focus on them here. Let's first run one experiment with the memory in the |&#8593;&#10217; state and observe the detection time of the single emitted photon. The memory state can be set with the `update_state` method:
+With the network built, we are ready to schedule simulation events and run our experiment.
+The details on scheduling events are covered in Tutorial 1, so we will not focus on them here.
+Let's first run one experiment with the memory in the |&#8593;&#10217; state and observe the detection time of the single emitted photon.
+We can obtain the memory object using the `Node.get_components_by_type` method, which returns a list of matching components on the node.
+The memory state can then be set with the `update_state` method.
 
 ```python
-node1.memory.update_state([complex(0), complex(1)])
+memories = node1.get_components_by_type("Memory")
+memory = memories[0]
+memory.update_state([complex(0), complex(1)])
 ```
 
-We set the state of this single memory to a quantum state, given as a complex array of coefficients for the |&#8593;&#10217; and |&#8595;&#10217; states. Let's also change our counter slightly to record the detection time. This can be done by accessing the `'time'` field of the detector info:
+We set the state of this single memory to a quantum state, given as a complex array of coefficients for the |&#8593;&#10217; and |&#8595;&#10217; states.
+Let's also change our counter slightly to record the detection time. This can be done by accessing the `'time'` field of the detector info:
 
 ```python
-class Counter():
+class Counter:
     def __init__(self):
         self.count = 0
-        self.time = None
+        self.time = 0
 
     def trigger(self, detector, info):
         self.count += 1
@@ -153,7 +184,7 @@ We must also schedule an excite event for the memory, which will send a photon t
 from sequence.kernel.process import Process
 from sequence.kernel.event import Event
 
-process = Process(node1.memory, "excite", ['node2'])
+process = Process(memory, "excite", ["node2"])
 event = Event(0, process)
 tl.schedule(event)
 ```
@@ -168,45 +199,78 @@ tl.run()
 We should see that the `count` field of our `Counter` class is now 1, and that we have a detection time greater than 0 resulting from the quantum channel delay. Quantum channel delay is calculated based on the speed of light in an optical fiber and the length of the fiber (delay = L / c). We can view the detection and detection time as follows:
 
 ```python
-print("detection count: {}".format(counter.count))
-print("detection time: {}".format(counter.time))
+print("detection count: {}".format(node2.counter.count))
+print("detection time: {}".format(node2.counter.time))
 ```
 
 ### Step 5: Repeated Operation
 
 Next, let's repeatedly set the memeory to the |+&#10217; state and record detection events. To give us a clean state, we'll remove the code we wrote for step 4.
 
-The events we wish to schedule are all for the memory. We want to first set it to a |+&#10217; state with the `update_state` method, and then excite the memory to measure emitted photons with the `excite` method. The `update_state` method will require a plus state as input. The `excite` method needs an argument for the desired destination node, so we'll supply the name of our `node2`. We’ll schedule both of these at a predetermined frequency `FREQUENCY` (given in Hz) for a set number of trials `NUM_TRIALS`.
+The events we wish to schedule are all for the memory.
+We want to first set it to a |+&#10217; state with the `update_state` method, and then excite the memory to measure emitted photons with the `excite` method.
+The `update_state` method will require a plus state as input.
+The `excite` method needs an argument for the desired destination node, so we'll supply the name of our `node2`.
+We'll schedule both of these at a predetermined period.
+
+To manage all of these requirements, we'll write our second protocol class, the `Sender`.
+The protocol will need a reference to the local node, as well as the name of the memory to trigger.
+Requiring a node and the names of local hardware is typical of protocols in SeQUeNCe.
+We'll include all memory modification in the `start` method, which will activate the protocol.
 
 ```python
 import math
-from sequence.kernel.process import Process
-from sequence.kernel.event import Event
 
-time_bin = int(1e12 / FREQUENCY)
+class Sender:
+    def __init__(self, own, memory_name):
+        self.own = own
+        self.memory = own.components[memory_name]
+
+    def start(self, period):
+        process1 = Process(self.memory, "update_state", [[complex(math.sqrt(1/2)), complex(math.sqrt(1/2))]])
+        process2 = Process(self.memory, "excite", ["node2"])
+        for i in range(NUM_TRIALS):
+            event1 = Event(i * period, process1)
+            event2 = Event(i * period + (period / 2), process2)
+            self.own.timeline.schedule(event1)
+            self.own.timeline.schedule(event2)
+```
+
+We'll then place this protocol in the `SenderNode` class:
+
+```python
+class SenderNode(Node):
+    def __init__(self, name, timeline):
+        super().__init__(name, timeline)
+        memory_name = name + ".memory"
+        memory = Memory(memory_name, timeline, fidelity=1, frequency=0,
+                        efficiency=1, coherence_time=0, wavelength=500)
+        self.add_component(memory)
+        memory.add_receiver(self)
+
+        self.sender = Sender(self, memory_name)
 
-process1 = Process(node1.memory, "update_state", [[complex(math.sqrt(1 / 2)), complex(math.sqrt(1 / 2))]])
-process2 = Process(node1.memory, "excite", ["node2"])
-for i in range(NUM_TRIALS):
-    event1 = Event(i * time_bin, process1)
-    event2 = Event(i * time_bin + (time_bin / 2), process2)
-    tl.schedule(event1)
-    tl.schedule(event2)
+    def get(self, photon, **kwargs):
+        self.send_qubit(kwargs['dst'], photon)
 ```
 
 ### Step 6: Running and Output
 
-The procedure to initialize and run the timeline is the same as Tutorial 1:
+The procedure to initialize and run the timeline is the same as Tutorial 1.
+We will also add a call to the `start` method of our protocol, using a calculated period.
+We'll use a predetermined frequency `FREQUENCY` (given in Hz) for a set number of trials `NUM_TRIALS`.
 
 ```python
 tl.init()
+period = int(1e12 / FREQUENCY)
+node1.sender.start(period)
 tl.run()
 ```
 
 To access the results of our simulation, we just need the count parameter of our custom counter class. We’ll read it, and present the number of detections we had as a percent of the number of excite operations:
 
 ```python
-print("percent measured: {}%".format(100 * counter.count / NUM_TRIALS))
+print("percent measured: {}%".format(100 * node2.counter.count / NUM_TRIALS))
 ```
 
 We expect the percent to be about 50%, as we initialized the memory in the |+&#10217; state each time. Try messing with parameters to achieve different measurement results!
diff --git a/docs/source/tutorial/chapter3/entangle.md b/docs/source/tutorial/chapter3/entangle.md
index 42f2b211..d0fd8c2d 100644
--- a/docs/source/tutorial/chapter3/entangle.md
+++ b/docs/source/tutorial/chapter3/entangle.md
@@ -2,6 +2,7 @@
 
 In previous chapters, we introduced the usage of hardware models. 
 In this chapter, we will use protocols in the entanglement management module to control these hardware devices and change the entanglement state of quantum memories.
+We will also use a simple manager protocol to control the entanglement generation protocols.
 We will show
 
 * how to use `EntanglementGenerationA` (Barret-Kok generation protocol) to entangle memories on different nodes
@@ -27,14 +28,15 @@ Classical channels and nodes create a complete classical graph, which is not sho
 
 `EntangleGenNode` includes:
 
-* **Hardware**: one quantum memory in the |+&#10217; state, prepared to entangle with the remote memory.
-* **Software**: the `EntanglementGenerationA` prtocol to excite the controlled memory and determine the quantum state via messages from `EntanglementGenerationB`; 
-the `SimpleManager` uses the `update` function to get the state of memory after the procedures in `EntanglementGenerationA`.
+* **Hardware**: one quantum memory in the |+&#10217; state, prepared to entangle with the remote memory on the other node.
+* **Software**: the `EntanglementGenerationA` protocol to excite the controlled memory and determine the quantum state via messages from `EntanglementGenerationB`; 
+a `SimpleManager` which uses the `update` function to get the state of the memory after the procedures in `EntanglementGenerationA`.
 
 ### Step 1: Customize Node 
 
 We can import `BSMNode` from SeQUeNCe package and thus only need to define the `EntangleGenNode` class.
-The code of `EntangleGenNode` class is shown below:
+We also define our `SimpleManager`, which will create entanglement protocol instances and monitor their success.
+The code for the `EntangleGenNode` and `SimpleManager` classes is shown below:
 
 ```python
 from sequence.topology.node import Node
@@ -42,8 +44,10 @@ from sequence.components.memory import Memory
 from sequence.entanglement_management.generation import EntanglementGenerationA
 
 
-class SimpleManager():
-    def __init__(self):
+class SimpleManager:
+    def __init__(self, own, memo_name):
+        self.own = own
+        self.memo_name = memo_name
         self.raw_counter = 0
         self.ent_counter = 0
 
@@ -54,26 +58,40 @@ class SimpleManager():
         else:
             self.ent_counter += 1
 
+    def create_protocol(self, middle: str, other: str):
+        self.own.protocols = [EntanglementGenerationA(self.own, '%s.eg' % self.own.name, middle, other,
+                                                      self.own.components[self.memo_name])]
+
 
 class EntangleGenNode(Node):
     def __init__(self, name: str, tl: Timeline):
         super().__init__(name, tl)
-        self.memory = Memory('%s.memo'%name, tl, 0.9, 2000, 1, -1, 500)
-        self.memory.owner = self
-        self.resource_manager = SimpleManager()
-        self.protocols = []
+
+        memo_name = '%s.memo' % name
+        memory = Memory(memo_name, tl, 0.9, 2000, 1, -1, 500)
+        memory.owner = self
+        memory.add_receiver(self)
+        self.add_component(memory)
+
+        self.resource_manager = SimpleManager(self, memo_name)
+
+    def init(self):
+        memory = self.get_components_by_type("Memory")[0]
+        memory.reset()
 
     def receive_message(self, src: str, msg: "Message") -> None:
         self.protocols[0].received_message(src, msg)
 
-    def create_protocol(self, middle: str, other: str):
-        self.protocols = [EntanglementGenerationA(self, '%s.eg'%self.name, middle, other, self.memory)]
+    def get(self, photon, **kwargs):
+        self.send_qubit(kwargs['dst'], photon)
 ```
 
-In this customized `Node` class, we inherit most functions and only overwrite the `receive_message` function.
-A node will use this function to receive the a classical message `msg` from the source node `src`. 
+In this customized `Node` class, we overwrite the `init`, `receive_message`, and `get` methods.
+The `init` method will be used to reset the local memory at the beginning of the simulation.
+The `get` method directs received photons from the memory to the attached quantum channel, as discussed in the previous tutorial.
+Finally, for the `receive_message` method, the node will receive a classical message `msg` from the source node `src`. 
 
-We also add a new function `create_protocol(self, middle: str, other: str)` to create the instance of the generation protocol.
+We also add a function `create_protocol(self, middle: str, other: str)` to the manager to create the local instance of the generation protocol.
 The `middle` and `other` parameters declare the name of the `BSMNode` and `EntangleGenNode`, respectively, used for generating entanglement.
 
 The constructor function of `EntanglementGenerationA` needs five arguments: 
@@ -88,11 +106,8 @@ The constructor function of `EntanglementGenerationA` needs five arguments:
 
 Q: Why is the `SimpleManager` necessary?
 
-A: We have cembedded code that calls the `update` function of a `resource_manager` into our current implementations of entanglement protocols.
-
-Q: Why does the constructor of `EntangleGenNen` set the `Memory.owner` field?
-
-A: The `Memory` class should contain a pointer to a `Node` object, as nodes provide the interface for `QuantumChannel` objects to send photons to the desired destination.
+A: We have embedded code that calls the `update` function of a `resource_manager` into our current implementations of entanglement protocols.
+We also need a protocol to track which hardware components of the local node should be used.
 
 Q: Why is the `EntanglementGenerationA` object placed in the list `EntangleGenNode.protocols`?
 
@@ -102,8 +117,7 @@ A: The implementation of `EntanglementGenerationA` assumes it has been placed in
 ### Step 2: Create Network
 
 As introduced in the previous chapter, we create nodes and channels to define the network.
-To avoid unnecessary errors, we will set the efficiency of our detectors to 1.
-Also note that we will set the timeline `show_progress` attribute to 0, since we will be calling the run method multiple times.
+To avoid unnecessary errors, we will set the efficiency of our detectors to 1. 
 
 ```python
 from sequence.kernel.timeline import Timeline
@@ -112,7 +126,6 @@ from sequence.components.optical_channel import QuantumChannel, ClassicalChannel
 
 
 tl = Timeline()
-tl.show_progress = False
 
 node1 = EntangleGenNode('node1', tl)
 node2 = EntangleGenNode('node2', tl)
@@ -121,7 +134,8 @@ node1.set_seed(0)
 node2.set_seed(1)
 bsm_node.set_seed(2)
 
-bsm_node.bsm.update_detectors_params('efficiency', 1)
+bsm = bsm_node.get_components_by_type("SingleAtomBSM")[0]
+bsm.update_detectors_params('efficiency', 1)
 
 qc1 = QuantumChannel('qc1', tl, attenuation=0, distance=1000)
 qc2 = QuantumChannel('qc2', tl, attenuation=0, distance=1000)
@@ -152,24 +166,27 @@ from sequence.entanglement_management.entanglement_protocol import EntanglementP
 def pair_protocol(node1: Node, node2: Node):
     p1 = node1.protocols[0]
     p2 = node2.protocols[0]
-    p1.set_others(p2.name, node2.name, [node2.memory.name])
-    p2.set_others(p1.name, node1.name, [node1.memory.name])
+    node1_memo_name = node1.get_components_by_type("Memory")[0].name
+    node2_memo_name = node2.get_components_by_type("Memory")[0].name
+    p1.set_others(p2.name, node2.name, [node2_memo_name])
+    p2.set_others(p1.name, node1.name, [node1_memo_name])
 
 
-node1.create_protocol('bsm_node', 'node2')
-node2.create_protocol('bsm_node', 'node1')
+node1.resource_manager.create_protocol('bsm_node', 'node2')
+node2.resource_manager.create_protocol('bsm_node', 'node1')
 pair_protocol(node1, node2)
 
-print('before', node1.memory.entangled_memory, node1.memory.fidelity)
+memory = node1.get_components_by_type("Memory")[0]
+
+print('before', memory.entangled_memory, memory.fidelity)
 # "before node1.memo {'node_id': None, 'memo_id': None} 0"
 
+tl.init()
 node1.protocols[0].start()
 node2.protocols[0].start()
-
-tl.init()
 tl.run()
 
-print('after', node1.memory.entangled_memory, node1.memory.fidelity)
+print('after', memory.entangled_memory, memory.fidelity)
 # (if the generation fails) "after node1.memo {'node_id': None, 'memo_id': None} 0"
 # (if the generation succeeds) "after node1.memo {'node_id': 'node2', 'memo_id': 'node2.memo'} 0.9"
 ```
@@ -192,17 +209,15 @@ We will use the `Memory.reset()` method to reset the state of quantum memories b
 tl.init()
 for i in range(1000):
     tl.time = tl.now() + 1e11
-    node1.create_protocol('bsm_node', 'node2')
-    node2.create_protocol('bsm_node', 'node1')
+    node1.resource_manager.create_protocol('bsm_node', 'node2')
+    node2.resource_manager.create_protocol('bsm_node', 'node1')
     pair_protocol(node1, node2)
 
-    node1.memory.reset()
-    node2.memory.reset()
-    
     node1.protocols[0].start()
     node2.protocols[0].start()
     tl.run()
 
+print("node1 entangled memories : available memories")
 print(node1.resource_manager.ent_counter, ':', node1.resource_manager.raw_counter)
 # (around 500:500; the exact number depends on the seed of numpy.random)
 ```
@@ -222,32 +237,53 @@ The `BBPSSW` purification protocol will consume one entanglement to improve the
 
 The custom `PurifyNode` class will inherit the `Node` class from SeQUeNCe. 
 Similar to `EntangleGenNode`, we need to define a `SimpleManager` and rewrite the `receive_message` method.
-The `PurifyNode.kept_memo` is the memory whose fidelity will be improved by the purification protocol.
-If the protocol purifies `kept_memo` successfully, we will keep the `kept_memo`.
+The `kept_memo` is the memory whose fidelity will be improved by the purification protocol.
+If the protocol purifies the `kept_memo` successfully, we will keep the `kept_memo`.
 Otherwise, we will discard it.
-The `PurifyNode.meas_memo` is the consumed memory. 
+The `meas_memo` is the consumed memory. 
 We will always discard the `meas_memo` after the completion of the purification protocol.
+We will also rewrite the code for the manager class to reflect our usage of two memories and the new purification protocol.
 
 
 ```python
 from sequence.entanglement_management.purification import BBPSSW
 
 
+class SimpleManager:
+    def __init__(self, own, kept_memo_name, meas_memo_name):
+        self.own = own
+        self.kept_memo_name = kept_memo_name
+        self.meas_memo_name = meas_memo_name
+        self.raw_counter = 0
+        self.ent_counter = 0
+
+    def update(self, protocol, memory, state):
+        if state == 'RAW':
+            self.raw_counter += 1
+            memory.reset()
+        else:
+            self.ent_counter += 1
+
+    def create_protocol(self):
+        kept_memo = self.own.components[self.kept_memo_name]
+        meas_memo = self.own.components[self.meas_memo_name]
+        self.own.protocols = [BBPSSW(self.own, 'purification_protocol', kept_memo, meas_memo)]
+
+
 class PurifyNode(Node):
     def __init__(self, name: str, tl: Timeline):
         super().__init__(name, tl)
-        self.kept_memo = Memory('%s.memoA' % name, tl, 0.9, 2000, 1, -1, 500)
-        self.kept_memo.owner = self
-        self.meas_memo = Memory('%s.memoB' % name, tl, 0.9, 2000, 1, -1, 500)
-        self.meas_memo.owner = self
-        self.resource_manager = SimpleManager()
-        self.protocols = []
+        kept_memo_name = '%s.kept_memo' % name
+        meas_memo_name = '%s.meas_memo' % name
+        kept_memo = Memory('%s.kept_memo' % name, tl, 0.9, 2000, 1, -1, 500)
+        meas_memo = Memory('%s.meas_memo' % name, tl, 0.9, 2000, 1, -1, 500)
+        self.add_component(kept_memo)
+        self.add_component(meas_memo)
+
+        self.resource_manager = SimpleManager(self, kept_memo_name, meas_memo_name)
 
     def receive_message(self, src: str, msg: "Message") -> None:
         self.protocols[0].received_message(src, msg)
-
-    def create_protocol(self):
-        self.protocols = [BBPSSW(self, 'purification_protocol', self.kept_memo, self.meas_memo)]
 ```
 
 The constructor function of BBPSSW requires four arguments:
@@ -263,7 +299,6 @@ We can now use the code below to create the simulated network.
 
 ```python
 tl = Timeline()
-tl.show_progress = False
 
 node1 = PurifyNode('node1', tl)
 node2 = PurifyNode('node2', tl)
@@ -296,8 +331,13 @@ def entangle_memory(memo1: Memory, memo2: Memory, fidelity: float):
     memo1.fidelity = memo2.fidelity = fidelity
 
 
-entangle_memory(node1.kept_memo, node2.kept_memo, 0.9)
-entangle_memory(node1.meas_memo, node2.meas_memo, 0.9)
+kept_memo_1 = node1.components[node1.resource_manager.kept_memo_name]
+kept_memo_2 = node2.components[node2.resource_manager.kept_memo_name]
+meas_memo_1 = node1.components[node1.resource_manager.meas_memo_name]
+meas_memo_2 = node2.components[node2.resource_manager.meas_memo_name]
+
+entangle_memory(kept_memo_1, kept_memo_2, 0.9)
+entangle_memory(meas_memo_1, meas_memo_2, 0.9)
 ```
 
 ### Step 4: Configure and Start BBPSSW Protocol
@@ -308,12 +348,16 @@ Similar to the previous example, we create, pair, and start the protocols.
 def pair_protocol(node1: Node, node2: Node):
     p1 = node1.protocols[0]
     p2 = node2.protocols[0]
-    p1.set_others(p2.name, node2.name, [node2.kept_memo.name, node2.meas_memo.name])
-    p2.set_others(p1.name, node1.name, [node1.kept_memo.name, node1.meas_memo.name])
+    kept_memo_1_name = node1.resource_manager.kept_memo_name
+    meas_memo_1_name = node1.resource_manager.meas_memo_name
+    kept_memo_2_name = node2.resource_manager.kept_memo_name
+    meas_memo_2_name = node2.resource_manager.meas_memo_name
+    p1.set_others(p2.name, node2.name, [kept_memo_2_name, meas_memo_2_name])
+    p2.set_others(p1.name, node1.name, [kept_memo_1_name, meas_memo_1_name])
 
 
-node1.create_protocol()
-node2.create_protocol()
+node1.resource_manager.create_protocol()
+node2.resource_manager.create_protocol()
 
 pair_protocol(node1, node2)
 
@@ -322,11 +366,11 @@ node1.protocols[0].start()
 node2.protocols[0].start()
 tl.run()
 
-print(node1.kept_memo.entangled_memory, node2.kept_memo.fidelity)
+print(kept_memo_1.name, kept_memo_1.entangled_memory, kept_memo_1.fidelity)
 # 'node1.kept_memo {'node_id': 'node2', 'memo_id': 'node2.kept_memo'} 0.9263959390862945'
 # or 'node1.kept_memo {'node_id': 'node2', 'memo_id': 'node2.kept_memo'} 0.9'
 
-print(node1.meas_memo.entangled_memory, node2.meas_memo.fidelity)
+print(meas_memo_1.name, meas_memo_1.entangled_memory, meas_memo_1.fidelity)
 # 'node1.meas_memo {'node_id': 'node2', 'memo_id': node2.meas_memo'} 0.9'
 ```
 
@@ -349,11 +393,11 @@ We can run the purification protocol multiple times to observe the state of memo
 ```python
 tl.init()
 for i in range(10):
-    entangle_memory(node1.kept_memo, node2.kept_memo, 0.9)
-    entangle_memory(node1.meas_memo, node2.meas_memo, 0.9)
+    entangle_memory(kept_memo_1, kept_memo_2, 0.9)
+    entangle_memory(meas_memo_1, meas_memo_2, 0.9)
 
-    node1.create_protocol()
-    node2.create_protocol()
+    node1.resource_manager.create_protocol()
+    node2.resource_manager.create_protocol()
 
     pair_protocol(node1, node2)
 
@@ -361,8 +405,8 @@ for i in range(10):
     node2.protocols[0].start()
     tl.run()
 
-    print(node1.kept_memo.entangled_memory, node2.kept_memo.fidelity)
-    print(node1.meas_memo.entangled_memory, node2.meas_memo.fidelity)
+    print(kept_memo_1.name, kept_memo_1.entangled_memory, kept_memo_1.fidelity)
+    print(meas_memo_1.name, meas_memo_1.entangled_memory, meas_memo_1.fidelity)
 ```
 
 ## Example: Use `EntanglementSwappingA` and `EntanglementSwappingB` to Extend Entanglement
@@ -387,63 +431,89 @@ After the swapping protocol, the two memories on `SwapNodeA` are no longer entan
 
 ### Step 1: Customized Node
 
-We reuse the `SimpleManager` defined in the previous example to create the `SwapNodeA` and `SwapNodeB` protocols.
-
-The code below shows the implementation of `SwapNodeA`. 
-The `SwapNodeA.left_memo` is the memory entangled with the memory on the left `SwapNodeB`.
-The `SwapNodeA.right_memo` is the memory entangled with the memory on the right `SwapNodeB`. 
-The `create_protocol()` function creates the `EntanglementSwappingA` protocol instances.
-The `EntanglementSwappingA` constructor requires six arguments:
-
-1. The node that holds the protocol instance
-2. The identity of protocol instance
-3. The first memory used for the swapping operation
-4. The second memory used for the swapping operation
-5. The success rate of swapping
-6. The degradation rate of swapping
-
-**Note**: the fidelity of entanglement after swapping is `f1 * f2 * fd`, where `f1`, `f2` denote the fidelity of the two entangled pairs and `fd` denotes the degradation rate.
+The code below shows the implementation of `SwapNodeA`.
+The `left_memo` is the memory entangled with the memory on the left `SwapNodeB`.
+The `right_memo` is the memory entangled with the memory on the right `SwapNodeB`.
 
 ```python
-from sequence.entanglement_management.swapping import EntanglementSwappingA, EntanglementSwappingB
-
-
 class SwapNodeA(Node):
     def __init__(self, name: str, tl: Timeline):
         super().__init__(name, tl)
-        self.left_memo = Memory('%s.left_memo' % name, tl, 0.9, 2000, 1, -1, 500)
-        self.left_memo.owner = self
-        self.right_memo = Memory('%s.right_memo' % name, tl, 0.9, 2000, 1, -1, 500)
-        self.right_memo.owner = self
-        self.resource_manager = SimpleManager()
-        self.protocols = []
+        left_memo_name = '%s.left_memo' % name
+        right_memo_name = '%s.right_memo' % name
+        left_memo = Memory(left_memo_name, tl, 0.9, 2000, 1, -1, 500)
+        right_memo = Memory(right_memo_name, tl, 0.9, 2000, 1, -1, 500)
+        self.add_component(left_memo)
+        self.add_component(right_memo)
+
+        self.resource_manager = SimpleManager(self, [left_memo_name, right_memo_name])
 
     def receive_message(self, src: str, msg: "Message") -> None:
         self.protocols[0].received_message(src, msg)
-
-    def create_protocol(self):
-        self.protocols = [EntanglementSwappingA(self, 'ESA', self.left_memo, self.right_memo, 1, 0.99)]
 ```
 
-The code for `SwapNodeB` has two differences from `SwapNodeA`:
-
-1. `SwapNodeB` only has one quantum memory.
-2. `create_protocol()` function creates an instance of `EntanglementSwappingB`.
+The code for `SwapNodeB` is identical to `SwapNodeA` but with only one memory:
 
 ```python
 class SwapNodeB(Node):
     def __init__(self, name: str, tl: Timeline):
         super().__init__(name, tl)
-        self.memo = Memory('%s.memo' % name, tl, 0.9, 2000, 1, -1, 500)
-        self.memo.owner = self
-        self.resource_manager = SimpleManager()
-        self.protocols = []
+        memo_name = '%s.memo' % name
+        memo = Memory(memo_name, tl, 0.9, 2000, 1, -1, 500)
+        self.add_component(memo)
+
+        self.resource_manager = SimpleManager(self, [memo_name])
 
     def receive_message(self, src: str, msg: "Message") -> None:
         self.protocols[0].received_message(src, msg)
+```
+
+We reuse the `SimpleManager` defined in the previous example to create the `SwapNodeA` and `SwapNodeB` protocols.
+We will thus add a field for storing multiple memory names and generate swapping protocols.
+The `EntanglementSwappingA` constructor requires six arguments:
+
+1. The node that holds the protocol instance
+2. The identity of protocol instance
+3. The first memory used for the swapping operation
+4. The second memory used for the swapping operation
+5. The success rate of swapping
+6. The degradation rate of swapping
+
+While the constructor for `EntanglementSwappingA` only requires three arguments:
+
+1. The node that holds the protocol instance
+2. The identity of protocol instance
+3. The memory having its entanglement swapped
+
+We will set up the manager so that it automatically creates the right type of swapping potocol depending on the local node.
+
+**Note**: the fidelity of entanglement after swapping is `f1 * f2 * fd`, where `f1`, `f2` denote the fidelity of the two entangled pairs and `fd` denotes the degradation rate.
+
+```python
+from sequence.entanglement_management.swapping import EntanglementSwappingA, EntanglementSwappingB
+
+class SimpleManager:
+    def __init__(self, own, memo_names):
+        self.own = own
+        self.memo_names = memo_names
+        self.raw_counter = 0
+        self.ent_counter = 0
+
+    def update(self, protocol, memory, state):
+        if state == 'RAW':
+            self.raw_counter += 1
+            memory.reset()
+        else:
+            self.ent_counter += 1
 
     def create_protocol(self):
-        self.protocols = [EntanglementSwappingB(self, '%s.ESB'%self.name, self.memo)]
+        if type(self.own) is SwapNodeA:
+            left_memo = self.own.components[self.memo_names[0]]
+            right_memo = self.own.components[self.memo_names[1]]
+            self.own.protocols = [EntanglementSwappingA(self.own, 'ESA', left_memo, right_memo, 1, 0.99)]
+        else:
+            memo = self.own.components[self.memo_names[0]]
+            self.own.protocols = [EntanglementSwappingB(self.own, '%s.ESB' % self.own.name, memo)]
 ```
 
 ### Step 2: Create Network
@@ -480,38 +550,42 @@ def pair_protocol(node1, node2, node_mid):
     p1 = node1.protocols[0]
     p2 = node2.protocols[0]
     pmid = node_mid.protocols[0]
-    p1.set_others(pmid.name, node_mid.name, [node_mid.left_memo.name, node_mid.right_memo.name])
-    p2.set_others(pmid.name, node_mid.name, [node_mid.left_memo.name, node_mid.right_memo.name])
-    pmid.set_others(p1.name, node1.name, [node1.memo.name])
-    pmid.set_others(p2.name, node2.name, [node2.memo.name])
-
-
-entangle_memory(left_node.memo, mid_node.left_memo, 0.9)
-entangle_memory(right_node.memo, mid_node.right_memo, 0.9)
+    p1.set_others(pmid.name, node_mid.name,
+                  [node_mid.resource_manager.memo_names[0], node_mid.resource_manager.memo_names[1]])
+    p2.set_others(pmid.name, node_mid.name,
+                  [node_mid.resource_manager.memo_names[0], node_mid.resource_manager.memo_names[1]])
+    pmid.set_others(p1.name, node1.name, [node1.resource_manager.memo_names[0]])
+    pmid.set_others(p2.name, node2.name, [node2.resource_manager.memo_names[0]])
+
+left_memo = left_node.components[left_node.resource_manager.memo_names[0]]
+right_memo = right_node.components[right_node.resource_manager.memo_names[0]]
+mid_left_memo = mid_node.components[mid_node.resource_manager.memo_names[0]]
+mid_right_memo = mid_node.components[mid_node.resource_manager.memo_names[1]]
+entangle_memory(left_memo, mid_left_memo, 0.9)
+entangle_memory(right_memo, mid_right_memo, 0.9)
 
 for node in nodes:
-    node.create_protocol()
+    node.resource_manager.create_protocol()
 
-pair_protocol(left_node.protocols[0], mid_node.protocols[0])
-pair_protocol(right_node.protocols[0], mid_node.protocols[0])
-for node in nodes:
-    node.protocols[0].start()
+pair_protocol(left_node, right_node, mid_node)
 
 tl.init()
+for node in nodes:
+    node.protocols[0].start()
 tl.run()
 
-print(left_node.memo.entangled_memory)
+print(left_memo.entangled_memory)
 # {'node_id': 'right', 'memo_id': 'right.memo'}
 
-print(mid_node.left_memo.entangled_memory)
+print(mid_left_memo.entangled_memory)
 # {'node_id': None, 'memo_id': None}
 
-print(mid_node.right_memo.entangled_memory)
+print(mid_right_memo.entangled_memory)
 # {'node_id': None, 'memo_id': None}
 
-print(right_node.memo.entangled_memory)
+print(right_memo.entangled_memory)
 # {'node_id': 'left', 'memo_id': 'left.memo'}
 
-print(left_node.memo.fidelity)
+print(left_memo.fidelity)
 # 0.8019000000000001
 ```
diff --git a/docs/source/tutorial/chapter3/example1.py b/docs/source/tutorial/chapter3/example1.py
index e352f8a5..81a08301 100644
--- a/docs/source/tutorial/chapter3/example1.py
+++ b/docs/source/tutorial/chapter3/example1.py
@@ -3,46 +3,59 @@
 from sequence.components.memory import Memory
 from sequence.components.optical_channel import QuantumChannel, ClassicalChannel
 from sequence.entanglement_management.generation import EntanglementGenerationA
-from sequence.entanglement_management.entanglement_protocol import EntanglementProtocol
-from sequence.message import Message
 
 
-class SimpleManager():
-    def __init__(self):
+class SimpleManager:
+    def __init__(self, own, memo_name):
+        self.own = own
+        self.memo_name = memo_name
         self.raw_counter = 0
         self.ent_counter = 0
 
     def update(self, protocol, memory, state):
         if state == 'RAW':
             self.raw_counter += 1
+            memory.reset()
         else:
             self.ent_counter += 1
 
+    def create_protocol(self, middle: str, other: str):
+        self.own.protocols = [EntanglementGenerationA(self.own, '%s.eg' % self.own.name, middle, other,
+                                                      self.own.components[self.memo_name])]
+
 
 class EntangleGenNode(Node):
     def __init__(self, name: str, tl: Timeline):
         super().__init__(name, tl)
-        self.memory = Memory('%s.memo'%name, tl, 0.9, 2000, 1, -1, 500)
-        self.memory.owner = self
-        self.resource_manager = SimpleManager()
-        self.protocols = []
+
+        memo_name = '%s.memo' % name
+        memory = Memory(memo_name, tl, 0.9, 2000, 1, -1, 500)
+        memory.add_receiver(self)
+        self.add_component(memory)
+
+        self.resource_manager = SimpleManager(self, memo_name)
+
+    def init(self):
+        memory = self.get_components_by_type("Memory")[0]
+        memory.reset()
 
     def receive_message(self, src: str, msg: "Message") -> None:
         self.protocols[0].received_message(src, msg)
 
-    def create_protocol(self, middle: str, other: str):
-        self.protocols = [EntanglementGenerationA(self, '%s.eg'%self.name, middle, other, self.memory)]
+    def get(self, photon, **kwargs):
+        self.send_qubit(kwargs['dst'], photon)
 
 
 def pair_protocol(node1: Node, node2: Node):
     p1 = node1.protocols[0]
     p2 = node2.protocols[0]
-    p1.set_others(p2.name, node2.name, [node2.memory.name])
-    p2.set_others(p1.name, node1.name, [node1.memory.name])
+    node1_memo_name = node1.get_components_by_type("Memory")[0].name
+    node2_memo_name = node2.get_components_by_type("Memory")[0].name
+    p1.set_others(p2.name, node2.name, [node2_memo_name])
+    p2.set_others(p1.name, node1.name, [node1_memo_name])
 
 
 tl = Timeline()
-tl.show_progress = False
 
 node1 = EntangleGenNode('node1', tl)
 node2 = EntangleGenNode('node2', tl)
@@ -51,7 +64,8 @@ def pair_protocol(node1: Node, node2: Node):
 node2.set_seed(1)
 bsm_node.set_seed(2)
 
-bsm_node.bsm.update_detectors_params('efficiency', 1)
+bsm = bsm_node.get_components_by_type("SingleAtomBSM")[0]
+bsm.update_detectors_params('efficiency', 1)
 
 qc1 = QuantumChannel('qc1', tl, attenuation=0, distance=1000)
 qc2 = QuantumChannel('qc2', tl, attenuation=0, distance=1000)
@@ -69,15 +83,18 @@ def pair_protocol(node1: Node, node2: Node):
 tl.init()
 for i in range(1000):
     tl.time = tl.now() + 1e11
-    node1.create_protocol('bsm_node', 'node2')
-    node2.create_protocol('bsm_node', 'node1')
+    node1.resource_manager.create_protocol('bsm_node', 'node2')
+    node2.resource_manager.create_protocol('bsm_node', 'node1')
     pair_protocol(node1, node2)
 
-    node1.memory.reset()
-    node2.memory.reset()
+    memory1 = node1.get_components_by_type("Memory")[0]
+    memory1.reset()
+    memory2 = node2.get_components_by_type("Memory")[0]
+    memory2.reset()
 
     node1.protocols[0].start()
     node2.protocols[0].start()
     tl.run()
 
+print("node1 entangled memories : available memories")
 print(node1.resource_manager.ent_counter, ':', node1.resource_manager.raw_counter)
diff --git a/docs/source/tutorial/chapter3/example2.py b/docs/source/tutorial/chapter3/example2.py
index da4c1b72..d57ac6a7 100644
--- a/docs/source/tutorial/chapter3/example2.py
+++ b/docs/source/tutorial/chapter3/example2.py
@@ -6,34 +6,43 @@
 from sequence.entanglement_management.purification import BBPSSW
 from sequence.message import Message
 
-class SimpleManager():
-    def __init__(self):
+
+class SimpleManager:
+    def __init__(self, own, kept_memo_name, meas_memo_name):
+        self.own = own
+        self.kept_memo_name = kept_memo_name
+        self.meas_memo_name = meas_memo_name
         self.raw_counter = 0
         self.ent_counter = 0
 
     def update(self, protocol, memory, state):
         if state == 'RAW':
             self.raw_counter += 1
+            memory.reset()
         else:
             self.ent_counter += 1
 
+    def create_protocol(self):
+        kept_memo = self.own.components[self.kept_memo_name]
+        meas_memo = self.own.components[self.meas_memo_name]
+        self.own.protocols = [BBPSSW(self.own, 'purification_protocol', kept_memo, meas_memo)]
+
 
 class PurifyNode(Node):
     def __init__(self, name: str, tl: Timeline):
         super().__init__(name, tl)
-        self.kept_memo = Memory('%s.kept_memo' % name, tl, 0.9, 2000, 1, -1, 500)
-        self.kept_memo.owner = self
-        self.meas_memo = Memory('%s.meas_memo' % name, tl, 0.9, 2000, 1, -1, 500)
-        self.meas_memo.owner = self
-        self.resource_manager = SimpleManager()
-        self.protocols = []
+        kept_memo_name = '%s.kept_memo' % name
+        meas_memo_name = '%s.meas_memo' % name
+        kept_memo = Memory('%s.kept_memo' % name, tl, 0.9, 2000, 1, -1, 500)
+        meas_memo = Memory('%s.meas_memo' % name, tl, 0.9, 2000, 1, -1, 500)
+        self.add_component(kept_memo)
+        self.add_component(meas_memo)
+
+        self.resource_manager = SimpleManager(self, kept_memo_name, meas_memo_name)
 
     def receive_message(self, src: str, msg: "Message") -> None:
         self.protocols[0].received_message(src, msg)
 
-    def create_protocol(self):
-        self.protocols = [BBPSSW(self, 'purification_protocol', self.kept_memo, self.meas_memo)]
-
 
 def entangle_memory(memo1: Memory, memo2: Memory, fidelity: float):
     memo1.reset()
@@ -50,14 +59,15 @@ def entangle_memory(memo1: Memory, memo2: Memory, fidelity: float):
 def pair_protocol(node1: Node, node2: Node):
     p1 = node1.protocols[0]
     p2 = node2.protocols[0]
-    p1.set_others(p2.name, node2.name,
-                  [node2.kept_memo.name, node2.meas_memo.name])
-    p2.set_others(p1.name, node1.name,
-                  [node1.kept_memo.name, node1.meas_memo.name])
+    kept_memo_1_name = node1.resource_manager.kept_memo_name
+    meas_memo_1_name = node1.resource_manager.meas_memo_name
+    kept_memo_2_name = node2.resource_manager.kept_memo_name
+    meas_memo_2_name = node2.resource_manager.meas_memo_name
+    p1.set_others(p2.name, node2.name, [kept_memo_2_name, meas_memo_2_name])
+    p2.set_others(p1.name, node1.name, [kept_memo_1_name, meas_memo_1_name])
 
 
 tl = Timeline()
-tl.show_progress = False
 
 node1 = PurifyNode('node1', tl)
 node2 = PurifyNode('node2', tl)
@@ -69,22 +79,26 @@ def pair_protocol(node1: Node, node2: Node):
 cc0.set_ends(node1, node2.name)
 cc1.set_ends(node2, node1.name)
 
+kept_memo_1 = node1.components[node1.resource_manager.kept_memo_name]
+kept_memo_2 = node2.components[node2.resource_manager.kept_memo_name]
+meas_memo_1 = node1.components[node1.resource_manager.meas_memo_name]
+meas_memo_2 = node2.components[node2.resource_manager.meas_memo_name]
+
+tl.init()
 for i in range(10):
-    entangle_memory(node1.kept_memo, node2.kept_memo, 0.9)
-    entangle_memory(node1.meas_memo, node2.meas_memo, 0.9)
+    entangle_memory(kept_memo_1, kept_memo_2, 0.9)
+    entangle_memory(meas_memo_1, meas_memo_2, 0.9)
 
-    node1.create_protocol()
-    node2.create_protocol()
+    node1.resource_manager.create_protocol()
+    node2.resource_manager.create_protocol()
 
     pair_protocol(node1, node2)
 
     node1.protocols[0].start()
     node2.protocols[0].start()
-
-    tl.init()
     tl.run()
 
-    print(node1.kept_memo.name, node1.kept_memo.entangled_memory,
-          node1.kept_memo.fidelity)
-    print(node1.meas_memo.name, node1.meas_memo.entangled_memory,
-          node1.meas_memo.fidelity)
+    print(kept_memo_1.name, kept_memo_1.entangled_memory,
+          kept_memo_1.fidelity)
+    print(meas_memo_1.name, meas_memo_1.entangled_memory,
+          meas_memo_1.fidelity)
diff --git a/docs/source/tutorial/chapter3/example3.py b/docs/source/tutorial/chapter3/example3.py
index a031273e..1760f70d 100644
--- a/docs/source/tutorial/chapter3/example3.py
+++ b/docs/source/tutorial/chapter3/example3.py
@@ -1,4 +1,3 @@
-from sequence.entanglement_management.entanglement_protocol import EntanglementProtocol
 from sequence.kernel.timeline import Timeline
 from sequence.topology.node import Node
 from sequence.components.memory import Memory
@@ -7,44 +6,54 @@
 from sequence.message import Message
 
 
-class SimpleManager():
-    def __init__(self):
+class SimpleManager:
+    def __init__(self, own, memo_names):
+        self.own = own
+        self.memo_names = memo_names
         self.raw_counter = 0
         self.ent_counter = 0
 
     def update(self, protocol, memory, state):
         if state == 'RAW':
             self.raw_counter += 1
-            memory.entangled_memory['node_id'] = None
-            memory.entangled_memory['memo_id'] = None
+            memory.reset()
         else:
             self.ent_counter += 1
 
+    def create_protocol(self):
+        if type(self.own) is SwapNodeA:
+            left_memo = self.own.components[self.memo_names[0]]
+            right_memo = self.own.components[self.memo_names[1]]
+            self.own.protocols = [EntanglementSwappingA(self.own, 'ESA', left_memo, right_memo, 1, 0.99)]
+        else:
+            memo = self.own.components[self.memo_names[0]]
+            self.own.protocols = [EntanglementSwappingB(self.own, '%s.ESB' % self.own.name, memo)]
+
 
 class SwapNodeA(Node):
     def __init__(self, name: str, tl: Timeline):
         super().__init__(name, tl)
-        self.left_memo = Memory('%s.left_memo' % name, tl, 0.9, 2000, 1, -1, 500)
-        self.left_memo.owner = self
-        self.right_memo = Memory('%s.right_memo' % name, tl, 0.9, 2000, 1, -1, 500)
-        self.right_memo.owner = self
-        self.resource_manager = SimpleManager()
-        self.protocols = []
+        left_memo_name = '%s.left_memo' % name
+        right_memo_name = '%s.right_memo' % name
+        left_memo = Memory(left_memo_name, tl, 0.9, 2000, 1, -1, 500)
+        right_memo = Memory(right_memo_name, tl, 0.9, 2000, 1, -1, 500)
+        self.add_component(left_memo)
+        self.add_component(right_memo)
+
+        self.resource_manager = SimpleManager(self, [left_memo_name, right_memo_name])
 
     def receive_message(self, src: str, msg: "Message") -> None:
         self.protocols[0].received_message(src, msg)
 
-    def create_protocol(self):
-        self.protocols = [EntanglementSwappingA(self, 'ESA', self.left_memo, self.right_memo, 1, 0.99)]
-
 
 class SwapNodeB(Node):
     def __init__(self, name: str, tl: Timeline):
         super().__init__(name, tl)
-        self.memo = Memory('%s.memo' % name, tl, 0.9, 2000, 1, -1, 500)
-        self.memo.owner = self
-        self.resource_manager = SimpleManager()
-        self.protocols = []
+        memo_name = '%s.memo' % name
+        memo = Memory(memo_name, tl, 0.9, 2000, 1, -1, 500)
+        self.add_component(memo)
+
+        self.resource_manager = SimpleManager(self, [memo_name])
 
     def receive_message(self, src: str, msg: "Message") -> None:
         self.protocols[0].received_message(src, msg)
@@ -70,15 +79,14 @@ def pair_protocol(node1, node2, node_mid):
     p2 = node2.protocols[0]
     pmid = node_mid.protocols[0]
     p1.set_others(pmid.name, node_mid.name,
-                  [node_mid.left_memo.name, node_mid.right_memo.name])
+                  [node_mid.resource_manager.memo_names[0], node_mid.resource_manager.memo_names[1]])
     p2.set_others(pmid.name, node_mid.name,
-                  [node_mid.left_memo.name, node_mid.right_memo.name])
-    pmid.set_others(p1.name, node1.name, [node1.memo.name])
-    pmid.set_others(p2.name, node2.name, [node2.memo.name])
+                  [node_mid.resource_manager.memo_names[0], node_mid.resource_manager.memo_names[1]])
+    pmid.set_others(p1.name, node1.name, [node1.resource_manager.memo_names[0]])
+    pmid.set_others(p2.name, node2.name, [node2.resource_manager.memo_names[0]])
 
 
 tl = Timeline()
-tl.show_progress = False
 
 left_node = SwapNodeB('left', tl)
 right_node = SwapNodeB('right', tl)
@@ -95,21 +103,25 @@ def pair_protocol(node1, node2, node_mid):
                               1000, 1e9)
         cc.set_ends(nodes[i], nodes[j].name)
 
-entangle_memory(left_node.memo, mid_node.left_memo, 0.9)
-entangle_memory(right_node.memo, mid_node.right_memo, 0.9)
+left_memo = left_node.components[left_node.resource_manager.memo_names[0]]
+right_memo = right_node.components[right_node.resource_manager.memo_names[0]]
+mid_left_memo = mid_node.components[mid_node.resource_manager.memo_names[0]]
+mid_right_memo = mid_node.components[mid_node.resource_manager.memo_names[1]]
+entangle_memory(left_memo, mid_left_memo, 0.9)
+entangle_memory(right_memo, mid_right_memo, 0.9)
 
 for node in nodes:
-    node.create_protocol()
+    node.resource_manager.create_protocol()
 
 pair_protocol(left_node, right_node, mid_node)
-for node in nodes:
-    node.protocols[0].start()
 
 tl.init()
+for node in nodes:
+    node.protocols[0].start()
 tl.run()
 
-print(left_node.memo.entangled_memory)
-print(mid_node.left_memo.entangled_memory)
-print(mid_node.right_memo.entangled_memory)
-print(right_node.memo.entangled_memory)
-print(left_node.memo.fidelity)
+print(left_memo.entangled_memory)
+print(mid_left_memo.entangled_memory)
+print(mid_right_memo.entangled_memory)
+print(right_memo.entangled_memory)
+print(left_memo.fidelity)
diff --git a/docs/source/tutorial/chapter4/resource_management.md b/docs/source/tutorial/chapter4/resource_management.md
index ba795281..6625548c 100644
--- a/docs/source/tutorial/chapter4/resource_management.md
+++ b/docs/source/tutorial/chapter4/resource_management.md
@@ -16,11 +16,17 @@ The Resource Management module in SeQUeNCe is responsible for managing the usage
 
 ![rm](figures/resource_management.png)
 
-The memory manager is used to track the current state of all quantum memories (including entanglement, fidelity, etc.) while thie rule manager uses a set of “rules” (containing a priority, condition to be met, and action to take) to direct local protocols.
+The memory manager is used to track the current state of all quantum memories (including entanglement, fidelity, etc.) while the rule manager uses a set of “rules” (containing a priority, condition to be met, and action to take) to direct local protocols.
 
 ### Step 1: Create custom node type
 
-We will first create a custom node type based on the `QuantumRouter` node from SeQUeNCe. Our node will need to include the memory array hardware and a resource manager, as well as some functions to handle normal operations of the node. The first function we will need to edit is the `received_message` method, similar to other tutorials. In this case we will need to add a line to handle messages for the resource manager. We will also need a method `get_idle_memories`, used by the resource manager to interact with any applications present (see tutorial 6). In our case, we can leave this method empty.
+We will first create a custom node type based on the `QuantumRouter` node from SeQUeNCe.
+Our node will need to include the memory array hardware and a resource manager, as well as some functions to handle normal operations of the node.
+The first function we will need to edit is the `received_message` method, similar to other tutorials.
+In this case we will need to add a line to handle messages for the resource manager.
+We will also need a method `get_idle_memories`, used by the resource manager to interact with any applications present (see tutorial 6).
+In our case, we can leave this method empty.
+Finally, we modify the `get` method as in previous tutorials to direct photons from the memory array.
 
 ```python
 from sequence.topology.node import Node
@@ -30,9 +36,12 @@ from sequence.resource_management.resource_manager import ResourceManager
 class RouterNode(Node):
     def __init__(self, name, tl, memo_size=50):
         super().__init__(name, tl)
-        self.memory_array = MemoryArray(name + ".MemoryArray", tl, num_memories=memo_size)
-        self.memory_array.owner = self
-        self.resource_manager = ResourceManager(self)
+        memory_array_name = name + ".MemoryArray"
+        memory_array = MemoryArray(memory_array_name, tl, num_memories=memo_size)
+        memory_array.add_receiver(self)
+        self.add_component(memory_array)
+
+        self.resource_manager = ResourceManager(self, memory_array_name)
 
     def receive_message(self, src: str, msg: "Message") -> None:
         if msg.receiver == "resource_manager":
@@ -50,6 +59,10 @@ class RouterNode(Node):
 
     def get_idle_memory(self, info: "MemoryInfo") -> None:
         pass
+
+    def get(self, photon: "Photon", **kwargs):
+        dst = kwargs['dst']
+        self.send_qubit(dst, photon)
 ```
 
 ### Step 2: Create Rule Conditions for Flow 1
@@ -75,7 +88,7 @@ def eg_rule_condition(memory_info: "MemoryInfo", manager: "MemoryManager", args)
 Rule conditions take 3 arguments:
 
 - `memory_info`, containing the information stored in the memory manager,
-- `manager`, which is a reference to the memory manager, annd
+- `manager`, which is a reference to the memory manager, and
 - `args`, which is a dictionary to store values of other arguments, like `index_upper` and `index_lower`
 
 In our case, we will use any memories in the `RAW` (unentangled) state. Other possible memory states are `OCCUPIED` (for
@@ -88,8 +101,8 @@ We will create a variable to store these when we load the rule into the rule man
 
 ### Step 3: Create Rule Actions for Flow 1
 
-Next, we will dictate what the resource manager should do when our custom condition is met. This is performed with a
-rule action.
+Next, we will dictate what the resource manager should do when our custom condition is met.
+This is performed with a rule action.
 
 Rule actions are provided one argument by the resource manager: a list of `memory_info` objects that have met the
 condition of the rule. They should return four objects:
@@ -99,16 +112,14 @@ condition of the rule. They should return four objects:
 3. Requirements that should be met on other nodes, and
 4. Arguments for the requirements function.
 
-In our case, we will create an entanglement generation protocol on both nodes and have router 1 confirm that router 2
-has created a protocol. `eg_rule_action1` will give the action for router 1 (including sending a requirement to router
-2), and `eg_rule_action2` will give the action for router 2. The action on router 1 will create
-the `EntanglementGenerationA` protocol and request the corresponding protocol from router 2 by sending the
-function `eg_req_func` and its arguments `req_args`. The `req_args` contain constraints about the target protocol. In
-this example, we use the type of protocol and memory index to filter the proper protocol. The action on router 2 will
-create an `EntanglementGenerationA` protocol and wait for a request from router 1. The `mid_name`
-and `other_name` in `args` define the name of BSM node and the other router used for generating entanglement. Note that
-the name of router 1 will be `“r1”`, the name of router 2 will be `“r2”`, and the name of the intermediate measurement
-node (see tutorial 3 for details) will be `“m12”`, which will be set when loading rules.
+In our case, we will create an entanglement generation protocol on both nodes and have router 1 confirm that router 2  has created a protocol.
+`eg_rule_action1` will give the action for router 1 (including sending a requirement to router  2), and `eg_rule_action2` will give the action for router 2.
+The action on router 1 will create the `EntanglementGenerationA` protocol and request the corresponding protocol from router 2 by sending the function `eg_req_func` and its arguments `req_args`.
+The `req_args` contain constraints about the target protocol.
+In this example, we use the type of protocol and memory index to filter the proper protocol.
+The action on router 2 will create an `EntanglementGenerationA` protocol and wait for a request from router 1.
+The `mid_name` and `other_name` in `args` define the name of BSM node and the other router used for generating entanglement.
+Note that the name of router 1 will be `“r1”`, the name of router 2 will be `“r2”`, and the name of the intermediate measurement node (see tutorial 3 for details) will be `“m12”`, which will be set when loading rules.
 
 ```python
 from sequence.entanglement_management.generation import EntanglementGenerationA
@@ -117,12 +128,13 @@ def eg_req_func(protocols, args):
     remote_node = args["remote_node"]
     index_upper = args["index_upper"]
     index_lower = args["index_lower"]
+
     for protocol in protocols:
         if not isinstance(protocol, EntanglementGenerationA):
             continue
-        index_func = protocol.memory.owner.memory_array.memories.index
-        if protocol.remote_node_name == remote_node \
-                and index_lower <= index_func(protocol.memory) <= index_upper:
+        mem_arr = protocol.own.get_components_by_type("MemoryArray")[0]
+        if protocol.remote_node_name == remote_node and \
+                index_lower <= mem_arr.memories.index(protocol.memory) <= index_upper:
             return protocol
 
 
@@ -153,11 +165,8 @@ def eg_rule_action2(memories_info: List["MemoryInfo"], args):
 
 ### Step 4: Build the Network
 
-We’ll now actually build our network and get ready to run the simulation. We will include the third router node, as this
-will be used for our second flow, but we will not use it in our first experiment. Also note that we have left the
-timeline `show_progress` attribute as its default true value, unlike previous tutorials. This will show a progress bar
-of the simulation during execution. This is most useful for longer experiments, where a user might wish to estimate the
-time remaining for a simulation.
+We’ll now actually build our network and get ready to run the simulation.
+We will include the third router node, as this will be used for our second flow, but we will not use it in our first experiment:
 
 ```python
 from sequence.kernel.timeline import Timeline
@@ -183,7 +192,7 @@ for i, node in enumerate(node_list):
 cc_delay = 1e9
 for node1 in node_list:
     for node2 in node_list:
-        cc = ClassicalChannel("cc_%s_%s"%(node1.name, node2.name), tl, 1e3, delay=cc_delay)
+        cc = ClassicalChannel("cc_%s_%s" % (node1.name, node2.name), tl, 1e3, delay=cc_delay)
         cc.set_ends(node1, node2.name)
 
 # create quantum channels linking r1 and r2 to m1
@@ -217,12 +226,10 @@ tl.init()
 action_args = {"mid_name": "m12", "other_name": "r2", "node_name": "r1",
                "index_upper": 9, "index_lower": 0}
 condition_args = {"index_lower": 0, "index_upper": 9}
-rule1 = Rule(10, eg_rule_action1, eg_rule_condition, action_args,
-             condition_args)
+rule1 = Rule(10, eg_rule_action1, eg_rule_condition, action_args, condition_args)
 r1.resource_manager.load(rule1)
 action_args2 = {"mid_name": "m12", "other_name": "r1"}
-rule2 = Rule(10, eg_rule_action2, eg_rule_condition, action_args2,
-             condition_args)
+rule2 = Rule(10, eg_rule_action2, eg_rule_condition, action_args2, condition_args)
 r2.resource_manager.load(rule2)
 
 tl.run()
@@ -244,7 +251,9 @@ We should notice that all of the memories 0-9 are entangled with router 2 within
 
 ### Step 6: Flow 2 Entanglement Generation
 
-Now, we'll create the custom rules to control our second flow between routers 1 and 3. The first step is again to set up entanglement generation in a similar manner to the first flow. To do this, we'll create a function `add_eg_rules` that takes as arguments
+Now, we'll create the custom rules to control our second flow between routers 1 and 3.
+The first step is again to set up entanglement generation in a similar manner to the first flow.
+To do this, we'll create a function `add_eg_rules` that takes as arguments
 - `index`, the index of the router (in the path) we will be adding rules to,
 - `path`, a list of router nodes that make up the path of the entanglement flow, and
 - `middles`, a list of bsm nodes along the path.
@@ -309,74 +318,75 @@ The arguments for our `add_ep_rules` function will be similar to our previous fu
 ```python
 from sequence.entanglement_management.purification import BBPSSW
 
-def add_ep_rules(index: int, path: List[RouterNode], target_fidelity: float):
-    node_names = [node.name for node in path]
-    node_mems = [[10, 20], [10, 30], [0, 10]]
+def ep_rule_condition1(memory_info: "MemoryInfo", manager: "MemoryManager",
+                       args):
+    index_upper = args["index_upper"]
+    index_lower = args["index_lower"]
+    target_fidelity = args["target_fidelity"]
+    if (index_lower <= memory_info.index <= index_upper
+            and memory_info.state == "ENTANGLED"
+            and memory_info.fidelity < target_fidelity):
+        for info in manager:
+            if (info != memory_info
+                    and index_lower <= info.index <= index_upper
+                    and info.state == "ENTANGLED"
+                    and info.remote_node == memory_info.remote_node
+                    and info.fidelity == memory_info.fidelity):
+                assert memory_info.remote_memo != info.remote_memo
+                return [memory_info, info]
+    return []
+
+def ep_req_func(protocols, args):
+    remote1 = args["remote1"]
+    remote2 = args["remote2"]
+
+    _protocols = []
+    for protocol in protocols:
+        if not isinstance(protocol, BBPSSW):
+            continue
 
-    node = path[index]
-    mem_range = node_mems[index]
+        if protocol.kept_memo.name == remote1:
+            _protocols.insert(0, protocol)
+        if protocol.kept_memo.name == remote2:
+            _protocols.insert(1, protocol)
 
-    if index > 0:
-        def ep_rule_condition(memory_info: "MemoryInfo", manager: "MemoryManager"):
-            if (memory_info.index in range(mem_range[0], mem_range[1]) and memory_info.state == "ENTANGLED" and memory_info.fidelity < target_fidelity):
-                for info in manager:
-                    if (info != memory_info and info.index in range(mem_range[0], mem_range[1])[:10]
-                            and info.state == "ENTANGLED" and info.remote_node == memory_info.remote_node
-                            and info.fidelity == memory_info.fidelity):
-                        assert memory_info.remote_memo != info.remote_memo
-                        return [memory_info, info]
-            return []
+    if len(_protocols) != 2:
+        return None
 
-        def ep_rule_action(memories_info: List["MemoryInfo"]):
-            memories = [info.memory for info in memories_info]
+    protocols.remove(_protocols[1])
+    _protocols[1].rule.protocols.remove(_protocols[1])
+    _protocols[1].kept_memo.detach(_protocols[1])
+    _protocols[0].meas_memo = _protocols[1].kept_memo
+    _protocols[0].memories = [_protocols[0].kept_memo,
+                              _protocols[0].meas_memo]
+    _protocols[0].name = _protocols[0].name + "." + _protocols[
+        0].meas_memo.name
+    _protocols[0].meas_memo.attach(_protocols[0])
 
-            def req_func(protocols):
-                _protocols = []
-                for protocol in protocols:
-                    if not isinstance(protocol, BBPSSW):
-                        continue
-
-                    if protocol.kept_memo.name == memories_info[0].remote_memo:
-                        _protocols.insert(0, protocol)
-                    if protocol.kept_memo.name == memories_info[1].remote_memo:
-                        _protocols.insert(1, protocol)
-
-                if len(_protocols) != 2:
-                    return None
-
-                protocols.remove(_protocols[1])
-                _protocols[1].rule.protocols.remove(_protocols[1])
-                _protocols[1].kept_memo.detach(_protocols[1])
-                _protocols[0].meas_memo = _protocols[1].kept_memo
-                _protocols[0].memories = [_protocols[0].kept_memo, _protocols[0].meas_memo]
-                _protocols[0].name = _protocols[0].name + "." + _protocols[0].meas_memo.name
-                _protocols[0].meas_memo.attach(_protocols[0])
-                _protocols[0].t0 = _protocols[0].kept_memo.timeline.now()
-
-                return _protocols[0]
-
-            name = "EP.%s.%s" % (memories[0].name, memories[1].name)
-            protocol = BBPSSW(None, name, memories[0], memories[1])
-            dsts = [memories_info[0].remote_node]
-            req_funcs = [req_func]
-            return [protocol, dsts, req_funcs]
+    return _protocols[0]
 
-        rule = Rule(10, ep_rule_action, ep_rule_condition)
-        node.resource_manager.load(rule)
 
-    if index < len(path) - 1:
-        if index == 0:
-            def ep_rule_condition(memory_info: "MemoryInfo", manager: "MemoryManager"):
-                if (memory_info.index in range(mem_range[0], mem_range[1])
-                        and memory_info.state == "ENTANGLED" and memory_info.fidelity < target_fidelity):
-                    return [memory_info]
-                return []
-        else:
-            def ep_rule_condition(memory_info: "MemoryInfo", manager: "MemoryManager"):
-                if (memory_info.index in range(mem_range[0], mem_range[1])[10:]
-                        and memory_info.state == "ENTANGLED" and memory_info.fidelity < target_fidelity):
-                    return [memory_info]
-                return []
+def ep_rule_action1(memories_info: List["MemoryInfo"], args):
+    memories = [info.memory for info in memories_info]
+    name = "EP.%s.%s" % (memories[0].name, memories[1].name)
+    protocol = BBPSSW(None, name, memories[0], memories[1])
+    dsts = [memories_info[0].remote_node]
+    req_funcs = [ep_req_func]
+    req_args = {"remote1": memories_info[0].remote_memo,
+                "remote2": memories_info[1].remote_memo}
+    return [protocol, dsts, req_funcs, [req_args]]
+
+
+def ep_rule_condition2(memory_info: "MemoryInfo", manager: "MemoryManager",
+                       args):
+    index_upper = args["index_upper"]
+    index_lower = args["index_lower"]
+    target_fidelity = args["target_fidelity"]
+    if (index_lower <= memory_info.index <= index_upper
+            and memory_info.state == "ENTANGLED"
+            and memory_info.fidelity < target_fidelity):
+        return [memory_info]
+    return []
 
         def ep_rule_action(memories_info: List["MemoryInfo"]):
             memories = [info.memory for info in memories_info]
@@ -386,130 +396,123 @@ def add_ep_rules(index: int, path: List[RouterNode], target_fidelity: float):
 
         rule = Rule(10, ep_rule_action, ep_rule_condition)
         node.resource_manager.load(rule)
-```
-
-### Step 8: Flow 2 Entanglement Swapping
-Finally, we'll generate rules for entanglement swapping. The rules will be a little more complicated as we require different protocols for the intermediate and end routers (see tutorial 3). However, the basic principles of rule creation remain the same, requiring a condition and an action. Our condition will be that we have memory pairs in the `ENTANGLED` state between the intermediate and each end router, and that the fidelity of the memory state is above some threshold. Our arguments will thus be as follows:
-- `index`, the index of the router (in the path) we will be adding rules to,
-- `path`, a list of router nodes that make up the path of the entanglement flow,
-- `target_fidelity`, the fidelity of entanglement we wish to achieve,
-- `succ_prob`, the probability of success for the swapping operation, and
-- `degredation`, the degradation of entanglement fidelity after the swapping operation.
-
-Also note that the code to generate swapping rules is much longer than our previous rules. Luckily, this will be done automatically by the network manager in future tutorials.
 
-```python
-from sequence.entanglement_management.swapping import EntanglementSwappingA, EntanglementSwappingB
 
-def add_es_rules(index: int, path: List[RouterNode], target_fidelity: float, succ_prob: float, degradation: float):
-    node_names = [node.name for node in path]
+def add_ep_rules(index: int, path: List[RouterNode], target_fidelity: float):
     node_mems = [[10, 20], [10, 30], [0, 10]]
 
     node = path[index]
     mem_range = node_mems[index]
 
-    def es_rule_actionB(memories_info: List["MemoryInfo"]):
-        memories = [info.memory for info in memories_info]
-        memory = memories[0]
-        protocol = EntanglementSwappingB(None, "ESB." + memory.name, memory)
-        return [protocol, [None], [None]]
-
-    if index == 0:
-        def es_rule_condition(memory_info: "MemoryInfo", manager: "MemoryManager"):
-            if (memory_info.state == "ENTANGLED"
-                    and memory_info.index in range(mem_range[0], mem_range[1])
-                    and memory_info.remote_node != node_names[-1]
-                    and memory_info.fidelity >= target_fidelity):
-                return [memory_info]
-            else:
-                return []
+    if index > 0:
+        condition_args = {"index_lower": mem_range[0],
+                          "index_upper": mem_range[1] - 1,
+                          "target_fidelity": target_fidelity}
 
-        rule = Rule(10, es_rule_actionB, es_rule_condition)
+        rule = Rule(10, ep_rule_action1, ep_rule_condition1, {}, condition_args)
         node.resource_manager.load(rule)
 
-    elif index == len(path) - 1:
-        def es_rule_condition(memory_info: "MemoryInfo", manager: "MemoryManager"):
-            if (memory_info.state == "ENTANGLED"
-                    and memory_info.index in range(mem_range[0], mem_range[1])
-                    and memory_info.remote_node != node_names[0]
-                    and memory_info.fidelity >= target_fidelity):
-                return [memory_info]
-            else:
-                return []
+    if index < len(path) - 1:
+        if index == 0:
+            condition_args = {"index_lower": mem_range[0],
+                              "index_upper": mem_range[1] - 1,
+                              "target_fidelity": target_fidelity}
+        else:
+            condition_args = {"index_lower": mem_range[1] - 10,
+                              "index_upper": mem_range[1] - 1,
+                              "target_fidelity": target_fidelity}
 
-        rule = Rule(10, es_rule_actionB, es_rule_condition)
+        rule = Rule(10, ep_rule_action2, ep_rule_condition2, {}, condition_args)
         node.resource_manager.load(rule)
+```
 
-    else:
-        _path = node_names[:]
-        while _path.index(node.name) % 2 == 0:
-            new_path = []
-            for i, n in enumerate(_path):
-                if i % 2 == 0 or i == len(path) - 1:
-                    new_path.append(n)
-            _path = new_path
-        _index = _path.index(node.name)
-        left, right = _path[_index - 1], _path[_index + 1]
-
-        def es_rule_conditionA(memory_info: "MemoryInfo", manager: "MemoryManager"):
-            if (memory_info.state == "ENTANGLED"
-                    and memory_info.index in range(mem_range[0], mem_range[1])
-                    and memory_info.remote_node == left
-                    and memory_info.fidelity >= target_fidelity):
-                for info in manager:
-                    if (info.state == "ENTANGLED"
-                            and info.index in range(mem_range[0], mem_range[1])
-                            and info.remote_node == right
-                            and info.fidelity >= target_fidelity):
-                        return [memory_info, info]
-            elif (memory_info.state == "ENTANGLED"
-                  and memory_info.index in range(mem_range[0], mem_range[1])
-                  and memory_info.remote_node == right
-                  and memory_info.fidelity >= target_fidelity):
-                for info in manager:
-                    if (info.state == "ENTANGLED"
-                            and info.index in range(mem_range[0], mem_range[1])
-                            and info.remote_node == left
-                            and info.fidelity >= target_fidelity):
-                        return [memory_info, info]
-            return []
-
-        def es_rule_actionA(memories_info: List["MemoryInfo"]):
-            memories = [info.memory for info in memories_info]
+### Step 8: Flow 2 Entanglement Swapping
+Finally, we'll generate rules for entanglement swapping.
+The rules will be a little more complicated as we require different protocols for the intermediate and end routers (see tutorial 3).
+However, the basic principles of rule creation remain the same, requiring a condition, an action, and args.
+Our condition will be that we have memory pairs in the `ENTANGLED` state between the intermediate and each end router, and that the fidelity of the memory state is above some threshold.
+For now, we will only define the actions and conditions and leave the rule creation for the script.
 
-            def req_func1(protocols):
-                for protocol in protocols:
-                    if (isinstance(protocol, EntanglementSwappingB)
-                            and protocol.memory.name == memories_info[0].remote_memo):
-                        return protocol
-
-            def req_func2(protocols):
-                for protocol in protocols:
-                    if (isinstance(protocol, EntanglementSwappingB)
-                            and protocol.memory.name == memories_info[1].remote_memo):
-                        return protocol
-
-            protocol = EntanglementSwappingA(None, "ESA.%s.%s" % (memories[0].name, memories[1].name),
-                                             memories[0], memories[1],
-                                             success_prob=succ_prob, degradation=degradation)
-            dsts = [info.remote_node for info in memories_info]
-            req_funcs = [req_func1, req_func2]
-            return [protocol, dsts, req_funcs]
-
-        rule = Rule(10, es_rule_actionA, es_rule_conditionA)
-        node.resource_manager.load(rule)
+```python
+from sequence.entanglement_management.swapping import EntanglementSwappingA, EntanglementSwappingB
 
-        def es_rule_conditionB(memory_info: "MemoryInfo", manager: "MemoryManager") -> List["MemoryInfo"]:
-            if (memory_info.state == "ENTANGLED"
-                    and memory_info.index in range(mem_range[0], mem_range[1])
-                    and memory_info.remote_node not in [left, right]
-                    and memory_info.fidelity >= target_fidelity):
-                return [memory_info]
-            else:
-                return []
+def es_rule_conditionA(memory_info: "MemoryInfo", manager: "MemoryManager",
+                       args):
+    index_lower = args["index_lower"]
+    index_upper = args["index_upper"]
+    target_fidelity = args["target_fidelity"]
+    left, right = args["left"], args["right"]
+
+    if (memory_info.state == "ENTANGLED"
+            and memory_info.index in range(index_lower, index_upper)
+            and memory_info.remote_node == left
+            and memory_info.fidelity >= target_fidelity):
+        for info in manager:
+            if (info.state == "ENTANGLED"
+                    and info.index in range(index_lower, index_upper)
+                    and info.remote_node == right
+                    and info.fidelity >= target_fidelity):
+                return [memory_info, info]
+    elif (memory_info.state == "ENTANGLED"
+          and memory_info.index in range(index_lower, index_upper)
+          and memory_info.remote_node == right
+          and memory_info.fidelity >= target_fidelity):
+        for info in manager:
+            if (info.state == "ENTANGLED"
+                    and info.index in range(index_lower, index_upper)
+                    and info.remote_node == left
+                    and info.fidelity >= target_fidelity):
+                return [memory_info, info]
+    return []
+
+
+def es_req_func(protocols, args):
+    target_memo = args["target_memo"]
+    for protocol in protocols:
+        if (isinstance(protocol, EntanglementSwappingB)
+                and protocol.memory.name == target_memo):
+            return protocol
 
-        rule = Rule(10, es_rule_actionB, es_rule_conditionB)
-        node.resource_manager.load(rule)
+
+def es_rule_actionA(memories_info: List["MemoryInfo"], args):
+    succ_prob = args["succ_prob"]
+    degradation = args["degradation"]
+
+    memories = [info.memory for info in memories_info]
+
+    protocol = EntanglementSwappingA(None, "ESA.%s.%s" % (
+        memories[0].name, memories[1].name),
+                                     memories[0], memories[1],
+                                     success_prob=succ_prob,
+                                     degradation=degradation)
+    dsts = [info.remote_node for info in memories_info]
+    req_funcs = [es_req_func, es_req_func]
+    req_args = [{"target_memo": memories_info[0].remote_memo},
+                {"target_memo": memories_info[1].remote_memo}]
+    return [protocol, dsts, req_funcs, req_args]
+
+
+def es_rule_conditionB(memory_info: "MemoryInfo", manager: "MemoryManager",
+                       args):
+    index_lower = args["index_lower"]
+    index_upper = args["index_upper"]
+    target_node = args["target_node"]
+    target_fidelity = args["target_fidelity"]
+
+    if (memory_info.state == "ENTANGLED"
+            and memory_info.index in range(index_lower, index_upper)
+            and memory_info.remote_node != target_node
+            and memory_info.fidelity >= target_fidelity):
+        return [memory_info]
+    else:
+        return []
+
+
+def es_rule_actionB(memories_info: List["MemoryInfo"], args):
+    memories = [info.memory for info in memories_info]
+    memory = memories[0]
+    protocol = EntanglementSwappingB(None, "ESB." + memory.name, memory)
+    return [protocol, [None], [None], [None]]
 ```
 
 ### Step 9: Run the Simulation Again
@@ -519,16 +522,42 @@ We will now run the simulation again, adding in our new custom rules for the sec
 ```python
 tl.init()
 
-# load rules
-rule1 = Rule(10, eg_rule_action_f1_1, eg_rule_condition_f1)
+# load rules for flow 1
+action_args = {"mid_name": "m12", "other_name": "r2", "node_name": "r1",
+               "index_upper": 9, "index_lower": 0}
+condition_args = {"index_lower": 0, "index_upper": 9}
+rule1 = Rule(10, eg_rule_action1, eg_rule_condition, action_args, condition_args)
 r1.resource_manager.load(rule1)
-rule2 = Rule(10, eg_rule_action_f1_2, eg_rule_condition_f1)
+action_args2 = {"mid_name": "m12", "other_name": "r1"}
+rule2 = Rule(10, eg_rule_action2, eg_rule_condition, action_args2, condition_args)
 r2.resource_manager.load(rule2)
 
+# load rules for flow 2
 for i in range(3):
     add_eg_rules(i, [r1, r2, r3], [m12, m23])
     add_ep_rules(i, [r1, r2, r3], 0.9)
-    add_es_rules(i, [r1, r2, r3], 0.9, 1, 1)
+
+condition_args = {"index_lower": 10,
+                  "index_upper": 20,
+                  "target_node": r3.name,
+                  "target_fidelity": 0.9}
+rule = Rule(10, es_rule_actionB, es_rule_conditionB, {}, condition_args)
+r1.resource_manager.load(rule)
+
+condition_args = {"index_lower": 0,
+                  "index_upper": 10,
+                  "target_node": r1.name,
+                  "target_fidelity": 0.9}
+rule = Rule(10, es_rule_actionB, es_rule_conditionB, {}, condition_args)
+r3.resource_manager.load(rule)
+
+action_args = {"succ_prob": 1, "degradation": 1}
+condition_args = {"index_lower": 10,
+                  "index_upper": 30,
+                  "target_fidelity": 0.9,
+                  "left": r1.name, "right": r3.name}
+rule = Rule(10, es_rule_actionA, es_rule_conditionA, action_args, condition_args)
+r2.resource_manager.load(rule)
 
 tl.run()
 
diff --git a/docs/source/tutorial/chapter4/two_flow.py b/docs/source/tutorial/chapter4/two_flow.py
index 1b6c83a5..ce0acced 100644
--- a/docs/source/tutorial/chapter4/two_flow.py
+++ b/docs/source/tutorial/chapter4/two_flow.py
@@ -18,17 +18,19 @@
 class RouterNode(Node):
     def __init__(self, name, tl, memo_size=50):
         super().__init__(name, tl)
-        self.memory_array = MemoryArray(name + ".MemoryArray", tl, num_memories=memo_size)
-        self.memory_array.owner = self
-        self.resource_manager = ResourceManager(self)
+        memory_array_name = name + ".MemoryArray"
+        memory_array = MemoryArray(memory_array_name, tl, num_memories=memo_size)
+        memory_array.add_receiver(self)
+        self.add_component(memory_array)
+
+        self.resource_manager = ResourceManager(self, memory_array_name)
 
     def receive_message(self, src: str, msg: "Message") -> None:
         if msg.receiver == "resource_manager":
             self.resource_manager.received_message(src, msg)
         else:
             if msg.receiver is None:
-                matching = [p for p in self.protocols if
-                            type(p) == msg.protocol_type]
+                matching = [p for p in self.protocols if type(p) == msg.protocol_type]
                 for p in matching:
                     p.received_message(src, msg)
             else:
@@ -40,13 +42,16 @@ def receive_message(self, src: str, msg: "Message") -> None:
     def get_idle_memory(self, info: "MemoryInfo") -> None:
         pass
 
+    def get(self, photon: "Photon", **kwargs):
+        dst = kwargs['dst']
+        self.send_qubit(dst, photon)
+
 
 ## flow 1
 
 # entanglement generation
 
-def eg_rule_condition(memory_info: "MemoryInfo", manager: "MemoryManager",
-                      args):
+def eg_rule_condition(memory_info: "MemoryInfo", manager: "MemoryManager", args):
     index_upper = args["index_upper"]
     index_lower = args["index_lower"]
     if memory_info.state == "RAW" \
@@ -60,13 +65,13 @@ def eg_req_func(protocols, args):
     remote_node = args["remote_node"]
     index_upper = args["index_upper"]
     index_lower = args["index_lower"]
+
     for protocol in protocols:
         if not isinstance(protocol, EntanglementGenerationA):
             continue
-        index_func = protocol.memory.owner.memory_array.memories.index
-        if protocol.remote_node_name == remote_node \
-                and index_lower <= index_func(
-            protocol.memory) <= index_upper:
+        mem_arr = protocol.own.get_components_by_type("MemoryArray")[0]
+        if protocol.remote_node_name == remote_node and \
+                index_lower <= mem_arr.memories.index(protocol.memory) <= index_upper:
             return protocol
 
 
@@ -97,6 +102,8 @@ def eg_rule_action2(memories_info: List["MemoryInfo"], args):
 
 ## flow 2
 
+# entanglement generation
+
 def add_eg_rules(index: int, path: List[RouterNode], middles: List[BSMNode]):
     assert len(path) == len(middles) + 1
     node_names = [node.name for node in path]
@@ -136,6 +143,7 @@ def add_eg_rules(index: int, path: List[RouterNode], middles: List[BSMNode]):
 
 
 # entanglement purification
+
 def ep_rule_condition1(memory_info: "MemoryInfo", manager: "MemoryManager",
                        args):
     index_upper = args["index_upper"]
@@ -154,7 +162,6 @@ def ep_rule_condition1(memory_info: "MemoryInfo", manager: "MemoryManager",
                 return [memory_info, info]
     return []
 
-
 def ep_req_func(protocols, args):
     remote1 = args["remote1"]
     remote2 = args["remote2"]
@@ -226,8 +233,7 @@ def add_ep_rules(index: int, path: List[RouterNode], target_fidelity: float):
                           "index_upper": mem_range[1] - 1,
                           "target_fidelity": target_fidelity}
 
-        rule = Rule(10, ep_rule_action1, ep_rule_condition1, {},
-                    condition_args)
+        rule = Rule(10, ep_rule_action1, ep_rule_condition1, {}, condition_args)
         node.resource_manager.load(rule)
 
     if index < len(path) - 1:
@@ -240,8 +246,7 @@ def add_ep_rules(index: int, path: List[RouterNode], target_fidelity: float):
                               "index_upper": mem_range[1] - 1,
                               "target_fidelity": target_fidelity}
 
-        rule = Rule(10, ep_rule_action2, ep_rule_condition2, {},
-                    condition_args)
+        rule = Rule(10, ep_rule_action2, ep_rule_condition2, {}, condition_args)
         node.resource_manager.load(rule)
 
 
@@ -393,7 +398,6 @@ def es_rule_actionB(memories_info: List["MemoryInfo"], args):
                       "index_upper": 10,
                       "target_node": r1.name,
                       "target_fidelity": 0.9}
-
     rule = Rule(10, es_rule_actionB, es_rule_conditionB, {}, condition_args)
     r3.resource_manager.load(rule)
 
@@ -402,9 +406,7 @@ def es_rule_actionB(memories_info: List["MemoryInfo"], args):
                       "index_upper": 30,
                       "target_fidelity": 0.9,
                       "left": r1.name, "right": r3.name}
-
-    rule = Rule(10, es_rule_actionA, es_rule_conditionA, action_args,
-                condition_args)
+    rule = Rule(10, es_rule_actionA, es_rule_conditionA, action_args, condition_args)
     r2.resource_manager.load(rule)
 
     tl.run()
diff --git a/docs/source/tutorial/chapter5/network_manager.md b/docs/source/tutorial/chapter5/network_manager.md
index 67653b15..3d6cb505 100644
--- a/docs/source/tutorial/chapter5/network_manager.md
+++ b/docs/source/tutorial/chapter5/network_manager.md
@@ -9,7 +9,7 @@ To achieve this, we will be using an example json file to build our network and
 
 ![topo](figures/star_network.png)
 
-This example is similar to the example jupyter notebook script `random_request_network.ipynb`, without applications and with slightly different paramters. The notebook can be viewed in the `example` directory of the SeQUeNCe download, including already written code and interactive input.
+This example is similar to the example jupyter notebook script `random_request_network.ipynb`, without applications and with slightly different parameters. The notebook can be viewed in the `example` directory of the SeQUeNCe download, including already written code and interactive input.
 
 ### Background
 
@@ -34,8 +34,8 @@ def NewNetworkManager(owner: "QuantumRouter") -> "NetworkManager":
 
 ### Step 1: Create the Network Configuration File
 
-For this example, we will be using a json file to specify the nodes and connectivity of the network. The json file
-should be structured as a dictionary with the following keys:
+For this example, we will be using a json file to specify the nodes and connectivity of the network.
+The json file should be structured as a dictionary with the following keys:
 
 - `is_parallel`, denoting if it's a parallel or sequential simulation
 - `stop_time`, the stop time of simulation
@@ -54,51 +54,54 @@ For this simulation, we use sequential simulation to simulate 2 seconds of the n
 "stop_time": 2000000000000
 ```
 
-Then, we make the `nodes` entry. All fields of the list will have the `name` field required by the node constructor and
-a specification of the node type, along with any more arguments (optional or not) specified by the specific node type.
-We will be using the `QuantumRouter` node type for this tutorial, which already includes all of the necessary hardware
-for entanglement distribution as well as all necessary modules (including network management). The `seed` attribute
-denotes the random seed used for the random number generator on the node, which could guarantee the reproducibility of
-the simulation. The `memo_size` define the size of memory array on the quantum router.
+Next, we will make the `nodes` entry.
+All fields of the list will have the `name` field required by the node constructor and a specification of the node type,
+along with any more arguments (optional or not) specified by the specific node type.
+We will be using the `QuantumRouter` node type for this tutorial,
+which already includes all of the necessary hardware for entanglement distribution as well as all necessary modules (including network management).
+The `seed` attribute denotes the random seed used for the random number generator on the node,
+which can guarantee the reproducibility of the simulation.
+The `memo_size` attribute defines the size of the memory array on the quantum router.
 
 ```json
 "nodes": [
-{
-"name": "center",
-"type": "QuantumRouter",
-"seed": 0,
-"memo_size": 50
-},
-{
-"name": "end1",
-"type": "QuantumRouter",
-"memo_size": 50,
-"seed": 1
-},
-{
-"name": "end2",
-"type": "QuantumRouter",
-"memo_size": 50,
-"seed": 2
-},
-{
-"name": "end3",
-"type": "QuantumRouter",
-"memo_size": 50,
-"seed": 3
-},
-{
-"name": "end4",
-"type": "QuantumRouter",
-"memo_size": 50,
-"seed": 4
-}
+  {
+    "name": "center",
+    "type": "QuantumRouter",
+    "seed": 0,
+    "memo_size": 50
+  },
+  {
+    "name": "end1",
+    "type": "QuantumRouter",
+    "seed": 1
+    "memo_size": 50,
+  },
+  {
+    "name": "end2",
+    "type": "QuantumRouter",
+    "seed": 2
+    "memo_size": 50,
+  },
+  {
+    "name": "end3",
+    "type": "QuantumRouter",
+    "seed": 3
+    "memo_size": 50,
+  },
+  {
+    "name": "end4",
+    "type": "QuantumRouter",
+    "seed": 4
+    "memo_size": 50,
+  }
 ]
 ```
 
-The `qconnections` entry should have multiple entries specifying the name of the two connected nodes, the attenuation
-and length of the fiber. The type of quantum connection should be specified by the attribute `type` . We predefined the
-type `meet_in_the_middle`  that automatically create a BSM node in the middle of node1 and node2.
+The `qconnections` entry should have multiple entries specifying the name of the two connected nodes and at least the attenuation and length of the fiber
+(plus any additional keyword arguments).
+The type of quantum connection should be specified by the attribute `type`.
+Here, we use the predefined type `meet_in_the_middle`  that automatically generates a BSM node in the middle of node1 and node2.
 
 ```json
 "qconnections": [
@@ -133,9 +136,8 @@ type `meet_in_the_middle`  that automatically create a BSM node in the middle of
 ]
 ```
 
-Finally, for the classical channels, we will be using the classical connections to create two classical
-channels (`node1` -> `node2` and `node2` -> `node1`). Two channels have the same delay specified by the
-attribute `delay`.
+Finally, for the classical channels, we will specify two-way `cconnections`.
+The node endpoints are specified by name as `node1` and `node2`, and each communication direction will have the same delay `delay`.
 
 ```json
 "cconnections": [
@@ -194,10 +196,11 @@ attribute `delay`.
 
 ### Step 2: Build the Network
 
-Now, we can move to our script and begin building the experiment from our json file. The file can be loaded with the
-construction function of `RouterNetTopo` . Note that we have the json file saved as `star_network.json` here.
-The `RouterNetTopo` will also create a timeline with 2 seconds of simulation time. We could use
-function `get_timeline()` to get the timeline for the simulation.
+Now, we can move to our script and begin building the experiment from our json file.
+The json file can be loaded with the construction function of `RouterNetTopo`.
+Note that we have the json file saved as `star_network.json` here.
+The `RouterNetTopo` will also create a timeline with 2 seconds of simulation time.
+We use the function `get_timeline()` to get the timeline for the simulation.
 
 ```python
 from sequence.topology.router_net_topo import RouterNetTopo
@@ -213,13 +216,13 @@ To edit network parameters quickly, we can define a custom function to interact
 
 We will edit a few hardware objects by accessing the hardware fields of network nodes. To access all nodes of a specific type in the network, we can use the `topology.get_nodes_by_type` method. This is useful for editing hardware objects that may only be found on one node type, e.g. memories only found on `QuantumRouter` nodes.
 
-We may also wish to edit parameters of our entanglement protocols. Since these are created by a node's resource manager,
-we will need to edit the appropriate fields of the resource manager on each node. This is achieved in much the same way
-as hardware elements.
+We may also wish to edit parameters of our entanglement protocols.
+Since these are created by a node's resource manager,
+we will need to edit the appropriate fields of the resource manager on each node.
+This is achieved in much the same way as hardware elements.
 
-For quantum and classical connections, the `get_qchannels` and `get_cchannels`  functions of the topology will return a
-list of quantum channels and classical channels respectively. These provide an iterable list of all connections in the
-network that may be edited directly.
+For quantum and classical connections, the `get_qchannels` and `get_cchannels`  functions of the topology will return a  list of quantum channels and classical channels, respectively.
+These provide an iterable list of all connections in the network that may be edited directly.
 
 ```python
 def set_parameters(topology: RouterNetTopo):
@@ -229,20 +232,21 @@ def set_parameters(topology: RouterNetTopo):
     MEMO_EFFICIENCY = 1
     MEMO_FIDELITY = 0.9349367588934053
     for node in topology.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER):
-        node.memory_array.update_memory_params("frequency", MEMO_FREQ)
-        node.memory_array.update_memory_params("coherence_time", MEMO_EXPIRE)
-        node.memory_array.update_memory_params("efficiency", MEMO_EFFICIENCY)
-        node.memory_array.update_memory_params("raw_fidelity", MEMO_FIDELITY)
+        memory_array = node.get_components_by_type("MemoryArray")[0]
+        memory_array.update_memory_params("frequency", MEMO_FREQ)
+        memory_array.update_memory_params("coherence_time", MEMO_EXPIRE)
+        memory_array.update_memory_params("efficiency", MEMO_EFFICIENCY)
+        memory_array.update_memory_params("raw_fidelity", MEMO_FIDELITY)
 
     # set detector parameters
     DETECTOR_EFFICIENCY = 0.9
     DETECTOR_COUNT_RATE = 5e7
     DETECTOR_RESOLUTION = 100
     for node in topology.get_nodes_by_type(RouterNetTopo.BSM_NODE):
-        node.bsm.update_detectors_params("efficiency", DETECTOR_EFFICIENCY)
-        node.bsm.update_detectors_params("count_rate", DETECTOR_COUNT_RATE)
-        node.bsm.update_detectors_params("time_resolution", DETECTOR_RESOLUTION)
-        
+        bsm = node.get_components_by_type("SingleAtomBSM")[0]
+        bsm.update_detectors_params("efficiency", DETECTOR_EFFICIENCY)
+        bsm.update_detectors_params("count_rate", DETECTOR_COUNT_RATE)
+        bsm.update_detectors_params("time_resolution", DETECTOR_RESOLUTION)
     # set entanglement swapping parameters
     SWAP_SUCC_PROB = 0.90
     SWAP_DEGRADATION = 0.99
@@ -267,7 +271,7 @@ set_parameters(network_topo)
 ### Step 3: Making Requests
 
 With the network built, we are now ready to make requests of the network manager and start entanglement. The `request` method of the network manager can be used for this. When invoked, it will automatically start the reservation process and create rules as required (see the background section). The method has 5 arguments:
-- `responder`, a string name of the other node with whitch to generate entangled pairs
+- `responder`, a string name of the other node with which to generate entangled pairs
 - `start_time`, the simulation time (in **picoseconds**) at which entanglement should begin
 - `end_time`, the simulation time at which entanglement may end and reserved resources released
 - `memory_size`, the number of entangled memory pairs requested
@@ -277,16 +281,18 @@ We will make one request at one network node, asking for 25 memories to be entan
 
 ```python
 # the start and end nodes may be edited as desired 
+start_node_name = "end1"
+end_node_name = "end2"
 node1 = node2 = None
 
 for router in network_topo.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER):
-    if router.name == "end1":
-      	node1 = router
-    elif router.name == "end2":
+    if router.name == start_node_name:
+        node1 = router
+    elif router.name == end_node_name:
         node2 = router
 
 nm = node1.network_manager
-nm.request(node2.name, start_time=1e12, end_time=10e12, memory_size=25, target_fidelity=0.9)
+nm.request(start_node_name, start_time=1e12, end_time=10e12, memory_size=25, target_fidelity=0.9)
 
 tl.init()
 tl.run()
diff --git a/docs/source/tutorial/chapter5/star_network.json b/docs/source/tutorial/chapter5/star_network.json
index 80b0ded3..4bcc1664 100644
--- a/docs/source/tutorial/chapter5/star_network.json
+++ b/docs/source/tutorial/chapter5/star_network.json
@@ -115,4 +115,4 @@
       "delay": 1000000000
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/docs/source/tutorial/chapter5/star_network.py b/docs/source/tutorial/chapter5/star_network.py
index 4c5a417a..cf514ba3 100644
--- a/docs/source/tutorial/chapter5/star_network.py
+++ b/docs/source/tutorial/chapter5/star_network.py
@@ -8,28 +8,28 @@ def set_parameters(topology: RouterNetTopo):
     MEMO_EFFICIENCY = 1
     MEMO_FIDELITY = 0.9349367588934053
     for node in topology.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER):
-        node.memory_array.update_memory_params("frequency", MEMO_FREQ)
-        node.memory_array.update_memory_params("coherence_time", MEMO_EXPIRE)
-        node.memory_array.update_memory_params("efficiency", MEMO_EFFICIENCY)
-        node.memory_array.update_memory_params("raw_fidelity", MEMO_FIDELITY)
+        memory_array = node.get_components_by_type("MemoryArray")[0]
+        memory_array.update_memory_params("frequency", MEMO_FREQ)
+        memory_array.update_memory_params("coherence_time", MEMO_EXPIRE)
+        memory_array.update_memory_params("efficiency", MEMO_EFFICIENCY)
+        memory_array.update_memory_params("raw_fidelity", MEMO_FIDELITY)
 
     # set detector parameters
     DETECTOR_EFFICIENCY = 0.9
     DETECTOR_COUNT_RATE = 5e7
     DETECTOR_RESOLUTION = 100
     for node in topology.get_nodes_by_type(RouterNetTopo.BSM_NODE):
-        node.bsm.update_detectors_params("efficiency", DETECTOR_EFFICIENCY)
-        node.bsm.update_detectors_params("count_rate", DETECTOR_COUNT_RATE)
-        node.bsm.update_detectors_params("time_resolution", DETECTOR_RESOLUTION)
+        bsm = node.get_components_by_type("SingleAtomBSM")[0]
+        bsm.update_detectors_params("efficiency", DETECTOR_EFFICIENCY)
+        bsm.update_detectors_params("count_rate", DETECTOR_COUNT_RATE)
+        bsm.update_detectors_params("time_resolution", DETECTOR_RESOLUTION)
         
     # set entanglement swapping parameters
     SWAP_SUCC_PROB = 0.90
     SWAP_DEGRADATION = 0.99
     for node in topology.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER):
-        node.network_manager.protocol_stack[1].set_swapping_success_rate(
-            SWAP_SUCC_PROB)
-        node.network_manager.protocol_stack[1].set_swapping_degradation(
-            SWAP_DEGRADATION)
+        node.network_manager.protocol_stack[1].set_swapping_success_rate(SWAP_SUCC_PROB)
+        node.network_manager.protocol_stack[1].set_swapping_degradation(SWAP_DEGRADATION)
         
     # set quantum channel parameters
     ATTENUATION = 1e-5
@@ -48,22 +48,23 @@ def set_parameters(topology: RouterNetTopo):
     set_parameters(network_topo)
 
     # the start and end nodes may be edited as desired
+    start_node_name = "end1"
+    end_node_name = "end2"
     node1 = node2 = None
 
     for router in network_topo.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER):
-        if router.name == "end1":
+        if router.name == start_node_name:
             node1 = router
-        elif router.name == "end2":
+        elif router.name == end_node_name:
             node2 = router
 
     nm = node1.network_manager
-    nm.request(node2.name, start_time=1e12, end_time=10e12, memory_size=25,
-               target_fidelity=0.9)
+    nm.request(end_node_name, start_time=1e12, end_time=10e12, memory_size=25, target_fidelity=0.9)
 
     tl.init()
     tl.run()
 
-    print(node1.name, "memories")
+    print(start_node_name, "memories")
     print("Index:\tEntangled Node:\tFidelity:\tEntanglement Time:")
     for info in node1.resource_manager.memory_manager:
         print("{:6}\t{:15}\t{:9}\t{}".format(str(info.index),
diff --git a/docs/source/tutorial/chapter6/application.md b/docs/source/tutorial/chapter6/application.md
index 8bdddf3c..2dd76aac 100644
--- a/docs/source/tutorial/chapter6/application.md
+++ b/docs/source/tutorial/chapter6/application.md
@@ -1,6 +1,6 @@
 # Chapter 6: Application
 
-In this sixth and final tutorial chapter, we will create a custom appliation to interact with the modules we have already seen. The goal of this tutorial is thus to see how applications are constructed in SeQUeNCe and how they interface with the network.
+In this sixth and final tutorial chapter, we will create a custom application to interact with the modules we have already seen. The goal of this tutorial is thus to see how applications are constructed in SeQUeNCe and how they interface with the network.
 
 ### Step 1: Building our Custom Application
 
@@ -8,7 +8,7 @@ We'll jump right in and build our custom application class `PeriodicApp`, which
 
 To start, we will create a constructor for the application that requires
 - `node`, the node to which the application is attached,
-- `other`, the string name of the other node with whitch to attempt communications,
+- `other`, the string name of the other node with which to attempt communications,
 - `memory_size`, the number of memories that are requested for entanglement, and
 - `target_fidelity`, the desired fidelity of the entangled pairs.
 
@@ -24,7 +24,7 @@ if TYPE_CHECKING:
     from sequence.topology.node import QuantumRouter
 
 
-class PeriodicApp():
+class PeriodicApp:
     def __init__(self, node: "QuantumRouter", other: str, memory_size=25, target_fidelity=0.9):
         self.node = node
         self.node.set_app(self)
@@ -68,7 +68,42 @@ The other method we require, `get_memory`, is called by the resource manager whe
             self.node.resource_manager.update(None, info.memory, "RAW")
 ```
 
-### Step 2: Building and Running the Simulation
+### Step 2: Reset Application
+
+To ensure the memories are utilized properly and returned to the memory manager, we will need a second application on the receiving node.
+This application will also take in memories and reset them to `"RAW"` if they are properly entangled.
+The `get_reserve_res` method will do nothing.
+
+```python
+class ResetApp:
+    def __init__(self, node, other_node_name, target_fidelity=0.9):
+        self.node = node
+        self.node.set_app(self)
+        self.other_node_name = other_node_name
+        self.target_fidelity = target_fidelity
+
+    def get_other_reservation(self, reservation):
+        """called when receiving the request from the initiating node.
+
+        For this application, we do not need to do anything.
+        """
+
+        pass
+
+    def get_memory(self, info):
+        """Similar to the get_memory method of the main application.
+
+        We check if the memory info meets the request first,
+        by noting the remote entangled memory and entanglement fidelity.
+        We then free the memory for future use.
+        """
+
+        if (info.state == "ENTANGLED" and info.remote_node == self.other_node_name
+                and info.fidelity > self.target_fidelity):
+            self.node.resource_manager.update(None, info.memory, "RAW")
+```
+
+### Step 3: Building and Running the Simulation
 
 With all of the tools we have seen through the tutorials, creating our network and running the simulation are now a very simple process. We will use the same json file as the last tutorial (`star_network.json`) to automatically build the network, and will add our custom application to one node. Finally, we will begin the application processes with the `start` method.
 
@@ -81,15 +116,17 @@ tl = network_topo.get_timeline()
 tl.stop_time = 2e12 * num_periods
 tl.show_progress = False
 
-node1 = "end1"
-node2 = "end2"
-for node in network_topo.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER):
-    if node.name == node1:
-        node1 = node
-    elif node.name == node2:
-        node2 = node
+start_node_name = "end1"
+end_node_name = "end2"
+node1 = node2 = None
 
-app = PeriodicApp(node1, node2.name)
+for router in network_topo.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER):
+    if router.name == start_node_name:
+        node1 = router
+    elif router.name == end_node_name:
+        node2 = router
+        
+app = PeriodicApp(node1, end_node_name)
 
 tl.init()
 app.start()
diff --git a/docs/source/tutorial/chapter6/custom_app.py b/docs/source/tutorial/chapter6/custom_app.py
index e1079772..bbc29f0b 100644
--- a/docs/source/tutorial/chapter6/custom_app.py
+++ b/docs/source/tutorial/chapter6/custom_app.py
@@ -7,7 +7,7 @@
     from sequence.topology.node import QuantumRouter
 
 
-class PeriodicApp():
+class PeriodicApp:
     def __init__(self, node: "QuantumRouter", other: str, memory_size=25, target_fidelity=0.9):
         self.node = node
         self.node.set_app(self)
@@ -21,7 +21,6 @@ def start(self):
         nm.request(self.other, start_time=(now + 1e12), end_time=(now + 2e12),
                    memory_size=self.memory_size,
                    target_fidelity=self.target_fidelity)
-
         # schedule future start
         process = Process(self, "start", [])
         event = Event(now + 2e12, process)
@@ -40,6 +39,34 @@ def get_memory(self, info: "MemoryInfo"):
             self.node.resource_manager.update(None, info.memory, "RAW")
 
 
+class ResetApp:
+    def __init__(self, node, other_node_name, target_fidelity=0.9):
+        self.node = node
+        self.node.set_app(self)
+        self.other_node_name = other_node_name
+        self.target_fidelity = target_fidelity
+
+    def get_other_reservation(self, reservation):
+        """called when receiving the request from the initiating node.
+
+        For this application, we do not need to do anything.
+        """
+
+        pass
+
+    def get_memory(self, info):
+        """Similar to the get_memory method of the main application.
+
+        We check if the memory info meets the request first,
+        by noting the remote entangled memory and entanglement fidelity.
+        We then free the memory for future use.
+        """
+
+        if (info.state == "ENTANGLED" and info.remote_node == self.other_node_name
+                and info.fidelity > self.target_fidelity):
+            self.node.resource_manager.update(None, info.memory, "RAW")
+
+
 if __name__ == "__main__":
     network_config = "star_network.json"
     num_periods = 5
@@ -49,16 +76,23 @@ def get_memory(self, info: "MemoryInfo"):
     tl.stop_time = 2e12 * num_periods
     tl.show_progress = False
 
-    node1 = "end1"
-    node2 = "end2"
-    for node in network_topo.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER):
-        if node.name == node1:
-            node1 = node
-        elif node.name == node2:
-            node2 = node
+    start_node_name = "end1"
+    end_node_name = "end2"
+    node1 = node2 = None
+
+    for router in network_topo.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER):
+        if router.name == start_node_name:
+            node1 = router
+        elif router.name == end_node_name:
+            node2 = router
 
-    app = PeriodicApp(node1, node2.name)
+    app = PeriodicApp(node1, end_node_name)
+    reset_app = ResetApp(node2, start_node_name)
     
     tl.init()
+
+    for name, component in tl.entities.items():
+        print("{}: {}".format(name, component.get_generator()))
+
     app.start()
     tl.run()
diff --git a/example/QCE_demos/Teleport_demo.ipynb b/example/QCE_demos/Teleport_demo.ipynb
new file mode 100644
index 00000000..68bd9e1c
--- /dev/null
+++ b/example/QCE_demos/Teleport_demo.ipynb
@@ -0,0 +1,477 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "pycharm": {
+     "name": "#%% md\n"
+    }
+   },
+   "source": [
+    "# Network with Applications\n",
+    "\n",
+    "In this file, we'll demonstrate the simulation of a more complicated network topology with applications. These applications will act on each node, first requesting memories to be entangled and then using these memories to teleport a qubit. The network topology, including hardware components, is shown below:\n",
+    "\n",
+    "<img src=\"./images/star_network.png\" width=\"800\"/>\n",
+    "\n",
+    "In this file, we construct the network described above and add a teleportation app to two nodes. Most of the code has been written, but some will need to be filled in. We'll be building the topology from an external json file `star_network.json`.\n",
+    "\n",
+    "## Imports\n",
+    "We must first import the necessary tools from SeQUeNCe. For building a teleportation circuit, we'll import:\n",
+    "\n",
+    "- `Circuit` is a class for describing and applying quantum circuits, with the use of the simulation kernel.\n",
+    "\n",
+    "For building our specific request, we will import:\n",
+    "\n",
+    "- `RequestApp` is an example application included with SeQUeNCe. We will investigate its behavior through a class that inherits from this.\n",
+    "- `Message` is a wrapper for classical messages to exchange between nodes.\n",
+    "\n",
+    "Finally, for creating the network itself, we will import:\n",
+    "\n",
+    "- `Topology` is a powerful class for creating and managing complex network topologies. We'll be using the `RouterNetTopo` subclass to build our network and intefrace with specific nodes and node types.\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {
+    "pycharm": {
+     "name": "#%%\n"
+    }
+   },
+   "outputs": [],
+   "source": [
+    "import pandas as pd\n",
+    "from ipywidgets import interact\n",
+    "import time"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {
+    "pycharm": {
+     "name": "#%%\n"
+    }
+   },
+   "outputs": [],
+   "source": [
+    "# for building teleportation functions\n",
+    "from sequence.components.circuit import Circuit\n",
+    "from numpy import sqrt\n",
+    "\n",
+    "# for building application\n",
+    "from sequence.app.request_app import RequestApp\n",
+    "from sequence.message import Message\n",
+    "\n",
+    "# for building network\n",
+    "from sequence.topology.router_net_topo import RouterNetTopo"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "pycharm": {
+     "name": "#%% md\n"
+    }
+   },
+   "source": [
+    "## Teleportation Functions\n",
+    "\n",
+    "Here, we define our functions for teleporting a qubit.\n",
+    "\n",
+    "### Teleport 1\n",
+    "This function will take in the arguments\n",
+    "\n",
+    "    e_pair_key: the key pointing to the state of the local entangled memory\n",
+    "    node: the local node from which we are teleporting a qubit\n",
+    "\n",
+    "The key may be obtained from the quantum memory, as outlined later. We will then generate a qubit to teleport, and use the `run_circuit` method of the quantum circuit to teleport it. This method takes a circuit, the keys to operate on, and a measurement sample (to use for determining measurement outcomes) as arguments. We will then return the measurement results, so the entangled memory at the other node may be corrected.\n",
+    "\n",
+    "### Teleport 2\n",
+    "This function will take in the key for the corresponding entangled memory on the receiving node, and will correct it to match the teleported qubit (and then measure it). We will take in the same arguments as the previous function, with the addition of\n",
+    "\n",
+    "    x_flip: dictates if we perform an x-gate correction\n",
+    "    z_flip: dictates if we perform a z-gate correction\n",
+    "\n",
+    "These will be passed as 0 or 1 depending on the measurement outcome of the first circuit."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "metadata": {
+    "pycharm": {
+     "name": "#%%\n"
+    }
+   },
+   "outputs": [],
+   "source": [
+    "def teleport_1(e_pair_key, node):\n",
+    "    # get quantum manager and measurement sample\n",
+    "    qm = node.timeline.quantum_manager\n",
+    "    meas_samp = node.get_generator().random()\n",
+    "    \n",
+    "    # generate teleported qubit\n",
+    "    state = (complex(sqrt(1/2)),\n",
+    "             complex(sqrt(1/2)))\n",
+    "    qubit_key = qm.new(state)\n",
+    "    \n",
+    "    # prepare teleportation circuit (TODO)\n",
+    "    \n",
+    "    # run circuit and return results (TODO)\n",
+    "\n",
+    "def teleport_2(e_pair_key, node, x_flip, z_flip):\n",
+    "    # get quantum manager and measurement sample\n",
+    "    qm = node.timeline.quantum_manager\n",
+    "    meas_samp = node.get_generator().random()\n",
+    "    \n",
+    "    # prepare rectification circuit (TODO)\n",
+    "    \n",
+    "    # run circuit and return results (TODO)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "pycharm": {
+     "name": "#%% md\n"
+    }
+   },
+   "source": [
+    "## Custom Application\n",
+    "\n",
+    "We may now define our custom application. When receiving an entangled memory, we will create our desired qubit and jointly measure it; we will then use the new `TeleportMessage` class to communicate results to the other node. The receiving node will wait whenn receiving an entangled memory. Once the classical message is sent from the originator, it will correct and measure the underlying qubit."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {
+    "pycharm": {
+     "name": "#%%\n"
+    }
+   },
+   "outputs": [],
+   "source": [
+    "class TeleportMessage(Message):\n",
+    "    def __init__(self, msg_type, receiver, index: int, x_flip: int, z_flip: int):\n",
+    "        super().__init__(msg_type, receiver)\n",
+    "        self.index = index\n",
+    "        self.x_flip = x_flip\n",
+    "        self.z_flip = z_flip\n",
+    "\n",
+    "class TeleportApp(RequestApp):\n",
+    "    def __init__(self, node, name, other_name):\n",
+    "        super().__init__(node)\n",
+    "        self.name = name\n",
+    "        self.other_name = other_name\n",
+    "        node.protocols.append(self)\n",
+    "        self.memos_to_measure = []\n",
+    "        \n",
+    "        self.results = [0, 0]\n",
+    "        \n",
+    "    def get_memory(self, info: \"MemoryInfo\") -> None:\n",
+    "        \"\"\"Method to receive entangled memories.\n",
+    "\n",
+    "        Will check if the received memory is qualified.\n",
+    "        If it's a qualified memory, the application sets memory to RAW state\n",
+    "        and release back to resource manager.\n",
+    "        The counter of entanglement memories, 'memory_counter', is added.\n",
+    "        Otherwise, the application does not modify the state of memory and\n",
+    "        release back to the resource manager.\n",
+    "\n",
+    "        Args:\n",
+    "            info (MemoryInfo): info on the qualified entangled memory.\n",
+    "        \"\"\"\n",
+    "\n",
+    "        if info.state != \"ENTANGLED\":\n",
+    "            return\n",
+    "\n",
+    "        if info.index in self.memo_to_reserve:\n",
+    "            reservation = self.memo_to_reserve[info.index]\n",
+    "            \n",
+    "            if info.remote_node == reservation.responder and info.fidelity >= reservation.fidelity:\n",
+    "                # we are initiator, and want to teleport qubit\n",
+    "                print(\"node {} memory {} entangled with other memory {}\".format(\n",
+    "                    self.node.name, info.index, info.remote_memo))\n",
+    "                \n",
+    "                # run local teleportation ops (TODO)\n",
+    "                \n",
+    "                # send message to other node (TODO)\n",
+    "                \n",
+    "                # reset local memory\n",
+    "                self.node.resource_manager.update(None, info.memory, \"RAW\")\n",
+    "                \n",
+    "            elif info.remote_node == reservation.initiator and info.fidelity >= reservation.fidelity:\n",
+    "                # we are responder, and want to receive qubit\n",
+    "                # need to wait on message from sender to correct entanled memory\n",
+    "                self.memos_to_measure.append(info.memory)\n",
+    "                \n",
+    "    def received_message(self, src, message):\n",
+    "        memo_index = message.index\n",
+    "        \n",
+    "        # run local teleportation correction and save result (TODO)\n",
+    "        memory = self.memos_to_measure.pop(0)\n",
+    "        \n",
+    "        # reset local memory\n",
+    "        self.node.resource_manager.update(None, memory, \"RAW\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "pycharm": {
+     "name": "#%% md\n"
+    }
+   },
+   "source": [
+    "## Building the Simulation\n",
+    "\n",
+    "We'll now construct the network and add our applications. This example follows the usual process to ensure that all tools function properly:\n",
+    "1. Create the topology instance for the simulation to manage our network\n",
+    "    - This class will create a simulation timeline\n",
+    "    - This instantiates the Simulation Kernel (see below)\n",
+    "2. Create the simulated network topology. In this case, we are using an external JSON file to specify nodes and their connectivity.\n",
+    "    - This includes specifying hardware parameters in the `set_parameters` function, defined later\n",
+    "    - This instantiates the Hardware, Entanglement Management, Resource Management, and Network Management modules\n",
+    "3. Install custom protocols/applications and ensure all are set up properly\n",
+    "    - This instantiates the Application module\n",
+    "4. Initialize and run the simulation\n",
+    "5. Collect and display the desired metrics\n",
+    "\n",
+    "The JSON file specifies that network nodes should be of type `QuantumRouter`, a node type defined by SeQUeNCe. This will automatically create all necessary hardware and protocol instances on the nodes, and the `Topology` class will automatically generate `BSMNode` instances on the quantum channels between such nodes.\n",
+    "\n",
+    "To construct an application, we need:\n",
+    "- The node to attach the application to\n",
+    "- The name of the application instance\n",
+    "- The name of the node to teleport qubits to.\n",
+    "\n",
+    "We can get a list of all desired application nodes, in this case routers, from the `Topology` class with the `get_nodes_by_type` method. We then set an application on each one."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {
+    "pycharm": {
+     "name": "#%%\n"
+    }
+   },
+   "outputs": [],
+   "source": [
+    "def test(sim_time=1.5, qc_atten=1e-5):\n",
+    "    \"\"\"\n",
+    "    sim_time: duration of simulation time (ms)\n",
+    "    qc_atten: quantum channel attenuation (dB/km)\n",
+    "    \"\"\"\n",
+    "    \n",
+    "    network_config = \"star_network.json\"\n",
+    "    \n",
+    "    # here, we make a new topology using the configuration JSON file.\n",
+    "    # we then modify some of the simulation parameters of the network.\n",
+    "    network_topo = RouterNetTopo(network_config)\n",
+    "    set_parameters(network_topo, sim_time, qc_atten)\n",
+    "    \n",
+    "    # get two end nodes\n",
+    "    start_node_name = \"router1\"\n",
+    "    end_node_name = \"router2\"\n",
+    "    node1 = node2 = None\n",
+    "\n",
+    "    for router in network_topo.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER):\n",
+    "        if router.name == start_node_name:\n",
+    "            node1 = router\n",
+    "        elif router.name == end_node_name:\n",
+    "            node2 = router\n",
+    "            \n",
+    "    start_app_name = \"start_app\"\n",
+    "    end_app_name = \"end_app\"\n",
+    "    \n",
+    "    # create applications (TODO)\n",
+    "    start_app = None\n",
+    "    end_app = None\n",
+    "        \n",
+    "    # run the simulation\n",
+    "    tl = network_topo.get_timeline()\n",
+    "    tl.show_progress = False\n",
+    "    tl.init()\n",
+    "    \n",
+    "    start_app.start(end_node_name, 1e12, 2e12, 10, 0.9)\n",
+    "    tick = time.time()\n",
+    "    tl.run()\n",
+    "    print(\"execution time %.2f sec\" % (time.time() - tick))\n",
+    "    \n",
+    "    print(\"measured |0>:\", end_app.results[0])\n",
+    "    print(\"measured |1>:\", end_app.results[1])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "pycharm": {
+     "name": "#%% md\n"
+    }
+   },
+   "source": [
+    "## Setting parameters\n",
+    "\n",
+    "Here we define the `set_parameters` function we used earlier. This function will take a `Topology` as input and change many parameters to desired values. This will be covered in greater detail in workshop 3.\n",
+    "\n",
+    "The simulation time limit will be set using the `get_timeline` method.\n",
+    "\n",
+    "Quantum memories and detectors are hardware elements, and so parameters are changed by accessing the hardware included with the `QuantumRouter` and `BSMNode` node types. Many complex hardware elements, such as bsm devices or memory arrays, have methods to update parameters for all included hardware elements. This includes `update_memory_params` to change all memories in an array or `update_detector_params` to change all detectors.\n",
+    "\n",
+    "We will also set the success probability and swapping degradation of the entanglement swapping protocol. This will be set in the Network management Module (specifically the reservation protocol), as this information is necessary to create and manage the rules for the Resource Management module.\n",
+    "\n",
+    "Lastly, we'll update some parameters of the quantum channels. Quantum channels (and, similarly, classical channels) can be accessed from the `Topology` class as the `qchannels` field. Since these are individual hardware elements, we will set the parameters directly."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {
+    "pycharm": {
+     "name": "#%%\n"
+    }
+   },
+   "outputs": [],
+   "source": [
+    "def set_parameters(topology, simulation_time, attenuation):\n",
+    "    \"\"\"\n",
+    "    simulation_time: duration of simulation time (s)\n",
+    "    attenuation: attenuation on quantum channels (db/m)\n",
+    "    \"\"\"\n",
+    "    \n",
+    "    PS_PER_S = 1e12\n",
+    "    \n",
+    "    # set timeline stop time\n",
+    "    topology.get_timeline().stop_time = (simulation_time * PS_PER_S)\n",
+    "    \n",
+    "    # set memory parameters\n",
+    "    MEMO_FREQ = 2e3\n",
+    "    MEMO_EXPIRE = 0\n",
+    "    MEMO_EFFICIENCY = 1\n",
+    "    MEMO_FIDELITY = 0.9349367588934053\n",
+    "    for node in topology.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER):\n",
+    "        memory_array = node.get_components_by_type(\"MemoryArray\")[0]\n",
+    "        memory_array.update_memory_params(\"frequency\", MEMO_FREQ)\n",
+    "        memory_array.update_memory_params(\"coherence_time\", MEMO_EXPIRE)\n",
+    "        memory_array.update_memory_params(\"efficiency\", MEMO_EFFICIENCY)\n",
+    "        memory_array.update_memory_params(\"raw_fidelity\", MEMO_FIDELITY)\n",
+    "\n",
+    "    # set detector parameters\n",
+    "    DETECTOR_EFFICIENCY = 0.9\n",
+    "    DETECTOR_COUNT_RATE = 5e7\n",
+    "    DETECTOR_RESOLUTION = 100\n",
+    "    for node in topology.get_nodes_by_type(RouterNetTopo.BSM_NODE):\n",
+    "        bsm = node.get_components_by_type(\"SingleAtomBSM\")[0]\n",
+    "        bsm.update_detectors_params(\"efficiency\", DETECTOR_EFFICIENCY)\n",
+    "        bsm.update_detectors_params(\"count_rate\", DETECTOR_COUNT_RATE)\n",
+    "        bsm.update_detectors_params(\"time_resolution\", DETECTOR_RESOLUTION)\n",
+    "        \n",
+    "    # set entanglement swapping parameters\n",
+    "    SWAP_SUCC_PROB = 0.90\n",
+    "    SWAP_DEGRADATION = 0.99\n",
+    "    for node in topology.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER):\n",
+    "        node.network_manager.protocol_stack[1].set_swapping_success_rate(SWAP_SUCC_PROB)\n",
+    "        node.network_manager.protocol_stack[1].set_swapping_degradation(SWAP_DEGRADATION)\n",
+    "        \n",
+    "    # set quantum channel parameters\n",
+    "    ATTENUATION = attenuation\n",
+    "    QC_FREQ = 1e11\n",
+    "    for qc in topology.qchannels:\n",
+    "        qc.attenuation = ATTENUATION\n",
+    "        qc.frequency = QC_FREQ"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "pycharm": {
+     "name": "#%% md\n"
+    }
+   },
+   "source": [
+    "## Running the Simulation\n",
+    "\n",
+    "All that is left is to run the simulation with user input. We'll specify:\n",
+    "\n",
+    "    sim_time: duration of simulation time (s)\n",
+    "    qc_atten: attenuation on quantum channels (dB/m)\n",
+    "\n",
+    "Note that different hardware parameters or network topologies may cause the simulation to run for a very long time."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "metadata": {
+    "pycharm": {
+     "name": "#%%\n"
+    },
+    "scrolled": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "a80357bdef314d2d9da27a6a38095611",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "interactive(children=(FloatSlider(value=1.5, description='sim_time', max=2.0, min=1.0), Dropdown(description='…"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/plain": [
+       "<function __main__.test(sim_time=1.5, qc_atten=1e-05)>"
+      ]
+     },
+     "execution_count": 11,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "interact(test, sim_time=(1, 2, 0.1), qc_atten=[0, 1e-5, 2e-5])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "pycharm": {
+     "name": "#%%\n"
+    }
+   },
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "venv",
+   "language": "python",
+   "name": "venv"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.10.2"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/example/QCE_demos/Teleport_demo_filled.ipynb b/example/QCE_demos/Teleport_demo_filled.ipynb
new file mode 100644
index 00000000..acadf7c1
--- /dev/null
+++ b/example/QCE_demos/Teleport_demo_filled.ipynb
@@ -0,0 +1,443 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Network with Applications\n",
+    "\n",
+    "In this file, we'll demonstrate the simulation of a more complicated network topology with applications. These applications will act on each node, first requesting memories to be entangled and then using these memories to teleport a qubit. The network topology, including hardware components, is shown below:\n",
+    "\n",
+    "<img src=\"./images/star_network.png\" width=\"800\"/>\n",
+    "\n",
+    "In this file, we construct the network described above and add a teleportation app to two nodes. Most of the code has been written, but some will need to be filled in. We'll be building the topology from an external json file `star_network.json`.\n",
+    "\n",
+    "## Imports\n",
+    "We must first import the necessary tools from SeQUeNCe. For building a teleportation circuit, we'll import:\n",
+    "\n",
+    "- `Circuit` is a class for describing and applying quantum circuits, with the use of the simulation kernel.\n",
+    "\n",
+    "For building our specific request, we will import:\n",
+    "\n",
+    "- `RequestApp` is an example application included with SeQUeNCe. We will investigate its behavior through a class that inherits from this.\n",
+    "- `Message` is a wrapper for classical messages to exchange between nodes.\n",
+    "\n",
+    "Finally, for creating the network itself, we will import:\n",
+    "\n",
+    "- `Topology` is a powerful class for creating and managing complex network topologies. We'll be using the `RouterNetTopo` subclass to build our network and intefrace with specific nodes and node types.\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import pandas as pd\n",
+    "from ipywidgets import interact\n",
+    "import time"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# for building teleportation functions\n",
+    "from sequence.components.circuit import Circuit\n",
+    "from numpy import sqrt\n",
+    "\n",
+    "# for building application\n",
+    "from sequence.app.request_app import RequestApp\n",
+    "from sequence.message import Message\n",
+    "\n",
+    "# for building network\n",
+    "from sequence.topology.router_net_topo import RouterNetTopo"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Teleportation Functions\n",
+    "\n",
+    "Here, we define our functions for teleporting a qubit.\n",
+    "\n",
+    "### Teleport 1\n",
+    "This function will take in the arguments\n",
+    "\n",
+    "    e_pair_key: the key pointing to the state of the local entangled memory\n",
+    "    node: the local node from which we are teleporting a qubit\n",
+    "\n",
+    "The key may be obtained from the quantum memory, as outlined later. We will then generate a qubit to teleport, and use the `run_circuit` method of the quantum circuit to teleport it. This method takes a circuit, the keys to operate on, and a measurement sample (to use for determining measurement outcomes) as arguments. We will then return the measurement results, so the entangled memory at the other node may be corrected.\n",
+    "\n",
+    "### Teleport 2\n",
+    "This function will take in the key for the corresponding entangled memory on the receiving node, and will correct it to match the teleported qubit (and then measure it). We will take in the same arguments as the previous function, with the addition of\n",
+    "\n",
+    "    x_flip: dictates if we perform an x-gate correction\n",
+    "    z_flip: dictates if we perform a z-gate correction\n",
+    "\n",
+    "These will be passed as 0 or 1 depending on the measurement outcome of the first circuit."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def teleport_1(e_pair_key, node):\n",
+    "    # get quantum manager and measurement sample\n",
+    "    qm = node.timeline.quantum_manager\n",
+    "    meas_samp = node.get_generator().random()\n",
+    "    \n",
+    "    # generate teleported qubit\n",
+    "    state = (complex(sqrt(1/2)),\n",
+    "             complex(sqrt(1/2)))\n",
+    "    qubit_key = qm.new(state)\n",
+    "    \n",
+    "    # prepare teleportation circuit\n",
+    "    circuit = Circuit(2)\n",
+    "    circuit.cx(0, 1)\n",
+    "    circuit.h(0)\n",
+    "    circuit.measure(0)\n",
+    "    circuit.measure(1)\n",
+    "    \n",
+    "    # run circuit and return results\n",
+    "    result = qm.run_circuit(circuit, [qubit_key, e_pair_key], meas_samp)\n",
+    "    return result[qubit_key], result[e_pair_key]\n",
+    "\n",
+    "def teleport_2(e_pair_key, node, x_flip, z_flip):\n",
+    "    # get quantum manager and measurement sample\n",
+    "    qm = node.timeline.quantum_manager\n",
+    "    meas_samp = node.get_generator().random()\n",
+    "    \n",
+    "    # prepare rectification circuit\n",
+    "    circuit = Circuit(1)\n",
+    "    if x_flip:\n",
+    "        circuit.x(0)\n",
+    "    if z_flip:\n",
+    "        circuit.z(0)\n",
+    "    circuit.measure(0)\n",
+    "    \n",
+    "    # run circuit and return results\n",
+    "    result = qm.run_circuit(circuit, [e_pair_key], meas_samp)\n",
+    "    return result[e_pair_key]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Custom Application\n",
+    "\n",
+    "We may now define our custom application. When receiving an entangled memory, we will create our desired qubit and jointly measure it; we will then use the new `TeleportMessage` class to communicate results to the other node. The receiving node will wait whenn receiving an entangled memory. Once the classical message is sent from the originator, it will correct and measure the underlying qubit."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class TeleportMessage(Message):\n",
+    "    def __init__(self, msg_type, receiver, index: int, x_flip: int, z_flip: int):\n",
+    "        super().__init__(msg_type, receiver)\n",
+    "        self.index = index\n",
+    "        self.x_flip = x_flip\n",
+    "        self.z_flip = z_flip\n",
+    "\n",
+    "class TeleportApp(RequestApp):\n",
+    "    def __init__(self, node, name, other_name):\n",
+    "        super().__init__(node)\n",
+    "        self.name = name\n",
+    "        self.other_name = other_name\n",
+    "        node.protocols.append(self)\n",
+    "        self.memos_to_measure = []\n",
+    "        \n",
+    "        self.results = [0, 0]\n",
+    "        \n",
+    "    def get_memory(self, info: \"MemoryInfo\") -> None:\n",
+    "        \"\"\"Method to receive entangled memories.\n",
+    "\n",
+    "        Will check if the received memory is qualified.\n",
+    "        If it's a qualified memory, the application sets memory to RAW state\n",
+    "        and release back to resource manager.\n",
+    "        The counter of entanglement memories, 'memory_counter', is added.\n",
+    "        Otherwise, the application does not modify the state of memory and\n",
+    "        release back to the resource manager.\n",
+    "\n",
+    "        Args:\n",
+    "            info (MemoryInfo): info on the qualified entangled memory.\n",
+    "        \"\"\"\n",
+    "\n",
+    "        if info.state != \"ENTANGLED\":\n",
+    "            return\n",
+    "\n",
+    "        if info.index in self.memo_to_reserve:\n",
+    "            reservation = self.memo_to_reserve[info.index]\n",
+    "            \n",
+    "            if info.remote_node == reservation.responder and info.fidelity >= reservation.fidelity:\n",
+    "                # we are initiator, and want to teleport qubit\n",
+    "                print(\"node {} memory {} entangled with other memory {}\".format(\n",
+    "                    self.node.name, info.index, info.remote_memo))\n",
+    "                \n",
+    "                # run local teleportation ops\n",
+    "                memory_key = info.memory.qstate_key\n",
+    "                z_flip, x_flip = teleport_1(memory_key, self.node)\n",
+    "                \n",
+    "                # send message to other node\n",
+    "                message = TeleportMessage(None, self.other_name, info.remote_memo, x_flip, z_flip)\n",
+    "                self.node.send_message(self.responder, message)\n",
+    "                \n",
+    "                # reset local memory\n",
+    "                self.node.resource_manager.update(None, info.memory, \"RAW\")\n",
+    "                \n",
+    "            elif info.remote_node == reservation.initiator and info.fidelity >= reservation.fidelity:\n",
+    "                # we are responder, and want to receive qubit\n",
+    "                # need to wait on message from sender to correct entanled memory\n",
+    "                self.memos_to_measure.append(info.memory)\n",
+    "                \n",
+    "    def received_message(self, src, message):\n",
+    "        memo_index = message.index\n",
+    "        \n",
+    "        # run local teleportation correction\n",
+    "        memory = self.memos_to_measure.pop(0)\n",
+    "        memory_key = memory.qstate_key\n",
+    "        res = teleport_2(memory_key, self.node, message.x_flip, message.z_flip)\n",
+    "        self.results[res] += 1\n",
+    "        \n",
+    "        # reset local memory\n",
+    "        self.node.resource_manager.update(None, memory, \"RAW\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Building the Simulation\n",
+    "\n",
+    "We'll now construct the network and add our applications. This example follows the usual process to ensure that all tools function properly:\n",
+    "1. Create the topology instance for the simulation to manage our network\n",
+    "    - This class will create a simulation timeline\n",
+    "    - This instantiates the Simulation Kernel (see below)\n",
+    "2. Create the simulated network topology. In this case, we are using an external JSON file to specify nodes and their connectivity.\n",
+    "    - This includes specifying hardware parameters in the `set_parameters` function, defined later\n",
+    "    - This instantiates the Hardware, Entanglement Management, Resource Management, and Network Management modules\n",
+    "3. Install custom protocols/applications and ensure all are set up properly\n",
+    "    - This instantiates the Application module\n",
+    "4. Initialize and run the simulation\n",
+    "5. Collect and display the desired metrics\n",
+    "\n",
+    "The JSON file specifies that network nodes should be of type `QuantumRouter`, a node type defined by SeQUeNCe. This will automatically create all necessary hardware and protocol instances on the nodes, and the `Topology` class will automatically generate `BSMNode` instances on the quantum channels between such nodes.\n",
+    "\n",
+    "To construct an application, we need:\n",
+    "- The node to attach the application to\n",
+    "- The name of the application instance\n",
+    "- The name of the node to teleport qubits to.\n",
+    "\n",
+    "We can get a list of all desired application nodes, in this case routers, from the `Topology` class with the `get_nodes_by_type` method. We then set an application on each one."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def test(sim_time=1.5, qc_atten=1e-5):\n",
+    "    \"\"\"\n",
+    "    sim_time: duration of simulation time (ms)\n",
+    "    qc_atten: quantum channel attenuation (dB/km)\n",
+    "    \"\"\"\n",
+    "    \n",
+    "    network_config = \"star_network.json\"\n",
+    "    \n",
+    "    # here, we make a new topology using the configuration JSON file.\n",
+    "    # we then modify some of the simulation parameters of the network.\n",
+    "    network_topo = RouterNetTopo(network_config)\n",
+    "    set_parameters(network_topo, sim_time, qc_atten)\n",
+    "    \n",
+    "    # get two end nodes and create application\n",
+    "    start_node_name = \"router1\"\n",
+    "    end_node_name = \"router2\"\n",
+    "    node1 = node2 = None\n",
+    "\n",
+    "    for router in network_topo.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER):\n",
+    "        if router.name == start_node_name:\n",
+    "            node1 = router\n",
+    "        elif router.name == end_node_name:\n",
+    "            node2 = router\n",
+    "            \n",
+    "    start_app_name = \"start_app\"\n",
+    "    end_app_name = \"end_app\"\n",
+    "    \n",
+    "    start_app = TeleportApp(node1, start_app_name, end_app_name)\n",
+    "    end_app = TeleportApp(node2, end_app_name, start_app_name)\n",
+    "        \n",
+    "    # run the simulation\n",
+    "    tl = network_topo.get_timeline()\n",
+    "    tl.show_progress = False\n",
+    "    tl.init()\n",
+    "    \n",
+    "    start_app.start(end_node_name, 1e12, 2e12, 10, 0.9)\n",
+    "    tick = time.time()\n",
+    "    tl.run()\n",
+    "    print(\"execution time %.2f sec\" % (time.time() - tick))\n",
+    "    \n",
+    "    print(\"measured |0>:\", end_app.results[0])\n",
+    "    print(\"measured |1>:\", end_app.results[1])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Setting parameters\n",
+    "\n",
+    "Here we define the `set_parameters` function we used earlier. This function will take a `Topology` as input and change many parameters to desired values. This will be covered in greater detail in workshop 3.\n",
+    "\n",
+    "The simulation time limit will be set using the `get_timeline` method.\n",
+    "\n",
+    "Quantum memories and detectors are hardware elements, and so parameters are changed by accessing the hardware included with the `QuantumRouter` and `BSMNode` node types. Many complex hardware elements, such as bsm devices or memory arrays, have methods to update parameters for all included hardware elements. This includes `update_memory_params` to change all memories in an array or `update_detector_params` to change all detectors.\n",
+    "\n",
+    "We will also set the success probability and swapping degradation of the entanglement swapping protocol. This will be set in the Network management Module (specifically the reservation protocol), as this information is necessary to create and manage the rules for the Resource Management module.\n",
+    "\n",
+    "Lastly, we'll update some parameters of the quantum channels. Quantum channels (and, similarly, classical channels) can be accessed from the `Topology` class as the `qchannels` field. Since these are individual hardware elements, we will set the parameters directly."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def set_parameters(topology, simulation_time, attenuation):\n",
+    "    \"\"\"\n",
+    "    simulation_time: duration of simulation time (s)\n",
+    "    attenuation: attenuation on quantum channels (db/m)\n",
+    "    \"\"\"\n",
+    "    \n",
+    "    PS_PER_S = 1e12\n",
+    "    \n",
+    "    # set timeline stop time\n",
+    "    topology.get_timeline().stop_time = (simulation_time * PS_PER_S)\n",
+    "    \n",
+    "    # set memory parameters\n",
+    "    MEMO_FREQ = 2e3\n",
+    "    MEMO_EXPIRE = 0\n",
+    "    MEMO_EFFICIENCY = 1\n",
+    "    MEMO_FIDELITY = 0.9349367588934053\n",
+    "    for node in topology.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER):\n",
+    "        memory_array = node.get_components_by_type(\"MemoryArray\")[0]\n",
+    "        memory_array.update_memory_params(\"frequency\", MEMO_FREQ)\n",
+    "        memory_array.update_memory_params(\"coherence_time\", MEMO_EXPIRE)\n",
+    "        memory_array.update_memory_params(\"efficiency\", MEMO_EFFICIENCY)\n",
+    "        memory_array.update_memory_params(\"raw_fidelity\", MEMO_FIDELITY)\n",
+    "\n",
+    "    # set detector parameters\n",
+    "    DETECTOR_EFFICIENCY = 0.9\n",
+    "    DETECTOR_COUNT_RATE = 5e7\n",
+    "    DETECTOR_RESOLUTION = 100\n",
+    "    for node in topology.get_nodes_by_type(RouterNetTopo.BSM_NODE):\n",
+    "        bsm = node.get_components_by_type(\"SingleAtomBSM\")[0]\n",
+    "        bsm.update_detectors_params(\"efficiency\", DETECTOR_EFFICIENCY)\n",
+    "        bsm.update_detectors_params(\"count_rate\", DETECTOR_COUNT_RATE)\n",
+    "        bsm.update_detectors_params(\"time_resolution\", DETECTOR_RESOLUTION)\n",
+    "        \n",
+    "    # set entanglement swapping parameters\n",
+    "    SWAP_SUCC_PROB = 0.90\n",
+    "    SWAP_DEGRADATION = 0.99\n",
+    "    for node in topology.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER):\n",
+    "        node.network_manager.protocol_stack[1].set_swapping_success_rate(SWAP_SUCC_PROB)\n",
+    "        node.network_manager.protocol_stack[1].set_swapping_degradation(SWAP_DEGRADATION)\n",
+    "        \n",
+    "    # set quantum channel parameters\n",
+    "    ATTENUATION = attenuation\n",
+    "    QC_FREQ = 1e11\n",
+    "    for qc in topology.qchannels:\n",
+    "        qc.attenuation = ATTENUATION\n",
+    "        qc.frequency = QC_FREQ"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Running the Simulation\n",
+    "\n",
+    "All that is left is to run the simulation with user input. We'll specify:\n",
+    "\n",
+    "    sim_time: duration of simulation time (s)\n",
+    "    qc_atten: attenuation on quantum channels (dB/m)\n",
+    "\n",
+    "Note that different hardware parameters or network topologies may cause the simulation to run for a very long time."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "metadata": {
+    "scrolled": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "a80357bdef314d2d9da27a6a38095611",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "interactive(children=(FloatSlider(value=1.5, description='sim_time', max=2.0, min=1.0), Dropdown(description='…"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/plain": [
+       "<function __main__.test(sim_time=1.5, qc_atten=1e-05)>"
+      ]
+     },
+     "execution_count": 11,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "interact(test, sim_time=(1, 2, 0.1), qc_atten=[0, 1e-5, 2e-5])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "venv",
+   "language": "python",
+   "name": "venv"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.10.2"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/example/QCE_demos/images/star_network.png b/example/QCE_demos/images/star_network.png
new file mode 100644
index 00000000..10b048e1
Binary files /dev/null and b/example/QCE_demos/images/star_network.png differ
diff --git a/example/QCE_demos/star_network.json b/example/QCE_demos/star_network.json
new file mode 100644
index 00000000..09b2e25a
--- /dev/null
+++ b/example/QCE_demos/star_network.json
@@ -0,0 +1,118 @@
+{
+  "nodes": [
+    {
+      "name": "center",
+      "type": "QuantumRouter",
+      "seed": 0,
+      "memo_size": 50
+    },
+    {
+      "name": "router1",
+      "type": "QuantumRouter",
+      "seed": 1,
+      "memo_size": 50
+    },
+    {
+      "name": "router2",
+      "type": "QuantumRouter",
+      "seed": 2,
+      "memo_size": 50
+    },
+    {
+      "name": "router3",
+      "type": "QuantumRouter",
+      "seed": 3,
+      "memo_size": 50
+    },
+    {
+      "name": "router4",
+      "type": "QuantumRouter",
+      "seed": 4,
+      "memo_size": 50
+    }
+  ],
+  "qconnections": [
+    {
+      "node1": "center",
+      "node2": "router1",
+      "attenuation": 0.0002,
+      "distance": 500,
+      "type": "meet_in_the_middle"
+    },
+    {
+      "node1": "center",
+      "node2": "router2",
+      "attenuation": 0.0002,
+      "distance": 500,
+      "type": "meet_in_the_middle"
+    },
+    {
+      "node1": "center",
+      "node2": "router3",
+      "attenuation": 0.0002,
+      "distance": 500,
+      "type": "meet_in_the_middle"
+    },
+    {
+      "node1": "center",
+      "node2": "router4",
+      "attenuation": 0.0002,
+      "distance": 500,
+      "type": "meet_in_the_middle"
+    }
+  ],
+  "is_parallel": false,
+  "stop_time": 2000000000000,
+  "cconnections": [
+    {
+      "node1": "center",
+      "node2": "router1",
+      "delay": 500000000
+    },
+    {
+      "node1": "center",
+      "node2": "router2",
+      "delay": 500000000
+    },
+    {
+      "node1": "center",
+      "node2": "router3",
+      "delay": 500000000
+    },
+    {
+      "node1": "center",
+      "node2": "router4",
+      "delay": 500000000
+    },
+    {
+      "node1": "router1",
+      "node2": "router2",
+      "delay": 1000000000
+    },
+    {
+      "node1": "router1",
+      "node2": "router3",
+      "delay": 1000000000
+    },
+    {
+      "node1": "router1",
+      "node2": "router4",
+      "delay": 1000000000
+    },
+    {
+      "node1": "router2",
+      "node2": "router3",
+      "delay": 1000000000
+    },
+    {
+      "node1": "router2",
+      "node2": "router4",
+      "delay": 1000000000
+    },
+    {
+      "node1": "router3",
+      "node2": "router4",
+      "delay": 1000000000
+    }
+  ]
+}
diff --git a/example/absorptive_experiment.py b/example/absorptive_experiment.py
new file mode 100644
index 00000000..b06f31b9
--- /dev/null
+++ b/example/absorptive_experiment.py
@@ -0,0 +1,316 @@
+from typing import List, Dict, Any
+from pathlib import Path
+
+from json5 import dump
+import numpy as np
+
+from sequence.components.bsm import make_bsm
+from sequence.components.detector import QSDetectorFockDirect, QSDetectorFockInterference
+from sequence.components.light_source import SPDCSource
+from sequence.components.memory import AbsorptiveMemory
+from sequence.components.photon import Photon
+from sequence.kernel.event import Event
+from sequence.kernel.process import Process
+from sequence.kernel.timeline import Timeline
+from sequence.topology.node import Node
+from sequence.topology.topology import Topology
+from sequence.protocol import Protocol
+from sequence.utils.encoding import absorptive
+
+
+# define constants
+FREQUENCY = 80e6
+ABS_EFFICIENCY = 1.0
+MODE_NUM = 10000
+COHERENCE_TIME = 1
+TELECOM_WAVELENGTH = 1000
+WAVELENGTH = 500
+OVERLAP_ERR = 0
+
+# experiment settings
+num_direct_trials = 10
+phase_settings = np.linspace(0, 2*np.pi, num=10, endpoint=False)
+
+
+# for absorptive quantum memory
+def efficiency(_: int) -> float:
+    return 1.0
+
+
+# protocol to control photon emission on end node
+class EmitProtocol(Protocol):
+    def __init__(self, own: "EndNode", name: str, other_node: str, num_qubits: int, source_name: str, memory_name: str):
+        """Constructor for Emission protocol.
+
+        Args:
+            own (EndNode): node on which the protocol is located.
+            name (str): name of the protocol instance.
+            other_node (str): name of the other node entangling photons
+            num_qubits (int): number of qubits to send in one execution.
+            delay_time (int): time to wait before re-starting execution.
+            source_name (str): name of the light source on the node.
+            memory_name (str): name of the memory on the node.
+        """
+
+        super().__init__(own, name)
+        self.other_node = other_node
+        self.num_qubits = num_qubits
+        self.source_name = source_name
+        self.memory_name = memory_name
+
+    def start(self):
+        self.own.components[self.memory_name]._prepare_AFC()
+        
+        states = [None] * self.num_qubits  # TODO: rewrite spdc class?
+        source = self.own.components[self.source_name]
+        source.emit(states)
+
+    def received_message(self, src: str, msg):
+        pass
+
+
+class EndNode(Node):
+    """Node for each end of the network.
+
+    This node stores an SPDC photon source and a quantum memory.
+    """
+
+    def __init__(self, name: str, timeline: "Timeline", other_node: str, bsm_node: str, measure_node: str):
+        super().__init__(name, timeline)
+
+        self.bsm_name = bsm_node
+        self.meas_name = measure_node
+
+        # hardware setup
+        spdc_name = name + ".spdc_source"
+        memo_name = name + ".memory"
+        spdc = SPDCSource(spdc_name, timeline, wavelengths=[TELECOM_WAVELENGTH, WAVELENGTH],
+                          frequency=FREQUENCY, encoding_type=absorptive, mean_photon_num=0.2)
+        memory = AbsorptiveMemory(memo_name, timeline, fidelity=0.85, frequency=FREQUENCY,
+                                  absorption_efficiency=ABS_EFFICIENCY, mode_number=MODE_NUM,
+                                  coherence_time=COHERENCE_TIME, wavelength=WAVELENGTH, overlap_error=OVERLAP_ERR,
+                                  efficiency=efficiency, prepare_time=0, destination=measure_node)
+        self.add_component(spdc)
+        self.add_component(memory)
+        spdc.add_receiver(self)
+        spdc.add_receiver(memory)
+        memory.add_receiver(self)
+
+        # protocols
+        self.emit_protocol = EmitProtocol(self, name + ".emit_protocol", other_node, MODE_NUM, spdc_name, memo_name)
+
+    def get(self, photon: "Photon", **kwargs):
+        dst = kwargs.get("dst")
+        if dst is None:
+            # from spdc source: send to bsm node
+            self.send_qubit(self.bsm_name, photon)
+        else:
+            # from memory: send to destination (measurement) node
+            self.send_qubit(dst, photon)
+
+
+class EntangleNode(Node):
+    def __init__(self, name: str, timeline: "Timeline"):
+        super().__init__(name, timeline)
+
+        # hardware setup
+        bsm_name = name + ".bsm"
+        bsm = make_bsm(bsm_name, timeline, "absorptive")
+        self.add_component(bsm)
+        bsm.attach(self)
+        self.set_first_component(bsm_name)
+
+        self.resolution = bsm.resolution
+        self.bsm_times = [[], []]
+
+    def bsm_update(self, bsm, info: Dict[str, Any]):
+        self.bsm_times[info['res']].append(info['time'])
+
+    def get_valid_bins(self, start_time: int, num_bins: int, frequency: float):
+        """Computes time bins containing a BSM measurement.
+
+        Args:
+            start_time (int): simulation start time of when photons received.
+            num_bins (int): number of arrival bins
+            frequency (float): frequency of photon arrival (in Hz).
+
+        Returns:
+            List[int]: list of length num_bins containing 0/1.
+                List element is 0 for an unsuccessful measurement and 1 for successful.
+        """
+
+        return_bins = [0] * num_bins
+
+        for time in self.bsm_times[0] + self.bsm_times[1]:
+            closest_bin = int(round((time - start_time) * frequency * 1e-12))
+            expected_time = (float(closest_bin) * 1e12 / frequency) + start_time
+            if abs(expected_time - time) < self.resolution and 0 <= closest_bin < num_bins:
+                return_bins[closest_bin] = not return_bins[closest_bin]
+
+        return return_bins
+
+
+class MeasureNode(Node):
+    def __init__(self, name: str, timeline: "Timeline", other_nodes: List[str]):
+        super().__init__(name, timeline)
+
+        self.direct_detector_name = name + ".direct"
+        direct_detector = QSDetectorFockDirect(self.direct_detector_name, timeline, other_nodes)
+        self.add_component(direct_detector)
+        direct_detector.attach(self)
+        self.set_first_component(self.direct_detector_name)
+
+        self.bs_detector_name = name + ".bs"
+        bs_detector = QSDetectorFockInterference(self.bs_detector_name, tl)
+        self.add_component(bs_detector)
+        bs_detector.add_receiver(self)
+
+        # time resolution of SPDs
+        self.resolution = max([d.time_resolution for d in direct_detector.detectors + bs_detector.detectors])
+
+    def receive_qubit(self, src: str, qubit) -> None:
+        self.components[self.first_component_name].get(qubit, src=src)
+
+    def set_phase(self, phase: float):
+        self.components[self.bs_detector_name].set_phase(phase)
+
+    def get_detector_entries(self, detector_name: str, start_time: int, num_bins: int, frequency: float):
+        """Computes distribution of detection events for density matrix.
+
+        Args:
+            detector_name (str): name of detector to get measurements from.
+            start_time (int): simulation start time of when photons received.
+            num_bins (int): number of arrival bins
+            frequency (float): frequency of photon arrival (in Hz).
+
+        Returns:
+            List[int]: list of length (duration * 1e-12 * frequency) with result for each time bin.
+        """
+
+        trigger_times = self.components[detector_name].get_photon_times()
+        return_res = [0] * num_bins
+
+        for time in trigger_times[0]:
+            closest_bin = int(round((time - start_time) * frequency * 1e-12))
+            expected_time = (float(closest_bin) * 1e12 / frequency) + start_time
+            if abs(expected_time - time) < self.resolution and 0 <= closest_bin < num_bins:
+                return_res[closest_bin] += 1
+
+        for time in trigger_times[1]:
+            closest_bin = int(round((time - start_time) * frequency * 1e-12))
+            expected_time = (float(closest_bin) * 1e12 / frequency) + start_time
+            if abs(expected_time - time) < self.resolution and 0 <= closest_bin < num_bins:
+                return_res[closest_bin] += 2
+
+        return return_res
+
+
+if __name__ == "__main__":
+    # open file to store experiment results
+    Path("results").mkdir(parents=True, exist_ok=True)
+    filename = "results/absorptive.json"
+    fh = open(filename, 'w')
+
+    """Run Simulation"""
+
+    tl = Timeline(1e12, 'density_matrix')
+    tl.seed(0)
+
+    anl_name = "Argonne"
+    hc_name = "Harper Court"
+    erc_name = "Eckhardt Research Center"
+    erc_2_name = "Eckhardt Research Center 2"
+
+    anl = EndNode(anl_name, tl, hc_name, erc_name, erc_2_name)
+    hc = EndNode(hc_name, tl, anl_name, erc_name, erc_2_name)
+    erc = EntangleNode(erc_name, tl)
+    erc_2 = MeasureNode(erc_2_name, tl, [anl_name, hc_name])
+
+    topo = Topology("Experiment Topo", tl)
+    for node in [anl, hc, erc, erc_2]:
+        topo.add_node(node)
+    topo.add_quantum_channel(anl_name, erc_name, distance=20, attenuation=0)
+    topo.add_quantum_channel(hc_name, erc_name, distance=20, attenuation=0)
+    topo.add_quantum_channel(anl_name, erc_2_name, distance=20, attenuation=0)
+    topo.add_quantum_channel(hc_name, erc_2_name, distance=20, attenuation=0)
+
+    tl.init()
+
+    # calculations for when to start protocol
+    delay_anl = topo.nodes[anl_name].qchannels[erc_name].delay
+    delay_hc = topo.nodes[hc_name].qchannels[erc_name].delay
+    time_anl = max(delay_anl, delay_hc) - delay_anl
+    time_hc = max(delay_anl, delay_hc) - delay_hc
+
+    # calculations for when to start recording measurements
+    start_time_bsm = time_anl + delay_anl
+    mem = anl.get_components_by_type("AbsorptiveMemory")[0]
+    total_time = mem.total_time
+    start_time_meas = time_anl + total_time + delay_anl
+
+    results_direct_measurement = []
+    results_bs_measurement = []
+
+    for i in range(num_direct_trials):
+        # start protocol for emitting
+        process = Process(anl.emit_protocol, "start", [])
+        event = Event(time_anl, process)
+        tl.schedule(event)
+        process = Process(hc.emit_protocol, "start", [])
+        event = Event(time_hc, process)
+        tl.schedule(event)
+
+        tl.run()
+        print("finished direct measurement trial {} out of {}".format(i+1, num_direct_trials))
+
+        # collect data
+        bsm_res = erc.get_valid_bins(start_time_bsm, MODE_NUM, FREQUENCY)
+        meas_res = erc_2.get_detector_entries(erc_2.direct_detector_name, start_time_meas, MODE_NUM, FREQUENCY)
+        num_bsm_res = sum(bsm_res)
+        meas_res_valid = [m for m, b in zip(meas_res, bsm_res) if b]
+        probs = [0.0] * 4
+        for j in range(4):
+            probs[j] = meas_res_valid.count(j) / num_bsm_res
+        results_direct_measurement.append(probs)
+
+        # reset timeline
+        tl.time = 0
+        tl.init()
+
+    # change to other measurement
+    erc_2.set_first_component(erc_2.bs_detector_name)
+    for i, phase in enumerate(phase_settings):
+        erc_2.set_phase(phase)
+
+        # start protocol for emitting
+        process = Process(anl.emit_protocol, "start", [])
+        event = Event(time_anl, process)
+        tl.schedule(event)
+        process = Process(hc.emit_protocol, "start", [])
+        event = Event(time_hc, process)
+        tl.schedule(event)
+
+        tl.run()
+        print("finished interference measurement trial {} out of {}".format(i+1, len(phase_settings)))
+
+        # collect data
+        bsm_res = erc.get_valid_bins(start_time_bsm, MODE_NUM, FREQUENCY)
+        meas_res = erc_2.get_detector_entries(erc_2.bs_detector_name, start_time_meas, MODE_NUM, FREQUENCY)
+        meas_res_valid = [m for m, b in zip(meas_res, bsm_res) if b]
+        num_detector_0 = meas_res.count(1) + meas_res_valid.count(3)
+        num_detector_1 = meas_res.count(2) + meas_res_valid.count(3)
+        freqs = [num_detector_0/(total_time * 1e-12), num_detector_1/(total_time * 1e-12)]
+        results_bs_measurement.append(freqs)
+
+        # reset timeline
+        tl.time = 0
+        tl.init()
+
+    """Store results"""
+
+    info = {"direct results": results_direct_measurement, "bs results": results_bs_measurement}
+    dump(info, fh)
+
+    # plt.bar(list(range(4)), counts)
+    # plt.yscale('log')
+    # plt.show()
diff --git a/example/absorptive_graph.py b/example/absorptive_graph.py
new file mode 100644
index 00000000..1ae6f400
--- /dev/null
+++ b/example/absorptive_graph.py
@@ -0,0 +1,16 @@
+from json5 import load
+import matplotlib.pyplot as plt
+
+
+filename = "results/absorptive.json"
+data = load(open(filename))
+
+direct_results = data["direct results"]
+bs_results = data["bs results"]
+
+# plotting bs results
+freq_0 = [res[0] for res in bs_results]
+freq_1 = [res[1] for res in bs_results]
+plt.plot(freq_0)
+plt.plot(freq_1)
+plt.show()
diff --git a/example/bb84_logging.py b/example/bb84_logging.py
index b55c0801..43c0873e 100644
--- a/example/bb84_logging.py
+++ b/example/bb84_logging.py
@@ -15,12 +15,14 @@
 distance = 1e3
 
 tl = Timeline(runtime)
+tl.show_progress = True
 
 # set log
 log.set_logger(__name__, tl, log_filename)
-log.set_logger_level("DEBUG")
-log.track_module("BB84")
+log.set_logger_level('DEBUG')
+log.track_module('BB84')
 log.track_module('timeline')
+log.track_module('light_source')
 
 qc0 = QuantumChannel("qc0", tl, distance=distance, polarization_fidelity=0.97, attenuation=0.0002)
 qc1 = QuantumChannel("qc1", tl, distance=distance, polarization_fidelity=0.97, attenuation=0.0002)
@@ -38,10 +40,8 @@
     alice.update_lightsource_params(name, param)
 
 # Bob
-detector_params = [{"efficiency": 0.8, "dark_count": 10, "time_resolution": 10,
-                    "count_rate": 50e6},
-                   {"efficiency": 0.8, "dark_count": 10, "time_resolution": 10,
-                    "count_rate": 50e6}]
+detector_params = [{"efficiency": 0.8, "dark_count": 10, "time_resolution": 10, "count_rate": 50e6},
+                   {"efficiency": 0.8, "dark_count": 10, "time_resolution": 10, "count_rate": 50e6}]
 bob = QKDNode("bob", tl, stack_size=1)
 bob.set_seed(1)
 
diff --git a/example/dark_count_timebin.py b/example/dark_count_timebin.py
index d0e6d4d5..9036d4df 100644
--- a/example/dark_count_timebin.py
+++ b/example/dark_count_timebin.py
@@ -34,7 +34,7 @@ def pop(self, key):
     def push(self):
         self.lower_protocols[0].push(self.keysize, self.keynum)
 
-    def received_message(self):
+    def received_message(self, src, msg):
         pass
 
 
diff --git a/example/distance_bb84.py b/example/distance_bb84.py
index 1aa27728..a62e0c18 100644
--- a/example/distance_bb84.py
+++ b/example/distance_bb84.py
@@ -27,6 +27,7 @@
         distance = max(1000, 10000 * int(i))
 
         tl = Timeline(runtime)
+        tl.show_progress = True
 
         qc0 = QuantumChannel("qc0", tl, distance=distance, polarization_fidelity=0.97, attenuation=0.0002)
         qc1 = QuantumChannel("qc1", tl, distance=distance, polarization_fidelity=0.97, attenuation=0.0002)
@@ -44,11 +45,8 @@
             alice.update_lightsource_params(name, param)
 
         # Bob
-        detector_params = [
-            {"efficiency": 0.8, "dark_count": 10, "time_resolution": 10,
-             "count_rate": 50e6},
-            {"efficiency": 0.8, "dark_count": 10, "time_resolution": 10,
-             "count_rate": 50e6}]
+        detector_params = [{"efficiency": 0.8, "dark_count": 10, "time_resolution": 10, "count_rate": 50e6},
+                           {"efficiency": 0.8, "dark_count": 10, "time_resolution": 10, "count_rate": 50e6}]
         bob = QKDNode("bob", tl, stack_size=1)
         bob.set_seed(1)
 
@@ -64,8 +62,7 @@
         # BB84 config
         pair_bb84_protocols(alice.protocol_stack[0], bob.protocol_stack[0])
 
-        process = Process(alice.protocol_stack[0], "push",
-                          [256, math.inf, 6e12])
+        process = Process(alice.protocol_stack[0], "push", [256, math.inf, 6e12])
         event = Event(0, process)
         tl.schedule(event)
 
@@ -82,21 +79,6 @@
         error_rate_list.append(mean(bba.error_rates))
         latency_list.append(bba.latency)
 
-        # fh.write(str(distance))
-        # fh.write(' ')
-        # if bba.throughputs:
-        #     fh.write(str(1e-6 * sum(bba.throughputs) / len(bba.throughputs)))
-        # else:
-        #     fh.write(str(None))
-        # fh.write(' ')
-        # if bba.error_rates:
-        #     fh.write(str(sum(bba.error_rates) / len(bba.error_rates)))
-        # else:
-        #     fh.write(str(None))
-        # fh.write(' ')
-        # fh.write(str(bba.latency))
-        # fh.write('\n')
-
     log = {'Distance': dist_list, "Throughput": tp_list, 'Error_rate': error_rate_list, 'Latency': latency_list}
     df = pd.DataFrame(log)
     df.to_csv('distance_bb84.csv')
diff --git a/example/distance_timebin.py b/example/distance_timebin.py
index d551578a..a69aad8c 100644
--- a/example/distance_timebin.py
+++ b/example/distance_timebin.py
@@ -29,7 +29,7 @@ def pop(self, info):
     def push(self):
         self.lower_protocols[0].push(self.keysize, 10)
 
-    def received_message(self):
+    def received_message(self, src, msg):
         pass
 
 
@@ -43,6 +43,7 @@ def received_message(self):
 
     for distance in distances:
         tl = Timeline(runtime)
+        tl.show_progress = True
 
         qc0 = QuantumChannel("qc0", tl, distance=distance * 1e3, polarization_fidelity=0.97, attenuation=0.0002)
         qc1 = QuantumChannel("qc1", tl, distance=distance * 1e3, polarization_fidelity=0.97, attenuation=0.0002)
@@ -58,10 +59,9 @@ def received_message(self):
             alice.update_lightsource_params(name, param)
 
         # Bob
-        detector_params = [
-            {"efficiency": 0.8, "dark_count": 1, "time_resolution": 10},
-            {"efficiency": 0.8, "dark_count": 1, "time_resolution": 10},
-            {"efficiency": 0.8, "dark_count": 1, "time_resolution": 10}]
+        detector_params = [{"efficiency": 0.8, "dark_count": 1, "time_resolution": 10},
+                           {"efficiency": 0.8, "dark_count": 1, "time_resolution": 10},
+                           {"efficiency": 0.8, "dark_count": 1, "time_resolution": 10}] 
         bob = QKDNode("bob", tl, encoding=time_bin, stack_size=1)
         bob.set_seed(1)
 
diff --git a/example/qkd.ipynb b/example/qkd.ipynb
index fec22fc0..ca6770cc 100644
--- a/example/qkd.ipynb
+++ b/example/qkd.ipynb
@@ -115,12 +115,13 @@
     "    \"\"\"\n",
     "    # begin by defining the simulation timeline with the correct simulation time\n",
     "    tl = Timeline(sim_time * 1e9)\n",
-    "    tl.seed(0)\n",
     "    \n",
     "    # Here, we create nodes for the network (QKD nodes for key distribution)\n",
     "    # stack_size=1 indicates that only the BB84 protocol should be included\n",
     "    n1 = QKDNode(\"n1\", tl, stack_size=1)\n",
     "    n2 = QKDNode(\"n2\", tl, stack_size=1)\n",
+    "    n1.set_seed(0)\n",
+    "    n2.set_seed(1)\n",
     "    pair_bb84_protocols(n1.protocol_stack[0], n2.protocol_stack[0])\n",
     "    \n",
     "    # connect the nodes and set parameters for the fibers\n",
@@ -131,6 +132,8 @@
     "    cc1 = ClassicalChannel(\"cc_n2_n1\", tl, distance=1e3)\n",
     "    cc0.set_ends(n1, n2.name)\n",
     "    cc1.set_ends(n2, n1.name)\n",
+    "    # construct a quantum communication channel\n",
+    "    # (with arguments for the channel name, timeline, attenuation (in dB/km), and distance (in m))\n",
     "    qc0 = QuantumChannel(\"qc_n1_n2\", tl, attenuation=1e-5, distance=1e3,\n",
     "                         polarization_fidelity=0.97)\n",
     "    qc1 = QuantumChannel(\"qc_n2_n1\", tl, attenuation=1e-5, distance=1e3,\n",
@@ -180,13 +183,13 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 6,
    "metadata": {},
    "outputs": [
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "ff78a97574d447029dbeb13e0399cda8",
+       "model_id": "28d22d2059344083b9c46aef7f89a6fc",
        "version_major": 2,
        "version_minor": 0
       },
@@ -203,7 +206,7 @@
        "<function __main__.test(sim_time, keysize)>"
       ]
      },
-     "execution_count": 5,
+     "execution_count": 6,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -267,11 +270,12 @@
     "    \"\"\"\n",
     "    # begin by defining the simulation timeline with the correct simulation time\n",
     "    tl = Timeline(sim_time * 1e9)\n",
-    "    tl.seed(0)\n",
     "    \n",
     "    # Here, we create nodes for the network (QKD nodes for key distribution)\n",
     "    n1 = QKDNode(\"n1\", tl)\n",
     "    n2 = QKDNode(\"n2\", tl)\n",
+    "    n1.set_seed(0)\n",
+    "    n2.set_seed(1)\n",
     "    pair_bb84_protocols(n1.protocol_stack[0], n2.protocol_stack[0])\n",
     "    pair_cascade_protocols(n1.protocol_stack[1], n2.protocol_stack[1])\n",
     "    \n",
@@ -345,7 +349,7 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "17ed86e425b24158a3e5b8da3e99ce4e",
+       "model_id": "d0703f7c810843e7b4e4c3d286b20381",
        "version_major": 2,
        "version_minor": 0
       },
diff --git a/example/random_request_network.ipynb b/example/random_request_network.ipynb
index 24d3565b..38ed1a5f 100644
--- a/example/random_request_network.ipynb
+++ b/example/random_request_network.ipynb
@@ -67,6 +67,7 @@
     "- The node to attach the application to\n",
     "- The names (given as strings) of other possible nodes to generate links with\n",
     "- A seed for the internal random number generator of the application\n",
+    "\n",
     "And, specifically for the random request app, we need:\n",
     "- the minimum and maximum duration of the reservation\n",
     "- the minimum and maximum amount of memories to reserve\n",
@@ -86,7 +87,6 @@
     "    sim_time: duration of simulation time (ms)\n",
     "    qc_atten: quantum channel attenuation (dB/km)\n",
     "    \"\"\"\n",
-    "    \n",
     "    network_config = \"star_network.json\"\n",
     "\n",
     "    # here, we make a new topology using the configuration JSON file.\n",
@@ -95,15 +95,20 @@
     "    set_parameters(network_topo, sim_time, qc_atten)\n",
     "    \n",
     "    # construct random request applications\n",
-    "    nodes = [node for node in network_topo.get_nodes_by_type(\"QuantumRouter\")]\n",
-    "    node_names = [node.name for node in nodes]\n",
+    "    quantum_router_nodes = network_topo.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER)\n",
+    "    node_names = [node.name for node in quantum_router_nodes]\n",
     "    apps = []\n",
-    "    for i, (node, name) in enumerate(zip(nodes, node_names)):\n",
-    "        other_nodes = node_names[:] # copy node name list\n",
+    "    for i, (name, node) in enumerate(zip(node_names, quantum_router_nodes)):\n",
+    "        # copy node name list\n",
+    "        other_nodes = node_names[:]\n",
     "        other_nodes.remove(name)\n",
     "        \n",
     "        # create our application\n",
-    "        # arguments are the host node, possible destination node names, and a seed for the random number generator.\n",
+    "        # arguments are:\n",
+    "        #   the host node,\n",
+    "        #   possible destination node names,\n",
+    "        #   a seed for the random number generator, and\n",
+    "        #   several arguments for request parameters.\n",
     "        app = RandomRequestApp(node, other_nodes, i,\n",
     "                               min_dur=1e13, max_dur=2e13, min_size=10,\n",
     "                               max_size=25, min_fidelity=0.8, max_fidelity=1.0)\n",
@@ -112,6 +117,7 @@
     "    \n",
     "    # run the simulation\n",
     "    tl = network_topo.get_timeline()\n",
+    "    tl.show_progress = True\n",
     "    tl.init()\n",
     "    tick = time.time()\n",
     "    tl.run()\n",
@@ -130,7 +136,7 @@
     "    start_times = []\n",
     "    end_times = []\n",
     "    memory_sizes = []\n",
-    "    for node in network_topo.get_nodes_by_type(\"QuantumRouter\"):\n",
+    "    for node in network_topo.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER):\n",
     "        node_name = node.name\n",
     "        for reservation in node.network_manager.protocol_stack[1].accepted_reservation:\n",
     "            s_t, e_t, size = reservation.start_time, reservation.end_time, reservation.memory_size\n",
@@ -164,20 +170,20 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": 4,
    "metadata": {},
    "outputs": [],
    "source": [
-    "def set_parameters(topology, sim_time, qc_atten):\n",
+    "def set_parameters(topology, simulation_time, attenuation):\n",
     "    \"\"\"\n",
-    "    sim_time: duration of simulation time (ms)\n",
-    "    qc_atten: attenuation on quantum channels (db/m)\n",
+    "    simulation_time: duration of simulation time (in ms)\n",
+    "    attenuation: attenuation on quantum channels (in db/m)\n",
     "    \"\"\"\n",
     "    \n",
     "    PS_PER_MS = 1e9\n",
     "    \n",
     "    # set timeline stop time\n",
-    "    topology.get_timeline().stop_time = (sim_time * PS_PER_MS)\n",
+    "    topology.get_timeline().stop_time = (simulation_time * PS_PER_MS)\n",
     "    \n",
     "    # set memory parameters\n",
     "    MEMO_FREQ = 2e3\n",
@@ -185,19 +191,21 @@
     "    MEMO_EFFICIENCY = 1\n",
     "    MEMO_FIDELITY = 0.9349367588934053\n",
     "    for node in topology.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER):\n",
-    "        node.memory_array.update_memory_params(\"frequency\", MEMO_FREQ)\n",
-    "        node.memory_array.update_memory_params(\"coherence_time\", MEMO_EXPIRE)\n",
-    "        node.memory_array.update_memory_params(\"efficiency\", MEMO_EFFICIENCY)\n",
-    "        node.memory_array.update_memory_params(\"raw_fidelity\", MEMO_FIDELITY)\n",
+    "        memory_array = node.get_components_by_type(\"MemoryArray\")[0]\n",
+    "        memory_array.update_memory_params(\"frequency\", MEMO_FREQ)\n",
+    "        memory_array.update_memory_params(\"coherence_time\", MEMO_EXPIRE)\n",
+    "        memory_array.update_memory_params(\"efficiency\", MEMO_EFFICIENCY)\n",
+    "        memory_array.update_memory_params(\"raw_fidelity\", MEMO_FIDELITY)\n",
     "\n",
     "    # set detector parameters\n",
     "    DETECTOR_EFFICIENCY = 0.9\n",
     "    DETECTOR_COUNT_RATE = 5e7\n",
     "    DETECTOR_RESOLUTION = 100\n",
     "    for node in topology.get_nodes_by_type(RouterNetTopo.BSM_NODE):\n",
-    "        node.bsm.update_detectors_params(\"efficiency\", DETECTOR_EFFICIENCY)\n",
-    "        node.bsm.update_detectors_params(\"count_rate\", DETECTOR_COUNT_RATE)\n",
-    "        node.bsm.update_detectors_params(\"time_resolution\", DETECTOR_RESOLUTION)\n",
+    "        bsm = node.get_components_by_type(\"SingleAtomBSM\")[0]\n",
+    "        bsm.update_detectors_params(\"efficiency\", DETECTOR_EFFICIENCY)\n",
+    "        bsm.update_detectors_params(\"count_rate\", DETECTOR_COUNT_RATE)\n",
+    "        bsm.update_detectors_params(\"time_resolution\", DETECTOR_RESOLUTION)\n",
     "        \n",
     "    # set entanglement swapping parameters\n",
     "    SWAP_SUCC_PROB = 0.90\n",
@@ -207,7 +215,7 @@
     "        node.network_manager.protocol_stack[1].set_swapping_degradation(SWAP_DEGRADATION)\n",
     "        \n",
     "    # set quantum channel parameters\n",
-    "    ATTENUATION = qc_atten\n",
+    "    ATTENUATION = attenuation\n",
     "    QC_FREQ = 1e11\n",
     "    for qc in topology.qchannels:\n",
     "        qc.attenuation = ATTENUATION\n",
@@ -230,7 +238,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 7,
+   "execution_count": 5,
    "metadata": {
     "scrolled": false
    },
@@ -238,7 +246,7 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "dd506771a175451fa4b6bb1c50b8232b",
+       "model_id": "e5b30c59d8854386a190d0ee3d9e6169",
        "version_major": 2,
        "version_minor": 0
       },
@@ -255,7 +263,7 @@
        "<function __main__.test(sim_time, qc_atten)>"
       ]
      },
-     "execution_count": 7,
+     "execution_count": 5,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -274,9 +282,9 @@
  ],
  "metadata": {
   "kernelspec": {
-   "display_name": "Python 3",
+   "display_name": "venv",
    "language": "python",
-   "name": "python3"
+   "name": "venv"
   },
   "language_info": {
    "codemirror_mode": {
@@ -288,7 +296,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.7.3"
+   "version": "3.10.2"
   }
  },
  "nbformat": 4,
diff --git a/example/random_request_network_mod.ipynb b/example/random_request_network_mod.ipynb
index f29475f3..2966650e 100644
--- a/example/random_request_network_mod.ipynb
+++ b/example/random_request_network_mod.ipynb
@@ -98,15 +98,20 @@
     "    set_parameters(network_topo, sim_time, qc_atten)\n",
     "    \n",
     "    # construct random request applications\n",
-    "    nodes = [node for node in network_topo.get_nodes_by_type(\"QuantumRouter\")]\n",
-    "    node_names = [node.name for node in nodes]\n",
+    "    quantum_router_nodes = network_topo.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER)\n",
+    "    node_names = [node.name for node in quantum_router_nodes]\n",
     "    apps = []\n",
-    "    for i, (node, name) in enumerate(zip(nodes, node_names)):\n",
-    "        other_nodes = node_names[:] # copy node name list\n",
+    "    for i, (name, node) in enumerate(zip(node_names, quantum_router_nodes)):\n",
+    "        # copy node name list\n",
+    "        other_nodes = node_names[:]\n",
     "        other_nodes.remove(name)\n",
     "        \n",
     "        # create our application\n",
-    "        # arguments are the host node, possible destination node names, and a seed for the random number generator.\n",
+    "        # arguments are:\n",
+    "        #   the host node,\n",
+    "        #   possible destination node names,\n",
+    "        #   a seed for the random number generator, and\n",
+    "        #   several arguments for request parameters.\n",
     "        app = RandomRequestApp(node, other_nodes, i,\n",
     "                               min_dur=1e13, max_dur=2e13, min_size=10,\n",
     "                               max_size=25, min_fidelity=0.8, max_fidelity=1.0)\n",
@@ -115,6 +120,7 @@
     "        \n",
     "    # run the simulation\n",
     "    tl = network_topo.get_timeline()\n",
+    "    tl.show_progress = True\n",
     "    tl.init()\n",
     "    tick = time.time()\n",
     "    tl.run()\n",
@@ -132,7 +138,7 @@
     "    start_times = []\n",
     "    end_times = []\n",
     "    memory_sizes = []\n",
-    "    for node in network_topo.get_nodes_by_type(\"QuantumRouter\"):\n",
+    "    for node in network_topo.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER):\n",
     "        node_name = node.name\n",
     "        for reservation in node.network_manager.protocol_stack[1].accepted_reservation:\n",
     "            s_t, e_t, size = reservation.start_time, reservation.end_time, reservation.memory_size\n",
@@ -172,46 +178,48 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "def set_parameters(topology, sim_time, qc_atten):\n",
+    "def set_parameters(topology, simulation_time, attenuation):\n",
     "    \"\"\"\n",
-    "    sim_time: duration of simulation time (ms)\n",
-    "    qc_atten: attenuation on quantum channels (db/m)\n",
+    "    simulation_time: duration of simulation time (ms)\n",
+    "    attenuation: attenuation on quantum channels (db/m)\n",
     "    \"\"\"\n",
     "    \n",
     "    PS_PER_MS = 1e9\n",
     "    \n",
     "    # set timeline stop time\n",
-    "    topology.get_timeline().stop_time = (sim_time * PS_PER_MS)\n",
+    "    topology.get_timeline().stop_time = (simulation_time * PS_PER_MS)\n",
     "    \n",
     "    # set memory parameters\n",
     "    MEMO_FREQ = 2e3\n",
     "    MEMO_EXPIRE = 0\n",
     "    MEMO_EFFICIENCY = 1\n",
     "    MEMO_FIDELITY = 0.9349367588934053\n",
-    "    for node in topology.get_nodes_by_type(\"QuantumRouter\"):\n",
-    "        node.memory_array.update_memory_params(\"frequency\", MEMO_FREQ)\n",
-    "        node.memory_array.update_memory_params(\"coherence_time\", MEMO_EXPIRE)\n",
-    "        node.memory_array.update_memory_params(\"efficiency\", MEMO_EFFICIENCY)\n",
-    "        node.memory_array.update_memory_params(\"raw_fidelity\", MEMO_FIDELITY)\n",
+    "    for node in topology.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER):\n",
+    "        memory_array = node.get_components_by_type(\"MemoryArray\")[0]\n",
+    "        memory_array.update_memory_params(\"frequency\", MEMO_FREQ)\n",
+    "        memory_array.update_memory_params(\"coherence_time\", MEMO_EXPIRE)\n",
+    "        memory_array.update_memory_params(\"efficiency\", MEMO_EFFICIENCY)\n",
+    "        memory_array.update_memory_params(\"raw_fidelity\", MEMO_FIDELITY)\n",
     "\n",
     "    # set detector parameters\n",
     "    DETECTOR_EFFICIENCY = 0.9\n",
     "    DETECTOR_COUNT_RATE = 5e7\n",
     "    DETECTOR_RESOLUTION = 100\n",
-    "    for node in topology.get_nodes_by_type(\"BSMNode\"):\n",
-    "        node.bsm.update_detectors_params(\"efficiency\", DETECTOR_EFFICIENCY)\n",
-    "        node.bsm.update_detectors_params(\"count_rate\", DETECTOR_COUNT_RATE)\n",
-    "        node.bsm.update_detectors_params(\"time_resolution\", DETECTOR_RESOLUTION)\n",
+    "    for node in topology.get_nodes_by_type(RouterNetTopo.BSM_NODE):\n",
+    "        bsm = node.get_components_by_type(\"SingleAtomBSM\")[0]\n",
+    "        bsm.update_detectors_params(\"efficiency\", DETECTOR_EFFICIENCY)\n",
+    "        bsm.update_detectors_params(\"count_rate\", DETECTOR_COUNT_RATE)\n",
+    "        bsm.update_detectors_params(\"time_resolution\", DETECTOR_RESOLUTION)\n",
     "        \n",
     "    # set entanglement swapping parameters\n",
     "    SWAP_SUCC_PROB = 0.90\n",
     "    SWAP_DEGRADATION = 0.99\n",
-    "    for node in topology.get_nodes_by_type(\"QuantumRouter\"):\n",
+    "    for node in topology.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER):\n",
     "        node.network_manager.protocol_stack[1].set_swapping_success_rate(SWAP_SUCC_PROB)\n",
     "        node.network_manager.protocol_stack[1].set_swapping_degradation(SWAP_DEGRADATION)\n",
     "        \n",
     "    # set quantum channel parameters\n",
-    "    ATTENUATION = qc_atten\n",
+    "    ATTENUATION = attenuation\n",
     "    QC_FREQ = 1e11\n",
     "    for qc in topology.qchannels:\n",
     "        qc.attenuation = ATTENUATION\n",
@@ -224,7 +232,12 @@
    "source": [
     "### Running the Simulation\n",
     "\n",
-    "All that is left is to run the simulation with user input. Note that different hardware parameters or network topologies may cause the simulation to run for a very long time."
+    "All that is left is to run the simulation with user input. We'll specify:\n",
+    "\n",
+    "    sim_time: duration of simulation time (ms)\n",
+    "    qc_atten: attenuation on quantum channels (db/m)\n",
+    "\n",
+    "Note that different hardware parameters or network topologies may cause the simulation to run for a very long time."
    ]
   },
   {
@@ -237,7 +250,7 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "7779e45afd3b4de9a73594ae711ee7e7",
+       "model_id": "463491e8346844aab2859ed3dfee55ec",
        "version_major": 2,
        "version_minor": 0
       },
diff --git a/example/starlight_experiments.py b/example/starlight_experiments.py
index ddd3a2c1..d3935373 100644
--- a/example/starlight_experiments.py
+++ b/example/starlight_experiments.py
@@ -1,7 +1,21 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from sequence.topology import Node
+
 import pandas as pd
 from sequence.app.random_request import RandomRequestApp
 from sequence.topology.router_net_topo import RouterNetTopo
 
+
+def get_component(node: Node, component_type: str):
+    for comp in node.components.values():
+        if type(comp).__name__ == component_type:
+            return comp
+
+    raise ValueError("No component of type {} on node {}".format(component_type, node.name))
+
+
 if __name__ == "__main__":
     # Experiment params and config
     network_config_file = "example/starlight.json"
@@ -16,20 +30,21 @@
     MEMO_EFFICIENCY = 0.75
     MEMO_FIDELITY = 0.9349367588934053
     for node in routers:
-        node.memory_array.update_memory_params("frequency", MEMO_FREQ)
-        node.memory_array.update_memory_params("coherence_time", MEMO_EXPIRE)
-        node.memory_array.update_memory_params("efficiency", MEMO_EFFICIENCY)
-        node.memory_array.update_memory_params("raw_fidelity", MEMO_FIDELITY)
+        memory_array = node.get_components_by_type("MemoryArray")[0]  # assume only 1 memory array
+        memory_array.update_memory_params("frequency", MEMO_FREQ)
+        memory_array.update_memory_params("coherence_time", MEMO_EXPIRE)
+        memory_array.update_memory_params("efficiency", MEMO_EFFICIENCY)
+        memory_array.update_memory_params("raw_fidelity", MEMO_FIDELITY)
 
     # set detector parameters
     DETECTOR_EFFICIENCY = 0.8
     DETECTOR_COUNT_RATE = 5e7
     DETECTOR_RESOLUTION = 100
     for node in bsm_nodes:
-        node.bsm.update_detectors_params("efficiency", DETECTOR_EFFICIENCY)
-        node.bsm.update_detectors_params("count_rate", DETECTOR_COUNT_RATE)
-        node.bsm.update_detectors_params("time_resolution",
-                                         DETECTOR_RESOLUTION)
+        bsm = node.get_components_by_type("SingleAtomBSM")[0]
+        bsm.update_detectors_params("efficiency", DETECTOR_EFFICIENCY)
+        bsm.update_detectors_params("count_rate", DETECTOR_COUNT_RATE)
+        bsm.update_detectors_params("time_resolution", DETECTOR_RESOLUTION)
 
     # set quantum channel parameters
     ATTENUATION = 0.0002
@@ -42,10 +57,8 @@
     SWAP_SUCC_PROB = 0.64
     SWAP_DEGRADATION = 0.99
     for node in routers:
-        node.network_manager.protocol_stack[1].set_swapping_success_rate(
-            SWAP_SUCC_PROB)
-        node.network_manager.protocol_stack[1].set_swapping_degradation(
-            SWAP_DEGRADATION)
+        node.network_manager.protocol_stack[1].set_swapping_success_rate(SWAP_SUCC_PROB)
+        node.network_manager.protocol_stack[1].set_swapping_degradation(SWAP_DEGRADATION)
 
     apps = []
     router_names = [node.name for node in routers]
@@ -117,8 +130,7 @@
     memory_sizes = []
     for node in routers:
         node_name = node.name
-        for reservation in node.network_manager.protocol_stack[
-            1].accepted_reservation:
+        for reservation in node.network_manager.protocol_stack[1].accepted_reservation:
             s_t, e_t, size = reservation.start_time, reservation.end_time, reservation.memory_size
             if reservation.initiator != node.name and reservation.responder != node.name:
                 size *= 2
diff --git a/example/three_node_eg_ep_es.ipynb b/example/three_node_eg_ep_es.ipynb
index a47f0251..72189e0f 100644
--- a/example/three_node_eg_ep_es.ipynb
+++ b/example/three_node_eg_ep_es.ipynb
@@ -89,9 +89,10 @@
     "    \"\"\"\n",
     "    sim_time: duration of simulation time (ms)\n",
     "    cc_delay: delay on classical channels (ms)\n",
-    "    qc_atten: attenuation on quantum channels (db/m)\n",
+    "    qc_atten: attenuation on quantum channels (dB/m)\n",
     "    qc_dist: distance of quantum channels (km)\n",
     "    \"\"\"\n",
+    "    \n",
     "    PS_PER_MS = 1e9\n",
     "    M_PER_KM = 1e3\n",
     "    \n",
@@ -116,26 +117,24 @@
     "    m1 = BSMNode(\"m1\", tl, [\"r1\", \"r2\"])\n",
     "    m2 = BSMNode(\"m2\", tl, [\"r2\", \"r3\"])\n",
     "    \n",
-    "    r1.set_seed(0)\n",
-    "    r2.set_seed(1)\n",
-    "    r3.set_seed(2)\n",
-    "    m1.set_seed(3)\n",
-    "    m2.set_seed(4)\n",
-    "    \n",
     "    r1.add_bsm_node(m1.name, r2.name)\n",
     "    r2.add_bsm_node(m1.name, r1.name)\n",
     "    r2.add_bsm_node(m2.name, r3.name)\n",
     "    r3.add_bsm_node(m2.name, r2.name)\n",
     "    \n",
+    "    # set seeds for random generators\n",
+    "    nodes = [r1, r2, r3, m1, m2]\n",
+    "    for i, node in enumerate(nodes):\n",
+    "        node.set_seed(i)\n",
+    "    \n",
     "    for node in [r1, r2, r3]:\n",
+    "        memory_array = node.get_components_by_type(\"MemoryArray\")[0]\n",
     "        # we update the coherence time (measured in seconds) here\n",
-    "        node.memory_array.update_memory_params(\"coherence_time\", 10)\n",
-    "    for node in [r1,r2,r3]:\n",
+    "        memory_array.update_memory_params(\"coherence_time\", 10)\n",
     "        # and similarly update the fidelity of entanglement for the memories\n",
-    "        node.memory_array.update_memory_params(\"raw_fidelity\", raw_fidelity)\n",
+    "        memory_array.update_memory_params(\"raw_fidelity\", raw_fidelity)\n",
     "    \n",
     "    # create all-to-all classical connections\n",
-    "    nodes = [r1, r2, r3, m1, m2]\n",
     "    for node1 in nodes:\n",
     "        for node2 in nodes:\n",
     "            if node1 == node2:\n",
@@ -145,9 +144,17 @@
     "            cc = ClassicalChannel(\"cc_%s_%s\"%(node1.name, node2.name), tl, 1e3, delay=cc_delay)\n",
     "            cc.set_ends(node1, node2.name)\n",
     "            \n",
-    "    # set seeds for random generators\n",
-    "    for i, node in enumerate(nodes):\n",
-    "        node.set_seed(i)\n",
+    "    # create quantum channels linking r1 and r2 to m1\n",
+    "    # (with arguments for the channel name, timeline, attenuation (in dB/km), and distance (in m))\n",
+    "    qc0 = QuantumChannel(\"qc_r1_m1\", tl, qc_atten, qc_dist)\n",
+    "    qc1 = QuantumChannel(\"qc_r2_m1\", tl, qc_atten, qc_dist)\n",
+    "    qc0.set_ends(r1, m1.name)\n",
+    "    qc1.set_ends(r2, m1.name)\n",
+    "    # create quantum channels linking r2 and r3 to m2\n",
+    "    qc2 = QuantumChannel(\"qc_r2_m2\", tl, qc_atten, qc_dist)\n",
+    "    qc3 = QuantumChannel(\"qc_r3_m2\", tl, qc_atten, qc_dist)\n",
+    "    qc2.set_ends(r2, m2.name)\n",
+    "    qc3.set_ends(r3, m2.name)\n",
     "\n",
     "    # create routing table manually\n",
     "    # note that the routing table is based on quantum links, not classical\n",
@@ -159,18 +166,8 @@
     "    r3.network_manager.protocol_stack[0].add_forwarding_rule(\"r1\", \"r2\")\n",
     "    r3.network_manager.protocol_stack[0].add_forwarding_rule(\"r2\", \"r2\")\n",
     "    \n",
-    "    # create quantum channels linking r1 and r2 to m1\n",
-    "    qc0 = QuantumChannel(\"qc_r1_m1\", tl, qc_atten, qc_dist)\n",
-    "    qc1 = QuantumChannel(\"qc_r2_m1\", tl, qc_atten, qc_dist)\n",
-    "    qc0.set_ends(r1, m1.name)\n",
-    "    qc1.set_ends(r2, m1.name)\n",
-    "    # create quantum channels linking r2 and r3 to m2\n",
-    "    qc2 = QuantumChannel(\"qc_r2_m2\", tl, qc_atten, qc_dist)\n",
-    "    qc3 = QuantumChannel(\"qc_r3_m2\", tl, qc_atten, qc_dist)\n",
-    "    qc2.set_ends(r2, m2.name)\n",
-    "    qc3.set_ends(r3, m2.name)\n",
+    "    ## run our simulation\n",
     "    \n",
-    "    # run our simulation\n",
     "    tl.init()\n",
     "    # we use the network manager of an end router to make our entanglement request\n",
     "    # here, the arguments are:\n",
@@ -185,7 +182,8 @@
     "    tl.run()\n",
     "    print(\"execution time %.2f sec\" % (time.time() - tick))\n",
     "    \n",
-    "    # display metrics for entangled memories\n",
+    "    ## display metrics for entangled memories\n",
+    "    \n",
     "    fig, (ax1, ax2, ax3) = plt.subplots(1, 3)\n",
     "    fig.set_size_inches(12, 5)\n",
     "\n",
@@ -290,7 +288,7 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "2a827b474ef6441aa31a56e6340ed219",
+       "model_id": "ae7c189c50ff4b7fab644d454646e5b6",
        "version_major": 2,
        "version_minor": 0
       },
@@ -329,9 +327,9 @@
  ],
  "metadata": {
   "kernelspec": {
-   "display_name": "Python 3",
+   "display_name": "venv",
    "language": "python",
-   "name": "python3"
+   "name": "venv"
   },
   "language_info": {
    "codemirror_mode": {
@@ -343,7 +341,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.7.3"
+   "version": "3.10.2"
   }
  },
  "nbformat": 4,
diff --git a/example/timebin_no_cascade.py b/example/timebin_no_cascade.py
index 08a501d2..432b3cf8 100644
--- a/example/timebin_no_cascade.py
+++ b/example/timebin_no_cascade.py
@@ -13,7 +13,6 @@
 
 
 if __name__ == "__main__":
-
     runtime = 1e12
     dark_count = 425
     distances = [1, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120]  # distances in km
@@ -31,10 +30,8 @@
         rate = distance if distance > 70 else 1
         tl = Timeline(runtime * rate)
 
-        qc0 = QuantumChannel("qc0", tl, distance=distance * 1e3,
-                             attenuation=0.0002)
-        qc1 = QuantumChannel("qc1", tl, distance=distance * 1e3,
-                             attenuation=0.0002)
+        qc0 = QuantumChannel("qc0", tl, distance=distance * 1e3, attenuation=0.0002)
+        qc1 = QuantumChannel("qc1", tl, distance=distance * 1e3, attenuation=0.0002)
         cc0 = ClassicalChannel("cc0", tl, distance=distance * 1e3)
         cc1 = ClassicalChannel("cc1", tl, distance=distance * 1e3)
 
@@ -47,12 +44,9 @@
             alice.update_lightsource_params(name, param)
 
         # Bob
-        detector_params = [{"efficiency": 0.072, "dark_count": dark_count,
-                            "time_resolution": 10},
-                           {"efficiency": 0.072, "dark_count": dark_count,
-                            "time_resolution": 10},
-                           {"efficiency": 0.072, "dark_count": dark_count,
-                            "time_resolution": 10}]
+        detector_params = [{"efficiency": 0.072, "dark_count": dark_count, "time_resolution": 10},
+                           {"efficiency": 0.072, "dark_count": dark_count, "time_resolution": 10},
+                           {"efficiency": 0.072, "dark_count": dark_count, "time_resolution": 10}]
         bob = QKDNode("bob", tl, encoding=time_bin, stack_size=1)
         bob.set_seed(0)
 
diff --git a/example/two_node_eg.ipynb b/example/two_node_eg.ipynb
index 85a094dd..ffd0ca00 100644
--- a/example/two_node_eg.ipynb
+++ b/example/two_node_eg.ipynb
@@ -2,11 +2,7 @@
  "cells": [
   {
    "cell_type": "markdown",
-   "metadata": {
-    "pycharm": {
-     "name": "#%% md\n"
-    }
-   },
+   "metadata": {},
    "source": [
     "# Two Node Entanglement Distribution\n",
     "\n",
@@ -17,11 +13,7 @@
   },
   {
    "cell_type": "markdown",
-   "metadata": {
-    "pycharm": {
-     "name": "#%% md\n"
-    }
-   },
+   "metadata": {},
    "source": [
     "## Example\n",
     "\n",
@@ -39,11 +31,7 @@
   {
    "cell_type": "code",
    "execution_count": 1,
-   "metadata": {
-    "pycharm": {
-     "name": "#%%\n"
-    }
-   },
+   "metadata": {},
    "outputs": [],
    "source": [
     "from ipywidgets import interact\n",
@@ -54,11 +42,7 @@
   {
    "cell_type": "code",
    "execution_count": 2,
-   "metadata": {
-    "pycharm": {
-     "name": "#%%\n"
-    }
-   },
+   "metadata": {},
    "outputs": [],
    "source": [
     "from sequence.kernel.timeline import Timeline\n",
@@ -70,11 +54,7 @@
   },
   {
    "cell_type": "markdown",
-   "metadata": {
-    "pycharm": {
-     "name": "#%% md\n"
-    }
-   },
+   "metadata": {},
    "source": [
     "### Defining Custom Behavior with Rules\n",
     "\n",
@@ -88,11 +68,7 @@
   {
    "cell_type": "code",
    "execution_count": 3,
-   "metadata": {
-    "pycharm": {
-     "name": "#%%\n"
-    }
-   },
+   "metadata": {},
    "outputs": [],
    "source": [
     "# our rule condition requires RAW (unentangled) memories\n",
@@ -130,11 +106,7 @@
   },
   {
    "cell_type": "markdown",
-   "metadata": {
-    "pycharm": {
-     "name": "#%% md\n"
-    }
-   },
+   "metadata": {},
    "source": [
     "### Building the Simulation\n",
     "\n",
@@ -153,12 +125,8 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 15,
-   "metadata": {
-    "pycharm": {
-     "name": "#%%\n"
-    }
-   },
+   "execution_count": 4,
+   "metadata": {},
    "outputs": [],
    "source": [
     "def test(sim_time, cc_delay, qc_atten, qc_dist):\n",
@@ -168,6 +136,7 @@
     "    qc_atten: attenuation on quantum channels (db/m)\n",
     "    qc_dist: distance of quantum channels (km)\n",
     "    \"\"\"\n",
+    "    \n",
     "    PS_PER_MS = 1e9\n",
     "    M_PER_KM = 1e3\n",
     "    \n",
@@ -180,18 +149,21 @@
     "    \n",
     "    # first, construct the quantum routers\n",
     "    # (with arguments for the node name, timeline, and number of quantum memories)\n",
-    "    r1 = QuantumRouter(\"r1\", tl)\n",
-    "    r2 = QuantumRouter(\"r2\", tl)\n",
+    "    r1 = QuantumRouter(\"r1\", tl, 50)\n",
+    "    r2 = QuantumRouter(\"r2\", tl, 50)\n",
     "    # next, construct the BSM nodes\n",
     "    # (with arguments for the node name, timeline, and the names of connected routers)\n",
     "    m1 = BSMNode(\"m1\", tl, [\"r1\", \"r2\"])\n",
+    "    \n",
     "    r1.set_seed(0)\n",
     "    r2.set_seed(1)\n",
     "    m1.set_seed(2)\n",
     "    \n",
     "    for node in [r1, r2]:\n",
+    "        # get memory array object from node (note: method returns list, so access first (should be only 1))\n",
+    "        memory_array = node.get_components_by_type(\"MemoryArray\")[0]\n",
     "        # update coherence time parameter for node memories (measured in seconds)\n",
-    "        node.memory_array.update_memory_params(\"coherence_time\", 0.3)\n",
+    "        memory_array.update_memory_params(\"coherence_time\", 0.3)\n",
     "    \n",
     "    # create all-to-all classical connections\n",
     "    nodes = [r1, r2, m1]\n",
@@ -237,11 +209,7 @@
   },
   {
    "cell_type": "markdown",
-   "metadata": {
-    "pycharm": {
-     "name": "#%% md\n"
-    }
-   },
+   "metadata": {},
    "source": [
     "### Running the Simulation\n",
     "\n",
@@ -259,18 +227,15 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 16,
+   "execution_count": 6,
    "metadata": {
-    "pycharm": {
-     "name": "#%%\n"
-    },
     "scrolled": false
    },
    "outputs": [
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "9f0d3be855cb4596a8f804ff4f1beb61",
+       "model_id": "ecb3513acd504236859c5e36197209b0",
        "version_major": 2,
        "version_minor": 0
       },
@@ -287,7 +252,7 @@
        "<function __main__.test(sim_time, cc_delay, qc_atten, qc_dist)>"
       ]
      },
-     "execution_count": 16,
+     "execution_count": 6,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -313,9 +278,9 @@
  ],
  "metadata": {
   "kernelspec": {
-   "display_name": "Python 3",
+   "display_name": "venv",
    "language": "python",
-   "name": "python3"
+   "name": "venv"
   },
   "language_info": {
    "codemirror_mode": {
@@ -327,9 +292,9 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.7.3"
+   "version": "3.10.2"
   }
  },
  "nbformat": 4,
  "nbformat_minor": 2
-}
\ No newline at end of file
+}
diff --git a/src/app/random_request.py b/src/app/random_request.py
index b20f884a..2ab33016 100644
--- a/src/app/random_request.py
+++ b/src/app/random_request.py
@@ -5,6 +5,7 @@
 """
 from __future__ import annotations
 from typing import List, TYPE_CHECKING
+
 if TYPE_CHECKING:
     from ..network_management.reservation import Reservation
 
@@ -19,20 +20,16 @@
 class RandomRequestApp(RequestApp):
     """Code for the random request application.
 
-    This application will create a request for entanglement with a random node
-    (and with other random parameters).
+    This application will create a request for entanglement with a random node (and with other random parameters).
     If the request is accepted, a new request will be made once it has expired.
     Otherwise, a new request will be made immediately.
-    The responder and fidelity of failed request will be kept in the new
-    request.
+    The responder and fidelity of failed request will be kept in the new request.
 
-    The RandomRequestApp class inherits three functions from the RequsetApp
-    class: get_memory(memory_info), get_throughput(),
-    get_other_reservation(reservation). The "get_memory" function consumes the
-    memory when the qualified entanglement is generate. The "get_throughput"
-    function provides the throughput of the serving reservation. The
-    "get_other_reservation" function accepts reservation when node is the
-    responder node.
+    The RandomRequestApp class inherits three functions from the RequsetApp class:
+    get_memory(memory_info), get_throughput(), get_other_reservation(reservation).
+    The "get_memory" function consumes the memory when the qualified entanglement is generated.
+    The "get_throughput" function provides the throughput of the serving reservation.
+    The "get_other_reservation" function accepts reservation when node is the responder node.
 
     Attributes:
         node (QuantumRouter): Node that code is attached to.
@@ -41,15 +38,15 @@ class RandomRequestApp(RequestApp):
         request_time (int): simulation time at which current reservation requested.
         memory_counter (int): number of successfully received memories.
         wait_time (List[int]): aggregates times between request and accepted reservation.
-        all_throughput (List[float]): aggregates average rate of memory entanglement per reservation
-        reserves (List[List[any]]): aggregates previous reservations 
+        all_throughput (List[float]): aggregates average rate of memory entanglement per reservation.
+        reserves (List[List[any]]): aggregates previous reservations.
         memo_to_reserve (Dict[int, Reservation]): mapping of memory index to corresponding reservation.
-        min_dur (int): the minimum duration of request (ps)
-        max_dur (int): the maximum duration of request (ps)
-        min_size (int): the minimum required memory of request
-        max_size (int): the maximum required memory of request
-        min_fidelity (float): the minimum required fidelity of entanglement
-        max_fidelity (float): the maximum required fidelity of entanglement
+        min_dur (int): the minimum duration of request (ps).
+        max_dur (int): the maximum duration of request (ps).
+        min_size (int): the minimum required memory of request.
+        max_size (int): the maximum required memory of request.
+        min_fidelity (float): the minimum required fidelity of entanglement.
+        max_fidelity (float): the maximum required fidelity of entanglement.
     """
 
     def __init__(self, node: QuantumRouter, others: List[str], seed: int,
@@ -61,12 +58,12 @@ def __init__(self, node: QuantumRouter, others: List[str], seed: int,
             node (QuantumRouter): node that application is attached to.
             others (List[str]): list of names for other available routers.
             seed (int): seed for internal random number generator.
-            min_dur (int): the minimum duration of request (ps)
-            max_dur (int): the maximum duration of request (ps)
-            min_size (int): the minimum required memory of request
-            max_size (int): the maximum required memory of request
-            min_fidelity (float): the minimum required fidelity of entanglement
-            max_fidelity (float): the maximum required fidelity of entanglement
+            min_dur (int): the minimum duration of request (ps).
+            max_dur (int): the maximum duration of request (ps).
+            min_size (int): the minimum required memory of request.
+            max_size (int): the maximum required memory of request.
+            min_fidelity (float): the minimum required fidelity of entanglement.
+            max_fidelity (float): the maximum required fidelity of entanglement.
         """
         super().__init__(node)
         assert 0 < min_dur <= max_dur
@@ -150,6 +147,7 @@ def get_reserve_res(self, reservation: Reservation, result: bool) -> None:
         Side Effects:
             May schedule a start/retry event based on reservation result.
         """
+
         super().get_reserve_res(reservation, result)
         if result:
             process = Process(self, "start", [])
diff --git a/src/app/request_app.py b/src/app/request_app.py
index bb8122c5..091109fc 100644
--- a/src/app/request_app.py
+++ b/src/app/request_app.py
@@ -1,16 +1,16 @@
 from __future__ import annotations
 from typing import TYPE_CHECKING, List, Dict
 
-from ..kernel.event import Event
-from ..kernel.process import Process
-
 if TYPE_CHECKING:
     from ..topology.node import QuantumRouter
     from ..network_management.reservation import Reservation
     from ..resource_management.memory_manager import MemoryInfo
 
+from ..kernel.event import Event
+from ..kernel.process import Process
+
 
-class RequestApp():
+class RequestApp:
     """Code for the request application.
 
         This application will create a request for entanglement.
@@ -66,11 +66,9 @@ def start(self, responder: str, start_t: int, end_t: int, memo_size: int,
         self.memo_size = memo_size
         self.fidelity = fidelity
 
-        self.node.reserve_net_resource(responder, start_t, end_t,
-                                       memo_size, fidelity)
+        self.node.reserve_net_resource(responder, start_t, end_t, memo_size, fidelity)
 
-    def get_reserve_res(self, reservation: "Reservation",
-                        result: bool) -> None:
+    def get_reserve_res(self, reservation: "Reservation", result: bool) -> None:
         """Method to receive reservation result from network manager.
 
         Args:
@@ -84,8 +82,7 @@ def get_reserve_res(self, reservation: "Reservation",
         if result:
             self.schedule_reservation(reservation)
 
-    def add_memo_reserve_map(self, index: int,
-                             reservation: "Reservation") -> None:
+    def add_memo_reserve_map(self, index: int, reservation: "Reservation") -> None:
         self.memo_to_reserve[index] = reservation
 
     def remove_memo_reserve_map(self, index: int) -> None:
@@ -110,11 +107,9 @@ def get_memory(self, info: "MemoryInfo") -> None:
 
         if info.index in self.memo_to_reserve:
             reservation = self.memo_to_reserve[info.index]
-            if info.remote_node == reservation.initiator \
-                    and info.fidelity >= reservation.fidelity:
+            if info.remote_node == reservation.initiator and info.fidelity >= reservation.fidelity:
                 self.node.resource_manager.update(None, info.memory, "RAW")
-            elif info.remote_node == reservation.responder \
-                    and info.fidelity >= reservation.fidelity:
+            elif info.remote_node == reservation.responder and info.fidelity >= reservation.fidelity:
                 self.memory_counter += 1
                 self.node.resource_manager.update(None, info.memory, "RAW")
 
@@ -138,11 +133,9 @@ def schedule_reservation(self, reservation: "Reservation") -> None:
             self.path = reservation.path
         for card in self.node.network_manager.protocol_stack[1].timecards:
             if reservation in card.reservations:
-                process = Process(self, "add_memo_reserve_map",
-                                  [card.memory_index, reservation])
+                process = Process(self, "add_memo_reserve_map", [card.memory_index, reservation])
                 event = Event(reservation.start_time, process)
                 self.node.timeline.schedule(event)
-                process = Process(self, "remove_memo_reserve_map",
-                                  [card.memory_index])
+                process = Process(self, "remove_memo_reserve_map", [card.memory_index])
                 event = Event(reservation.end_time, process)
                 self.node.timeline.schedule(event)
diff --git a/src/components/beam_splitter.py b/src/components/beam_splitter.py
index 6d1a0838..eda30302 100644
--- a/src/components/beam_splitter.py
+++ b/src/components/beam_splitter.py
@@ -10,7 +10,10 @@
 if TYPE_CHECKING:
     from ..kernel.timeline import Timeline
 
+from numpy import trace
+
 from .photon import Photon
+from ..kernel.quantum_utils import povm_0
 from ..utils.encoding import polarization
 from ..kernel.entity import Entity
 
@@ -25,7 +28,6 @@ class BeamSplitter(Entity):
         name (str): label for beamsplitter instance.
         timeline (Timeline): timeline for simulation.
         fidelity (float): probability of transmitting a received photon.
-        receivers (List[Entities]): entities to receive transmitted photons.
         start_time (int): start time (in ps) of photon interaction.
         frequency (float): frequency with which to switch measurement bases.
         basis_list (List[int]): 0/1 indices of measurement bases over time.
@@ -37,14 +39,11 @@ def __init__(self, name: str, timeline: "Timeline", fidelity=1):
         Args:
             name (str): name of the beamsplitter instance.
             timeline (Timeline): simulation timeline.
-
-        Keyword Args:
             fidelity (float): probability of transmitting a received photon (default 1).
         """
 
         Entity.__init__(self, name, timeline)  # Splitter is part of the QSDetector, and does not have its own name
         self.fidelity = fidelity
-        self.receivers = []
         # for BB84
         self.start_time = 0
         self.frequency = 0
@@ -53,19 +52,19 @@ def __init__(self, name: str, timeline: "Timeline", fidelity=1):
     def init(self) -> None:
         """Implementation of Entity interface (see base class)."""
 
-        pass
+        assert len(self._receivers) == 2, "BeamSplitter should only be attached to 2 outputs."
 
-    def get(self, photon: "Photon") -> None:
+    def get(self, photon, **kwargs) -> None:
         """Method to receive a photon for measurement.
 
         Args:
             photon (Photon): photon to measure (must have polarization encoding)
 
         Side Effects:
-            May call get method of one receiver from the receivers attribute if start_time, frequency, and basis_list attributes are set up properly.
+            May call get method of one receiver.
         """
 
-        assert photon.encoding_type["name"] == "polarization"
+        assert photon.encoding_type["name"] == "polarization", "Beamsplitter should only be used with polarization."
 
         if self.get_generator().random() < self.fidelity:
             index = int((self.timeline.now() - self.start_time) * self.frequency * 1e-12)
@@ -75,18 +74,47 @@ def get(self, photon: "Photon") -> None:
 
             res = Photon.measure(polarization["bases"][self.basis_list[index]],
                                  photon, self.get_generator())
-            self.receivers[res].get()
+            self._receivers[res].get(photon)
 
-    def set_basis_list(self, basis_list: "List[int]", start_time: int, frequency: int) -> None:
+    def set_basis_list(self, basis_list: List[int], start_time: int, frequency: float) -> None:
         """Sets the basis_list, start_time, and frequency attributes."""
 
         self.basis_list = basis_list
         self.start_time = start_time
         self.frequency = frequency
 
-    def set_receiver(self, index: int, receiver: "Entity") -> None:
-        """Sets the receivers attribute at the specified index."""
 
-        if index > len(self.receivers):
-            raise Exception("index is larger than the length of receivers")
-        self.receivers.insert(index, receiver)
+class FockBeamSplitter(Entity):
+    """WIP"""
+
+    def __init__(self, name, timeline, fidelity=1):
+        super().__init__(name, timeline)
+        self.fidelity = fidelity
+        self.most_recent_time = -1
+
+    def init(self) -> None:
+        """Implementation of Entity interface (see base class)."""
+
+        assert len(self._receivers) == 2, "BeamSplitter should only be attached to 2 outputs."
+
+    def get(self, photon, **kwargs) -> None:
+        assert photon.encoding_type["name"] == "absorptive"
+
+        if not photon.is_null:
+            state = self.timeline.quantum_manager.get(photon.quantum_state)
+
+            if len(state.keys) == 2:  # entangled; calculate probability of measurement
+                prob_0 = trace(state.state @ povm_0).real
+                if prob_0 > 1:
+                    prob_0 = 1
+                elif prob_0 < 0:
+                    prob_0 = 0
+
+            else:  # unentangled; send to a random output
+                if self.timeline.now() == self.most_recent_time:  # if already measured right now, return (HOM effect)
+                    return
+                prob_0 = 0.5
+
+            detector_num = self.get_generator().choice([0, 1], p=[prob_0, 1-prob_0])
+            self.most_recent_time = self.timeline.now()
+            self._receivers[detector_num].get()
diff --git a/src/components/bsm.py b/src/components/bsm.py
index 9bec9bd5..90ac0dfc 100644
--- a/src/components/bsm.py
+++ b/src/components/bsm.py
@@ -7,12 +7,15 @@
 from abc import abstractmethod
 from typing import TYPE_CHECKING, Any, Dict, List
 
+if TYPE_CHECKING:
+    from ..kernel.quantum_manager import QuantumManager
+    from ..kernel.quantum_state import State
+
 from numpy import outer, add, zeros, array_equal
 
 from .circuit import Circuit
 from .detector import Detector
 from .photon import Photon
-from .memory import Memory
 from ..kernel.entity import Entity
 from ..kernel.event import Event
 from ..kernel.process import Process
@@ -20,9 +23,6 @@
 from ..utils.encoding import *
 from ..utils import log
 
-if TYPE_CHECKING:
-    from ..kernel.quantum_manager import QuantumManager, State
-
 
 def make_bsm(name, timeline, encoding_type='time_bin', phase_error=0, detectors=[]):
     """Function to construct BSM of specified type.
@@ -41,10 +41,59 @@ def make_bsm(name, timeline, encoding_type='time_bin', phase_error=0, detectors=
         return TimeBinBSM(name, timeline, phase_error, detectors)
     elif encoding_type == "single_atom":
         return SingleAtomBSM(name, timeline, phase_error, detectors)
+    elif encoding_type == "absorptive":
+        return AbsorptiveBSM(name, timeline, phase_error, detectors)
     else:
         raise Exception("invalid encoding {} given for BSM {}".format(encoding_type, name))
 
 
+def _set_state_with_fidelity(keys: List[int], desired_state: List[complex], fidelity: float, rng, qm: "QuantumManager"):
+    possible_states = [BSM._phi_plus, BSM._phi_minus,
+                       BSM._psi_plus, BSM._psi_minus]
+    assert desired_state in possible_states
+
+    if qm.formalism == KET_STATE_FORMALISM:
+        probabilities = [(1 - fidelity) / 3] * 4
+        probabilities[possible_states.index(desired_state)] = fidelity
+        state_ind = rng.choice(4, p=probabilities)
+        qm.set(keys, possible_states[state_ind])
+
+    elif qm.formalism == DENSITY_MATRIX_FORMALISM:
+        multipliers = [(1 - fidelity) / 3] * 4
+        multipliers[possible_states.index(desired_state)] = fidelity
+        state = zeros((4, 4))
+        for mult, pure in zip(multipliers, possible_states):
+            state = add(state, mult * outer(pure, pure))
+        qm.set(keys, state)
+
+    else:
+        raise Exception("Invalid quantum manager with formalism {}".format(qm.formalism))
+
+
+def _set_pure_state(keys: List[int], ket_state: List[complex], qm: "QuantumManager"):
+    if qm.formalism == KET_STATE_FORMALISM:
+        qm.set(keys, ket_state)
+    elif qm.formalism == DENSITY_MATRIX_FORMALISM:
+        state = outer(ket_state, ket_state)
+        qm.set(keys, state)
+    else:
+        raise NotImplementedError("formalism of quantum state {} is not "
+                                  "implemented in the set_pure_quantum_state "
+                                  "function of bsm.py".format(qm.formalism))
+
+
+def _eq_psi_plus(state: "State", formalism: str):
+    if formalism == KET_STATE_FORMALISM:
+        return array_equal(state.state, BSM._psi_plus)
+    elif formalism == DENSITY_MATRIX_FORMALISM:
+        d_state = outer(BSM._phi_plus, BSM._psi_plus)
+        return array_equal(state.state, d_state)
+    else:
+        raise NotImplementedError("formalism of quantum state {} is not "
+                                  "implemented in the eq_phi_plus "
+                                  "function of bsm.py".format(formalism))
+
+
 class BSM(Entity):
     """Parent class for bell state measurement devices.
 
@@ -61,7 +110,7 @@ class BSM(Entity):
     _psi_plus = [complex(0), complex(sqrt(1 / 2)), complex(sqrt(1 / 2)), complex(0)]
     _psi_minus = [complex(0), complex(sqrt(1 / 2)), -complex(sqrt(1 / 2)), complex(0)]
 
-    def __init__(self, name, timeline, phase_error=0, detectors=[]):
+    def __init__(self, name, timeline, phase_error=0, detectors=None):
         """Constructor for base BSM object.
 
         Args:
@@ -72,18 +121,21 @@ def __init__(self, name, timeline, phase_error=0, detectors=[]):
         """
 
         super().__init__(name, timeline)
+        self.encoding = "None"
         self.phase_error = phase_error
         self.photons = []
         self.photon_arrival_time = -1
 
         self.detectors = []
-        for i, d in enumerate(detectors):
-            if d is not None:
-                detector = Detector("%s_%d" % (self.name, i), timeline, **d)
-                detector.attach(self)
-            else:
-                detector = None
-            self.detectors.append(detector)
+        if detectors is not None:
+            for i, d in enumerate(detectors):
+                if d is not None:
+                    detector = Detector("%s_%d" % (self.name, i), timeline, **d)
+                    detector.attach(self)
+                    detector.owner = self
+                else:
+                    detector = None
+                self.detectors.append(detector)
 
         # get resolution
         self.resolution = max(d.time_resolution for d in self.detectors)
@@ -96,16 +148,22 @@ def __init__(self, name, timeline, phase_error=0, detectors=[]):
 
     def init(self):
         """Implementation of Entity interface (see base class)."""
-        for detector in self.detectors:
-            detector.owner = self.owner
+
+        self.photons = []
+        self.photon_arrival_time = -1
 
     @abstractmethod
-    def get(self, photon):
+    def get(self, photon, **kwargs):
         """Method to receive a photon for measurement (abstract).
 
         Arguments:
             photon (Photon): photon to measure.
         """
+
+        assert photon.encoding_type["name"] == self.encoding, \
+            "BSM expecting photon with encoding '{}' received photon with encoding '{}'".format(
+                self.encoding, photon.encoding_type["name"])
+
         # check if photon arrived later than current photon
         if self.photon_arrival_time < self.timeline.now():
             # clear photons
@@ -148,24 +206,24 @@ class PolarizationBSM(BSM):
         timeline (Timeline): timeline for simulation.
         phase_error (float): phase error applied to measurement.
         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=[]):
+    def __init__(self, name, timeline, phase_error=0, detectors=None):
         """Constructor for Polarization BSM.
 
         Args:
-            name (str): name of the beamsplitter instance.
+            name (str): name of the BSM instance.
             timeline (Timeline): simulation timeline.
             phase_error (float): phase error applied to polarization photons (default 0).
             detectors (List[Dict]): list of parameters for attached detectors, in dictionary format (must be of length 4) (default []).
         """
 
         super().__init__(name, timeline, phase_error, detectors)
+        self.encoding = "polarization"
         self.last_res = [-2 * self.resolution, -1]
         assert len(self.detectors) == 4
 
-    def get(self, photon):
+    def get(self, photon, **kwargs):
         """See base class.
 
         This method adds additional side effects not present in the base class.
@@ -184,8 +242,7 @@ def get(self, photon):
         self.photons[0].entangle(self.photons[1])
 
         # measure in bell basis
-        res = Photon.measure_multiple(self.bell_basis, self.photons,
-                                      self.get_generator())
+        res = Photon.measure_multiple(self.bell_basis, self.photons, self.get_generator())
 
         # check if we've measured as Phi+ or Phi-; these cannot be measured by the BSM
         if res == 0 or res == 1:
@@ -245,10 +302,9 @@ class TimeBinBSM(BSM):
         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=[]):
+    def __init__(self, name, timeline, phase_error=0, detectors=None):
         """Constructor for the time bin BSM class.
 
         Args:
@@ -259,11 +315,12 @@ def __init__(self, name, timeline, phase_error=0, detectors=[]):
         """
 
         super().__init__(name, timeline, phase_error, detectors)
+        self.encoding = "time_bin"
         self.encoding_type = time_bin
         self.last_res = [-1, -1]
         assert len(self.detectors) == 2
 
-    def get(self, photon):
+    def get(self, photon, **kwargs):
         """See base class.
 
         This method adds additional side effects not present in the base class.
@@ -284,8 +341,7 @@ def get(self, photon):
         self.photons[0].entangle(self.photons[1])
 
         # measure in bell basis
-        res = Photon.measure_multiple(self.bell_basis, self.photons,
-                                      self.get_generator())
+        res = Photon.measure_multiple(self.bell_basis, self.photons, self.get_generator())
 
         # check if we've measured as Phi+ or Phi-; these cannot be measured by the BSM
         if res == 0 or res == 1:
@@ -350,56 +406,6 @@ def trigger(self, detector: Detector, info: Dict[str, Any]):
         self.last_res = [time, detector_num]
 
 
-def _set_state_with_fidelity(keys: List[int], desired_state: List[complex],
-                             fidelity: float, rng, qm: "QuantumManager"):
-    possible_states = [BSM._phi_plus, BSM._phi_minus,
-                       BSM._psi_plus, BSM._psi_minus]
-    assert desired_state in possible_states
-
-    if qm.formalism == KET_STATE_FORMALISM:
-        probabilities = [(1 - fidelity) / 3] * 4
-        probabilities[possible_states.index(desired_state)] = fidelity
-        state_ind = rng.choice(4, p=probabilities)
-        qm.set(keys, possible_states[state_ind])
-
-    elif qm.formalism == DENSITY_MATRIX_FORMALISM:
-        multipliers = [(1 - fidelity) / 3] * 4
-        multipliers[possible_states.index(desired_state)] = fidelity
-        state = zeros((4, 4))
-        for mult, pure in zip(multipliers, possible_states):
-            state = add(state, mult * outer(pure, pure))
-        qm.set(keys, state)
-
-    else:
-        raise Exception("Invalid quantum manager "
-                        "with formalism {}".format(qm.formalism))
-
-
-def _set_pure_state(keys: List[int], ket_state: List[complex],
-                    qm: "QuantumManager"):
-    if qm.formalism == KET_STATE_FORMALISM:
-        qm.set(keys, ket_state)
-    elif qm.formalism == DENSITY_MATRIX_FORMALISM:
-        state = outer(ket_state, ket_state)
-        qm.set(keys, state)
-    else:
-        raise NotImplementedError("formalism of quantum state {} is not "
-                                  "implemented in the set_pure_quantum_state "
-                                  "function of bsm.py".format(qm.formalism))
-
-
-def _eq_psi_plus(state: "State", formalism: str):
-    if formalism == KET_STATE_FORMALISM:
-        return array_equal(state.state, BSM._psi_plus)
-    elif formalism == DENSITY_MATRIX_FORMALISM:
-        d_state = outer(BSM._phi_plus, BSM._psi_plus)
-        return array_equal(state.state, d_state)
-    else:
-        raise NotImplementedError("formalism of quantum state {} is not "
-                                  "implemented in the eq_phi_plus "
-                                  "function of bsm.py".format(formalism))
-
-
 class SingleAtomBSM(BSM):
     """Class modeling a single atom BSM device.
 
@@ -414,7 +420,7 @@ class SingleAtomBSM(BSM):
     _meas_circuit = Circuit(1)
     _meas_circuit.measure(0)
 
-    def __init__(self, name, timeline, phase_error=0, detectors=[]):
+    def __init__(self, name, timeline, phase_error=0, detectors=None):
         """Constructor for the single atom BSM class.
 
         Args:
@@ -424,12 +430,13 @@ def __init__(self, name, timeline, phase_error=0, detectors=[]):
             detectors (List[Dict]): list of parameters for attached detectors, in dictionary format (must be of length 2) (default []).
         """
 
-        if detectors == []:
+        if detectors is None:
             detectors = [{}, {}]
         super().__init__(name, timeline, phase_error, detectors)
+        self.encoding = "single_atom"
         assert len(self.detectors) == 2
 
-    def get(self, photon):
+    def get(self, photon, **kwargs):
         """See base class.
 
         This method adds additional side effects not present in the base class.
@@ -445,7 +452,7 @@ def get(self, photon):
         if len(self.photons) == 2:
             qm = self.timeline.quantum_manager
             p0, p1 = self.photons
-            key0, key1 = p0.qstate_key, p1.qstate_key
+            key0, key1 = p0.quantum_state, p1.quantum_state
             keys = [key0, key1]
             state0, state1 = qm.get(key0), qm.get(key1)
             meas0, meas1 = [qm.run_circuit(self._meas_circuit, [key],
@@ -470,12 +477,12 @@ def get(self, photon):
                     log.logger.info(self.name + " passed stage 2")
                     if _eq_psi_plus(state0, qm.formalism) ^ detector_num:
                         _set_state_with_fidelity(keys, BSM._psi_minus,
-                                                 p0.fidelity,
+                                                 p0.encoding_type["raw_fidelity"],
                                                  self.get_generator(),
                                                  qm)
                     else:
                         _set_state_with_fidelity(keys, BSM._psi_plus,
-                                                 p0.fidelity,
+                                                 p0.encoding_type["raw_fidelity"],
                                                  self.get_generator(),
                                                  qm)
                 else:
@@ -484,6 +491,7 @@ def get(self, photon):
                 photon = p0 if meas0 else p1
                 if self.get_generator().random() > photon.loss:
                     self.detectors[detector_num].get()
+
             else:
                 if meas0 and self.get_generator().random() > p0.loss:
                     detector_num = self.get_generator().choice([0, 1])
@@ -509,7 +517,78 @@ def trigger(self, detector: Detector, info: Dict[str, Any]):
         info = {'entity': 'BSM', 'info_type': 'BSM_res', 'res': res, 'time': time}
         self.notify(info)
 
-    def change_timeline(self, timeline):
-        self.timeline = timeline
-        for d in self.detectors:
-            d.change_timeline(timeline)
+
+class AbsorptiveBSM(BSM):
+    """Class modeling a BSM device for absorptive quantum memories.
+
+    Measures photons and manages entanglement state of entangled photons.
+
+    Attributes:
+        name (str): label for BSM instance
+        timeline (Timeline): timeline for simulation
+        detectors (List[Detector]): list of attached photon detection devices (length 2).
+    """
+
+    def __init__(self, name, timeline, phase_error=0, detectors=None):
+        """Constructor for the AbsorptiveBSM class."""
+
+        if detectors is None:
+            detectors = [{}, {}]
+        super().__init__(name, timeline, phase_error, detectors)
+        self.encoding = "absorptive"
+        assert len(self.detectors) == 2
+
+    def get(self, photon, **kwargs):
+        """"""
+
+        super().get(photon)
+
+        # get other photon, set to measured state
+        key = photon.quantum_state
+        state = self.timeline.quantum_manager.get(key)
+        other_keys = state.keys[:]
+        other_keys.remove(key)
+        if photon.is_null:
+            self.timeline.quantum_manager.set(other_keys, [complex(1), complex(0)])
+        else:
+            detector_num = self.get_generator().choice([0, 1])
+            self.detectors[detector_num].get()
+            self.timeline.quantum_manager.set(other_keys, [complex(0), complex(1)])
+
+        if len(self.photons) == 2:
+            null_0 = self.photons[0].is_null
+            null_1 = self.photons[1].is_null
+            is_valid = null_0 ^ null_1
+
+            # check if we can set to entangled Psi+ state
+            if is_valid:
+                # get other photons to entangle
+                key_0 = self.photons[0].quantum_state
+                key_1 = self.photons[1].quantum_state
+                state_0 = self.timeline.quantum_manager.get(key_0)
+                state_1 = self.timeline.quantum_manager.get(key_1)
+                other_keys_0 = state_0.keys[:]
+                other_keys_1 = state_1.keys[:]
+                other_keys_0.remove(key_0)
+                other_keys_1.remove(key_1)
+                assert len(other_keys_0) == 1 and len(other_keys_1) == 1
+
+                # set to Psi+ state
+                combined = other_keys_0 + other_keys_1
+                self.timeline.quantum_manager.set(combined, BSM._psi_plus)
+
+    def trigger(self, detector: Detector, info: Dict[str, Any]):
+        """See base class.
+
+        This method adds additional side effects not present in the base class.
+
+        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/src/components/circuit.py b/src/components/circuit.py
index 25d5df42..6b17524c 100644
--- a/src/components/circuit.py
+++ b/src/components/circuit.py
@@ -11,7 +11,7 @@
 from qutip.qip.operations import gate_sequence_product
 from qutip import Qobj
 
-GATE_INFO_TYPE = List[Union[str, List[int]]]
+GATE_INFO_TYPE = List[Union[str, List[int], float]]
 
 
 def x_gate():
@@ -47,8 +47,9 @@ def t_gate():
 def validator(func):
     def wrapper(self, *args, **kwargs):
         for q in args:
-            assert q < self.size, 'qubit index out of range'
-            assert q not in self.measured_qubits, 'qubit has been measured'
+            if type(q) is int:
+                assert q < self.size, 'qubit index out of range'
+                assert q not in self.measured_qubits, 'qubit has been measured'
         if func.__name__ != 'measure':
             self._cache = None
         return func(self, *args, **kwargs)
@@ -56,7 +57,7 @@ def wrapper(self, *args, **kwargs):
     return wrapper
 
 
-class Circuit():
+class Circuit:
     """Class for a quantum circuit.
 
     Attributes:
@@ -96,7 +97,7 @@ def get_unitary_matrix(self) -> np.ndarray:
                              "S": s_gate,
                              "T": t_gate}
             for gate in self.gates:
-                name, indices = gate
+                name, indices, arg = gate
                 if name == 'h':
                     qc.add_gate('SNOT', indices[0])
                 elif name == 'x':
@@ -115,15 +116,17 @@ def get_unitary_matrix(self) -> np.ndarray:
                     qc.add_gate('T', indices[0])
                 elif name == 's':
                     qc.add_gate('S', indices[0])
+                elif name == 'phase':
+                    qc.add_gate('PHASEGATE', indices[0], arg_value=arg)
                 else:
                     raise NotImplementedError
             self._cache = gate_sequence_product(qc.propagators()).full()
-            return self._cache
+
         return self._cache
 
     def serialize(self) -> Dict:
-        gates = [{"name": g_name, "indices": indices}
-                 for g_name, indices in self.gates]
+        gates = [{"name": g_name, "indices": indices, "arg": arg}
+                 for g_name, indices, arg in self.gates]
         return {"size": self.size, "gates": gates,
                 "measured_qubits": self.measured_qubits}
 
@@ -132,7 +135,8 @@ def deserialize(self, json_data: Dict):
         for gate in json_data["gates"]:
             name: str = gate["name"]
             indices: List[int] = gate["indices"]
-            self.gates.append([name, indices])
+            arg: float = gate["arg"]
+            self.gates.append([name, indices, arg])
         self.measured_qubits = json_data["measured_qubits"]
         self._cache = None
 
@@ -144,7 +148,7 @@ def h(self, qubit: int):
             qubit (int): the index of qubit in the circuit.
         """
 
-        self.gates.append(['h', [qubit]])
+        self.gates.append(['h', [qubit], None])
 
     @validator
     def x(self, qubit: int):
@@ -154,7 +158,7 @@ def x(self, qubit: int):
             qubit (int): the index of qubit in the circuit.
         """
 
-        self.gates.append(['x', [qubit]])
+        self.gates.append(['x', [qubit], None])
 
     @validator
     def y(self, qubit: int):
@@ -164,7 +168,7 @@ def y(self, qubit: int):
             qubit (int): the index of qubit in the circuit.
         """
 
-        self.gates.append(['y', [qubit]])
+        self.gates.append(['y', [qubit], None])
 
     @validator
     def z(self, qubit: int):
@@ -174,18 +178,18 @@ def z(self, qubit: int):
             qubit (int): the index of qubit in the circuit.
         """
 
-        self.gates.append(['z', [qubit]])
+        self.gates.append(['z', [qubit], None])
 
     @validator
     def cx(self, control: int, target: int):
         """Method to apply Control-X gate on three qubits.
 
         Args:
-            control1 (int): the index of control1 in the circuit.
+            control (int): the index of control1 in the circuit.
             target (int): the index of target in the circuit.
         """
 
-        self.gates.append(['cx', [control, target]])
+        self.gates.append(['cx', [control, target], None])
 
     @validator
     def ccx(self, control1: int, control2: int, target: int):
@@ -197,7 +201,7 @@ def ccx(self, control1: int, control2: int, target: int):
             target (int): the index of target in the circuit.
         """
 
-        self.gates.append(['ccx', [control1, control2, target]])
+        self.gates.append(['ccx', [control1, control2, target], None])
 
     @validator
     def swap(self, qubit1: int, qubit2: int):
@@ -208,7 +212,7 @@ def swap(self, qubit1: int, qubit2: int):
             qubit2 (int): the index of qubit2 in the circuit.
         """
 
-        self.gates.append(['swap', [qubit1, qubit2]])
+        self.gates.append(['swap', [qubit1, qubit2], None])
 
     @validator
     def t(self, qubit: int):
@@ -218,7 +222,7 @@ def t(self, qubit: int):
             qubit (int): the index of qubit in the circuit.
         """
 
-        self.gates.append(['t', [qubit]])
+        self.gates.append(['t', [qubit], None])
 
     @validator
     def s(self, qubit: int):
@@ -228,7 +232,18 @@ def s(self, qubit: int):
             qubit (int): the index of qubit in the circuit.
         """
 
-        self.gates.append(['s', [qubit]])
+        self.gates.append(['s', [qubit], None])
+
+    @validator
+    def phase(self, qubit: int, theta: float):
+        """Method to apply a phase gate to a qubit.
+
+        Args:
+            qubit (int): index of the qubit in the circuit.
+            theta (float): phase to apply
+        """
+
+        self.gates.append(['phase', [qubit], theta])
 
     @validator
     def measure(self, qubit: int):
@@ -239,4 +254,3 @@ def measure(self, qubit: int):
         """
 
         self.measured_qubits.append(qubit)
-
diff --git a/src/components/detector.py b/src/components/detector.py
index b495dd28..4a8453ed 100644
--- a/src/components/detector.py
+++ b/src/components/detector.py
@@ -6,16 +6,16 @@
 """
 
 from abc import ABC, abstractmethod
-from typing import TYPE_CHECKING, Any, Dict
+from typing import TYPE_CHECKING, Any, Dict, List
 
 if TYPE_CHECKING:
     from ..kernel.timeline import Timeline
-    from ..components.photon import Photon
-    from typing import List
 
-from ..components.beam_splitter import BeamSplitter
+from ..components.photon import Photon
+from ..components.beam_splitter import BeamSplitter, FockBeamSplitter
 from ..components.switch import Switch
 from ..components.interferometer import Interferometer
+from ..components.circuit import Circuit
 from ..kernel.entity import Entity
 from ..kernel.event import Event
 from ..kernel.process import Process
@@ -38,6 +38,9 @@ class Detector(Entity):
         photon_counter (int): counts number of detection events.
     """
 
+    _meas_circuit = Circuit(1)
+    _meas_circuit.measure(0)
+
     def __init__(self, name: str, timeline: "Timeline", efficiency=0.9, dark_count=0, count_rate=int(25e6),
                  time_resolution=150):
         Entity.__init__(self, name, timeline)  # Detector is part of the QSDetector, and does not have its own name
@@ -50,12 +53,17 @@ def __init__(self, name: str, timeline: "Timeline", efficiency=0.9, dark_count=0
 
     def init(self):
         """Implementation of Entity interface (see base class)."""
+        self.next_detection_time = -1
+        self.photon_counter = 0
         self.add_dark_count()
 
-    def get(self, dark_get=False) -> None:
+    def get(self, photon=None, **kwargs) -> None:
         """Method to receive a photon for measurement.
 
         Args:
+            photon (Photon): photon to detect (currently unused)
+
+        Keyword Args:
             dark_get (bool): Signifies if the call is the result of a false positive dark count event.
                 If true, will ignore probability calculations (default false).
 
@@ -63,10 +71,20 @@ def get(self, dark_get=False) -> None:
             May notify upper entities of a detection event.
         """
 
+        dark_get = kwargs.get("dark_get", False)
+
         self.photon_counter += 1
         now = self.timeline.now()
         time = round(now / self.time_resolution) * self.time_resolution
 
+        # if get a photon and it has single_atom encoding, measure
+        if photon and photon.encoding_type["name"] == "single_atom":
+            key = photon.quantum_state
+            res = self.timeline.quantum_manager.run_circuit(Detector._meas_circuit, [key],
+                                                            self.get_generator().random())
+            if not res[key]:
+                return
+
         if (self.get_generator().random() < self.efficiency or dark_get) and now > self.next_detection_time:
             self.notify({'time': time})
             self.next_detection_time = now + (1e12 / self.count_rate)  # period in ps
@@ -87,7 +105,7 @@ def add_dark_count(self) -> None:
             time = time_to_next + self.timeline.now()  # time of next dark count
 
             process1 = Process(self, "add_dark_count", [])  # schedule photon detection and dark count add in future
-            process2 = Process(self, "get", [True])
+            process2 = Process(self, "get", [None], {"dark_get": True})
             event1 = Event(time, process1)
             event2 = Event(time, process2)
             self.timeline.schedule(event1)
@@ -107,14 +125,15 @@ class QSDetector(Entity, ABC):
     Attributes:
         name (str): label for QSDetector instance.
         timeline (Timeline): timeline for simulation.
+        componennts (List[entity]): list of all aggregated hardware components.
         detectors (List[Detector]): list of attached detectors.
         trigger_times (List[List[int]]): tracks simulation time of detection events for each detector.
     """
 
     def __init__(self, name: str, timeline: "Timeline"):
         Entity.__init__(self, name, timeline)
-        self.detectors = []
         self.components = []
+        self.detectors = []
         self.trigger_times = []
 
     def init(self):
@@ -126,12 +145,13 @@ def update_detector_params(self, detector_id: int, arg_name: str, value: Any) ->
         self.detectors[detector_id].__setattr__(arg_name, value)
 
     @abstractmethod
-    def get(self, photon: "Photon") -> None:
+    def get(self, photon: "Photon", **kwargs) -> None:
         """Abstract method for receiving photons for measurement."""
 
         pass
 
     def trigger(self, detector: Detector, info: Dict[str, Any]) -> None:
+        # TODO: rewrite
         detector_index = self.detectors.index(detector)
         self.trigger_times[detector_index].append(info['time'])
 
@@ -139,7 +159,7 @@ def get_photon_times(self):
         return self.trigger_times
 
     @abstractmethod
-    def set_basis_list(self, basis_list: "List", start_time: int, frequency: int) -> None:
+    def set_basis_list(self, basis_list: List[int], start_time: int, frequency: float) -> None:
         pass
 
 
@@ -159,21 +179,24 @@ class QSDetectorPolarization(QSDetector):
 
     def __init__(self, name: str, timeline: "Timeline"):
         QSDetector.__init__(self, name, timeline)
-        self.detectors = [Detector(name + ".detector" + str(i), timeline) for i in range(2)]
+        for i in range(2):
+            d = Detector(name + ".detector" + str(i), timeline)
+            self.detectors.append(d)
+            d.attach(self)
         self.splitter = BeamSplitter(name + ".splitter", timeline)
-        self.splitter.set_receiver(0, self.detectors[0])
-        self.splitter.set_receiver(1, self.detectors[1])
-        
-        self.components = [self.splitter, self.detectors[0], self.detectors[1]]
+        self.splitter.add_receiver(self.detectors[0])
+        self.splitter.add_receiver(self.detectors[1])
         self.trigger_times = [[], []]
 
+        self.components = [self.splitter] + self.detectors
+
     def init(self) -> None:
         """Implementation of Entity interface (see base class)."""
 
         assert len(self.detectors) == 2
         super().init()
 
-    def get(self, photon: "Photon") -> None:
+    def get(self, photon: "Photon", **kwargs) -> None:
         """Method to receive a photon for measurement.
 
         Forwards the photon to the internal polariaztion beamsplitter.
@@ -188,10 +211,11 @@ def get(self, photon: "Photon") -> None:
         self.splitter.get(photon)
 
     def get_photon_times(self):
-        times, self.trigger_times = self.trigger_times, [[], []]
+        times = self.trigger_times
+        self.trigger_times = [[], []]
         return times
 
-    def set_basis_list(self, basis_list: "List", start_time: int, frequency: int) -> None:
+    def set_basis_list(self, basis_list: List[int], start_time: int, frequency: float) -> None:
         self.splitter.set_basis_list(basis_list, start_time, frequency)
 
     def update_splitter_params(self, arg_name: str, value: Any) -> None:
@@ -218,11 +242,11 @@ def __init__(self, name: str, timeline: "Timeline"):
         QSDetector.__init__(self, name, timeline)
         self.switch = Switch(name + ".switch", timeline)
         self.detectors = [Detector(name + ".detector" + str(i), timeline) for i in range(3)]
-        self.switch.set_detector(self.detectors[0])
+        self.switch.add_receiver(self.detectors[0])
         self.interferometer = Interferometer(name + ".interferometer", timeline, time_bin["bin_separation"])
-        self.interferometer.set_receiver(0, self.detectors[1])
-        self.interferometer.set_receiver(1, self.detectors[2])
-        self.switch.set_interferometer(self.interferometer)
+        self.interferometer.add_receiver(self.detectors[1])
+        self.interferometer.add_receiver(self.detectors[2])
+        self.switch.add_receiver(self.interferometer)
 
         self.components = [self.switch, self.interferometer] + self.detectors
         self.trigger_times = [[], [], []]
@@ -233,7 +257,7 @@ def init(self):
         assert len(self.detectors) == 3
         super().init()
 
-    def get(self, photon):
+    def get(self, photon, **kwargs):
         """Method to receive a photon for measurement.
 
         Forwards the photon to the internal fiber switch.
@@ -251,8 +275,96 @@ def get_photon_times(self):
         times, self.trigger_times = self.trigger_times, [[], [], []]
         return times
 
-    def set_basis_list(self, basis_list: "List", start_time: int, frequency: int) -> None:
+    def set_basis_list(self, basis_list: List[int], start_time: int, frequency: float) -> None:
         self.switch.set_basis_list(basis_list, start_time, frequency)
 
     def update_interferometer_params(self, arg_name: str, value: Any) -> None:
         self.interferometer.__setattr__(arg_name, value)
+
+
+class QSDetectorFockDirect(QSDetector):
+    """QSDetector to directly measure photons in Fock state.
+
+    Used to calculate diagonal density matrix elements.
+
+    Attributes:
+        name (str): label for QSDetector instance.
+        timeline (Timeline): timeline for simulation.
+        src_list (List[int]):
+        detectors (List[Detector]): list of attached detectors (length 2).
+        trigger_times (List[List[int]]): tracks simulation time of detection events for each detector.
+    """
+
+    def __init__(self, name: str, timeline: "Timeline", src_list: List[str]):
+        super().__init__(name, timeline)
+        assert len(src_list) == 2
+        self.src_list = src_list
+
+        for i in range(2):
+            d = Detector(name + ".detector" + str(i), timeline)
+            self.detectors.append(d)
+
+        self.trigger_times = [[], []]
+
+        self.components = self.detectors
+
+    def get(self, photon: "Photon", **kwargs):
+        src = kwargs["src"]
+        # measure (0/1 determines existence of photons in encoding)
+        res = Photon.measure(None, photon, self.get_generator())
+        if res:
+            detector_num = self.src_list.index(src)
+            self.detectors[detector_num].get()
+
+    def get_photon_times(self) -> List[List[int]]:
+        trigger_times = self.trigger_times
+        self.trigger_times = [[], []]
+        return trigger_times
+
+    # does nothing for this class
+    def set_basis_list(self, basis_list: List[int], start_time: int, frequency: int) -> None:
+        pass
+
+
+class QSDetectorFockInterference(QSDetector):
+    """WIP"""
+
+    def __init__(self, name: str, timeline: "Timeline"):
+        super().__init__(name, timeline)
+
+        self.beamsplitter = FockBeamSplitter(name + ".beamsplitter", timeline)
+        for i in range(2):
+            d = Detector(name + ".detector" + str(i), timeline)
+            self.beamsplitter.add_receiver(d)
+            self.detectors.append(d)
+
+        # default circuit: no phase applied
+        self._circuit = Circuit(2)
+
+        self.trigger_times = [[], []]
+
+        self.components = self.detectors + [self.beamsplitter]
+
+    def get(self, photon, **kwargs):
+        # check if we have non-null photon
+        if not photon.is_null:
+            state = self.timeline.quantum_manager.get(photon.quantum_state)
+
+            # if entangled, apply phase gate
+            if len(state.keys) == 2:
+                self.timeline.quantum_manager.run_circuit(self._circuit, state.keys)
+
+            self.beamsplitter.get(photon)
+
+    def get_photon_times(self) -> List[List[int]]:
+        trigger_times = self.trigger_times
+        self.trigger_times = [[], []]
+        return trigger_times
+
+    # does nothing for this class
+    def set_basis_list(self, basis_list: List[int], start_time: int, frequency: float) -> None:
+        pass
+
+    def set_phase(self, phase: float):
+        self._circuit = Circuit(2)
+        self._circuit.phase(0, phase)
diff --git a/src/components/fiber_stretcher.py b/src/components/fiber_stretcher.py
new file mode 100644
index 00000000..f0c2b8f3
--- /dev/null
+++ b/src/components/fiber_stretcher.py
@@ -0,0 +1,64 @@
+"""Models for simulation of fiber stretching.
+
+This module introduces the FiberStretcher class.
+The fiber stretcher modifies the phase of incoming photons, but does not add additional delay.
+"""
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from .photon import Photon
+
+from ..kernel.entity import Entity
+from .circuit import Circuit
+
+
+class FiberStretcher(Entity):
+    """Class implementing a simple fiber stretcher.
+
+    Attributes:
+        name (str): name of the fiber stretcher instance.
+        timeline (Timeline): simulation timeline.
+        _circuit (Circuit): quantum circuit used to apply a phase shift to incoming photons.
+    """
+
+    def __init__(self, name, timeline, phase=0.0):
+        """Constructor for Fiber Stretcher class.
+
+        Args:
+            name (str): name of the fiber stretcher.
+            timeline (Timeline): simulation timeline.
+            phase (float): phase to apply to incoming photons (default 0.0).
+        """
+
+        super().__init__(name, timeline)
+        self._circuit = Circuit(1)
+        self._circuit.phase(0, phase)
+
+    def init(self):
+        pass
+
+    def set_phase(self, phase: float):
+        """Method to change the phase applied to incoming photons.
+
+        Args:
+            phase (float): new phase to use.
+        """
+
+        self._circuit = Circuit(1)
+        self._circuit.phase(0, phase)
+
+    def get(self, photon: "Photon", **kwargs):
+        """Method to receive a photon.
+
+        Applies the local phase quantum circuit, then forwards to first receiver.
+        This phase is only applied if the photon is using the absorptive encoding scheme.
+
+        Args:
+            photon (Photon): photon to transmit.
+        """
+
+        if photon.encoding_type['name'] == "absorptive":
+            key = photon.quantum_state
+            self.timeline.quantum_manager.run_circuit(self._circuit, [key])
+        self._receivers[0].get(photon)
diff --git a/src/components/interferometer.py b/src/components/interferometer.py
index 2393f651..562df745 100644
--- a/src/components/interferometer.py
+++ b/src/components/interferometer.py
@@ -5,7 +5,9 @@
 """
 
 from math import sqrt
+from numpy import multiply
 from typing import TYPE_CHECKING
+
 if TYPE_CHECKING:
     from ..kernel.timeline import Timeline
     from ..components.photon import Photon
@@ -26,7 +28,6 @@ class Interferometer(Entity):
         timeline (Timeline): timeline for simulation
         path_difference (int): difference (in ps) of photon transit time in interferometer branches
         phase_error (float): phase error applied to measurement
-        receivers (List[Entities]): entities to receive transmitted photons
     """
 
     def __init__(self, name: str, timeline: "Timeline", path_diff, phase_error=0):
@@ -42,25 +43,17 @@ def __init__(self, name: str, timeline: "Timeline", path_diff, phase_error=0):
         Entity.__init__(self, name, timeline)
         self.path_difference = path_diff  # time difference in ps
         self.phase_error = phase_error  # chance of measurement error in phase
-        self.receivers = []
 
     def init(self) -> None:
         """See base class."""
 
-        assert len(self.receivers) == 2
-
-    def set_receiver(self, index: int, receiver: "Detector") -> None:
-        """Sets the receivers attribute at the specified index."""
-
-        if index > len(self.receivers):
-            raise Exception("index is larger than the length of receivers")
-        self.receivers.insert(index, receiver)
+        assert len(self._receivers) == 2, "Interferometer should only be attached to 2 outputs."
 
-    def get(self, photon: "Photon") -> None:
+    def get(self, photon: "Photon", **kwargs) -> None:
         """Method to receive a photon for measurement.
 
         Arguments:
-            photon (Photon): photon to measure (must have polarization encoding)
+            photon (Photon): photon to measure (must have time bin encoding)
 
         Returns:
             None
@@ -68,6 +61,12 @@ def get(self, photon: "Photon") -> None:
         Side Effects:
             May call get method of one attached receiver from the receivers attribute.
         """
+
+        assert photon.encoding_type["name"] == "time_bin", \
+            "Invalid photon encoding {} received by interferometer".format(photon.encoding_type["name"])
+        if photon.use_qm:
+            raise NotImplementedError("Interferometer usage not configured for quantum manager.")
+
         detector_num = self.get_generator().choice([0, 1])
         quantum_state = photon.quantum_state
         time = 0
@@ -106,6 +105,6 @@ def get(self, photon: "Photon") -> None:
             else:
                 return
 
-        process = Process(self.receivers[detector_num], "get", [])
+        process = Process(self._receivers[detector_num], "get", [])
         event = Event(self.timeline.now() + time, process)
         self.timeline.schedule(event)
diff --git a/src/components/light_source.py b/src/components/light_source.py
index 5886cedf..7d4338d5 100644
--- a/src/components/light_source.py
+++ b/src/components/light_source.py
@@ -4,6 +4,8 @@
 These classes should be connected to one or two entities, respectively, that are capable of receiving photons.
 """
 
+from typing import List
+
 from numpy import multiply
 
 from .photon import Photon
@@ -11,6 +13,7 @@
 from ..kernel.event import Event
 from ..kernel.process import Process
 from ..utils.encoding import polarization
+from ..utils import log
 
 
 class LightSource(Entity):
@@ -53,13 +56,6 @@ def __init__(self, name, timeline, frequency=8e7, wavelength=1550, bandwidth=0,
         self.encoding_type = encoding_type
         self.phase_error = phase_error
         self.photon_counter = 0
-        # for BB84
-        # self.basis_lists = []
-        # self.basis_list = []
-        # self.bit_lists = []
-        # self.bit_list = []
-        # self.is_on = False
-        # self.pulse_id = 0
 
     def init(self):
         """Implementation of Entity interface (see base class)."""
@@ -67,7 +63,7 @@ def init(self):
         pass
 
     # for general use
-    def emit(self, state_list, dst: str) -> None:
+    def emit(self, state_list) -> None:
         """Method to emit photons.
 
         Will emit photons for a length of time determined by the `state_list` parameter.
@@ -75,9 +71,10 @@ def emit(self, state_list, dst: str) -> None:
 
         Arguments:
             state_list (List[List[complex]]): list of complex coefficient arrays to send as photon-encoded qubits.
-            dst (str): name of destination node to receive photons.
         """
 
+        log.logger.info("{} emitting {} photons".format(self.name, len(state_list)))
+
         time = self.timeline.now()
         period = int(round(1e12 / self.frequency))
 
@@ -89,15 +86,16 @@ def emit(self, state_list, dst: str) -> None:
 
             for _ in range(num_photons):
                 wavelength = self.linewidth * self.get_generator().standard_normal() + self.wavelength
-                new_photon = Photon(str(i),
+                new_photon = Photon(str(i), self.timeline,
                                     wavelength=wavelength,
                                     location=self.owner,
                                     encoding_type=self.encoding_type,
                                     quantum_state=state)
-                process = Process(self.owner, "send_qubit", [dst, new_photon])
+                process = Process(self._receivers[0], "get", [new_photon])
                 event = Event(time, process)
-                self.owner.timeline.schedule(event)
+                self.timeline.schedule(event)
                 self.photon_counter += 1
+
             time += period
 
 
@@ -111,23 +109,23 @@ class SPDCSource(LightSource):
         name (str): label for beamsplitter instance
         timeline (Timeline): timeline for simulation
         frequency (float): frequency (in Hz) of photon creation.
-        wavelengths (float): wavelengths (in nm) of emitted entangled photons.
-        linewidth (float): st. dev. in photon wavelength (in nm).
+        wavelengths (List[float]): wavelengths (in nm) of emitted entangled photons.
+        linewidth (float): st. dev. in photon wavelength (in nm) (currently unused).
         mean_photon_num (float): mean number of photons emitted each period.
         encoding_type (Dict): encoding scheme of emitted photons (as defined in the encoding module).
         phase_error (float): phase error applied to qubits.
-        photon_counter (int): counter for number of photons emitted.
-        direct_receiver (Entity): device to receive one entangled photon.
-        another_receiver (Entity): device to receive another entangled photon.
     """
 
-    def __init__(self, name, timeline, direct_receiver=None, another_receiver=None, wavelengths=[], frequency=8e7, wavelength=1550,
-                 bandwidth=0, mean_photon_num=0.1, encoding_type=polarization, phase_error=0):
-        super().__init__(name, timeline, frequency, wavelength, bandwidth, mean_photon_num, encoding_type, phase_error)
-        self.direct_receiver = direct_receiver
-        self.another_receiver = another_receiver
+    def __init__(self, name, timeline, wavelengths=None, frequency=8e7, bandwidth=0, mean_photon_num=0.1,
+                 encoding_type=polarization, phase_error=0):
+        super().__init__(name, timeline, frequency, 0, bandwidth, mean_photon_num, encoding_type, phase_error)
+        if wavelengths is None:
+            wavelengths = [1550, 1550]
         self.wavelengths = wavelengths
 
+    def init(self):
+        assert len(self._receivers) == 2, "SPDC source must connect to 2 receivers."
+
     def emit(self, state_list):
         """Method to emit photons.
 
@@ -136,40 +134,87 @@ def emit(self, state_list):
 
         Arguments:
             state_list (List[List[complex]]): list of complex coefficient arrays to send as photon-encoded qubits.
+                This is ignored for the absorptive encoding type.
         """
 
+        log.logger.info("SPDC sourcee {} emitting {} photons".format(self.name, len(state_list)))
+
         time = self.timeline.now()
 
-        for state in state_list:
-            num_photon_pairs = self.get_generator().poisson(
+        if self.encoding_type["name"] == "absorptive":
+            for _ in state_list:
+                num_photon_pairs = self.get_generator().poisson(self.mean_photon_num)
+
+                for _ in range(num_photon_pairs):
+                    new_photon0 = Photon("", self.timeline,
+                                         wavelength=self.wavelengths[0],
+                                         location=self,
+                                         encoding_type=self.encoding_type,
+                                         use_qm=True)
+                    new_photon1 = Photon("", self.timeline,
+                                         wavelength=self.wavelengths[1],
+                                         location=self,
+                                         encoding_type=self.encoding_type,
+                                         use_qm=True)
+
+                    new_photon0.entangle(new_photon1)
+                    new_photon0.set_state((complex(0), complex(0), complex(0), complex(1)))
+                    self.send_photons(time, [new_photon0, new_photon1])
+                    self.photon_counter += 1
+
+                if num_photon_pairs is 0:
+                    # send two null photons for purposes of entanglement
+                    new_photon0 = Photon("", self.timeline,
+                                         wavelength=self.wavelengths[0],
+                                         location=self,
+                                         encoding_type=self.encoding_type,
+                                         use_qm=True)
+                    new_photon1 = Photon("", self.timeline,
+                                         wavelength=self.wavelengths[1],
+                                         location=self,
+                                         encoding_type=self.encoding_type,
+                                         use_qm=True)
+
+                    new_photon0.is_null = True
+                    new_photon1.is_null = True
+                    new_photon0.entangle(new_photon1)
+                    new_photon0.set_state((complex(1), complex(0), complex(0), complex(0)))
+                    self.send_photons(time, [new_photon0, new_photon1])
+
+                time += 1e12 / self.frequency
+
+        else:
+            for state in state_list:
+                num_photon_pairs = self.get_generator().poisson(
                 self.mean_photon_num)
 
-            if self.get_generator().random() < self.phase_error:
-                state = multiply([1, -1], state)
-
-            for _ in range(num_photon_pairs):
-                new_photon0 = Photon(None,
-                                     wavelength=self.wavelengths[0],
-                                     location=self.direct_receiver,
-                                     encoding_type=self.encoding_type)
-                new_photon1 = Photon(None,
-                                     wavelength=self.wavelengths[1],
-                                     location=self.direct_receiver,
-                                     encoding_type=self.encoding_type)
-
-                new_photon0.entangle(new_photon1)
-                new_photon0.set_state((state[0], complex(0), complex(0), state[1]))
-
-                process0 = Process(self.direct_receiver, "get", [new_photon0])
-                process1 = Process(self.another_receiver, "get", [new_photon1])
-                event0 = Event(int(round(time)), process0)
-                event1 = Event(int(round(time)), process1)
-                self.timeline.schedule(event0)
-                self.timeline.schedule(event1)
-
-                self.photon_counter += 1
-
-            time += 1e12 / self.frequency
-
-    def assign_another_receiver(self, receiver):
-        self.another_receiver = receiver
+                if self.get_generator().random() < self.phase_error:
+                    state = multiply([1, -1], state)
+
+                for _ in range(num_photon_pairs):
+                    new_photon0 = Photon("", self.timeline,
+                                         wavelength=self.wavelengths[0],
+                                         location=self,
+                                         encoding_type=self.encoding_type)
+                    new_photon1 = Photon("", self.timeline,
+                                         wavelength=self.wavelengths[1],
+                                         location=self,
+                                         encoding_type=self.encoding_type)
+
+                    new_photon0.entangle(new_photon1)
+                    new_photon0.set_state((state[0], complex(0), complex(0), state[1]))
+                    self.send_photons(time, [new_photon0, new_photon1])
+                    self.photon_counter += 1
+
+                time += 1e12 / self.frequency
+
+    def send_photons(self, time, photons: List["Photon"]):
+        log.logger.debug("SPDC source {} sending photons to {} at time {}".format(
+            self.name, self._receivers, time
+        ))
+
+        assert len(photons) == 2
+        for dst, photon in zip(self._receivers, photons):
+            process = Process(dst, "get", [photon])
+            event = Event(int(round(time)), process)
+            self.timeline.schedule(event)
diff --git a/src/components/memory.py b/src/components/memory.py
index ae434e38..8e4f0065 100644
--- a/src/components/memory.py
+++ b/src/components/memory.py
@@ -5,22 +5,21 @@
 Photons should be routed to a BSM device for entanglement generation, or through optical hardware for purification and swapping.
 """
 
+from copy import copy
 from math import inf
-from typing import Any, List, TYPE_CHECKING, Dict
+from typing import Any, List, TYPE_CHECKING, Dict, Callable
 
 from scipy import stats
 
 if TYPE_CHECKING:
     from ..entanglement_management.entanglement_protocol import EntanglementProtocol
     from ..kernel.timeline import Timeline
-    from ..topology.node import QuantumRouter
 
 from .photon import Photon
 from ..kernel.entity import Entity
 from ..kernel.event import Event
 from ..kernel.process import Process
 from ..utils.encoding import single_atom
-from ..utils.quantum_state import QuantumState
 
 
 # array of single atom memories
@@ -52,13 +51,13 @@ def __init__(self, name: str, timeline: "Timeline", num_memories=10,
 
         Entity.__init__(self, name, timeline)
         self.memories = []
-        self.owner = None
 
         for i in range(num_memories):
             memory = Memory(self.name + "[%d]" % i, timeline, fidelity, frequency, efficiency, coherence_time,
                             wavelength)
             memory.attach(self)
             self.memories.append(memory)
+            memory.set_memory_array(self)
 
     def __getitem__(self, key):
         return self.memories[key]
@@ -72,9 +71,8 @@ def init(self):
         Set the owner of memory as the owner of memory array.
         """
 
-        for mem in self.memories:
-            mem.owner = self.owner
-            mem.set_memory_array(self)
+        for memory in self.memories:
+            memory.owner = self.owner
 
     def memory_expire(self, memory: "Memory"):
         """Method to receive expiration events from memories.
@@ -89,8 +87,9 @@ def update_memory_params(self, arg_name: str, value: Any) -> None:
         for memory in self.memories:
             memory.__setattr__(arg_name, value)
 
-    def set_node(self, node: "QuantumRouter") -> None:
-        self.owner = node
+    def add_receiver(self, receiver: "Entity"):
+        for m in self.memories:
+            m.add_receiver(receiver)
 
 
 class Memory(Entity):
@@ -107,12 +106,12 @@ class Memory(Entity):
         efficiency (float): probability of emitting a photon when excited.
         coherence_time (float): average usable lifetime of memory (in seconds).
         wavelength (float): wavelength (in nm) of emitted photons.
-        entangled_memory (Dict[str, Any]): tracks entanglement state of memory.
         qstate_key (int): key for associated quantum state in timeline's quantum manager.
+        entangled_memory (Dict[str, Any]): tracks entanglement state of memory.
     """
 
     def __init__(self, name: str, timeline: "Timeline", fidelity: float, frequency: float,
-                 efficiency: float, coherence_time: int, wavelength: int):
+                 efficiency: float, coherence_time: float, wavelength: int):
         """Constructor for the Memory class.
 
         Args:
@@ -136,9 +135,12 @@ def __init__(self, name: str, timeline: "Timeline", fidelity: float, frequency:
         self.coherence_time = coherence_time  # coherence time in seconds
         self.wavelength = wavelength
         self.qstate_key = timeline.quantum_manager.new()
-
         self.memory_array = None
 
+        # for photons
+        self.encoding = copy(single_atom)
+        self.encoding["raw_fidelity"] = self.raw_fidelity
+
         # keep track of previous BSM result (for entanglement generation)
         # -1 = no result, 0/1 give detector number
         self.previous_bsm = -1
@@ -176,11 +178,9 @@ def excite(self, dst="") -> None:
             return
 
         # create photon
-        photon = Photon("", wavelength=self.wavelength, location=self.name,
-                        encoding_type=single_atom)
-
-        photon.qstate_key = self.qstate_key
-        photon.fidelity = self.raw_fidelity
+        photon = Photon("", self.timeline, wavelength=self.wavelength, location=self.name, encoding_type=self.encoding,
+                        quantum_state=self.qstate_key, use_qm=True)
+        photon.timeline = None  # facilitate cross-process exchange of photons
         photon.is_null = True
         photon.add_loss(1 - self.efficiency)
 
@@ -188,8 +188,8 @@ def excite(self, dst="") -> None:
             period = 1e12 / self.frequency
             self.next_excite_time = self.timeline.now() + period
 
-        # send to node
-        self.owner.send_qubit(dst, photon)
+        # send to receiver
+        self._receivers[0].get(photon, dst=dst)
         self.excited_photon = photon
 
     def expire(self) -> None:
@@ -286,6 +286,316 @@ def detach(self, observer: 'EntanglementProtocol'):
             self._observers.remove(observer)
 
 
+class AbsorptiveMemory(Entity):
+    """Atomic ensemble absorptive memory.
+
+    This class models an AFC(-spinwave) absorptive memory, where the quantum state is stored as collective excitation of atomic ensemble.
+    Retrieved photon sequence might be reversed (only for AFC spinwave), which is physically determine by RF pulses used during spinwave.
+    This class will not support qubit state manipulation.
+    Before invoking methods like "get" and "retrieve", must call "prepare" first to prepare the AFC, will take finite simulation time.
+    This class models an AFC absorptive memory, where the quantum state is stored as collective excitation of an atomic ensemble.
+    This class does not support qubit state manipulation, individual photons should be manipulated instead.
+    Before invoking methods like "get" and "retrieve", need to call "prepare" first to prepare the AFC, will take finite simulation time.
+    Rephasing time (predetermined storage time for AFC type) is given by temporal mode number and length of each temporal mode bin.
+    Note that for AFC (not spinwave) type, is_reversed must be False.
+
+    Attributes:
+        name (str): label for memory instance.
+        timeline (Timeline): timeline for simulation.
+        fidelity (float): (current) fidelity of memory's entanglement.
+        frequency (float): maximum frequency of absorption for memory (total frequency bandwidth of AFC memory) (in Hz).
+        absorption_efficiency (float): probability of absorbing a photon when arriving at the memory.
+        efficiency (Callable): probability of emitting a photon as a function of storage time.
+        mode_number (int): number of temporal modes available for storing photons, i.e. number of peaks in Atomic Frequency Comb.
+        mode_bin (int):
+        coherence_time (float): average usable lifetime of memory (in s).
+        wavelength (float): wavelength (in nm) of absorbed and emitted photons.
+        total_time (float): AFC re-phasing time (in ps)
+        overlap_error (float): error due to photon overlap in one temporal mode, will degrade fidelity.
+        prepare_time (float): time to prepare AFC (in ps).
+        photon_counter (int): counts number of detection events.
+        absorb_start_time (int): start time (in ps) of photon absorption.
+        retrieve_start_time (int): start time (in ps) of photon retrieval.
+        is_spinwave (Bool): determines if the memory is AFC or AFC-spinwave, default False.
+        is_reversed (Bool): determines re-emission sequence, physically determined by RF pulses during spinwave, default False.
+        is_prepared (Bool): determines if AFC is successfully prepared.
+        memory_array (MemoryArray): memory array aggregating current memory.
+        destination (str): name of predetermined re-emission destination node, default None.
+        entangled_memory (Dict[str, Any]): tracks entanglement state of memory with a memory.
+        stored_photons (Dict[str, Any]): photons stored in memory temporal modes.
+    """
+
+    def __init__(self, name: str, timeline: "Timeline", fidelity: float, frequency: float, absorption_efficiency: float,
+                 efficiency: Callable, mode_number: int, coherence_time: int, wavelength: int, overlap_error: float,
+                 prepare_time: int, is_spinwave=False, is_reversed=False, destination=None):
+        """Constructor for the AbsorptiveMemory class.
+
+        Args:
+            name (str): name of the memory instance.
+            timeline (Timeline): simulation timeline.
+            fidelity (float): fidelity of memory.
+            frequency (float): maximum frequency of absorption for memory (total frequency bandwidth of AFC memory).
+            absorption_efficiency (float): probability of absorbing a photon when arriving at the memory.
+            efficiency (Callable): probability of emitting a photon as a function of storage time.
+            mode_number (int): number of modes supported for storing photons.
+            coherence_time (float): average time (in s) that memory state is valid.
+            wavelength (int): wavelength (in nm) of photons emitted by memories.
+            overlap_error (float): error due to photon overlap in one temporal mode.
+            prepare_time (float): time to prepare AFC (in ps).
+            is_spinwave (Bool): determines if the memory is AFC or AFC-spinwave (default False).
+            is_reversed (Bool): determines re-emission sequence, physically determined by RF pulses during spinwave (default False).
+            destination (str): name of predetermined re-emission destination node (default None).
+        """
+
+        super().__init__(name, timeline)
+        assert 0 <= fidelity <= 1
+        assert 0 <= absorption_efficiency <= 1
+
+        self.fidelity = 0
+        self.raw_fidelity = fidelity
+        self.frequency = frequency
+        self.absorption_efficiency = absorption_efficiency
+        self.efficiency = efficiency
+        self.mode_number = mode_number
+        self.coherence_time = coherence_time  # coherence time in seconds
+        self.wavelength = wavelength
+        self.mode_bin = 1e12 / self.frequency  # time bin for each separate temporal mode
+        self.total_time = self.mode_number * self.mode_bin  # AFC rephasing time
+        self.overlap_error = overlap_error
+        self.prepare_time = prepare_time
+
+        self.photon_counter = 0
+        self.absorb_start_time = 0
+        self.retrieve_start_time = 0
+
+        self.is_spinwave = is_spinwave
+        self.is_reversed = is_reversed
+        self.is_prepared = False
+
+        self.memory_array = None
+        self.destination = destination
+
+        # keep track of previous BSM result (for entanglement generation)
+        # -1 = no result, 0/1 give detector number
+        self.previous_bsm = -1
+
+        # keep track of entanglement with memory
+        # photon entanglement stored internally within photons
+        self.entangled_memory = {'node_id': None, 'memo_id': None}
+
+        # keep track of current memory write (ignore expiration of past states)
+        self.expiration_event = None
+        self.excited_photons = []
+
+        # initialization of stored_photons dictionary
+        self.stored_photons = [None] * self.mode_number
+
+    def init(self):
+        """Implementation of Entity interface (see base class)."""
+
+        self.photon_counter = 0
+
+    def set_memory_array(self, memory_array: MemoryArray):
+        """Method to set the memory array to which the memory belongs
+
+        Args:
+            memory_array (MemoryArray): memory array to which the memory belongs
+        """
+
+        self.memory_array = memory_array
+
+    def prepare(self):
+        """Method to emulate the effect on timeline by AFC preparation.
+
+        Will schedule a preparation event in the future, after which `is_prepared` is set to true.
+        """
+
+        process = Process(self, "_prepare_AFC", [])
+        event = Event(self.timeline.now() + self.prepare_time, process)
+        self.timeline.schedule(event)
+
+    def _prepare_AFC(self):
+        """Method to get AFC prepared, will change is_prepared field.
+
+        Will raise exception if already prepared.
+        Should not be called directly, only scheduled by prepare method.
+        """
+
+        if self.is_prepared:
+            raise Exception("AFC has already been prepared")
+        else:
+            self.is_prepared = True
+            self.photon_counter = 0
+
+    def get(self, photon: "Photon", **kwargs):
+        """Method to receive a photon to store in the absorptive memory."""
+
+        # AFC needs to be prepared first
+        if not self.is_prepared:
+            raise Exception("AFC is not prepared yet.")
+
+        now = -1
+        # require resonant absorption of photons
+        if photon.wavelength == self.wavelength and self.get_generator().random() < self.absorption_efficiency:
+            self.photon_counter += 1
+            now = self.timeline.now()
+
+        # determine absorb_start_time
+        if self.photon_counter == 1:
+            self.absorb_start_time = now
+
+            # schedule re-emission if memory is not spinwave type
+            if not self.is_spinwave:
+                process = Process(self, "retrieve", [])
+                event = Event(self.absorb_start_time + self.total_time, process)
+                self.timeline.schedule(event)
+
+        # schedule expiration at absorb_start_time
+        if self.coherence_time > 0 and self.absorb_start_time == now:
+            self._schedule_expiration()
+
+        # determine which temporal mode the photon is stored in
+        absorb_time = now - self.absorb_start_time
+        index = int(absorb_time / self.mode_bin)
+        if index < 0 or index >= self.mode_number:
+            return
+
+        # keep one photon per mode since most hardware cannot resolve photon number
+        # photon_counter might be larger than mode_number, multi-photon events counted by "number"
+        # if "degradation" is True, memory fidelity will be corrected by overlap_error
+        if self.stored_photons[index] is None:
+            self.stored_photons[index] = {"photon": photon, "time": absorb_time, "number": 1, "degradation": False}
+            self.excited_photons.append(photon)
+        else:
+            self.stored_photons[index]["number"] += 1
+            self.stored_photons[index]["degradation"] = True
+
+    def retrieve(self, dst=""):
+        """Method to re-emit all stored photons in a reverse sequence on demand.
+
+        Efficiency is a function of time.
+        """
+
+        # AFC needs to be prepared first
+        if not self.is_prepared:
+            raise Exception("AFC is not prepared yet.")
+
+        # do nothing if there are no photons stored
+        if len(self.excited_photons) == 0:
+            return
+
+        now = self.timeline.now()
+        store_time = now - self.absorb_start_time - self.total_time
+
+        # TODO: clear locally stored photons
+        for index in range(self.mode_number):
+            if self.stored_photons[index] is not None:
+                if self.get_generator().random() < self.efficiency(store_time):
+                    photon = self.stored_photons[index]["photon"]
+                    absorb_time = self.stored_photons[index]["time"]
+
+                    if self.is_reversed:
+                        if self.is_spinwave:
+                            raise Exception("AFC memory can only have normal order of re-emission")
+                        emit_time = self.total_time - self.mode_bin - absorb_time  # reversed order of re-emission
+                    else:
+                        emit_time = absorb_time  # normal order of re-emission
+
+                    if self.destination is not None:
+                        dst = self.destination
+
+                    # process = Process(self.owner, "send_qubit", [dst, photon])
+                    # self._receivers[0].get(photon, dst)
+                    process = Process(self._receivers[0], "get", [photon], {"dst": dst})
+                    event = Event(self.timeline.now() + emit_time, process)
+                    self.timeline.schedule(event)
+
+        # TODO: make sure if after each round of re-emission AFC needs to be prepared before next storage task
+        self.is_prepared = False
+
+    def expire(self) -> None:
+        """Method to handle memory expiration.
+
+        Side Effects:
+            Will notify upper entities of expiration via the `pop` interface.
+            Will modify the quantum state of the memory.
+        """
+
+        # AFC needs to be prepared first
+        if not self.is_prepared:
+            raise Exception("AFC is not prepared yet.")
+
+        if self.excited_photons:
+            for i in range(len(self.excited_photons)):
+                self.excited_photons[i].is_null = True
+
+        self.reset()
+        # pop expiration message
+        self.notify(self)
+
+    def reset(self) -> None:
+        """Method to clear quantum memory.
+
+        Will reset memory state to no photon stored and will clear entanglement information.
+
+        Side Effects:
+            Will modify internal parameters and photon storage information.
+        """
+
+        self.fidelity = 0
+        self.entangled_memory = {'node_id': None, 'memo_id': None}
+        self.photon_counter = 0
+        self.absorb_start_time = 0
+        self.excited_photons = []
+        self.is_prepared = False
+
+        if self.expiration_event is not None:
+            self.timeline.remove_event(self.expiration_event)
+            self.expiration_event = None
+
+    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)
+        process = Process(self, "expire", [])
+        event = Event(decay_time, process)
+        self.timeline.schedule(event)
+
+        self.expiration_event = event
+
+    def update_expire_time(self, time: int):
+        """Method to change time of expiration.
+
+        Should not normally be called by protocols.
+
+        Args:
+            time (int): new expiration time.
+        """
+
+        time = max(time, self.timeline.now())
+        if self.expiration_event is None:
+            if time >= self.timeline.now():
+                process = Process(self, "expire", [])
+                event = Event(time, process)
+                self.timeline.schedule(event)
+        else:
+            self.timeline.update_event_time(self.expiration_event, time)
+
+    def get_expire_time(self) -> int:
+        """Method to get the simulation time when the memory is expired"""
+
+        return self.expiration_event.time if self.expiration_event else inf
+
+    def notify(self, msg: Dict[str, Any]):
+        for observer in self._observers:
+            observer.memory_expire(self)
+
+    def detach(self, observer: 'EntanglementProtocol'):
+        if observer in self._observers:
+            self._observers.remove(observer)
+
+
 class MemoryWithRandomCoherenceTime(Memory):
     """Individual single-atom memory.
 
@@ -306,6 +616,7 @@ class MemoryWithRandomCoherenceTime(Memory):
         coherence_time_stdev (float): standard deviation of coherence time
         wavelength (float): wavelength (in nm) of emitted photons.
         qstate_key (int): key for associated quantum state in timeline's quantum manager.
+        entangled_memory (Dict[str, Any]): tracks entanglement state of memory.
     """
 
     def __init__(self, name: str, timeline: "Timeline", fidelity: float, frequency: float,
@@ -328,23 +639,23 @@ def __init__(self, name: str, timeline: "Timeline", fidelity: float, frequency:
         
         # coherence time standard deviation in seconds
         self.coherence_time_stdev = coherence_time_stdev
-        self.random_coherence_time = ( coherence_time_stdev > 0.0 and
-                                      self.coherence_time > 0.0 )
+        self.random_coherence_time = (coherence_time_stdev > 0.0 and
+                                      self.coherence_time > 0.0)
         
     def coherence_time_distribution(self) -> None:
         return stats.truncnorm.rvs(
             -0.95 * self.coherence_time / self.coherence_time_stdev,
             19.0 * self.coherence_time / self.coherence_time_stdev,
             self.coherence_time,
-            self.coherence_time_stdev )        
+            self.coherence_time_stdev)
 
     def _schedule_expiration(self) -> None:
         if self.expiration_event is not None:
             self.timeline.remove_event(self.expiration_event)
             
-        coherence_period = ( self.coherence_time_distribution() 
+        coherence_period = (self.coherence_time_distribution()
                             if self.random_coherence_time else 
-                            self.coherence_time )
+                            self.coherence_time)
 
         decay_time = self.timeline.now() + int(coherence_period * 1e12)
         process = Process(self, "expire", [])
@@ -352,5 +663,3 @@ def _schedule_expiration(self) -> None:
         self.timeline.schedule(event)
 
         self.expiration_event = event
-
-
diff --git a/src/components/mirror.py b/src/components/mirror.py
index 8dd73978..55fed793 100644
--- a/src/components/mirror.py
+++ b/src/components/mirror.py
@@ -36,7 +36,7 @@ def __init__(self, name: str, timeline: "Timeline", fidelity=0.98,
     def init(self):
         pass
 
-    def get(self, photon: Photon) -> None:
+    def get(self, photon, **kwargs) -> None:
         """Method to get a photon to reflect.
 
         Will send to destination node specified by destination attribute through owner node.
diff --git a/src/components/optical_channel.py b/src/components/optical_channel.py
index 2ac18faf..2dc1c611 100644
--- a/src/components/optical_channel.py
+++ b/src/components/optical_channel.py
@@ -28,7 +28,7 @@ class OpticalChannel(Entity):
         timeline (Timeline): timeline for simulation.
         sender (Node): node at sending end of optical channel.
         receiver (Node): node at receiving end of optical channel.
-        atteunuation (float): attenuation of the fiber (in dB/m).
+        attenuation (float): attenuation of the fiber (in dB/m).
         distance (int): length of the fiber (in m).
         polarization_fidelity (float): probability of no polarization error for a transmitted qubit.
         light_speed (float): speed of light within the fiber (in m/ps).
@@ -72,7 +72,7 @@ class QuantumChannel(OpticalChannel):
         timeline (Timeline): timeline for simulation.
         sender (Node): node at sending end of optical channel.
         receiver (Node): node at receiving end of optical channel.
-        atteunuation (float): attenuation of the fiber (in dB/m).
+        attenuation (float): attenuation of the fiber (in dB/m).
         distance (int): length of the fiber (in m).
         polarization_fidelity (float): probability of no polarization error for a transmitted qubit.
         light_speed (float): speed of light within the fiber (in m/ps).
@@ -81,9 +81,8 @@ class QuantumChannel(OpticalChannel):
         frequency (float): maximum frequency of qubit transmission (in Hz).
     """
 
-    def __init__(self, name: str, timeline: "Timeline", attenuation: float,
-                 distance: int, polarization_fidelity=1, light_speed=2e-4,
-                 frequency=8e7):
+    def __init__(self, name: str, timeline: "Timeline", attenuation: float, distance: int,
+                 polarization_fidelity=1.0, light_speed=2e-4, frequency=8e7):
         """Constructor for Quantum Channel class.
 
         Args:
@@ -139,7 +138,7 @@ def transmit(self, qubit: "Photon", source: "Node") -> None:
 
         log.logger.info(
             "{} send qubit with state {} to {} by Channel {}".format(
-                self.sender.name, qubit.quantum_state.state, self.receiver,
+                self.sender.name, qubit.quantum_state, self.receiver,
                 self.name))
 
         assert self.delay != 0 and self.loss != 1, \
@@ -158,7 +157,7 @@ def transmit(self, qubit: "Photon", source: "Node") -> None:
         if (self.sender.get_generator().random() > self.loss) or qubit.is_null:
             if self._receiver_on_other_tl():
                 self.timeline.quantum_manager.move_manage_to_server(
-                    qubit.qstate_key)
+                    qubit.quantum_state)
 
             if qubit.is_null:
                 qubit.add_loss(self.loss)
@@ -170,8 +169,7 @@ def transmit(self, qubit: "Photon", source: "Node") -> None:
 
             # schedule receiving node to receive photon at future time determined by light speed
             future_time = self.timeline.now() + self.delay
-            process = Process(self.receiver, "receive_qubit",
-                              [source.name, qubit])
+            process = Process(self.receiver, "receive_qubit", [source.name, qubit])
             event = Event(future_time, process)
             self.timeline.schedule(event)
 
@@ -192,6 +190,8 @@ def schedule_transmit(self, min_time: int) -> int:
             int: simulation time for next available transmission window.
         """
 
+        # TODO: move this to node?
+
         min_time = max(min_time, self.timeline.now())
         time_bin = min_time * (self.frequency / 1e12)
         if time_bin - int(time_bin) > 0.00001:
@@ -223,7 +223,6 @@ class ClassicalChannel(OpticalChannel):
         sender (Node): node at sending end of optical channel.
         receiver (Node): node at receiving end of optical channel.
         distance (float): length of the fiber (in m).
-        light_speed (float): speed of light within the fiber (in m/ps).
         delay (float): delay (in ps) of message transmission (default distance / light_speed).
     """
 
diff --git a/src/components/photon.py b/src/components/photon.py
index 7e4911f8..2802edbc 100644
--- a/src/components/photon.py
+++ b/src/components/photon.py
@@ -3,15 +3,20 @@
 This module defines the Photon class for tracking individual photons.
 Photons may be encoded directly with polarization or time bin schemes, or may herald the encoded state of single atom memories.
 """
-from numpy.random._generator import Generator
-from typing import Dict, Any, Optional
+from typing import Dict, Any, List, Union, TYPE_CHECKING
 
-from ..kernel.entity import Entity
+if TYPE_CHECKING:
+    from numpy.random._generator import Generator
+    from ..kernel.timeline import Timeline
+    from ..kernel.entity import Entity
+    from ..kernel.quantum_state import State
+
+from ..components.circuit import Circuit
 from ..utils.encoding import polarization
-from ..utils.quantum_state import QuantumState
+from ..kernel.quantum_state import FreeQuantumState
 
 
-class Photon():
+class Photon:
     """Class for a single photon.
 
     Attributes:
@@ -19,80 +24,151 @@ class Photon():
         wavelength (float): wavelength of photon (in nm).
         location (Entity): current location of photon.
         encoding_type (Dict[str, Any]): encoding type of photon (as defined in encoding module).
-        quantum_state (QuantumState): quantum state of photon.
+        quantum_state (Union[int, Tuple[complex]]): quantum state of photon.
+            If `use_qm` is false, this will be a QuantumState object.
+            Otherwise, it will be an integer key for the quantum manager.
         is_null (bool): defines whether photon is real or a "ghost" photon (not detectable but used in memory encoding).
+        loss (float): similarly defined for memory encoding, used to track loss and improve performance.
+            Does not need to be utilized for all encoding schemes.
+        use_qm (bool): determines if photon stores state locally (False) or uses timeline quantum manager (True).
+
+    Note: the `loss` attribute is currently specifically used for the `"single_atom"` encoding scheme.
+    This encoding scheme also removes the local timeline reference and sets the quantum state to the local key.
+    This is to both facilitate parallel execution and improve the performance of overall simulation.
     """
 
-    def __init__(self, name, wavelength=0, location=None, encoding_type=polarization,
-                 quantum_state=(complex(1), complex(0))):
+    _entangle_circuit = Circuit(2)
+    _measure_circuit = Circuit(1)
+    _measure_circuit.measure(0)
+
+    def __init__(self, name: str, timeline: "Timeline", wavelength=0, location=None, encoding_type=polarization,
+                 quantum_state=None, use_qm=False):
         """Constructor for the photon class.
 
         Args:
             name (str): name of the photon instance.
+            timeline (Timeline): simulation timeline reference
             wavelength (int): wavelength of photon (in nm) (default 0).
             location (Entity): location of the photon (default None).
             encoding_type (Dict[str, Any]): encoding type of photon (from encoding module) (default polarization).
-            quantum_state (List[complex]): complex coefficients for photon's quantum state (default [1, 0]).
+            quantum_state (Union[int, Tuple[complex]]): reference key for quantum manager, or complex coefficients for photon's quantum state.
+                Default state is (1, 0).
+                If left blank and `use_qm` is true, will create new key from timeline quantum manager.
+            use_qm (bool): determines if the quantum state is obtained from the quantum manager or stored locally.
         """
 
+        if encoding_type["name"] == "absorptive" and not use_qm:
+            raise ValueError("Photons with 'absorptive' encoding scheme must use quantum manager.")
+
         self.name: str = name
+        self.timeline = timeline
         self.wavelength: int = wavelength
         self.location: Entity = location
         self.encoding_type: Dict[str, Any] = encoding_type
-        if self.encoding_type["name"] == "single_atom":
-            self.fidelity: Optional[float] = None
-            self.detector_num: Optional[int] = None
-            self.loss: float = 0
-
-        self.quantum_state: QuantumState = QuantumState()
-        self.quantum_state.set_state_single(quantum_state)
-        self.qstate_key = None
         self.is_null: bool = False
+        self.loss: float = 0
+        self.use_qm = use_qm
+
+        self.quantum_state: Union[State, int] = -1
+        if self.use_qm:
+            if quantum_state is None:
+                self.quantum_state = timeline.quantum_manager.new()
+            else:
+                assert type(quantum_state) is int
+                self.quantum_state = quantum_state
+        else:
+            if quantum_state is None:
+                quantum_state=(complex(1), complex(0))
+            else:
+                assert type(quantum_state) is tuple
+            self.quantum_state = FreeQuantumState()
+            self.quantum_state.state = quantum_state
+
+    def __del__(self):
+        if self.use_qm and self.timeline is not None:
+            self.timeline.quantum_manager.remove(self.quantum_state)
 
     def entangle(self, photon):
-        """Method to entangle photons (see `QuantumState` module)."""
+        """Method to entangle photons (see `QuantumState` module).
+
+        This method does not modify the current state of the photon, but combines the internal quantum state object.
+        This ensures that two photons share a quantum state object.
+        """
 
-        self.quantum_state.entangle(photon.quantum_state)
+        if self.use_qm:
+            qm = self.timeline.quantum_manager
+            all_keys = qm.get(self.quantum_state).keys + \
+                self.timeline.quantum_manager.get(photon.quantum_state).keys
+            qm.run_circuit(Photon._entangle_circuit, all_keys)
+        else:
+            self.quantum_state.entangle(photon.quantum_state)
 
-    def random_noise(self, rng: Generator):
+    def random_noise(self, rng: "Generator"):
         """Method to add random noise to photon's state (see `QuantumState` module)."""
 
         self.quantum_state.random_noise(rng)
 
     def set_state(self, state):
-        self.quantum_state.set_state(state)
+        if self.use_qm:
+            qm = self.timeline.quantum_manager
+            all_keys = qm.get(self.quantum_state).keys
+            self.timeline.quantum_manager.set(all_keys, state)
+        else:
+            self.quantum_state.set_state(state)
 
     @staticmethod
-    def measure(basis, photon, rng: Generator):
+    def measure(basis, photon: "Photon", rng: "Generator"):
         """Method to measure a photon (see `QuantumState` module).
 
         Args:
             basis (List[List[complex]]): basis (given as lists of complex coefficients) with which to measure the photon.
             photon (Photon): photon to measure.
-            rng (Generator): PRGN to use  for measurement results.
+            rng (Generator): PRNG to use for measurement results.
 
         Returns:
             int: 0/1 value giving result of measurement in given basis.
         """
 
-        return photon.quantum_state.measure(basis, rng)
+        if photon.use_qm:
+            qm = photon.timeline.quantum_manager
+            key = photon.quantum_state
+            all_keys = qm.get(key).keys
+            meas_samp = rng.random()
+
+            # see if we don't need rearranging:
+            if len(all_keys) == 1 or all_keys.index(key) == 0:
+                res = qm.run_circuit(Photon._measure_circuit, [key], meas_samp)
+            # if we do, run bigger circuit to save time swapping
+            else:
+                circuit = Circuit(len(all_keys))
+                circuit.measure(all_keys.index(key))
+                res = qm.run_circuit(circuit, all_keys, meas_samp)
+
+            return res[photon.quantum_state]
+
+        else:
+            return photon.quantum_state.measure(basis, rng)
 
     @staticmethod
-    def measure_multiple(basis, photons, rng: Generator):
+    def measure_multiple(basis, photons: List["Photon"], rng: "Generator"):
         """Method to measure 2 entangled photons (see `QuantumState` module).
 
         Args:
             basis (List[List[complex]]): basis (given as lists of complex coefficients) with which to measure the photons.
             photons (List[Photon]): list of 2 photons to measure.
-            rng (Generator): PRGN to use  for measurement results.
+            rng (Generator): PRNG to use for measurement results.
 
         Returns:
             int: 0-3 value giving the result of measurement in given basis.
         """
 
-        return QuantumState.measure_multiple(basis, [photons[0].quantum_state,
-                                                     photons[1].quantum_state],
-                                             rng)
+        assert len(photons) == 2, "Photon.measure_multiple() must be called on two photons only."
+        if photons[0].use_qm:
+            raise NotImplementedError("Photon.measure_multiple() not implemented for quantum manager.")
+
+        return FreeQuantumState.measure_multiple(basis,
+                                                 [photons[0].quantum_state, photons[1].quantum_state],
+                                                 rng)
 
     def add_loss(self, loss: float):
         assert 0 <= loss <= 1
diff --git a/src/components/spdc_lens.py b/src/components/spdc_lens.py
index fbeed0b3..c71d25e6 100644
--- a/src/components/spdc_lens.py
+++ b/src/components/spdc_lens.py
@@ -1,4 +1,4 @@
-"""Model for simulation of a SPDC Lens.
+"""Model for simulation of an SPDC Lens.
 
 This module defines the SPDCLens class for creation of entangled photons.
 """
@@ -15,29 +15,26 @@ class SPDCLens(Entity):
         name (str): label for SPDCLens instance.
         timeline (Timeline): timeline for simulation.
         rate (float): probability of successful down conversion.
-        direct_receiver (Entity): entity to receive entangled photons.
     """
 
-    def __init__(self, name, timeline, rate=1, direct_receiver=None):
+    def __init__(self, name, timeline, rate=1):
         """Constructor for the spdc lens class.
 
         Args:
             name (str): name of the spdc lens instance.
             timeline (Timeline): simulation timeline.
-            rate (float): probability of successfull down conversion (default 1).
-            direct_receiver (Entity): entity to receive down-converted photons (default None).
+            rate (float): probability of successful down conversion (default 1).
         """
 
         Entity.__init__(self, name, timeline)
         self.rate = rate
-        self.direct_receiver = direct_receiver
 
     def init(self):
         """Implementation of Entity interface (see base class)."""
 
         pass
 
-    def get(self, photon):
+    def get(self, photon, **kwargs):
         """Method to receive a photon for transmission.
 
         Based on rate probability, may split photon into two entangled photons.
@@ -57,8 +54,5 @@ def get(self, photon):
             photon.entangle(new_photon)
             photon.set_state((state[0], complex(0), complex(0), state[1]))
 
-            self.direct_receiver.get(photon)
-            self.direct_receiver.get(new_photon)
-
-    def assign_receiver(self, receiver):
-        self.direct_receiver = receiver
+            self._receivers[0].get(photon)
+            self._receivers[1].get(new_photon)
diff --git a/src/components/switch.py b/src/components/switch.py
index 9520b097..68f90fe7 100644
--- a/src/components/switch.py
+++ b/src/components/switch.py
@@ -1,14 +1,14 @@
 """Model for simulation of an optical switch.
 
 This module defines the Switch class for directing the flow of photons.
-The switch is usually created as part of a time bin QSDetector object, but may be used individually.
+The switch is usually created as part of a time bin QSDetector object, and will only accept time bin photons.
+A more general switch class is in development.
 """
 
 from typing import TYPE_CHECKING, List
 
 if TYPE_CHECKING:
     from ..kernel.timeline import Timeline
-    from ..components.interferometer import Interferometer
 
 from .photon import Photon
 from ..kernel.entity import Entity
@@ -23,10 +23,8 @@ class Switch(Entity):
         name (str): label for switch instance.
         timeline (Timeline): timeline for simulation.
         start_time (int): simulation start time (in ps) for transmission.
-        frequency (float): frequency with whitch to switch destinations.
+        frequency (float): frequency with which to switch destinations.
         basis_list (List[int]): 0/1 list denoting which receiver to rout photons to each period.
-        interferometer (Interferometer): linked interferometer to receive photons in basis 1.
-        detector (Detector): linked SPD to receive photons in basis 0.
     """
 
     def __init__(self, name: str, timeline: "Timeline"):
@@ -41,57 +39,48 @@ def __init__(self, name: str, timeline: "Timeline"):
         self.start_time = 0
         self.frequency = 0
         self.basis_list = []
-        self.interferometer = None
-        self.detector = None
 
     def init(self) -> None:
         """Implementation of Entity interface (see base class)."""
 
         pass
 
-    def set_detector(self, detector: "Detector") -> None:
-        self.detector = detector
-
-    def set_interferometer(self, interferometer: "Interferometer") -> None:
-        self.interferometer = interferometer
-
-    def set_basis_list(self, basis_list: "List[int]", start_time: int, frequency: int) -> None:
+    def set_basis_list(self, basis_list: "List[int]", start_time: int, frequency: float) -> None:
         self.basis_list = basis_list
         self.start_time = start_time
         self.frequency = frequency
 
-    def get(self, photon: "Photon") -> None:
+    def get(self, photon, **kwargs) -> None:
         """Method to receive photon for transmission
 
         Args:
             photon (Photon): photon to transmit.
 
         Side Effects:
-            May call `get` method of attached detector or interferometer.
+            May call `get` method of attached receivers.
         """
 
+        assert photon.encoding_type["name"] == "time_bin"
+
         index = int((self.timeline.now() - self.start_time) * self.frequency * 1e-12)
         if index < 0 or index >= len(self.basis_list):
             return
 
+        receiver = self._receivers[self.basis_list[index]]
+
+        # check if receiver is detector; schedule in late/early time bin
         if self.basis_list[index] == 0:
-            receiver = self.detector
-            # check if receiver is detector, if we're using time bin, and if the photon is "late" to schedule measurement
-            assert photon.encoding_type["name"] == "time_bin"
-            if Photon.measure(photon.encoding_type["bases"][0], photon,
-                              self.get_generator()):
-                time = self.timeline.now() + photon.encoding_type[
-                    "bin_separation"]
-                process = Process(receiver, "get", [])
+            if Photon.measure(photon.encoding_type["bases"][0], photon, self.get_generator()):
+                time = self.timeline.now() + photon.encoding_type["bin_separation"]
+                process = Process(receiver, "get", [photon])
                 event = Event(time, process)
                 self.timeline.schedule(event)
             else:
                 time = self.timeline.now()
-                process = Process(receiver, "get", [])
+                process = Process(receiver, "get", [photon])
                 event = Event(time, process)
                 self.timeline.schedule(event)
         else:
-            receiver = self.interferometer
             time = self.timeline.now()
             process = Process(receiver, "get", [photon])
             event = Event(time, process)
diff --git a/src/entanglement_management/entanglement_protocol.py b/src/entanglement_management/entanglement_protocol.py
index 6f646d71..a9a539e8 100644
--- a/src/entanglement_management/entanglement_protocol.py
+++ b/src/entanglement_management/entanglement_protocol.py
@@ -28,14 +28,13 @@ def __init__(self, own: "Node", name: str):
         self.memories = []
 
     @abstractmethod
-    def set_others(self, remote_protocol: str, remote_node: str,
-                   memories: List[str]) -> None:
+    def set_others(self, remote_protocol: str, remote_node: str, memories: List[str]) -> None:
         """Method to set other entanglement protocol instance.
 
         Args:
-            protocol (str): other protocol name.
-            node (str): other node name.
-            memories (List[str]): the list of memories name used on other node.
+            remote_protocol (str): other protocol name.
+            remote_node (str): other node name.
+            memories (List[str]): the list of memory names used on other node.
         """
 
         pass
diff --git a/src/entanglement_management/generation.py b/src/entanglement_management/generation.py
index 921c4529..e19a6e09 100644
--- a/src/entanglement_management/generation.py
+++ b/src/entanglement_management/generation.py
@@ -7,6 +7,7 @@
 * EntanglementGenerationA should be used on the QuantumRouter (with one node set as the primary) and should be started via the "start" method
 * EntanglementGeneraitonB should be used on the BSMNode and does not need to be started
 """
+
 from __future__ import annotations
 from enum import Enum, auto
 from math import sqrt
@@ -14,8 +15,8 @@
 
 if TYPE_CHECKING:
     from ..components.memory import Memory
-    from ..topology.node import Node, BSMNode
     from ..components.bsm import SingleAtomBSM
+    from ..topology.node import Node, BSMNode
 
 from .entanglement_protocol import EntanglementProtocol
 from ..message import Message
@@ -23,7 +24,6 @@
 from ..kernel.process import Process
 from ..components.circuit import Circuit
 from ..utils import log
-from ..components.bsm import SingleAtomBSM
 
 
 class GenerationMsgType(Enum):
@@ -93,10 +93,8 @@ def __repr__(self):
 class EntanglementGenerationA(EntanglementProtocol):
     """Entanglement generation protocol for quantum router.
 
-    The EntanglementGenerationA protocol should be instantiated on a quantum
-    router node.
-    Instances will communicate with each other (and with the B instance on a
-    BSM node) to generate entanglement.
+    The EntanglementGenerationA protocol should be instantiated on a quantum router node.
+    Instances will communicate with each other (and with the B instance on a BSM node) to generate entanglement.
 
     Attributes:
         own (QuantumRouter): node that protocol instance is attached to.
@@ -106,13 +104,13 @@ class EntanglementGenerationA(EntanglementProtocol):
         memory (Memory): quantum memory object to attempt entanglement for.
     """
 
-    _plus_state = [sqrt(1 / 2), sqrt(1 / 2)]
+    _plus_state = [sqrt(1/2), sqrt(1/2)]
     _flip_circuit = Circuit(1)
     _flip_circuit.x(0)
     _z_circuit = Circuit(1)
     _z_circuit.z(0)
 
-    def __init__(self, own: Node, name: str, middle: str, other: str, memory: Memory):
+    def __init__(self, own: "Node", name: str, middle: str, other: str, memory: "Memory"):
         """Constructor for entanglement generation A class.
 
         Args:
@@ -140,8 +138,7 @@ def __init__(self, own: Node, name: str, middle: str, other: str, memory: Memory
 
         # memory internal info
         self.ent_round = 0  # keep track of current stage of protocol
-        self.bsm_res = [-1,
-                        -1]  # keep track of bsm measurements to distinguish Psi+ and Psi-
+        self.bsm_res = [-1, -1]  # keep track of bsm measurements to distinguish Psi+ and Psi-
 
         self.scheduled_events = []
 
@@ -151,14 +148,13 @@ def __init__(self, own: Node, name: str, middle: str, other: str, memory: Memory
 
         self._qstate_key: int = self.memory.qstate_key
 
-    def set_others(self, protocol: str, node: str,
-                   memories: List[str]) -> None:
+    def set_others(self, protocol: str, node: str, memories: List[str]) -> None:
         """Method to set other entanglement protocol instance.
 
         Args:
             protocol (str): other protocol name.
             node (str): other node name.
-            memories (List[str]): the list of memories name used on other node.
+            memories (List[str]): the list of memory names used on other node.
         """
         assert self.remote_protocol_name is None
         self.remote_protocol_name = protocol
@@ -220,13 +216,6 @@ def update_memory(self):
 
         elif self.ent_round == 3 and self.bsm_res[1] != -1:
             # successful entanglement
-            log.logger.info(
-                self.own.name + " successful entanglement of memory {}".format(
-                    self.memory))
-            self.memory.entangled_memory["node_id"] = self.remote_node_name
-            self.memory.entangled_memory["memo_id"] = self.remote_memo_id
-            self.memory.fidelity = self.memory.raw_fidelity
-
             # state correction
             if self.primary:
                 self.own.timeline.quantum_manager.run_circuit(
@@ -235,14 +224,11 @@ def update_memory(self):
                 self.own.timeline.quantum_manager.run_circuit(
                     EntanglementGenerationA._z_circuit, [self._qstate_key])
 
-            self.update_resource_manager(self.memory, "ENTANGLED")
+            self._entanglement_succeed()
 
         else:
             # entanglement failed
-            log.logger.info(
-                self.own.name + " failed entanglement of memory {}".format(
-                    self.memory))
-            self.update_resource_manager(self.memory, "RAW")
+            self._entanglement_fail()
             return False
 
         return True
@@ -250,7 +236,7 @@ def update_memory(self):
     def emit_event(self) -> None:
         """Method to set up memory and emit photons.
 
-        If the protocol is in round 1, the memory will be first set to the \|+> state.
+        If the protocol is in round 1, the memory will be first set to the |+> state.
         Otherwise, it will apply an x_gate to the memory.
         Regardless of the round, the memory `excite` method will be invoked.
 
@@ -258,6 +244,7 @@ def emit_event(self) -> None:
             May change state of attached memory.
             May cause attached memory to emit photon.
         """
+
         if self.ent_round == 1:
             self.memory.update_state(EntanglementGenerationA._plus_state)
         self.memory.excite(self.middle)
@@ -282,8 +269,8 @@ def received_message(self, src: str, msg: EntanglementGenerationMessage) -> None
         msg_type = msg.msg_type
 
         log.logger.debug("{} EG protocol received_message of type {} from node"
-                         " {}, round={}".format(self.own.name, msg.msg_type,
-                                                src, self.ent_round + 1))
+                         " {}, round={}".format(
+            self.own.name, msg.msg_type, src, self.ent_round + 1))
 
         if msg_type is GenerationMsgType.NEGOTIATE:
             # configure params
@@ -296,8 +283,7 @@ def received_message(self, src: str, msg: EntanglementGenerationMessage) -> None
             memory_excite_time = self.memory.next_excite_time
             min_time = max(self.own.timeline.now(), memory_excite_time) \
                        + total_quantum_delay - self.qc_delay + cc_delay
-            emit_time = self.own.schedule_qubit(self.middle,
-                                                min_time)  # used to send memory
+            emit_time = self.own.schedule_qubit(self.middle, min_time)  # used to send memory
             self.expected_time = emit_time + self.qc_delay
 
             # schedule emit
@@ -316,9 +302,7 @@ def received_message(self, src: str, msg: EntanglementGenerationMessage) -> None
 
             # schedule start if necessary, else schedule update_memory
             # TODO: base future start time on resolution
-            future_start_time = self.expected_time \
-                                + self.own.cchannels[self.middle].delay \
-                                + 10
+            future_start_time = self.expected_time + self.own.cchannels[self.middle].delay + 10
             if self.ent_round == 1:
                 process = Process(self, "start", [])
             else:
@@ -336,9 +320,8 @@ def received_message(self, src: str, msg: EntanglementGenerationMessage) -> None
 
             # schedule emit
             emit_time = self.own.schedule_qubit(self.middle, msg.emit_time)
-            assert emit_time == msg.emit_time, "%d %d %d" % (emit_time,
-                                                             msg.emit_time,
-                                                             self.own.timeline.now())
+            assert emit_time == msg.emit_time, \
+                "Invalid eg emit times %d %d %d" % (emit_time,  msg.emit_time, self.own.timeline.now())
 
             process = Process(self, "emit_event", [])
             event = Event(msg.emit_time, process)
@@ -347,9 +330,7 @@ def received_message(self, src: str, msg: EntanglementGenerationMessage) -> None
 
             # schedule start if memory_stage is 0, else schedule update_memory
             # TODO: base future start time on resolution
-            future_start_time = self.expected_time \
-                                + self.own.cchannels[self.middle].delay \
-                                + 10
+            future_start_time = self.expected_time + self.own.cchannels[self.middle].delay + 10
             if self.ent_round == 1:
                 process = Process(self, "start", [])
             else:
@@ -364,9 +345,8 @@ def received_message(self, src: str, msg: EntanglementGenerationMessage) -> None
             resolution = msg.resolution
 
             log.logger.debug("{} received MEAS_RES {} at time {}, expected {},"
-                             " round={}".format(self.own.name, detector, time,
-                                                self.expected_time,
-                                                self.ent_round))
+                             " round={}".format(
+                self.own.name, detector, time, self.expected_time, self.ent_round))
 
             def valid_trigger_time(trigger_time, target_time, resolution):
                 upper = target_time + resolution
@@ -408,6 +388,21 @@ def memory_expire(self, memory: "Memory") -> None:
             if event.time >= self.own.timeline.now():
                 self.own.timeline.remove_event(event)
 
+    def _entanglement_succeed(self):
+        log.logger.info(self.own.name + " successful entanglement of memory {}".format(self.memory))
+        self.memory.entangled_memory["node_id"] = self.remote_node_name
+        self.memory.entangled_memory["memo_id"] = self.remote_memo_id
+        self.memory.fidelity = self.memory.raw_fidelity
+
+        self.update_resource_manager(self.memory, 'ENTANGLED')
+
+    def _entanglement_fail(self):
+        for event in self.scheduled_events:
+            self.own.timeline.remove_event(event)
+        log.logger.info(self.own.name + " failed entanglement of memory {}".format(self.memory))
+        
+        self.update_resource_manager(self.memory, 'RAW')
+
 
 class EntanglementGenerationB(EntanglementProtocol):
     """Entanglement generation protocol for BSM node.
@@ -421,7 +416,7 @@ class EntanglementGenerationB(EntanglementProtocol):
         others (List[str]): list of neighboring quantum router nodes
     """
 
-    def __init__(self, own: BSMNode, name: str, others: List[str]):
+    def __init__(self, own: "BSMNode", name: str, others: List[str]):
         """Constructor for entanglement generation B protocol.
 
         Args:
@@ -434,7 +429,7 @@ def __init__(self, own: BSMNode, name: str, others: List[str]):
         assert len(others) == 2
         self.others = others  # end nodes
 
-    def bsm_update(self, bsm: SingleAtomBSM, info: Dict[str, Any]):
+    def bsm_update(self, bsm: "SingleAtomBSM", info: Dict[str, Any]):
         """Method to receive detection events from BSM on node.
 
         Args:
@@ -450,9 +445,7 @@ def bsm_update(self, bsm: SingleAtomBSM, info: Dict[str, Any]):
 
         for i, node in enumerate(self.others):
             message = EntanglementGenerationMessage(GenerationMsgType.MEAS_RES,
-                                                    None, detector=res,
-                                                    time=time,
-                                                    resolution=resolution)
+                                                    None, detector=res, time=time, resolution=resolution)
             self.own.send_message(node, message)
 
     def received_message(self, src: str, msg: EntanglementGenerationMessage):
@@ -462,13 +455,11 @@ def received_message(self, src: str, msg: EntanglementGenerationMessage):
     def start(self) -> None:
         pass
 
-    def set_others(self, protocol: str, node: str,
-                   memories: List[str]) -> None:
+    def set_others(self, protocol: str, node: str, memories: List[str]) -> None:
         pass
 
     def is_ready(self) -> bool:
         return True
 
     def memory_expire(self, memory: "Memory") -> None:
-        raise Exception("EntanglementGenerationB protocol '{}' should not have"
-                        " memory_expire".format(self.name))
+        raise Exception("Memory expire called for EntanglementGenerationB protocol '{}'".format(self.name))
diff --git a/src/entanglement_management/purification.py b/src/entanglement_management/purification.py
index fb1fad2f..2d7b6731 100644
--- a/src/entanglement_management/purification.py
+++ b/src/entanglement_management/purification.py
@@ -9,8 +9,6 @@
 from typing import List, TYPE_CHECKING
 from functools import lru_cache
 
-from numpy.random import random
-
 if TYPE_CHECKING:
     from ..components.memory import Memory
     from ..topology.node import Node
@@ -94,14 +92,13 @@ def __init__(self, own: "Node", name: str, kept_memo: "Memory", meas_memo: "Memo
     def is_ready(self) -> bool:
         return self.remote_node_name is not None
 
-    def set_others(self, protocol: str, node: str,
-                   memories: List[str]) -> None:
+    def set_others(self, protocol: str, node: str, memories: List[str]) -> None:
         """Method to set other entanglement protocol instance.
 
         Args:
             protocol (str): other protocol name.
             node (str): other node name.
-            memories (List[str]): the list of memories name used on other node.
+            memories (List[str]): the list of memory names used on other node.
         """
         self.remote_node_name = node
         self.remote_protocol_name = protocol
@@ -137,10 +134,9 @@ def start(self) -> None:
             Will send message to other protocol instance.
         """
 
-        log.logger.info(f"{self.own.name} protocol start with partner "
-                        f"{self.remote_node_name}")
+        log.logger.info(f"{self.own.name} protocol start with partner {self.remote_node_name}")
 
-        assert self.remote_protocol_name is not None, "other protocol is not set; please use set_others function to set it."
+        assert self.is_ready(), "other protocol is not set; please use set_others function to set it."
         kept_memo_ent = self.kept_memo.entangled_memory["node_id"]
         meas_memo_ent = self.meas_memo.entangled_memory["node_id"]
         assert kept_memo_ent == meas_memo_ent, "mismatch of entangled memories {}, {} on node {}".format(
@@ -178,8 +174,7 @@ def received_message(self, src: str, msg: BBPSSWMessage) -> None:
 
         self.update_resource_manager(self.meas_memo, "RAW")
         if self.meas_res == msg.meas_res:
-            self.kept_memo.fidelity = self.improved_fidelity(
-                self.kept_memo.fidelity)
+            self.kept_memo.fidelity = self.improved_fidelity(self.kept_memo.fidelity)
             self.update_resource_manager(self.kept_memo, state="ENTANGLED")
         else:
             self.update_resource_manager(self.kept_memo, state="RAW")
@@ -229,4 +224,3 @@ def improved_fidelity(F: float) -> float:
         """
 
         return (F ** 2 + ((1 - F) / 3) ** 2) / (F ** 2 + 2 * F * (1 - F) / 3 + 5 * ((1 - F) / 3) ** 2)
-
diff --git a/src/entanglement_management/swapping.py b/src/entanglement_management/swapping.py
index d1a486f0..63aa0ec1 100644
--- a/src/entanglement_management/swapping.py
+++ b/src/entanglement_management/swapping.py
@@ -15,8 +15,6 @@
 from typing import TYPE_CHECKING, List
 from functools import lru_cache
 
-from numpy.random import random
-
 if TYPE_CHECKING:
     from ..components.memory import Memory
     from ..topology.node import Node
@@ -60,25 +58,22 @@ def __init__(self, msg_type: SwappingMsgType, receiver: str, **kwargs):
 
     def __str__(self):
         if self.msg_type == SwappingMsgType.SWAP_RES:
-            return "EntanglementSwappingMessage: msg_type: %s; " \
-                   "fidelity: %.2f; remote_node: %s; remote_memo: %s; " \
-                   % (self.msg_type, self.fidelity,
-                      self.remote_node, self.remote_memo)
+            return "EntanglementSwappingMessage: msg_type: %s; fidelity: %.2f; remote_node: %s; remote_memo: %s; " % (
+                self.msg_type, self.fidelity, self.remote_node, self.remote_memo)
 
 
 class EntanglementSwappingA(EntanglementProtocol):
     """Entanglement swapping protocol for middle router.
 
     The entanglement swapping protocol is an asymmetric protocol.
-    EntanglementSwappingA should be instantiated on the middle node,
-    where it measures a memory from each pair to be swapped.
+    EntanglementSwappingA should be instantiated on the middle node, where it measures a memory from each pair to be swapped.
     Results of measurement and swapping are sent to the end routers.
 
     Variables:
         EntanglementSwappingA.circuit (Circuit): circuit that does swapping operations.
 
     Attributes:
-        own (QuantumRouter): node that protocol instance is attached to.
+        own (Node): node that protocol instance is attached to.
         name (str): label for protocol instance.
         left_memo (Memory): a memory from one pair to be swapped.
         right_memo (Memory): a memory from the other pair to be swapped.
@@ -87,7 +82,7 @@ class EntanglementSwappingA(EntanglementProtocol):
         right_node (str): name of node that contains memory entangling with right_memo.
         right_remote_memo (str): name of memory that entangles with right_memo.
         success_prob (float): probability of a successful swapping operation.
-        degradation (float): degradation factor of memory fidelity after the swapping operation.
+        degradation (float): degradation factor of memory fidelity after swapping.
         is_success (bool): flag to show the result of swapping
         left_protocol_name (str): name of left protocol.
         right_protocol_name (str): name of right protocol.
@@ -104,7 +99,7 @@ def __init__(self, own: "Node", name: str, left_memo: "Memory", right_memo: "Mem
         """Constructor for entanglement swapping A protocol.
 
         Args:
-            own (Node): node that protocol is attached to.
+            own (Node): node that protocol instance is attached to.
             name (str): label for swapping protocol instance.
             left_memo (Memory): memory entangled with a memory on one distant node.
             right_memo (Memory): memory entangled with a memory on the other distant node.
@@ -131,8 +126,7 @@ def is_ready(self) -> bool:
         return self.left_protocol_name is not None \
                and self.right_protocol_name is not None
 
-    def set_others(self, protocol: str, node: str,
-                   memories: List[str]) -> None:
+    def set_others(self, protocol: str, node: str, memories: List[str]) -> None:
         """Method to set other entanglement protocol instance.
 
         Args:
@@ -146,8 +140,7 @@ def set_others(self, protocol: str, node: str,
         elif node == self.right_memo.entangled_memory["node_id"]:
             self.right_protocol_name = protocol
         else:
-            raise Exception("Cannot pair protocol %s with %s" % (self.name,
-                                                                 protocol))
+            raise Exception("Cannot pair protocol %s with %s" % (self.name, protocol))
 
     def start(self) -> None:
         """Method to start entanglement swapping protocol.
@@ -167,13 +160,11 @@ def start(self) -> None:
         assert self.left_memo.entangled_memory["node_id"] == self.left_node
         assert self.right_memo.entangled_memory["node_id"] == self.right_node
 
-        if random() < self.success_probability():
-            fidelity = self.updated_fidelity(self.left_memo.fidelity,
-                                             self.right_memo.fidelity)
+        if self.own.get_generator().random() < self.success_probability():
+            fidelity = self.updated_fidelity(self.left_memo.fidelity, self.right_memo.fidelity)
             self.is_success = True
 
-            expire_time = min(self.left_memo.get_expire_time(),
-                              self.right_memo.get_expire_time())
+            expire_time = min(self.left_memo.get_expire_time(), self.right_memo.get_expire_time())
 
             meas_samp = self.own.get_generator().random()
             meas_res = self.own.timeline.quantum_manager.run_circuit(
@@ -184,23 +175,15 @@ def start(self) -> None:
             msg_l = EntanglementSwappingMessage(SwappingMsgType.SWAP_RES,
                                                 self.left_protocol_name,
                                                 fidelity=fidelity,
-                                                remote_node=
-                                                self.right_memo.entangled_memory[
-                                                    "node_id"],
-                                                remote_memo=
-                                                self.right_memo.entangled_memory[
-                                                    "memo_id"],
+                                                remote_node=self.right_memo.entangled_memory["node_id"],
+                                                remote_memo=self.right_memo.entangled_memory["memo_id"],
                                                 expire_time=expire_time,
                                                 meas_res=[])
             msg_r = EntanglementSwappingMessage(SwappingMsgType.SWAP_RES,
                                                 self.right_protocol_name,
                                                 fidelity=fidelity,
-                                                remote_node=
-                                                self.left_memo.entangled_memory[
-                                                    "node_id"],
-                                                remote_memo=
-                                                self.left_memo.entangled_memory[
-                                                    "memo_id"],
+                                                remote_node=self.left_memo.entangled_memory["node_id"],
+                                                remote_memo=self.left_memo.entangled_memory["memo_id"],
                                                 expire_time=expire_time,
                                                 meas_res=meas_res)
         else:
@@ -275,8 +258,7 @@ def release_remote_protocol(self, remote_node: str):
         self.own.resource_manager.release_remote_protocol(remote_node, self)
 
     def release_remote_memory(self, remote_node: str, remote_memo: str):
-        self.own.resource_manager.release_remote_memory(remote_node,
-                                                        remote_memo)
+        self.own.resource_manager.release_remote_memory(remote_node, remote_memo)
 
 
 class EntanglementSwappingB(EntanglementProtocol):
@@ -286,14 +268,16 @@ class EntanglementSwappingB(EntanglementProtocol):
     EntanglementSwappingB should be instantiated on the end nodes, where it waits for swapping results from the middle node.
 
     Variables:
-            EntanglementSwappingB.x_cir (Circuit): circuit that corrects state with an x gate.
-            EntanglementSwappingB.z_cir (Circuit): circuit that corrects state with z gate.
-            EntanglementSwappingB.x_z_cir (Circuit): circuit that corrects state with an x and z gate.
+        EntanglementSwappingB.x_cir (Circuit): circuit that corrects state with an x gate.
+        EntanglementSwappingB.z_cir (Circuit): circuit that corrects state with z gate.
+        EntanglementSwappingB.x_z_cir (Circuit): circuit that corrects state with an x and z gate.
 
     Attributes:
         own (QuantumRouter): node that protocol instance is attached to.
-        name (str): label for protocol instance.
-        hold_memory (Memory): quantum memory to be swapped.
+        name (str): name of protocol instance.
+        memory (Memory): memory to swap.
+        remote_protocol_name (str): name of another protocol to communicate with for swapping.
+        remote_node_name (str): name of node hosting the other protocol.
     """
 
     x_cir = Circuit(1)
@@ -310,7 +294,7 @@ def __init__(self, own: "Node", name: str, hold_memo: "Memory"):
         """Constructor for entanglement swapping B protocol.
 
         Args:
-            own (Node): node protocol is attached to.
+            own (Node): node protocol instance is attached to.
             name (str): name of protocol instance.
             hold_memo (Memory): memory entangled with a memory on middle node.
         """
@@ -325,20 +309,18 @@ def __init__(self, own: "Node", name: str, hold_memo: "Memory"):
     def is_ready(self) -> bool:
         return self.remote_protocol_name is not None
 
-    def set_others(self, protocol: str, node: str,
-                   memories: List[str]) -> None:
+    def set_others(self, protocol: str, node: str, memories: List[str]) -> None:
         """Method to set other entanglement protocol instance.
 
         Args:
             protocol (str): other protocol name.
             node (str): other node name.
-            memories (List[str]): the list of memories name used on other node.
+            memories (List[str]): the list of memory names used on other node.
         """
         self.remote_node_name = node
         self.remote_protocol_name = protocol
 
-    def received_message(self, src: str,
-                         msg: "EntanglementSwappingMessage") -> None:
+    def received_message(self, src: str, msg: "EntanglementSwappingMessage") -> None:
         """Method to receive messages from EntanglementSwappingA.
 
         Args:
@@ -371,8 +353,7 @@ def received_message(self, src: str,
             self.update_resource_manager(self.memory, "RAW")
 
     def start(self) -> None:
-        log.logger.info(f"{self.own.name} end protocol start with partner "
-                        f"{self.remote_node_name}")
+        log.logger.info(f"{self.own.name} end protocol start with partner {self.remote_node_name}")
 
     def memory_expire(self, memory: "Memory") -> None:
         """Method to deal with expired memories.
diff --git a/src/gui/app.py b/src/gui/app.py
index 147b083e..2ce7a299 100644
--- a/src/gui/app.py
+++ b/src/gui/app.py
@@ -308,8 +308,8 @@ def graph_to_topology(self):
                 {
                     Topology.CONNECT_NODE_1: edge[2]['data']['source'],
                     Topology.CONNECT_NODE_2: edge[2]['data']['target'],
-                    Topology.ATTENUATION: edge[2]['data']['attenuation'],
-                    Topology.DISTANCE: edge[2]['data']['distance']
+                    Topology.ATTENUATION: float(edge[2]['data']['attenuation']),
+                    Topology.DISTANCE: int(edge[2]['data']['distance'])
                 }
             )
 
@@ -325,7 +325,7 @@ def graph_to_topology(self):
                     {
                         Topology.SRC: src,
                         Topology.DST: dst,
-                        Topology.DELAY: delay
+                        Topology.DELAY: int(delay)
                     }
                 )
 
@@ -435,11 +435,14 @@ def parse_edge(self, from_node, to_node, type_in, children):
         output = EDGE_DICT_ORDER.copy()
 
         for x in children:
-            if x['type'] == 'Input':
-                out = x['props']['value']
-                output['_'.join(x['props']['id'].split('_')[:-1])] = out
-        output['source'] = from_node[6:]
-        output['target'] = to_node[4:]
+            more_children = x['props']['children']
+            for y in more_children:
+                if y['props']['children']['type'] == 'Input':
+                    out = y['props']['children']['props']['value']
+                    output['_'.join(y['props']['children']['props']['id'].split('_')[:-1])] = out
+
+        output['source'] = from_node
+        output['target'] = to_node
         output['link_type'] = type_in
         return output
 
diff --git a/src/kernel/entity.py b/src/kernel/entity.py
index d5a8434e..5d1f64e2 100644
--- a/src/kernel/entity.py
+++ b/src/kernel/entity.py
@@ -10,20 +10,21 @@
 
 if TYPE_CHECKING:
     from .timeline import Timeline
+    from ..components.photon import Photon
 
 
 class Entity(ABC):
     """Abstract Entity class.
     Entity should use the provided pseudo random number generator (PRNG) to
     produce reproducible random numbers. As a result, simulations with the same
-    seed could reproduce identical results. Function "get_generator" allows to
-    get the PRNG.
+    seed can reproduce identical results. Function "get_generator" returns the PRNG.
 
     Attributes:
         name (str): name of the entity.
         timeline (Timeline): the simulation timeline for the entity.
         owner (Entity): another entity that owns or aggregates the current entity.
-        _observer (List): a list of observers for the entity.
+        _observers (List): a list of observers for the entity.
+        _receivers (List[Entity]): a list of entities that receive photons from current component.
     """
 
     def __init__(self, name: str, timeline: "Timeline") -> None:
@@ -36,7 +37,10 @@ def __init__(self, name: str, timeline: "Timeline") -> None:
         self.name = "" if name is None else name
         self.timeline = timeline
         self.owner = None
+
+        self._receivers = []
         self._observers = []
+
         timeline.add_entity(self)
 
     @abstractmethod
@@ -49,10 +53,13 @@ def init(self) -> None:
 
         pass
 
+    def add_receiver(self, receiver: "Entity") -> None:
+        self._receivers.append(receiver)
+
     def attach(self, observer: Any) -> None:
-        """Method to add an ovserver (to receive hardware updates)."""
+        """Method to add an observer (to receive hardware updates)."""
 
-        if not observer in self._observers:
+        if observer not in self._observers:
             self._observers.append(observer)
 
     def detach(self, observer: Any) -> None:
@@ -66,6 +73,19 @@ def notify(self, info: Dict[str, Any]) -> None:
         for observer in self._observers:
             observer.update(self, info)
 
+    def get(self, photon: "Photon", **kwargs):
+        """Method for an entity to receive a photon.
+
+        If entity is a node, may forward to external quantum channel.
+        Must be overwritten to be used, or will raise exception.
+
+        Args:
+            photon (Photon): photon received by the entity.
+            **kwargs: other arguments required by a particular hardware component.
+        """
+
+        raise Exception("get method called on non-receiving class.")
+
     def remove_from_timeline(self) -> None:
         """Method to remove entity from attached timeline.
 
@@ -78,7 +98,7 @@ def get_generator(self) -> Generator:
 
         If entity is not attached to a node, return default generator.
         """
-        if hasattr(self.owner, "generator"):
+        if hasattr(self.owner, "get_generator"):
             return self.owner.get_generator()
         else:
             return default_rng()
diff --git a/src/kernel/process.py b/src/kernel/process.py
index 0e95c26a..57874683 100644
--- a/src/kernel/process.py
+++ b/src/kernel/process.py
@@ -16,10 +16,11 @@ class Process:
         act_params (List[Any]): the arguments of object.
     """
 
-    def __init__(self, owner: Any, activation_method: str, act_params: List[Any]):
+    def __init__(self, owner: Any, activation_method: str, act_params: List[Any], act_kwargs={}):
         self.owner = owner
         self.activation = activation_method
         self.act_params = act_params
+        self.act_kwargs = act_kwargs
 
     def run(self) -> None:
         """Method to execute process.
@@ -27,4 +28,4 @@ def run(self) -> None:
         Will run the `activation_method` method of `owner` with `act_params` passed as args.
         """
 
-        return getattr(self.owner, self.activation)(*self.act_params)
+        return getattr(self.owner, self.activation)(*self.act_params, **self.act_kwargs)
diff --git a/src/kernel/quantum_manager.py b/src/kernel/quantum_manager.py
index 0013e569..c4d441d6 100644
--- a/src/kernel/quantum_manager.py
+++ b/src/kernel/quantum_manager.py
@@ -11,22 +11,22 @@
 from abc import abstractmethod
 from typing import List, Dict, TYPE_CHECKING
 
+if TYPE_CHECKING:
+    from ..components.circuit import Circuit
+    from .quantum_state import State
+
 from qutip.qip.circuit import QubitCircuit, Gate
 from qutip.qip.operations import gate_sequence_product
-from numpy import log2, outer
+from numpy import log2
 
+from .quantum_state import KetState, DensityState
 from .quantum_utils import *
-from ..components.circuit import Circuit
-from ..utils.quantum_state import QuantumState
-
-if TYPE_CHECKING:
-    from ..components.circuit import Circuit
 
 KET_STATE_FORMALISM = "ket_vector"
 DENSITY_MATRIX_FORMALISM = "density_matrix"
 
 
-class QuantumManager():
+class QuantumManager:
     """Class to track and manage quantum states (abstract).
 
     All states stored are of a single formalism (by default as a ket vector).
@@ -53,14 +53,14 @@ def new(self, state: any) -> int:
         """
         pass
 
-    def get(self, key: int) -> "QuantumState":
+    def get(self, key: int) -> "State":
         """Method to get quantum state stored at an index.
 
         Args:
             key (int): key for quantum state.
 
         Returns:
-            QuantumState: quantum state at supplied key.
+            State: quantum state at supplied key.
         """
         return self.states[key]
 
@@ -134,8 +134,7 @@ def set(self, keys: List[int], amplitudes: any) -> None:
         num_qubits = log2(len(amplitudes))
         assert num_qubits.is_integer(), "Length of amplitudes should be 2 ** n, where n is the number of keys"
         num_qubits = int(num_qubits)
-        assert num_qubits == len(
-            keys), "Length of amplitudes should be 2 ** n, where n is the number of keys"
+        assert num_qubits == len(keys), "Length of amplitudes should be 2 ** n, where n is the number of keys"
 
     def remove(self, key: int) -> None:
         """Method to remove state stored at key."""
@@ -282,7 +281,7 @@ def run_circuit(self, circuit: Circuit, keys: List[int], meas_samp=None) -> Dict
         super().run_circuit(circuit, keys, meas_samp)
         new_state, all_keys, circ_mat = super()._prepare_circuit(circuit, keys)
 
-        new_state = circ_mat @ new_state @ circ_mat.T
+        new_state = circ_mat @ new_state @ circ_mat.conj().T
 
         if len(circuit.measured_qubits) == 0:
             # set state, return no measurement result
@@ -296,6 +295,17 @@ def run_circuit(self, circuit: Circuit, keys: List[int], meas_samp=None) -> Dict
             return self._measure(new_state, keys, all_keys, meas_samp)
 
     def set(self, keys: List[int], state: List[List[complex]]) -> None:
+        """Method to set the quantum state at the given keys.
+
+        The `state` argument should be passed as List[List[complex]], where each internal list is a row.
+        However, the `state` may also be given as a one-dimensional pure state.
+        If the list is one-dimensional, will be converted to matrix with the outer product operation.
+
+        Args:
+            keys (List[int]): list of quantum manager keys to modify.
+            state: quantum state to set input keys to.
+        """
+
         super().set(keys, state)
         new_state = DensityState(state, keys)
         for key in keys:
@@ -337,8 +347,8 @@ def _measure(self, state: List[List[complex]], keys: List[int],
                 key = keys[0]
                 num_states = len(all_keys)
                 state_index = all_keys.index(key)
-                state_0, state_1, prob_0 = measure_entangled_state_with_cache_density(tuple(map(tuple, state)),
-                        state_index, num_states)
+                state_0, state_1, prob_0 =\
+                    measure_entangled_state_with_cache_density(tuple(map(tuple, state)), state_index, num_states)
                 if meas_samp < prob_0:
                     new_state = array(state_0, dtype=complex)
                     result = 0
@@ -375,96 +385,3 @@ def _measure(self, state: List[List[complex]], keys: List[int],
             self.states[key] = new_state_obj
 
         return dict(zip(keys, result_digits))
-
-
-class State():
-    """Class to represent state of qubits.
-
-    Attributes:
-        state (any): internal representation of qubit state. Varies based on formalism.
-        keys (list[int]): associated keys for a quantum manager.
-    """
-
-    def __init__(self, state, keys):
-        self.state = state
-        self.keys = keys
-
-    def __str__(self):
-        return "\n".join(["Keys:", str(self.keys), "State:", str(self.state)])
-
-    def deserialize(self, json_data) -> None:
-        self.keys = json_data["keys"]
-        self.state = []
-        for i in range(0, len(json_data["state"]), 2):
-            complex_val = complex(json_data["state"][i],
-                                  json_data["state"][i + 1])
-            self.state.append(complex_val)
-
-    def serialize(self) -> Dict:
-        res = {"keys": self.keys}
-        state = []
-        for cplx_n in self.state:
-            if type(cplx_n) == float:
-                state.append(cplx_n)
-                state.append(0)
-            elif isinstance(cplx_n, complex):
-                state.append(cplx_n.real)
-                state.append(cplx_n.imag)
-            else:
-                raise ValueError("Unknown type of state")
-
-        res["state"] = state
-        return res
-
-
-class KetState(State):
-    """Class inheriting State class to represent an individual quantum state
-    as a ket vector.
-
-    Attributes:
-        state (np.array): state vector. Should be of length 2 ** len(keys).
-        keys (List[int]): list of keys (qubits) associated with this state.
-    """
-
-    def __init__(self, amplitudes: List[complex], keys: List[int]):
-        # check formatting
-        assert all([abs(a) <= 1.01 for a in
-                    amplitudes]), "Illegal value with abs > 1 in ket vector"
-        assert abs(sum([a ** 2 for a in
-                        amplitudes]) - 1) < 1e-5, "Squared amplitudes do not sum to 1"
-        num_qubits = log2(len(amplitudes))
-        assert num_qubits.is_integer(), "Length of amplitudes should be 2 ** n, where n is the number of qubits"
-        assert num_qubits == len(keys), "Length of amplitudes should be 2 ** n, where n is the number of qubits"
-        super().__init__(array(amplitudes, dtype=complex), keys)
-
-
-class DensityState(State):
-    """Class inheriting State class to represent an individual quantum state
-    as a density matrix.
-
-    Attributes:
-        state (np.array): density matrix values. NxN matrix with N = 2 ** len(keys).
-        keys (List[int]): list of keys (qubits) associated with this state.
-    """
-
-    def __init__(self, state: List[List[complex]], keys: List[int]):
-        """Constructor for density state class.
-
-        Args:
-            state (List[List[complex]]): density matrix elements given as a list.
-                If the list is one-dimensional, will be converted to matrix with the outer product operation.
-            keys (List[int]): list of keys to this state in quantum manager.
-        """
-
-        state = array(state, dtype=complex)
-        if state.ndim == 1:
-            state = outer(state.conj(), state)
-
-        # check formatting
-        assert abs(trace(array(state)) - 1) < 0.1, "density matrix trace must be 1"
-        for row in state:
-            assert len(state) == len(row), "density matrix must be square"
-        num_qubits = log2(len(state))
-        assert num_qubits.is_integer(), "Dimensions of density matrix should be 2 ** n, where n is the number of qubits"
-        assert num_qubits == len(keys), "Dimensions of density matrix should be 2 ** n, where n is the number of qubits"
-        super().__init__(state, keys)
diff --git a/src/utils/quantum_state.py b/src/kernel/quantum_state.py
similarity index 54%
rename from src/utils/quantum_state.py
rename to src/kernel/quantum_state.py
index 782c4c4e..5ca460da 100644
--- a/src/utils/quantum_state.py
+++ b/src/kernel/quantum_state.py
@@ -1,15 +1,21 @@
-"""Definition of the quantum state class.
+"""Definition of the quantum state classes.
 
-This module defines the QuantumState class, used by photons and memories to track internal quantum states.
-The class provides interfaces for measurement and entanglement.
+This module defines the classes used to track quantum states in SeQUeNCe.
+These include 2 classes used by a quantum manager, and one used for individual photons:
+
+1. The `KetState` class represents the ket vector formalism and is used by a quantum manager.
+2. The `DensityState` class represents the density matrix formalism and is also used by a quantum manager.
+3. The `FreeQuantumState` class uses the ket vector formalism, and is used by individual photons (not the quantum manager).
 """
-from functools import lru_cache
-from math import sqrt
-from typing import Tuple
 
-from numpy import pi, cos, sin, array, outer, kron, identity, arange
+from abc import ABC
+from typing import Tuple, Dict, List
+
+from numpy import pi, cos, sin, arange, log2
 from numpy.random import Generator
 
+from .quantum_utils import *
+
 
 def swap_bits(num, pos1, pos2):
     """Swaps bits in num at positions 1 and 2.
@@ -24,23 +30,126 @@ def swap_bits(num, pos1, pos2):
     return num ^ x
 
 
-class QuantumState():
-    """Class to manage a quantum state.
+class State(ABC):
+    """Base class for storing quantum states (abstract).
+
+    Attributes:
+        state (any): internal representation of the state, may vary by state type.
+        keys (List[int]): list of keys pointing to the state, for use with a quantum manager.
+    """
+
+    def __init__(self):
+        super().__init__()
+
+        self.state = None
+        self.keys = []
+
+    def deserialize(self, json_data) -> None:
+        self.keys = json_data["keys"]
+        self.state = []
+        for i in range(0, len(json_data["state"]), 2):
+            complex_val = complex(json_data["state"][i],
+                                  json_data["state"][i + 1])
+            self.state.append(complex_val)
+
+    def serialize(self) -> Dict:
+        res = {"keys": self.keys}
+        state = []
+        for cplx_n in self.state:
+            if type(cplx_n) == float:
+                state.append(cplx_n)
+                state.append(0)
+            elif isinstance(cplx_n, complex):
+                state.append(cplx_n.real)
+                state.append(cplx_n.imag)
+            else:
+                raise ValueError("Unknown type of state")
+
+        res["state"] = state
+        return res
+
+    def __str__(self):
+        return "\n".join(["Keys:", str(self.keys), "State:", str(self.state)])
+
+
+class KetState(State):
+    """Class to represent an individual quantum state as a ket vector.
+
+    Attributes:
+        state (np.array): state vector. Should be of length 2 ** len(keys).
+        keys (List[int]): list of keys (qubits) associated with this state.
+    """
+
+    def __init__(self, amplitudes: List[complex], keys: List[int]):
+        super().__init__()
+
+        # check formatting
+        assert all([abs(a) <= 1.01 for a in amplitudes]), "Illegal value with abs > 1 in ket vector"
+        assert abs(sum([a ** 2 for a in amplitudes]) - 1) < 1e-5, "Squared amplitudes do not sum to 1"
+        num_qubits = log2(len(amplitudes))
+        assert num_qubits.is_integer(), "Length of amplitudes should be 2 ** n, where n is the number of qubits"
+        assert num_qubits == len(keys), "Length of amplitudes should be 2 ** n, where n is the number of qubits"
+
+        self.state = array(amplitudes, dtype=complex)
+        self.keys = keys
+
 
-    Tracks quantum state coefficients (in Z-basis) and entangled states.
-    Functions related to the randomness uses the provided pseudo random number
-    generator (PRNG) to generate random numbers for a reproducible simulation.
+class DensityState(State):
+    """Class to represent an individual quantum state as a density matrix.
+
+    Attributes:
+        state (np.array): density matrix values. NxN matrix with N = 2 ** len(keys).
+        keys (List[int]): list of keys (qubits) associated with this state.
+    """
+
+    def __init__(self, state: List[List[complex]], keys: List[int]):
+        """Constructor for density state class.
+
+        Args:
+            state (List[List[complex]]): density matrix elements given as a list.
+                If the list is one-dimensional, will be converted to matrix with outer product operation.
+            keys (List[int]): list of keys to this state in quantum manager.
+        """
+
+        super().__init__()
+
+        state = array(state, dtype=complex)
+        if state.ndim == 1:
+            state = outer(state.conj(), state)
+
+        # check formatting
+        assert abs(trace(array(state)) - 1) < 0.1, "density matrix trace must be 1"
+        for row in state:
+            assert len(state) == len(row), "density matrix must be square"
+        num_qubits = log2(len(state))
+        assert num_qubits.is_integer(), "Dimensions of density matrix should be 2 ** n, where n is the number of qubits"
+        assert num_qubits == len(keys), "Dimensions of density matrix should be 2 ** n, where n is the number of qubits"
+
+        self.state = state
+        self.keys = keys
+
+
+class FreeQuantumState(State):
+    """Class used by photons to track internal quantum states.
+
+    This is an alternative to tracking states in a dedicated quantum manager, which adds simulation overhead.
+    It defines several operations, including entanglement and measurement.
+    For memories with an internal quantum state and certain photons, such as those stored in a memory or in parallel
+    simulation, this class should not be used.
+    Quantum states stored in a quantum manager class should be used instead.
+    This module uses the ket vector formalism for storing and manipulating states.
 
     Attributes:
         state (Tuple[complex]): list of complex coefficients in Z-basis.
-        entangled_states (List[QuantumState]): list of entangled states (indludng self).
+        entangled_states (List[QuantumState]): list of entangled states (including self).
     """
 
     def __init__(self):
+        super().__init__()
         self.state = (complex(1), complex(0))
         self.entangled_states = [self]
 
-    def entangle(self, another_state: "QuantumState"):
+    def entangle(self, another_state: "FreeQuantumState"):
         """Method to entangle two quantum states.
 
         Arguments:
@@ -122,7 +231,7 @@ def measure(self, basis: Tuple[Tuple[complex]], rng: Generator) -> int:
         if len(self.entangled_states) > 1:
             num_states = len(self.entangled_states)
             state_index = self.entangled_states.index(self)
-            state0, state1, prob = _measure_entangled_state_with_cache(self.state, basis, state_index, num_states)
+            state0, state1, prob = measure_entangled_state_with_cache(self.state, basis, state_index, num_states)
             if rng.random() < prob:
                 new_state = state0
                 result = 0
@@ -133,7 +242,7 @@ def measure(self, basis: Tuple[Tuple[complex]], rng: Generator) -> int:
 
         # handle unentangled case
         else:
-            prob = _measure_state_with_cache(self.state, basis)
+            prob = measure_state_with_cache(self.state, basis)
             if rng.random() < prob:
                 new_state = basis[0]
                 result = 0
@@ -157,7 +266,7 @@ def measure_multiple(basis, states, rng: Generator):
 
         Args:
             basis (List[List[complex]]): list of basis vectors.
-            states (List[QuantumState]): list of quantum state objects to meausre.
+            states (List[QuantumState]): list of quantum state objects to measure.
 
         Returns:
             int: measurement result in given basis.
@@ -194,7 +303,7 @@ def measure_multiple(basis, states, rng: Generator):
         # math for probability calculations
         length_diff = len(entangled_list) - len(states)
 
-        new_states, probabilities = _measure_multiple_with_cache(state, basis, length_diff)
+        new_states, probabilities = measure_multiple_with_cache(state, basis, length_diff)
 
         possible_results = arange(0, basis_dimension, 1)
         # result gives index of the basis vector that will be projected to
@@ -206,79 +315,3 @@ def measure_multiple(basis, states, rng: Generator):
             state.entangled_photons = entangled_list
 
         return res
-
-
-@lru_cache(maxsize=1000)
-def _measure_state_with_cache(state: Tuple[complex, complex], basis: Tuple[Tuple[complex]]) -> float:
-    state = array(state)
-    u = array(basis[0], dtype=complex)
-    v = array(basis[1], dtype=complex)
-    # measurement operator
-    M0 = outer(u.conj(), u)
-    M1 = outer(v.conj(), v)
-
-    # probability of measuring basis[0]
-    prob_0 = (state.conj().transpose() @ M0.conj().transpose() @ M0 @ state).real
-    return prob_0
-
-@lru_cache(maxsize=1000)
-def _measure_entangled_state_with_cache(state: Tuple[complex], basis:Tuple[Tuple[complex]],
-                                        state_index: int, num_states: int) -> Tuple[
-        Tuple[complex], Tuple[complex], float]:
-    state = array(state)
-    u = array(basis[0], dtype=complex)
-    v = array(basis[1], dtype=complex)
-    # measurement operator
-    M0 = outer(u.conj(), u)
-    M1 = outer(v.conj(), v)
-
-    # generate projectors
-    projector0 = [1]
-    projector1 = [1]
-    for i in range(num_states):
-        if i == state_index:
-            projector0 = kron(projector0, M0)
-            projector1 = kron(projector1, M1)
-        else:
-            projector0 = kron(projector0, identity(2))
-            projector1 = kron(projector1, identity(2))
-
-    # probability of measuring basis[0]
-    prob_0 = (state.conj().transpose() @ projector0.conj().transpose() @ projector0 @ state).real
-
-    if prob_0 >= 1:
-        state1 = None
-    else:
-        state1 = (projector1 @ state) / sqrt(1 - prob_0)
-
-    if prob_0 <= 0:
-        state0 = None
-    else:
-        state0 = (projector0 @ state) / sqrt(prob_0)
-
-    return (state0, state1, prob_0)
-
-@lru_cache(maxsize=1000)
-def _measure_multiple_with_cache(state: Tuple[Tuple[complex]], basis: Tuple[Tuple[complex]], length_diff: int) -> Tuple[
-        Tuple[Tuple[complex]], Tuple[float]]:
-    state = array(state)
-    # construct measurement operators, projectors, and probabilities of measurement
-    projectors = [None] * len(basis)
-    probabilities = [0] * len(basis)
-    for i, vector in enumerate(basis):
-        vector = array(vector, dtype=complex)
-        M = outer(vector.conj(), vector)  # measurement operator
-        projectors[i] = kron(M, identity(2 ** length_diff))  # projector
-        probabilities[i] = (state.conj().transpose() @ projectors[i].conj().transpose() @ projectors[i] @ state).real
-        if probabilities[i] < 0:
-            probabilities[i] = 0
-
-    return_states = [None] * len(projectors)
-    for i, proj in enumerate(projectors):
-        # project to new state
-        if probabilities[i] > 0:
-            new_state = (proj @ state) / sqrt(probabilities[i])
-            new_state = tuple(new_state)
-            return_states[i] = new_state
-
-    return (tuple(return_states), tuple(probabilities))
diff --git a/src/kernel/quantum_utils.py b/src/kernel/quantum_utils.py
index 87d56952..197b0a9b 100644
--- a/src/kernel/quantum_utils.py
+++ b/src/kernel/quantum_utils.py
@@ -1,13 +1,97 @@
-"""This module defines functions to handle cached measurement of quantum states.
+"""This module defines functions and objects to manipulate quantum states.
 
-These should not be used directly, but accessed by a QuantumManager instance.
+This includes cached measurement of quantum states, and certain useful operators.
+These should not be used directly, but accessed by a QuantumManager instance or by a quantum state.
 """
 
 from functools import lru_cache
 from typing import Tuple
 from math import sqrt
 
-from numpy import array, kron, identity, zeros, trace 
+from numpy import array, kron, identity, zeros, trace, outer, eye
+
+
+a = array([[0, 1], [0, 0]])
+a_dag = array([[0, 0], [1, 0]])
+
+povm_0 = (1/2) * (kron(a_dag @ a, eye(2)) - 1j*kron(a, a_dag) + 1j*kron(a_dag, a) + kron(eye(2), a_dag @ a))
+povm_1 = (1/2) * (kron(a_dag @ a, eye(2)) + 1j*kron(a, a_dag) - 1j*kron(a_dag, a) + kron(eye(2), a_dag @ a))
+
+
+@lru_cache(maxsize=1000)
+def measure_state_with_cache(state: Tuple[complex, complex], basis: Tuple[Tuple[complex]]) -> float:
+    state = array(state)
+    u = array(basis[0], dtype=complex)
+    # measurement operator
+    M0 = outer(u.conj(), u)
+
+    # probability of measuring basis[0]
+    prob_0 = (state.conj().transpose() @ M0.conj().transpose() @ M0 @ state).real
+    return prob_0
+
+
+@lru_cache(maxsize=1000)
+def measure_entangled_state_with_cache(state: Tuple[complex], basis: Tuple[Tuple[complex]],
+                                        state_index: int, num_states: int) -> Tuple[
+    Tuple[complex], Tuple[complex], float]:
+    state = array(state)
+    u = array(basis[0], dtype=complex)
+    v = array(basis[1], dtype=complex)
+    # measurement operator
+    M0 = outer(u.conj(), u)
+    M1 = outer(v.conj(), v)
+
+    # generate projectors
+    projector0 = [1]
+    projector1 = [1]
+    for i in range(num_states):
+        if i == state_index:
+            projector0 = kron(projector0, M0)
+            projector1 = kron(projector1, M1)
+        else:
+            projector0 = kron(projector0, identity(2))
+            projector1 = kron(projector1, identity(2))
+
+    # probability of measuring basis[0]
+    prob_0 = (state.conj().transpose() @ projector0.conj().transpose() @ projector0 @ state).real
+
+    if prob_0 >= 1:
+        state1 = None
+    else:
+        state1 = (projector1 @ state) / sqrt(1 - prob_0)
+
+    if prob_0 <= 0:
+        state0 = None
+    else:
+        state0 = (projector0 @ state) / sqrt(prob_0)
+
+    return state0, state1, prob_0
+
+
+@lru_cache(maxsize=1000)
+def measure_multiple_with_cache(state: Tuple[complex], basis: Tuple[Tuple[complex]], length_diff: int) \
+        -> Tuple[Tuple[Tuple[complex]], Tuple[float]]:
+    state = array(state)
+    # construct measurement operators, projectors, and probabilities of measurement
+    projectors = [None] * len(basis)
+    probabilities = [0] * len(basis)
+    for i, vector in enumerate(basis):
+        vector = array(vector, dtype=complex)
+        M = outer(vector.conj(), vector)  # measurement operator
+        projectors[i] = kron(M, identity(2 ** length_diff))  # projector
+        probabilities[i] = (state.conj().transpose() @ projectors[i].conj().transpose() @ projectors[i] @ state).real
+        if probabilities[i] < 0:
+            probabilities[i] = 0
+
+    return_states = [None] * len(projectors)
+    for i, proj in enumerate(projectors):
+        # project to new state
+        if probabilities[i] > 0:
+            new_state = (proj @ state) / sqrt(probabilities[i])
+            new_state = tuple(new_state)
+            return_states[i] = new_state
+
+    return tuple(return_states), tuple(probabilities)
 
 
 @lru_cache(maxsize=1000)
diff --git a/src/kernel/timeline.py b/src/kernel/timeline.py
index 5cfc2e18..989c0344 100644
--- a/src/kernel/timeline.py
+++ b/src/kernel/timeline.py
@@ -113,31 +113,20 @@ def run(self) -> None:
         if self.show_progress:
             self.progress_bar()
 
-        # log = {}
         while len(self.events) > 0:
             event = self.events.pop()
 
             if event.time >= self.stop_time:
-                self.schedule(event)
+                self.schedule(event)  # return to event list
                 break
-
             assert self.time <= event.time, f"invalid event time for process scheduled on {event.process.owner}"
-
             if event.is_invalid():
                 continue
 
             self.time = event.time
-            # if not event.process.activation in log:
-            #     log[event.process.activation] = 0
-            # log[event.process.activation]+=1
-
             event.process.run()
-
             self.run_counter += 1
 
-        # print('number of event', self.event_counter)
-        # print('log:',log)
-
         self.is_running = False
         time_elapsed = time_ns() - tick
         log.logger.info("Timeline end simulation. Execution Time: %d ns; Scheduled Event: %d; Executed Event: %d" %
diff --git a/src/message.py b/src/message.py
index b4562000..8a86e595 100644
--- a/src/message.py
+++ b/src/message.py
@@ -10,6 +10,5 @@ class Message(ABC):
     def __init__(self, msg_type: Enum, receiver: str):
         self.msg_type = msg_type
         self.receiver = receiver
+        self.protocol_type = None
         self.payload = None
-
-
diff --git a/src/network_management/network_manager.py b/src/network_management/network_manager.py
index 7e22c564..c0b0f081 100644
--- a/src/network_management/network_manager.py
+++ b/src/network_management/network_manager.py
@@ -55,6 +55,7 @@ def __init__(self, owner: "QuantumRouter", protocol_stack: "List[StackProtocol]"
             owner (QuantumRouter): node network manager is attached to.
             protocol_stack (List[StackProtocol]): stack of protocols to use for processing.
         """
+
         log.logger.info("Create network manager of Node {}".format(owner.name))
         self.name = "network_manager"
         self.owner = owner
@@ -118,6 +119,7 @@ def received_message(self, src: str, msg: "NetworkManagerMessage"):
         Side Effects:
             Will invoke `pop` method of 0 indexed protocol in `protocol_stack`.
         """
+
         log.logger.info(
             "{} network manager receives message {} from {}".format(
                 self.owner.name, msg.payload, src))
@@ -132,7 +134,7 @@ def request(self, responder: str, start_time: int, end_time: int, memory_size: i
             responder (str): name of node to establish entanglement with.
             start_time (int): simulation start time of entanglement.
             end_time (int): simulation end time of entanglement.
-            memory_size (int): number of entangledd memory pairs to create.
+            memory_size (int): number of entangled memory pairs to create.
             target_fidelity (float): desired fidelity of entanglement.
 
         Side Effects:
@@ -142,7 +144,7 @@ def request(self, responder: str, start_time: int, end_time: int, memory_size: i
         self.protocol_stack[-1].push(responder, start_time, end_time, memory_size, target_fidelity)
 
 
-def NewNetworkManager(owner: "QuantumRouter") -> "NetworkManager":
+def NewNetworkManager(owner: "QuantumRouter", memory_array_name: str) -> "NetworkManager":
     """Function to create a new network manager.
 
     Will create a network manager with default protocol stack.
@@ -150,6 +152,7 @@ def NewNetworkManager(owner: "QuantumRouter") -> "NetworkManager":
 
     Args:
         owner (QuantumRouter): node to attach network manager to.
+        memory_array_name (str): name of the memory array component on owner.
 
     Returns:
         NetworkManager: network manager object created.
@@ -157,7 +160,7 @@ def NewNetworkManager(owner: "QuantumRouter") -> "NetworkManager":
 
     manager = NetworkManager(owner, [])
     routing = StaticRoutingProtocol(owner, owner.name + ".StaticRoutingProtocol", {})
-    rsvp = ResourceReservationProtocol(owner, owner.name + ".RSVP")
+    rsvp = ResourceReservationProtocol(owner, owner.name + ".RSVP", memory_array_name)
     routing.upper_protocols.append(rsvp)
     rsvp.lower_protocols.append(routing)
     manager.load_stack([routing, rsvp])
diff --git a/src/network_management/reservation.py b/src/network_management/reservation.py
index 719939f2..b48ea770 100644
--- a/src/network_management/reservation.py
+++ b/src/network_management/reservation.py
@@ -1,10 +1,9 @@
 """Definition of Reservation protocol and related tools.
 
-This module provides a definition for the reservation protocol used by the
-network manager. This includes the Reservation, MemoryTimeCard, and QCap
-classes, which are used by the network manager to track reservations. Also
-included is the definition of the message type used by the reservation
-protocol. """
+This module provides a definition for the reservation protocol used by the network manager.
+This includes the Reservation, MemoryTimeCard, and QCap classes, which are used by the network manager to track reservations.
+Also included is the definition of the message type used by the reservation protocol.
+"""
 
 from enum import Enum, auto
 from typing import List, TYPE_CHECKING, Any, Dict
@@ -12,14 +11,12 @@
 if TYPE_CHECKING:
     from ..topology.node import QuantumRouter
     from ..resource_management.memory_manager import MemoryInfo, MemoryManager
-    from ..entanglement_management.entanglement_protocol import \
-        EntanglementProtocol
+    from ..entanglement_management.entanglement_protocol import EntanglementProtocol
 
 from ..resource_management.rule_manager import Rule, Arguments
 from ..entanglement_management.generation import EntanglementGenerationA
 from ..entanglement_management.purification import BBPSSW
-from ..entanglement_management.swapping import EntanglementSwappingA, \
-    EntanglementSwappingB
+from ..entanglement_management.swapping import EntanglementSwappingA, EntanglementSwappingB
 from ..message import Message
 from ..protocol import StackProtocol
 from ..kernel.event import Event
@@ -37,8 +34,7 @@ class RSVPMsgType(Enum):
 class ResourceReservationMessage(Message):
     """Message used by resource reservation protocol.
 
-    This message contains all information passed between reservation protocol
-    instances.
+    This message contains all information passed between reservation protocol instances.
     Messages of different types contain different information.
 
     Attributes:
@@ -49,8 +45,7 @@ class ResourceReservationMessage(Message):
         path (List[str]): cumulative node list for entanglement path (if `msg_type == APPROVE`)
     """
 
-    def __init__(self, msg_type: any, receiver: str,
-                 reservation: "Reservation", **kwargs):
+    def __init__(self, msg_type: any, receiver: str, reservation: "Reservation", **kwargs):
         Message.__init__(self, msg_type, receiver)
         self.reservation = reservation
         if self.msg_type is RSVPMsgType.REQUEST:
@@ -181,7 +176,8 @@ def ep_req_func1(protocols, args: Arguments) -> "BBPSSW":
 
 
 def ep_rule_action1(memories_info: List["MemoryInfo"], args: Arguments):
-    """Action function used by BBPSSW protocol on nodes except the initiator
+    """Action function used by BBPSSW protocol on nodes except the
+    responder node
 
     """
     memories = [info.memory for info in memories_info]
@@ -324,32 +320,33 @@ def es_rule_conditionB2(memory_info: "MemoryInfo", manager: "MemoryManager", arg
 class ResourceReservationProtocol(StackProtocol):
     """ReservationProtocol for  node resources.
 
-    The reservation protocol receives network entanglement requests and
-    attempts to reserve local resources.
-    If successful, it will forward the request to another node in the
-    entanglement path and create local rules.
+    The reservation protocol receives network entanglement requests and attempts to reserve local resources.
+    If successful, it will forward the request to another node in the entanglement path and create local rules.
     These rules are passed to the node's resource manager.
     If unsuccessful, the protocol will notify the network manager of failure.
 
     Attributes:
         own (QuantumRouter): node that protocol instance is attached to.
         name (str): label for protocol instance.
+        memo_arr (MemoryArray): memory array to track.
         timecards (List[MemoryTimeCard]): list of reservation cards for all memories on node.
         es_succ_prob (float): sets `success_probability` of `EntanglementSwappingA` protocols created by rules.
         es_degradation (float): sets `degradation` of `EntanglementSwappingA` protocols created by rules.
         accepted_reservation (List[Reservation]): list of all approved reservation requests.
     """
 
-    def __init__(self, own: "QuantumRouter", name: str):
+    def __init__(self, own: "QuantumRouter", name: str, memory_array_name: str):
         """Constructor for the reservation protocol class.
 
         Args:
             own (QuantumRouter): node to attach protocol to.
             name (str): label for reservation protocol instance.
+            memory_array_name (str): name of the memory array component on own.
         """
 
         super().__init__(own, name)
-        self.timecards = [MemoryTimeCard(i) for i in range(len(own.memory_array))]
+        self.memo_arr = own.components[memory_array_name]
+        self.timecards = [MemoryTimeCard(i) for i in range(len(self.memo_arr))]
         self.es_succ_prob = 1
         self.es_degradation = 0.95
         self.accepted_reservation = []
@@ -410,8 +407,7 @@ def pop(self, src: str, msg: "ResourceReservationMessage"):
                 msg.qcaps.append(qcap)
                 if self.own.name == msg.reservation.responder:
                     path = [qcap.node for qcap in msg.qcaps]
-                    rules = self.create_rules(path,
-                                              reservation=msg.reservation)
+                    rules = self.create_rules(path, reservation=msg.reservation)
                     self.load_rules(rules, msg.reservation)
                     msg.reservation.set_path(path)
                     new_msg = ResourceReservationMessage(RSVPMsgType.APPROVE,
@@ -476,8 +472,7 @@ def schedule(self, reservation: "Reservation") -> bool:
     def create_rules(self, path: List[str], reservation: "Reservation") -> List["Rule"]:
         """Method to create rules for a successful request.
 
-        Rules are used to direct the flow of information/entanglement in the
-        resource manager.
+        Rules are used to direct the flow of information/entanglement in the resource manager.
 
         Args:
             path (List[str]): list of node names in entanglement path.
@@ -496,18 +491,15 @@ def create_rules(self, path: List[str], reservation: "Reservation") -> List["Rul
         # create rules for entanglement generation
         index = path.index(self.own.name)
         if index > 0:
-            condition_args = {
-                "memory_indices": memory_indices[:reservation.memory_size]}
+            condition_args = {"memory_indices": memory_indices[:reservation.memory_size]}
             action_args = {"mid": self.own.map_to_middle_node[path[index - 1]],
                            "path": path, "index": index}
-            rule = Rule(10, eg_rule_action1, eg_rule_condition, action_args,
-                        condition_args)
+            rule = Rule(10, eg_rule_action1, eg_rule_condition, action_args, condition_args)
             rules.append(rule)
 
         if index < len(path) - 1:
             if index == 0:
-                condition_args = {"memory_indices":
-                                      memory_indices[:reservation.memory_size]}
+                condition_args = {"memory_indices": memory_indices[:reservation.memory_size]}
             else:
                 condition_args = {"memory_indices":
                                       memory_indices[reservation.memory_size:]}
@@ -515,8 +507,7 @@ def create_rules(self, path: List[str], reservation: "Reservation") -> List["Rul
             action_args = {"mid": self.own.map_to_middle_node[path[index + 1]],
                            "path": path, "index": index, "name": self.own.name,
                            "reservation": reservation}
-            rule = Rule(10, eg_rule_action2, eg_rule_condition, action_args,
-                        condition_args)
+            rule = Rule(10, eg_rule_action2, eg_rule_condition, action_args, condition_args)
             rules.append(rule)
 
         # create rules for entanglement purification
@@ -524,10 +515,8 @@ def create_rules(self, path: List[str], reservation: "Reservation") -> List["Rul
             condition_args = {"memory_indices":
                                   memory_indices[:reservation.memory_size],
                               "reservation": reservation}
-
             action_args = {}
-            rule = Rule(10, ep_rule_action1, ep_rule_condition1, action_args,
-                        condition_args)
+            rule = Rule(10, ep_rule_action1, ep_rule_condition1, action_args, condition_args)
             rules.append(rule)
 
         if index < len(path) - 1:
@@ -535,24 +524,20 @@ def create_rules(self, path: List[str], reservation: "Reservation") -> List["Rul
                 condition_args = {"memory_indices": memory_indices,
                                   "fidelity": reservation.fidelity}
             else:
-                condition_args = {"memory_indices":
-                                      memory_indices[reservation.memory_size:],
+                condition_args = {"memory_indices": memory_indices[reservation.memory_size:],
                                   "fidelity": reservation.fidelity}
 
             action_args = {}
-            rule = Rule(10, ep_rule_action2, ep_rule_condition2, action_args,
-                        condition_args)
+            rule = Rule(10, ep_rule_action2, ep_rule_condition2, action_args, condition_args)
             rules.append(rule)
 
         # create rules for entanglement swapping
-
         if index == 0:
-            action_args = {}
             condition_args = {"memory_indices": memory_indices,
                               "target_remote": path[-1],
                               "fidelity": reservation.fidelity}
-            rule = Rule(10, es_rule_actionB, es_rule_conditionB1, action_args,
-                        condition_args)
+            action_args = {}
+            rule = Rule(10, es_rule_actionB, es_rule_conditionB1, action_args, condition_args)
             rules.append(rule)
 
         elif index == len(path) - 1:
@@ -560,8 +545,7 @@ def create_rules(self, path: List[str], reservation: "Reservation") -> List["Rul
             condition_args = {"memory_indices": memory_indices,
                               "target_remote": path[0],
                               "fidelity": reservation.fidelity}
-            rule = Rule(10, es_rule_actionB, es_rule_conditionB1, action_args,
-                        condition_args)
+            rule = Rule(10, es_rule_actionB, es_rule_conditionB1, action_args, condition_args)
             rules.append(rule)
 
         else:
@@ -575,28 +559,28 @@ def create_rules(self, path: List[str], reservation: "Reservation") -> List["Rul
             _index = _path.index(self.own.name)
             left, right = _path[_index - 1], _path[_index + 1]
 
+            condition_args = {"memory_indices": memory_indices,
+                              "left": left,
+                              "right": right,
+                              "fidelity": reservation.fidelity}
             action_args = {"es_succ_prob": self.es_succ_prob,
                            "es_degradation": self.es_degradation}
-            condition_args = {"memory_indices": memory_indices, "left": left,
-                              "right": right, "fidelity": reservation.fidelity}
-            rule = Rule(10, es_rule_actionA, es_rule_conditionA, action_args,
-                        condition_args)
+            rule = Rule(10, es_rule_actionA, es_rule_conditionA, action_args, condition_args)
             rules.append(rule)
 
             action_args = {}
-            rule = Rule(10, es_rule_actionB, es_rule_conditionB2, action_args,
-                        condition_args)
+            rule = Rule(10, es_rule_actionB, es_rule_conditionB2, action_args, condition_args)
             rules.append(rule)
 
         for rule in rules:
             rule.set_reservation(reservation)
+
         return rules
 
     def load_rules(self, rules: List["Rule"], reservation: "Reservation") -> None:
         """Method to add created rules to resource manager.
 
-        This method will schedule the resource manager to load all rules at the
-        reservation start time.
+        This method will schedule the resource manager to load all rules at the reservation start time.
         The rules will be set to expire at the reservation end time.
 
         Args:
@@ -608,7 +592,7 @@ def load_rules(self, rules: List["Rule"], reservation: "Reservation") -> None:
         for card in self.timecards:
             if reservation in card.reservations:
                 process = Process(self.own.resource_manager, "update",
-                                  [None, self.own.memory_array[card.memory_index], "RAW"])
+                                  [None, self.memo_arr[card.memory_index], "RAW"])
                 event = Event(reservation.end_time, process, 1)
                 self.own.timeline.schedule(event)
 
@@ -621,11 +605,9 @@ def load_rules(self, rules: List["Rule"], reservation: "Reservation") -> None:
             self.own.timeline.schedule(event)
 
     def received_message(self, src, msg):
-        """Method to receive messages directly (should not be used; receive
-        through network manager).
-        """
+        """Method to receive messages directly (should not be used; receive through network manager)."""
 
-        raise Exception("RSVP protocol should not call this function")
+        raise Exception("RSVP protocol {} received a message (disallowed)".format(self.name))
 
     def set_swapping_success_rate(self, prob: float) -> None:
         assert 0 <= prob <= 1
@@ -636,7 +618,7 @@ def set_swapping_degradation(self, degradation: float) -> None:
         self.es_degradation = degradation
 
 
-class Reservation():
+class Reservation:
     """Tracking of reservation parameters for the network manager.
 
     Attributes:
@@ -688,7 +670,7 @@ def __eq__(self, other: "Reservation"):
                other.fidelity == self.fidelity
 
 
-class MemoryTimeCard():
+class MemoryTimeCard:
     """Class for tracking reservations on a specific memory.
 
     Attributes:
@@ -709,8 +691,7 @@ def __init__(self, memory_index: int):
     def add(self, reservation: "Reservation") -> bool:
         """Method to add reservation.
 
-        Will use `schedule_reservation` method to determine index to insert
-        reservation.
+        Will use `schedule_reservation` method to determine index to insert reservation.
 
         Args:
             reservation (Reservation): reservation to add.
@@ -746,8 +727,7 @@ def remove(self, reservation: "Reservation") -> bool:
     def schedule_reservation(self, resv: "Reservation") -> int:
         """Method to add reservation to a memory.
 
-        Will return index at which reservation can be inserted into memory
-        reservation list.
+        Will return index at which reservation can be inserted into memory reservation list.
         If no space found for reservation, will raise an exception.
 
         Args:
diff --git a/src/protocol.py b/src/protocol.py
index 2c343970..82048075 100644
--- a/src/protocol.py
+++ b/src/protocol.py
@@ -55,7 +55,7 @@ def __init__(self, own: "Node", name: str):
 
         Args:
             own (Node): node protocol is attached to.
-            name (str): name of protocl instance.
+            name (str): name of protocol instance.
         """
 
         super().__init__(own, name)
@@ -86,5 +86,3 @@ def received_message(self, src: str, msg: "Message"):
         """Method to receive messages from distant nodes."""
 
         pass
-
-
diff --git a/src/qkd/BB84.py b/src/qkd/BB84.py
index a74608ab..99376b4e 100644
--- a/src/qkd/BB84.py
+++ b/src/qkd/BB84.py
@@ -7,6 +7,10 @@
 
 import math
 from enum import Enum, auto
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from ..topology.node import QKDNode
 
 import numpy
 
@@ -52,14 +56,14 @@ class BB84Message(Message):
         frequency (float): frequency for qubit generation (if `msg_type == BEGIN_PHOTON_PULSE`).
         light_time (float): lenght of time to send qubits (if `msg_type == BEGIN_PHOTON_PULSE`).
         start_time (int): simulation start time of qubit pulse (if `msg_type == BEGIN_PHOTON_PULSE`).
-        wavelenght (float): wavelength (in nm) of photons (if `msg_type == BEGIN_PHOTON_PULSE`).
+        wavelength (float): wavelength (in nm) of photons (if `msg_type == BEGIN_PHOTON_PULSE`).
         bases (List[int]): list of measurement bases (if `msg_type == BASIS_LIST`).
         indices (List[int]): list of indices for matching bases (if `msg_type == MATCHING_INDICES`).
     """
 
     def __init__(self, msg_type: BB84MsgType, receiver: str, **kwargs):
         Message.__init__(self, msg_type, receiver)
-        self.owner_type = BB84
+        self.protocol_type = BB84
         if self.msg_type is BB84MsgType.BEGIN_PHOTON_PULSE:
             self.frequency = kwargs["frequency"]
             self.light_time = kwargs["light_time"]
@@ -94,25 +98,29 @@ class BB84(StackProtocol):
         key (int): generated key as an integer.
         key_bits (List[int]): generated key as a list of 0/1 bits.
         another (BB84): other BB84 protocol instance (on opposite node).
-        key_lenghts (List[int]): list of desired key lengths.
+        key_lengths (List[int]): list of desired key lengths.
         self.keys_left_list (List[int]): list of desired number of keys.
         self.end_run_times (List[int]): simulation time for end of each request.
     """
 
-    def __init__(self, own: "QKDNode", name: str, role=-1):
+    def __init__(self, own: "QKDNode", name: str, lightsource: str, qsdetector: str, role=-1):
         """Constructor for BB84 class.
 
         Args:
             own (QKDNode): node hosting protocol instance.
             name (str): name of protocol instance.
+            lightsource (str): name of lightsource for QKD
+            qsdetector (str): name of QSDetector for QKD
 
         Keyword Args:
             role (int): 0/1 role defining Alice and Bob protocols (default -1).
         """
 
-        if own == None: # used only for unit test purposes
+        if own is None:  # used only for unit test purposes
             return
         super().__init__(own, name)
+        self.ls_name = lightsource
+        self.qsd_name = qsdetector
         self.role = role
 
         self.working = False
@@ -198,16 +206,17 @@ def start_protocol(self) -> None:
             self.working = True
             self.another.working = True
 
-            self.ls_freq = self.own.lightsource.frequency
+            ls = self.own.components[self.ls_name]
+            self.ls_freq = ls.frequency
 
             # calculate light time based on key length
-            self.light_time = self.key_lengths[0] / (self.ls_freq * self.own.lightsource.mean_photon_num)
+            self.light_time = self.key_lengths[0] / (self.ls_freq * ls.mean_photon_num)
 
             # send message that photon pulse is beginning, then send bits
             self.start_time = int(self.own.timeline.now()) + round(self.own.cchannels[self.another.own.name].delay)
             message = BB84Message(BB84MsgType.BEGIN_PHOTON_PULSE, self.another.name,
                                   frequency=self.ls_freq, light_time=self.light_time,
-                                  start_time=self.start_time, wavelength=self.own.lightsource.wavelength)
+                                  start_time=self.start_time, wavelength=ls.wavelength)
             self.own.send_message(self.another.own.name, message)
 
             process = Process(self, "begin_photon_pulse", [])
@@ -225,6 +234,7 @@ def begin_photon_pulse(self) -> None:
         Also records bits sent for future processing.
 
         Side Effects:
+            Will set destination of photons for local node.
             Will invoke emit method of node lightsource.
             Will schedule another `begin_photon_pulse` event after the emit period.
         """
@@ -232,19 +242,21 @@ def begin_photon_pulse(self) -> None:
         log.logger.debug(self.name + " starting photon pulse")
         
         if self.working and self.own.timeline.now() < self.end_run_times[0]:
+            self.own.destination = self.another.own.name
+
             # generate basis/bit list
             num_pulses = round(self.light_time * self.ls_freq)
             basis_list = numpy.random.choice([0, 1], num_pulses)
             bit_list = numpy.random.choice([0, 1], num_pulses)
 
             # control hardware
-            lightsource = self.own.lightsource
+            lightsource = self.own.components[self.ls_name]
             encoding_type = lightsource.encoding_type
             state_list = []
             for i, bit in enumerate(bit_list):
                 state = (encoding_type["bases"][basis_list[i]])[bit]
                 state_list.append(state)
-            lightsource.emit(state_list, self.another.own.name)
+            lightsource.emit(state_list)
 
             self.basis_lists.append(basis_list)
             self.bit_lists.append(bit_list)
@@ -254,6 +266,7 @@ def begin_photon_pulse(self) -> None:
             process = Process(self, "begin_photon_pulse", [])
             event = Event(self.start_time + int(round(self.light_time * 1e12)), process)
             self.own.timeline.schedule(event)
+
         else:
             self.working = False
             self.another.working = False
@@ -278,7 +291,7 @@ def set_measure_basis_list(self) -> None:
         num_pulses = int(self.light_time * self.ls_freq)
         basis_list = numpy.random.choice([0, 1], num_pulses)
         self.basis_lists.append(basis_list)
-        self.own.qsdetector.set_basis_list(basis_list, self.start_time, self.ls_freq)
+        self.own.components[self.qsd_name].set_basis_list(basis_list, self.start_time, self.ls_freq)
 
     def end_photon_pulse(self) -> None:
         """Method to process sent qubits."""
@@ -287,7 +300,7 @@ def end_photon_pulse(self) -> None:
 
         if self.working and self.own.timeline.now() < self.end_run_times[0]:
             # get bits
-            self.bit_lists.append(self.own.get_bits(self.light_time, self.start_time, self.ls_freq))
+            self.bit_lists.append(self.own.get_bits(self.light_time, self.start_time, self.ls_freq, self.qsd_name))
             self.start_time = self.own.timeline.now()
             # set bases for measurement
             self.set_measure_basis_list()
@@ -374,7 +387,7 @@ def received_message(self, src: str, msg: "Message") -> None:
                         self.set_key()  # convert from binary list to int
                         self._pop(info=self.key)
                         self.another.set_key()
-                        self.another._pop(info=self.another.key)
+                        self.another._pop(info=self.another.key)  # TODO: why access another node?
 
                         # for metrics
                         if self.latency == 0:
@@ -404,5 +417,3 @@ def set_key(self):
         key_bits = self.key_bits[0:self.key_lengths[0]]
         del self.key_bits[0:self.key_lengths[0]]
         self.key = int("".join(str(x) for x in key_bits), 2)  # convert from binary list to int
-
-
diff --git a/src/qkd/cascade.py b/src/qkd/cascade.py
index 457d9094..caf1f8a3 100644
--- a/src/qkd/cascade.py
+++ b/src/qkd/cascade.py
@@ -8,6 +8,10 @@
 import math
 from enum import Enum, auto
 from functools import lru_cache
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from ..topology.node import QKDNode
 
 from numpy import random
 
@@ -66,7 +70,7 @@ class CascadeMessage(Message):
 
     def __init__(self, msg_type: Enum, receiver: str, **kwargs):
         super().__init__(msg_type, receiver)
-        self.owner_type = Cascade
+        self.protocol_type = Cascade
         if msg_type is CascadeMsgType.KEY:
             self.key = kwargs["key"]
         elif msg_type is CascadeMsgType.PARAMS:
@@ -123,7 +127,7 @@ class Cascade(StackProtocol):
         frame_num (int): frame number.
         run_time (int): time to run protocol.
         bits (List[int]): bits to operate on (received from BB84).
-        tl (int): cascade parameter.
+        t1 (int): cascade parameter.
         t2 (int): cascade parameter.
         k1 (int): cascade parameter.
         checksum_tables (List[List[int]]): lists of generated checksums.
@@ -256,13 +260,13 @@ def get_k1(p, lower, upper):
 
             @lru_cache(maxsize=128)
             def get_diff_bit_num(key1, key2):
-                val = key1^key2
+                val = key1 ^ key2
                 counter = 0
                 i = 0
-                while val>>i:
-                    if (val>>i)&1:
+                while val >> i:
+                    if (val >> i) & 1:
                         counter += 1
-                    i+=1
+                    i += 1
                 return counter
 
             p = get_diff_bit_num(key, self.bits[0]) / 10000
@@ -371,7 +375,8 @@ def flip_bit_at_pos(val, pos):
                         _block = index_to_block_id[_pass][pos]
                         checksum_table[_pass][_block] ^= 1
 
-                    if not self.check_checksum(key_id): return
+                    if not self.check_checksum(key_id):
+                        return
 
                 else:
                     self.interactive_binary_search(key_id, pass_id, block_id, start, end)
@@ -386,7 +391,7 @@ def flip_bit_at_pos(val, pos):
             key_id = msg.key_id
 
             for i in range(int(self.frame_len / self.keylen)):
-                self.valid_keys.append((self.bits[key_id]>>(i*self.keylen)) & ((1<<self.keylen)-1))
+                self.valid_keys.append((self.bits[key_id] >> (i*self.keylen)) & ((1 << self.keylen)-1))
                 if self.frame_num > 0:
                     log.logger.info(self.name + ' state={} got valid key'.format(self.state))
                     self._pop(key=self.valid_keys[-1])
@@ -406,7 +411,7 @@ def generate_key(self, keylen: int, frame_num=math.inf, run_time=math.inf) -> No
         Args:
             keylen (int): length of key to generate.
             frame_num (int): number of keys to generate (default inf).
-            runtime (int): max simulation time allowed for key generation (default inf).
+            run_time (int): max simulation time allowed for key generation (default inf).
         """
 
         log.logger.info(self.name + ' state={} generate_key, keylen={}, keynum={}'.format(self.state, keylen, frame_num))
@@ -514,16 +519,16 @@ def check_checksum(self, key_id: int) -> bool:
         cur_key = key_id
         another_checksum = self.another_checksums[cur_key]
         block_id_to_index = self.block_id_to_index_lists[cur_key]
-        for _pass in range(1,len(another_checksum)):
+        for _pass in range(1, len(another_checksum)):
             for _block in range(len(another_checksum[_pass])):
                 if self.checksum_tables[cur_key][_pass][_block] != self.another_checksums[cur_key][_pass][_block]:
-                    log.logger.debug(self.name + ' state={} two checksums are different'.format(self.state, [cur_key,_pass,_block]))
+                    log.logger.debug(self.name + ' state={} two checksums are different'.format(self.state, [cur_key, _pass, _block]))
                     block_size = len(block_id_to_index[_pass][_block])
                     self.interactive_binary_search(cur_key, _pass, _block, 0, block_size)
                     return False
 
         for i in range(int(self.frame_len / self.keylen)):
-            self.valid_keys.append((self.bits[key_id]>>(i*self.keylen)) & ((1<<self.keylen)-1))
+            self.valid_keys.append((self.bits[key_id] >> (i*self.keylen)) & ((1 << self.keylen)-1))
             if self.frame_num > 0:
                 log.logger.info(self.name + ' state={} got_valid_key'.format(self.state))
                 self._pop(key=self.valid_keys[-1])
@@ -543,8 +548,7 @@ def end_cascade(self):
 
         self.state = 2
     
-    def interactive_binary_search(self, key_id: int, pass_id: int, block_id: int,
-            start: int, end: int) -> None:
+    def interactive_binary_search(self, key_id: int, pass_id: int, block_id: int, start: int, end: int) -> None:
         """Method to search for errors in key.
 
         Split block[start:end] to block[start:(start+end)/2], block[(start+end)/2,end].
@@ -561,7 +565,8 @@ def interactive_binary_search(self, key_id: int, pass_id: int, block_id: int,
             Will send SEND_FOR_BINARY messages to other protocol.
         """
 
-        log.logger.debug(self.name + ' state={} interactive_binary_search, params={}'.format(self.state, [key_id, pass_id, block_id, start, end]))
+        log.logger.debug(self.name + ' state={} interactive_binary_search, params={}'.format(
+            self.state, [key_id, pass_id, block_id, start, end]))
 
         # first half checksum
         message = CascadeMessage(CascadeMsgType.SEND_FOR_BINARY, self.another.name,
@@ -594,29 +599,29 @@ def performance_measure(self) -> None:
             counter = 0
             for i in range(len(self.t1)):
                 if self.t2[i] != -1:
-                    self.latency+=self.t2[i]-self.t1[i]
-                    counter+=1
-            if counter>0: self.latency /= counter
-            else: self.latency = None
+                    self.latency += self.t2[i]-self.t1[i]
+                    counter += 1
+            if counter > 0:
+                self.latency /= counter
+            else:
+                self.latency = None
             self.another.latency = self.latency
 
         if self.own.timeline.now() - self.start_time:
             self.throughput = 1e12 * len(self.valid_keys) * self.keylen / (self.own.timeline.now() - self.start_time)
-            self.privacy_throughput = 1e12 * (len(self.valid_keys) * (self.keylen) - int(len(self.valid_keys)/40) * self.secure_params - self.disclosed_bits_counter) / (self.own.timeline.now() - self.start_time)
+            self.privacy_throughput = 1e12 * (len(self.valid_keys) * self.keylen - int(len(self.valid_keys)/40) * self.secure_params - self.disclosed_bits_counter) / (self.own.timeline.now() - self.start_time)
 
         counter = 0
         for j in range(min(len(self.valid_keys), len(self.another.valid_keys))):
             i = 0
             val = self.valid_keys[j] ^ self.another.valid_keys[j]
-            while val>>i:
-                if (val>>i)&1 == 1:
+            while val >> i:
+                if (val >> i) & 1 == 1:
                     counter += 1
-                i+=1
+                i += 1
 
         if len(self.valid_keys) > 1:
             self.error_bit_rate = counter / (self.keylen * (len(self.valid_keys)))
         else:
             self.error_bit_rate = 0
         self.time_cost = self.end_time - self.start_time
-
-
diff --git a/src/resource_management/resource_manager.py b/src/resource_management/resource_manager.py
index 109c4c90..b22382b5 100644
--- a/src/resource_management/resource_manager.py
+++ b/src/resource_management/resource_manager.py
@@ -89,7 +89,7 @@ class ResourceManager():
         waiting_protocols (List[Protocol]): list of protocols awaiting a request from a remote protocol.
     """
 
-    def __init__(self, owner: "QuantumRouter"):
+    def __init__(self, owner: "QuantumRouter", memory_array_name: str):
         """Constructor for resource manager.
         
         Args:
@@ -98,7 +98,7 @@ def __init__(self, owner: "QuantumRouter"):
 
         self.name = "resource_manager"
         self.owner = owner
-        self.memory_manager = MemoryManager(owner.memory_array)
+        self.memory_manager = MemoryManager(owner.components[memory_array_name])
         self.memory_manager.set_resource_manager(self)
         self.rule_manager = RuleManager()
         self.rule_manager.set_resource_manager(self)
@@ -120,8 +120,8 @@ def load(self, rule: "Rule") -> bool:
         Returns:
             bool: if rule was loaded successfully.
         """
-        log.logger.info(
-            'load rule {}'.format(rule))
+
+        log.logger.info('load rule {}'.format(rule))
         self.rule_manager.load(rule)
 
         for memory_info in self.memory_manager:
@@ -142,8 +142,8 @@ def expire(self, rule: "Rule") -> None:
         Args:
             rule (Rule): rule to remove.
         """
-        log.logger.info(
-            'expired rule {}'.format(rule))
+
+        log.logger.info('expired rule {}'.format(rule))
         created_protocols = self.rule_manager.expire(rule)
         while created_protocols:
             protocol = created_protocols.pop()
@@ -203,8 +203,7 @@ def get_memory_manager(self):
         return self.memory_manager
 
     def send_request(self, protocol: "EntanglementProtocol", req_dst: str,
-                     req_condition_func: RequestConditionFunc,
-                     req_args: Arguments):
+                     req_condition_func: RequestConditionFunc, req_args: Arguments):
         """Method to send protocol request to another node.
 
         Send the request to pair the local 'protocol' with the protocol on the remote node 'req_dst'.
@@ -213,14 +212,16 @@ def send_request(self, protocol: "EntanglementProtocol", req_dst: str,
         Args:
             protocol (EntanglementProtocol): protocol sending the request.
             req_dst (str): name of destination node.
-            req_condition_func (Callable[[List[EntanglementProtocol]], EntanglementProtocol]): function used to evaluate condition on distant node.
+            req_condition_func (Callable[[List[EntanglementProtocol]], EntanglementProtocol]):
+                function used to evaluate condition on distant node.
+            req_args (Dict[str, Any]): arguments for req_cond_func.
         """
 
         protocol.own = self.owner
         if req_dst is None:
             self.waiting_protocols.append(protocol)
             return
-        if not protocol in self.pending_protocols:
+        if protocol not in self.pending_protocols:
             self.pending_protocols.append(protocol)
         memo_names = [memo.name for memo in protocol.memories]
         msg = ResourceManagerMessage(ResourceManagerMsgType.REQUEST,
@@ -231,9 +232,7 @@ def send_request(self, protocol: "EntanglementProtocol", req_dst: str,
                                      req_args=req_args)
         self.owner.send_message(req_dst, msg)
         log.logger.info(
-            "{} network manager send {} message to {}".format(self.owner.name,
-                                                              msg.msg_type.name,
-                                                              req_dst))
+            "{} network manager send {} message to {}".format(self.owner.name, msg.msg_type.name, req_dst))
 
     def received_message(self, src: str, msg: "ResourceManagerMessage") -> None:
         """Method to receive resoruce manager messages.
@@ -244,12 +243,12 @@ def received_message(self, src: str, msg: "ResourceManagerMessage") -> None:
             src (str): name of the node that sent the message.
             msg (ResourceManagerMessage): message received.
         """
+
         log.logger.info("{} receive {} message from {}".format(self.name,
                                                                msg.msg_type.name,
                                                                src))
         if msg.msg_type is ResourceManagerMsgType.REQUEST:
-            protocol = msg.req_condition_func(self.waiting_protocols,
-                                              msg.req_args)
+            protocol = msg.req_condition_func(self.waiting_protocols, msg.req_args)
             if protocol is not None:
                 protocol.set_others(msg.ini_protocol_name, msg.ini_node_name,
                                     msg.ini_memories_name)
@@ -278,6 +277,7 @@ def received_message(self, src: str, msg: "ResourceManagerMessage") -> None:
                                              paired_node=None,
                                              paired_memories=None)
             self.owner.send_message(src, new_msg)
+
         elif msg.msg_type is ResourceManagerMsgType.RESPONSE:
             protocol_name = msg.ini_protocol_name
 
@@ -292,8 +292,7 @@ def received_message(self, src: str, msg: "ResourceManagerMessage") -> None:
                 return
 
             if msg.is_approved:
-                protocol.set_others(msg.paired_protocol, msg.paired_node,
-                                    msg.paired_memories)
+                protocol.set_others(msg.paired_protocol, msg.paired_node, msg.paired_memories)
                 if protocol.is_ready():
                     self.pending_protocols.remove(protocol)
                     self.owner.protocols.append(protocol)
@@ -310,10 +309,12 @@ def received_message(self, src: str, msg: "ResourceManagerMessage") -> None:
                     else:
                         self.update(None, memory, "ENTANGLED")
                 self.pending_protocols.remove(protocol)
+
         elif msg.msg_type is ResourceManagerMsgType.RELEASE_PROTOCOL:
             for p in self.owner.protocols:
                 if p.name == msg.protocol:
                     p.release()
+
         elif msg.msg_type is ResourceManagerMsgType.RELEASE_MEMORY:
             target_id = msg.memory
             for protocol in self.owner.protocols:
@@ -331,14 +332,12 @@ def release_remote_protocol(self, dst: str, protocol: str) -> None:
         """Method to release protocols from memories on distant nodes.
 
         Release the remote protocol 'protocol' on the remote node 'dst'.
-        The local protocol was paired with the remote protocol but local
-        protocol becomes invalid.
-        The resource manager needs to notify the remote node to cancel the
-        paired protocol.
+        The local protocol was paired with the remote protocol but local protocol becomes invalid.
+        The resource manager needs to notify the remote node to cancel the paired protocol.
 
         Args:
             dst (str): name of the destination node.
-            protocol (EntanglementProtocol): protocol to release on node.
+            protocol (str): name of protocol to release on node.
         """
 
         msg = ResourceManagerMessage(ResourceManagerMsgType.RELEASE_PROTOCOL,
@@ -354,7 +353,6 @@ def release_remote_memory(self, dst: str, memory_id: str) -> None:
         The resource manager needs to notify the remote node to release the occupied memory.
 
         Args:
-            init_protocol (EntanglementProtocol): protocol holding memory.
             dst (str): name of destination node.
             memory_id (str): name of memory to release.
         """
diff --git a/src/resource_management/rule_manager.py b/src/resource_management/rule_manager.py
index c90dfc09..1de195e8 100644
--- a/src/resource_management/rule_manager.py
+++ b/src/resource_management/rule_manager.py
@@ -7,13 +7,23 @@
 from typing import Callable, TYPE_CHECKING, List, Tuple, Any, Dict
 from ..utils import log
 if TYPE_CHECKING:
-    from ..entanglement_management.entanglement_protocol import EntanglementProtocol, Protocol
+    from ..entanglement_management.entanglement_protocol import EntanglementProtocol
     from .memory_manager import MemoryInfo, MemoryManager
     from .resource_manager import ResourceManager
     from ..network_management.reservation import Reservation
 
 
-class RuleManager():
+ActionFunc = Callable[[List["MemoryInfo"], Dict[str, Any]],
+                      Tuple["EntanglementProtocol", List["str"],
+                            List[Callable[["EntanglementProtocol"], bool]]]]
+
+ConditionFunc = Callable[["MemoryInfo", "MemoryManager", Dict[str, Any]],
+                         List["MemoryInfo"]],
+
+Arguments = Dict[str, Any]
+
+
+class RuleManager:
     """Class to manage and follow installed rules.
 
     The RuleManager checks available rules when the state of a memory is updated.
@@ -80,9 +90,7 @@ def get_memory_manager(self):
         return self.resource_manager.get_memory_manager()
 
     def send_request(self, protocol, req_dst, req_condition_func, req_args):
-        log.logger.info(
-            'Rule manager send request for protocol {} to {}'.format(
-                protocol.name, req_dst))
+        log.logger.info('Rule manager send request for protocol {} to {}'.format(protocol.name, req_dst))
         return self.resource_manager.send_request(protocol, req_dst,
                                                   req_condition_func, req_args)
 
@@ -93,17 +101,7 @@ def __getitem__(self, item):
         return self.rules[item]
 
 
-ActionFunc = Callable[[List["MemoryInfo"], Dict[str, Any]],
-                      Tuple["EntanglementProtocol", List["str"],
-                            List[Callable[["EntanglementProtocol"], bool]]]]
-
-ConditionFunc = Callable[["MemoryInfo", "MemoryManager", Dict[str, Any]],
-                         List["MemoryInfo"]],
-
-Arguments = Dict[str, Any]
-
-
-class Rule():
+class Rule:
     """Definition of rule for the rule manager.
 
     Rule objects are installed on and interacted with by the rule manager.
@@ -115,6 +113,7 @@ class Rule():
         condition (Callable[["MemoryInfo", "MemoryManager"], List["MemoryInfo"]]): condition required by rule.
         protocols (List[Protocols]): protocols created by rule.
         rule_manager (RuleManager): reference to rule manager object where rule is installed.
+        reservation (Reservation): associated reservation.
     """
 
     def __init__(self, priority: int, action: ActionFunc, condition: ConditionFunc,
@@ -155,9 +154,7 @@ def do(self, memories_info: List["MemoryInfo"]) -> None:
         for info in memories_info:
             info.memory.detach(info.memory.memory_array)
             info.memory.attach(protocol)
-        for dst, req_func, args in zip(req_dsts,
-                                       req_condition_funcs,
-                                       req_args):
+        for dst, req_func, args in zip(req_dsts, req_condition_funcs, req_args):
             self.rule_manager.send_request(protocol, dst, req_func, args)
 
     def is_valid(self, memory_info: "MemoryInfo") -> List["MemoryInfo"]:
diff --git a/src/topology/node.py b/src/topology/node.py
index 2531bd0a..59faf96a 100644
--- a/src/topology/node.py
+++ b/src/topology/node.py
@@ -17,13 +17,14 @@
     from ..network_management.reservation import Reservation
     from ..components.optical_channel import QuantumChannel, ClassicalChannel
     from ..components.memory import Memory
+    from ..components.photon import Photon
     from ..app.random_request import RandomRequestApp
 
 from ..kernel.entity import Entity
 from ..components.memory import MemoryArray
 from ..components.bsm import SingleAtomBSM
 from ..components.light_source import LightSource
-from ..components.detector import QSDetectorPolarization, QSDetectorTimeBin
+from ..components.detector import QSDetector, QSDetectorPolarization, QSDetectorTimeBin
 from ..qkd.BB84 import BB84
 from ..qkd.cascade import Cascade
 from ..resource_management.resource_manager import ResourceManager
@@ -41,9 +42,11 @@ class Node(Entity):
         name (str): label for node instance.
         timeline (Timeline): timeline for simulation.
         cchannels (Dict[str, ClassicalChannel]): mapping of destination node names to classical channel instances.
-        qchannels (Dict[str, ClassicalChannel]): mapping of destination node names to quantum channel instances.
+        qchannels (Dict[str, QuantumChannel]): mapping of destination node names to quantum channel instances.
         protocols (List[Protocol]): list of attached protocols.
         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.
     """
 
     def __init__(self, name: str, timeline: "Timeline", seed=None):
@@ -61,6 +64,8 @@ def __init__(self, name: str, timeline: "Timeline", seed=None):
         self.qchannels = {}  # mapping of destination node names to quantum channels
         self.protocols = []
         self.generator = np.random.default_rng(seed)
+        self.components = {}
+        self.first_component_name = None
 
     def init(self) -> None:
         pass
@@ -71,6 +76,19 @@ def set_seed(self, seed: int) -> None:
     def get_generator(self):
         return self.generator
 
+    def add_component(self, component: Entity) -> None:
+        """Adds a hardware component to the node.
+
+        Args:
+            component (Entity): local hardware component to add.
+        """
+
+        self.components[component.name] = component
+        component.owner = self
+
+    def set_first_component(self, name: str):
+        self.first_component_name = name
+
     def assign_cchannel(self, cchannel: "ClassicalChannel", another: str) -> None:
         """Method to assign a classical channel to the node.
 
@@ -141,20 +159,37 @@ def send_qubit(self, dst: str, qubit) -> None:
         self.qchannels[dst].transmit(qubit, self)
 
     def receive_qubit(self, src: str, qubit) -> None:
-        """Method to receive qubits from quantum channel (does nothing for this class)."""
+        """Method to receive qubits from quantum channel.
 
-        pass
+        By default, forwards qubit to hardware element designated by field `receiver_name`.
+
+        Args:
+            src (str): name of node where qubit was sent from.
+            qubit (any): transmitted qubit.
+        """
+
+        self.components[self.first_component_name].get(qubit)
+
+    def get_components_by_type(self, component_type: str) -> [Entity]:
+        return [comp for comp in self.components.values() if type(comp).__name__ == component_type]
+
+    def change_timeline(self, timeline: "Timeline"):
+        self.timeline = timeline
+        for component in self.components.values():
+            component.change_timeline(timeline)
+        for cc in self.cchannels.values():
+            cc.change_timeline(timeline)
 
 
 class BSMNode(Node):
     """Bell state measurement node.
 
     This node provides bell state measurement and the EntanglementGenerationB protocol for entanglement generation.
+    Creates a SingleAtomBSM object within local components.
 
     Attributes:
         name (str): label for node instance.
         timeline (Timeline): timeline for simulation.
-        bsm (SingleAtomBSM): BSM instance object.
         eg (EntanglementGenerationB): entanglement generation protocol instance.
     """
 
@@ -164,66 +199,51 @@ def __init__(self, name: str, timeline: "Timeline", other_nodes: List[str]) -> N
         Args:
             name (str): name of node.
             timeline (Timeline): simulation timeline.
-            other_nodes (str): 2-member list of node names for adjacent quantum routers.
+            other_nodes (List[str]): 2-member list of node names for adjacent quantum routers.
         """
 
-        from ..entanglement_management.generation import \
-            EntanglementGenerationB
+        from ..entanglement_management.generation import EntanglementGenerationB
+
         Node.__init__(self, name, timeline)
-        self.bsm = SingleAtomBSM("%s_bsm" % name, timeline)
-        self.bsm.owner = self
-        self.eg = EntanglementGenerationB(self, "{}_eg".format(name),
-                                          other_nodes)
-        self.bsm.attach(self.eg)
+        bsm_name = name + ".BSM"
+        bsm = SingleAtomBSM(bsm_name, timeline)
+        self.eg = EntanglementGenerationB(self, "{}_eg".format(name), other_nodes)
+
+        bsm.attach(self.eg)
+        self.add_component(bsm)
+        self.set_first_component(bsm_name)
 
     def receive_message(self, src: str, msg: "Message") -> None:
         # signal to protocol that we've received a message
         for protocol in self.protocols:
-            if type(protocol) == msg.owner_type:
+            if type(protocol) == msg.protocol_type:
                 if protocol.received_message(src, msg):
                     return
 
         # if we reach here, we didn't successfully receive the message in any protocol
         print(src, msg)
-        raise Exception("Unkown protocol")
-
-    def receive_qubit(self, src: str, qubit):
-        """Method to receive qubit from quantum channel.
-
-        Invokes get method of internal bsm with `qubit` as argument.
-
-        Args:
-            src (str): name of node where qubit was sent from.
-            qubit (any): transmitted qubit.
-        """
-
-        self.bsm.get(qubit)
+        raise Exception("Unknown protocol")
 
     def eg_add_others(self, other):
-        """Method to addd other protocols to entanglement generation protocol.
+        """Method to add other protocols to entanglement generation protocol.
+
+        Local entanglement generation protocol stores name of other protocol for communication.
+        NOTE: entanglement generation protocol should be first protocol in protocol list.
 
         Args:
             other (EntanglementProtocol): other entanglement protocol instance.
         """
 
-        self.eg.others.append(other.name)
-
-    def change_timeline(self, timeline: "Timeline"):
-        self.timeline = timeline
-        self.bsm.change_timeline(timeline)
-        for cc in self.cchannels.values():
-            cc.change_timeline(timeline)
+        self.protocols[0].others.append(other.name)
 
 
 class QuantumRouter(Node):
     """Node for entanglement distribution networks.
 
     This node type comes pre-equipped with memory hardware, along with the default SeQUeNCe modules (sans application).
+    By default, a quantum memory array is included in the components of this node.
 
     Attributes:
-        name (str): label for node instance.
-        timeline (Timeline): timeline for simulation.
-        memory_array (MemoryArray): internal memory array object.
         resource_manager (ResourceManager): resource management module.
         network_manager (NetworkManager): network management module.
         map_to_middle_node (Dict[str, str]): mapping of router names to intermediate bsm node names.
@@ -240,16 +260,20 @@ def __init__(self, name, tl, memo_size=50):
         """
 
         Node.__init__(self, name, tl)
-        self.memory_array = MemoryArray(name + ".MemoryArray", tl, num_memories=memo_size)
-        self.memory_array.owner = self
-        self.resource_manager = ResourceManager(self)
-        self.network_manager = NewNetworkManager(self)
+
+        # hardware setup
+        memo_arr_name = name + ".MemoryArray"
+        memory_array = MemoryArray(memo_arr_name, tl, num_memories=memo_size)
+        self.add_component(memory_array)
+        memory_array.add_receiver(self)
+
+        self.resource_manager = ResourceManager(self, memo_arr_name)
+        self.network_manager = NewNetworkManager(self, memo_arr_name)
         self.map_to_middle_node = {}
         self.app = None
 
     def receive_message(self, src: str, msg: "Message") -> None:
-        log.logger.info("{} receive message {} from {}".format
-                        (self.name, msg, src))
+        log.logger.info("{} receive message {} from {}".format(self.name, msg, src))
         if msg.receiver == "resource_manager":
             self.resource_manager.received_message(src, msg)
         elif msg.receiver == "network_manager":
@@ -282,6 +306,13 @@ def add_bsm_node(self, bsm_name: str, router_name: str):
         """
         self.map_to_middle_node[router_name] = bsm_name
 
+    def get(self, photon: "Photon", **kwargs):
+        """Receives photon from last hardware element (in this case, quantum memory)."""
+        dst = kwargs.get("dst", None)
+        if dst is None:
+            raise ValueError("Destination should be supplied for 'get' method on QuantumRouter")
+        self.send_qubit(dst, photon)
+
     def memory_expire(self, memory: "Memory") -> None:
         """Method to receive expired memories.
 
@@ -300,6 +331,8 @@ def reserve_net_resource(self, responder: str, start_time: int, end_time: int, m
                              target_fidelity: float) -> None:
         """Method to request a reservation.
 
+        Can be used by local applications.
+
         Args:
             responder (str): name of the node with which entanglement is requested.
             start_time (int): desired simulation start time of entanglement.
@@ -323,7 +356,7 @@ def get_reserve_res(self, reservation: "Reservation", res: bool) -> None:
             self.app.get_reserve_res(reservation, res)
 
     def get_other_reservation(self, reservation: "Reservation"):
-        """Method for application to get another reservation."""
+        """Method for application to receive another reservation."""
 
         if self.app:
             self.app.get_other_reservation(reservation)
@@ -343,13 +376,17 @@ class QKDNode(Node):
     1. Error Correction <= implemented by cascade
     0. Sifting <= implemented by BB84
 
+    Additionally, the `components` dictionary contains the following hardware:
+
+    1. lightsource (LightSource): laser light source to generate keys.
+    2. qsdetector (QSDetector): quantum state detector for qubit measurement.
+
     Attributes:
         name (str): label for node instance.
         timeline (Timeline): timeline for simulation.
         encoding (Dict[str, Any]): encoding type for qkd qubits (from encoding module).
-        lightsource (LightSource): laser light source to generate keys.
-        qsdetector (QSDetector): quantum state detector for qubit measurement.
-        protocol_stack (List[StackProtocol]): protocols for qkdd process.
+        destination (str): name of destination node for photons
+        protocol_stack (List[StackProtocol]): protocols for QKD process.
     """
 
     def __init__(self, name: str, timeline: "Timeline", encoding=polarization, stack_size=5):
@@ -364,22 +401,28 @@ def __init__(self, name: str, timeline: "Timeline", encoding=polarization, stack
 
         super().__init__(name, timeline)
         self.encoding = encoding
-        self.lightsource = LightSource(name + ".lightsource", timeline, encoding_type=encoding)
-        self.lightsource.owner = self
-
+        self.destination = None
+
+        # hardware setup
+        ls_name = name + ".lightsource"
+        lightsource = LightSource(ls_name, timeline, encoding_type=encoding)
+        self.add_component(lightsource)
+        lightsource.add_receiver(self)
+        qsd_name = name + ".qsdetector"
         if encoding["name"] == "polarization":
-            self.qsdetector = QSDetectorPolarization(name + ".qsdetector", timeline)
+            qsdetector = QSDetectorPolarization(qsd_name, timeline)
         elif encoding["name"] == "time_bin":
-            self.qsdetector = QSDetectorTimeBin(name + ".qsdetector", timeline)
+            qsdetector = QSDetectorTimeBin(qsd_name, timeline)
         else:
             raise Exception("invalid encoding {} given for QKD node {}".format(encoding["name"], name))
-        self.qsdetector.owner = self
+        self.add_component(qsdetector)
+        self.set_first_component(qsd_name)
 
         self.protocol_stack = [None] * 5
 
         if stack_size > 0:
             # Create BB84 protocol
-            self.protocol_stack[0] = BB84(self, name + ".BB84")
+            self.protocol_stack[0] = BB84(self, name + ".BB84", ls_name, qsd_name)
             self.protocols.append(self.protocol_stack[0])
 
         if stack_size > 1:
@@ -391,7 +434,8 @@ def __init__(self, name: str, timeline: "Timeline", encoding=polarization, stack
 
     def init(self) -> None:
         super().init()
-        assert self.protocol_stack[0].role != -1
+        if self.protocol_stack[0] is not None:
+            assert self.protocol_stack[0].role != -1
 
     def set_protocol_layer(self, layer: int, protocol: "StackProtocol") -> None:
         """Method to set a layer of the protocol stack.
@@ -401,8 +445,8 @@ def set_protocol_layer(self, layer: int, protocol: "StackProtocol") -> None:
             protocol (StackProtocol): protocol to insert.
         """
 
-        if layer < 0 or layer > 5:
-            raise ValueError("layer must be between 0 and 5; given {}".format(layer))
+        if layer < 0 or layer > 4:
+            raise ValueError("layer must be between 0 and 4; given {}".format(layer))
 
         if self.protocol_stack[layer] is not None:
             self.protocols.remove(self.protocol_stack[layer])
@@ -418,32 +462,41 @@ def set_protocol_layer(self, layer: int, protocol: "StackProtocol") -> None:
             self.protocol_stack[layer + 1].lower_protocols.append(protocol)
 
     def update_lightsource_params(self, arg_name: str, value: Any) -> None:
-        self.lightsource.__setattr__(arg_name, value)
+        for component in self.components.values():
+            if type(component) is LightSource:
+                component.__setattr__(arg_name, value)
+                return
 
     def update_detector_params(self, detector_id: int, arg_name: str, value: Any) -> None:
-        self.qsdetector.update_detector_params(detector_id, arg_name, value)
+        for component in self.components.values():
+            if type(component) is QSDetector:
+                component.update_detector_params(detector_id, arg_name, value)
+                return
 
-    def get_bits(self, light_time, start_time, frequency):
+    def get_bits(self, light_time: int, start_time: int, frequency: float, detector_name: str):
         """Method for QKD protocols to get received qubits from the node.
 
         Uses the detection times from attached detectors to calculate which bits were received.
         Returns 0/1 for successfully transmitted bits and -1 for lost/ambiguous bits.
 
         Args:
-            light_time (int): time for which qubits were transmitted.
+            light_time (int): time duration for which qubits were transmitted.
             start_time (int): time at which qubits were first received.
             frequency (float): frequency of qubit transmission.
+            detector_name (str): name of the QSDetector measuring qubits.
 
         Returns:
             List[int]: list of calculated bits.
         """
 
+        qsdetector = self.components[detector_name]
+
         # compute received bits based on encoding scheme
         encoding = self.encoding["name"]
         bits = [-1] * int(round(light_time * frequency))  # -1 used for invalid bits
 
         if encoding == "polarization":
-            detection_times = self.qsdetector.get_photon_times()
+            detection_times = qsdetector.get_photon_times()
 
             # determine indices from detection times and record bits
             for time in detection_times[0]:  # detection times for |0> detector
@@ -460,7 +513,7 @@ def get_bits(self, light_time, start_time, frequency):
                         bits[index] = 1
 
         elif encoding == "time_bin":
-            detection_times = self.qsdetector.get_photon_times()
+            detection_times = qsdetector.get_photon_times()
             bin_separation = self.encoding["bin_separation"]
         
             # single detector (for early, late basis) times
@@ -501,16 +554,17 @@ def get_bits(self, light_time, start_time, frequency):
 
         return bits
 
-    def set_bases(self, basis_list, start_time, frequency, component):
+    def set_bases(self, basis_list: List[int], start_time: int, frequency: float, component_name: str):
         """Method to set basis list for measurement component.
 
         Args:
             basis_list (List[int]): list of bases to measure in.
             start_time (int): time to start measurement.
             frequency (float): frequency with which to measure.
-            component (Entity): measurement component to edit (normally a QSDetector).
+            component_name (str): name of measurement component to edit (normally a QSDetector).
         """
 
+        component = self.components[component_name]
         encoding_type = component.encoding_type
         basis_start_time = start_time - 1e12 / (2 * frequency)
 
@@ -536,13 +590,13 @@ def set_bases(self, basis_list, start_time, frequency, component):
     def receive_message(self, src: str, msg: "Message") -> None:
         # signal to protocol that we've received a message
         for protocol in self.protocols:
-            if type(protocol) == msg.owner_type:
+            if type(protocol) == msg.protocol_type:
                 protocol.received_message(src, msg)
                 return
 
         # if we reach here, we didn't successfully receive the message in any protocol
         print(self.protocols)
-        raise Exception("Message received for unknown protocol '{}' on node {}".format(msg.owner_type, self.name))
+        raise Exception("Message received for unknown protocol '{}' on node {}".format(msg.protocol_type, self.name))
 
-    def receive_qubit(self, src: str, qubit) -> None:
-        self.qsdetector.get(qubit)
+    def get(self, photon: "Photon", **kwargs):
+        self.send_qubit(self.destination, photon)
diff --git a/src/topology/topology.py b/src/topology/topology.py
index 067f6a9e..a536e89a 100644
--- a/src/topology/topology.py
+++ b/src/topology/topology.py
@@ -15,7 +15,7 @@
 from ..components.optical_channel import QuantumChannel, ClassicalChannel
 
 
-class Topology():
+class Topology:
     """Class for generating network from configuration file.
 
     The topology class provides a simple interface for managing the nodes
diff --git a/src/utils/encoding.py b/src/utils/encoding.py
index 8ff82d84..cadafd4a 100644
--- a/src/utils/encoding.py
+++ b/src/utils/encoding.py
@@ -6,7 +6,8 @@
 Attributes:
     polarization (Dict[str, any]): defines the polarization encoding scheme, including the Z- and X-basis.
     time_bin (Dict[str, any]): defines the time bin encoding scheme, including the Z- and X-basis. Also defines the bin separation time.
-    single_atom (Dict[str, any]): defines the single atom memory scheme, including the Z-basis.
+    single_atom (Dict[str, any]): defines the emissive memory scheme, including the Z-basis.
+    absorptive (Dict[str, any]): defines the absorptive memory scheme, including the Z-basis.
 """
 
 from math import sqrt
@@ -25,9 +26,14 @@
      "bin_separation": 1400  # measured in ps
      }
 
-# single_atom must be copied by photon so the memory field can be overwritten 
+# single_atom must be copied by a memory object so the fidelity field can be overwritten
 single_atom = \
     {"name": "single_atom",
      "bases": [((complex(1), complex(0)), (complex(0), complex(1))), None],
-     "memory": None  # overwritten by photon
+     "raw_fidelity": 1
+     }
+
+absorptive = \
+    {"name": "absorptive",
+     "bases": [((complex(1), complex(0)), (complex(0), complex(1))), None]
      }


Test patch:
diff --git a/example/photon_test.py b/example/photon_test.py
new file mode 100644
index 00000000..22ddef48
--- /dev/null
+++ b/example/photon_test.py
@@ -0,0 +1,64 @@
+import time
+
+from numpy import sin, cos
+from numpy.random import random_sample
+from sequence.components.circuit import Circuit
+from sequence.kernel.quantum_manager import QuantumManagerKet
+from sequence.utils.quantum_state import QuantumState
+
+
+SAMPLE_SIZE = 10000
+
+basis = ((complex(1), complex(0)), (complex(0), complex(1)))
+qm = QuantumManagerKet()
+meas_circ = Circuit(1)
+meas_circ.measure(0)
+
+runtimes = []
+
+
+def timeit_wrapper(func):
+    def wrapper(*args, **kwargs):
+        start = time.perf_counter()
+        return_val = func(*args, **kwargs)
+        end = time.perf_counter()
+        runtimes.append(end - start)
+        return return_val
+    return wrapper
+
+
+class ParticleOld:
+    def __init__(self, state):
+        self.qs = QuantumState()
+        self.qs.state = state
+
+    def measure(self):
+        return self.qs.measure(basis)
+
+
+class ParticleNew:
+    def __init__(self, state):
+        self.qs_key = qm.new(state)
+
+    def measure(self):
+        return qm.run_circuit(meas_circ, [self.qs_key])
+
+
+if __name__ == "__main__":
+    random_angles = random_sample(SAMPLE_SIZE)
+    states = []
+    for theta in random_angles:
+        states.append((cos(theta), sin(theta)))
+
+    @timeit_wrapper
+    def create_and_measure(states, particle_type):
+        for s in states:
+            p = particle_type(s)
+            p.measure()
+
+    create_and_measure(states, ParticleOld)
+    create_and_measure(states, ParticleNew)
+    print("Old Runtime: {}".format(runtimes[0]))
+    print("New Runtime: {}".format(runtimes[1]))
+    print("Old Per Photon: {}".format(runtimes[0]/SAMPLE_SIZE))
+    print("New Per Photon: {}".format(runtimes[1]/SAMPLE_SIZE))
diff --git a/tests/app/test_random_request.py b/tests/app/test_random_request.py
index 77f38879..4a5e3529 100644
--- a/tests/app/test_random_request.py
+++ b/tests/app/test_random_request.py
@@ -2,6 +2,7 @@
 from sequence.kernel.timeline import Timeline
 from sequence.network_management.reservation import Reservation
 from sequence.topology.node import QuantumRouter
+from sequence.components.memory import MemoryArray
 
 
 class FakeNode(QuantumRouter):
@@ -14,6 +15,13 @@ def reserve_net_resource(self, responder: str, start_time: int, end_time: int, m
         self.reserve_log.append(responder)
 
 
+def get_memo_name(node):
+    for name in node.components.keys():
+        if type(node.components[name]) is MemoryArray:
+            return name
+    raise Exception("no memory array on node {}".format(node.name))
+
+
 def test_RandomRequestApp_update_last_rsvp_metrics():
     tl = Timeline()
     node = QuantumRouter("n1", tl)
@@ -98,33 +106,36 @@ def test_RandomRequestApp_get_memory():
 
     tl.run()
 
-    node.memory_array[0].entangled_memory["node_id"] = "n2"
-    node.memory_array[0].entangled_memory["memo_id"] = "1"
-    node.memory_array[0].fidelity = 0.9
-    node.resource_manager.update(None, node.memory_array[0], "ENTANGLED")
+    memo_name = get_memo_name(node)
+    memo_arr = node.components[memo_name]
+
+    memo_arr[0].entangled_memory["node_id"] = "n2"
+    memo_arr[0].entangled_memory["memo_id"] = "1"
+    memo_arr[0].fidelity = 0.9
+    node.resource_manager.update(None, memo_arr[0], "ENTANGLED")
     app.get_memory(node.resource_manager.memory_manager[0])
     assert node.resource_manager.memory_manager[0].state == "RAW"
-    assert node.memory_array[0].entangled_memory["node_id"] == None
-    assert node.memory_array[0].fidelity == 0
+    assert memo_arr[0].entangled_memory["node_id"] is None
+    assert memo_arr[0].fidelity == 0
 
-    node.memory_array[1].entangled_memory["node_id"] = "n3"
-    node.memory_array[1].entangled_memory["memo_id"] = "1"
-    node.memory_array[1].fidelity = 0.9
-    node.resource_manager.update(None, node.memory_array[1], "ENTANGLED")
+    memo_arr[1].entangled_memory["node_id"] = "n3"
+    memo_arr[1].entangled_memory["memo_id"] = "1"
+    memo_arr[1].fidelity = 0.9
+    node.resource_manager.update(None, memo_arr[1], "ENTANGLED")
     app.get_memory(node.resource_manager.memory_manager[1])
     assert node.resource_manager.memory_manager[1].state == "ENTANGLED"
 
-    node.memory_array[2].entangled_memory["node_id"] = "n2"
-    node.memory_array[2].entangled_memory["memo_id"] = "1"
-    node.memory_array[2].fidelity = 0.84
-    node.resource_manager.update(None, node.memory_array[2], "ENTANGLED")
+    memo_arr[2].entangled_memory["node_id"] = "n2"
+    memo_arr[2].entangled_memory["memo_id"] = "1"
+    memo_arr[2].fidelity = 0.84
+    node.resource_manager.update(None, memo_arr[2], "ENTANGLED")
     app.get_memory(node.resource_manager.memory_manager[2])
     assert node.resource_manager.memory_manager[2].state == "ENTANGLED"
 
-    node.memory_array[3].entangled_memory["node_id"] = "n2"
-    node.memory_array[3].entangled_memory["memo_id"] = "1"
-    node.memory_array[3].fidelity = 0.9
-    node.resource_manager.update(None, node.memory_array[3], "ENTANGLED")
+    memo_arr[3].entangled_memory["node_id"] = "n2"
+    memo_arr[3].entangled_memory["memo_id"] = "1"
+    memo_arr[3].fidelity = 0.9
+    node.resource_manager.update(None, memo_arr[3], "ENTANGLED")
     app.get_memory(node.resource_manager.memory_manager[3])
     assert node.resource_manager.memory_manager[3].state == "ENTANGLED"
 
diff --git a/tests/components/test_beam_splitter.py b/tests/components/test_beam_splitter.py
index 20f47c32..e93134a7 100644
--- a/tests/components/test_beam_splitter.py
+++ b/tests/components/test_beam_splitter.py
@@ -1,47 +1,51 @@
-import pytest
-from numpy import random
+import numpy as np
+
 from sequence.components.beam_splitter import BeamSplitter
 from sequence.components.photon import Photon
 from sequence.kernel.timeline import Timeline
 from sequence.utils.encoding import polarization
 
-random.seed(1)
+np.random.seed(0)
+SEED = 0
 
 
 def test_BeamSplitter_init():
     tl = Timeline()
     bs = BeamSplitter("bs", tl)
+    bs.add_receiver(None)
+    bs.add_receiver(None)
     tl.init()
 
 
-class Receiver():
+class Owner:
+    def __init__(self):
+        self.generator = np.random.default_rng(SEED)
+
+    def get_generator(self):
+        return self.generator
+
+
+class Receiver:
     def __init__(self, tl):
         self.timeline = tl
         self.log = []
 
-    def get(self):
+    def get(self, photon=None):
         self.log.append((self.timeline.now()))
 
-
-def test_BeamSplitter_set_receiver():
-    tl = Timeline()
-    bs = BeamSplitter("bs", tl)
-    receiver = Receiver(tl)
-    bs.set_receiver(0, receiver)
-    receiver = Receiver(tl)
-    bs.set_receiver(1, receiver)
-    receiver = Receiver(tl)
-    with pytest.raises(Exception):
-        bs.set_receiver(5, receiver)
+    def reset(self):
+        self.log = []
 
 
 def test_BeamSplitter_get():
     tl = Timeline()
     bs = BeamSplitter("bs", tl)
+    own = Owner()
+    bs.owner = own
     receiver0 = Receiver(tl)
-    bs.set_receiver(0, receiver0)
+    bs.add_receiver(receiver0)
     receiver1 = Receiver(tl)
-    bs.set_receiver(1, receiver1)
+    bs.add_receiver(receiver1)
 
     frequency = 8e7
     start_time = 0
@@ -58,15 +62,15 @@ def test_BeamSplitter_get():
     for i in range(basis_len):
         time = 1e12 / frequency * i
         tl.time = time
-        bit = random.randint(2)
+        bit = np.random.randint(2)
         bits.append(bit)
-        photon = Photon(str(i), quantum_state=polarization["bases"][0][bit])
+        photon = Photon(str(i), tl, quantum_state=polarization["bases"][0][bit])
         bs.get(photon)
 
     for i in range(basis_len):
         time = 1e12 / frequency * i
         r_i = bits[i]
-        assert time in bs.receivers[r_i].log
+        assert time in bs._receivers[r_i].log
 
     # x-basis states, measurement
     receiver0.log = []
@@ -81,15 +85,15 @@ def test_BeamSplitter_get():
     for i in range(basis_len):
         time = 1e12 / frequency * i
         tl.time = time
-        bit = random.randint(2)
+        bit = np.random.randint(2)
         bits2.append(bit)
-        photon = Photon(str(i), quantum_state=polarization["bases"][1][bit])
+        photon = Photon(str(i), tl, quantum_state=polarization["bases"][1][bit])
         bs.get(photon)
 
     for i in range(basis_len):
         time = 1e12 / frequency * i
         r_i = bits2[i]
-        assert time in bs.receivers[r_i].log
+        assert time in bs._receivers[r_i].log
 
     # z-basis states, x-basis measurement
     receiver0.log = []
@@ -104,9 +108,9 @@ def test_BeamSplitter_get():
     for i in range(basis_len):
         time = 1e12 / frequency * i
         tl.time = time
-        bit = random.randint(2)
+        bit = np.random.randint(2)
         bits.append(bit)
-        photon = Photon(str(i), quantum_state=polarization["bases"][0][bit])
+        photon = Photon(str(i), tl, quantum_state=polarization["bases"][0][bit])
         bs.get(photon)
 
     print(len(receiver1.log), len(receiver0.log))
@@ -114,8 +118,90 @@ def test_BeamSplitter_get():
     for i in range(basis_len):
         time = 1e12 / frequency * i
         r_i = bits[i]
-        if time in bs.receivers[r_i].log:
+        if time in bs._receivers[r_i].log:
             true_counter += 1
         else:
             false_counter += 1
     assert true_counter / basis_len - 0.5 < 0.1
+
+
+# def test_FockBeamSplitter_get():
+#     NUM_TRIALS = 1000
+#     psi_minus = [complex(0), complex(sqrt(1 / 2)), -complex(sqrt(1 / 2)), complex(0)]
+#
+#     tl = Timeline(formalism="density_matrix")
+#     rec_0 = Receiver(tl)
+#     rec_1 = Receiver(tl)
+#
+#     bs = FockBeamSplitter("bs", tl)
+#     bs.add_receiver(rec_0)
+#     bs.add_receiver(rec_1)
+#
+#     tl.init()
+#
+#     # measure unentangled
+#     for _ in range(NUM_TRIALS):
+#         p0 = Photon("", tl, encoding_type=absorptive, use_qm=True)
+#         p1 = Photon("", tl, encoding_type=absorptive, use_qm=True)
+#         p0.is_null = True
+#         bs.get(p0)
+#         bs.get(p1)
+#         tl.time += 1
+#
+#     assert abs(len(rec_0.log) / len(rec_1.log)) - 1 < 0.1
+#
+#     # measure entangled, no phase
+#     tl.time = 0
+#     rec_0.reset()
+#     rec_1.reset()
+#     for _ in range(NUM_TRIALS):
+#         p0 = Photon("", tl, encoding_type=absorptive, use_qm=True)
+#         p1 = Photon("", tl, encoding_type=absorptive, use_qm=True)
+#         p0.is_null = True
+#         p0.entangle(p1)
+#         p0.set_state(psi_minus)
+#         bs.get(p0)
+#         bs.get(p1)
+#         tl.time += 1
+#
+#     assert abs(len(rec_0.log) / len(rec_1.log)) - 1 < 0.1
+#
+#     # measure entangled, pi/2 phase
+#     tl.time = 0
+#     rec_0.reset()
+#     rec_1.reset()
+#     circuit = Circuit(1)
+#     circuit.phase(0, pi/2)
+#     for _ in range(NUM_TRIALS):
+#         p0 = Photon("", tl, encoding_type=absorptive, use_qm=True)
+#         p1 = Photon("", tl, encoding_type=absorptive, use_qm=True)
+#         p0.is_null = True
+#         p0.entangle(p1)
+#         p0.set_state(psi_minus)
+#         tl.quantum_manager.run_circuit(circuit, [p0.quantum_state])
+#
+#         bs.get(p0)
+#         bs.get(p1)
+#         tl.time += 1
+#
+#     assert len(rec_1.log) == NUM_TRIALS
+#
+#     # measure entangled, 3pi/2 phase
+#     tl.time = 0
+#     rec_0.reset()
+#     rec_1.reset()
+#     circuit = Circuit(1)
+#     circuit.phase(0, 3*pi/2)
+#     for _ in range(NUM_TRIALS):
+#         p0 = Photon("", tl, encoding_type=absorptive, use_qm=True)
+#         p1 = Photon("", tl, encoding_type=absorptive, use_qm=True)
+#         p0.is_null = True
+#         p0.entangle(p1)
+#         p0.set_state(psi_minus)
+#         tl.quantum_manager.run_circuit(circuit, [p0.quantum_state])
+#
+#         bs.get(p0)
+#         bs.get(p1)
+#         tl.time += 1
+#
+#     assert len(rec_0.log) == NUM_TRIALS
diff --git a/tests/components/test_bsm.py b/tests/components/test_bsm.py
index 74e79943..9a2f79c2 100644
--- a/tests/components/test_bsm.py
+++ b/tests/components/test_bsm.py
@@ -1,12 +1,16 @@
 import pytest
+import numpy as np
+
 from sequence.components.bsm import *
 from sequence.components.memory import *
 from sequence.components.circuit import Circuit
 from sequence.kernel.timeline import Timeline
+from sequence.kernel.process import Process
+from sequence.kernel.event import Event
 from sequence.utils.encoding import *
 
 
-class Parent():
+class Parent:
         def __init__(self):
             self.results = []
 
@@ -26,21 +30,18 @@ def test_construct_func():
 
     # unknown encoding scheme
     with pytest.raises(Exception):
-        bsm = make_bsm("bsm", tl, encoding_type="unknown",
-                       detectors=detectors4)
+        bsm = make_bsm("bsm", tl, encoding_type="unknown", detectors=detectors4)
 
     # implemented encoding schemes
-    polar_bsm = make_bsm("bsm1", tl, encoding_type="polarization",
-                         detectors=detectors4)
-    time_bin_bsm = make_bsm("bsm2", tl, encoding_type="time_bin",
-                            detectors=detectors2)
-    atom_bsm = make_bsm("bsm3", tl, encoding_type="single_atom",
-                        detectors=detectors2)
+    polar_bsm = make_bsm("bsm1", tl, encoding_type="polarization", detectors=detectors4)
+    time_bin_bsm = make_bsm("bsm2", tl, encoding_type="time_bin", detectors=detectors2)
+    atom_bsm = make_bsm("bsm3", tl, encoding_type="single_atom", detectors=detectors2)
 
     assert type(polar_bsm) == PolarizationBSM
     assert type(time_bin_bsm) == TimeBinBSM
     assert type(atom_bsm) == SingleAtomBSM
 
+
 def test_init():
     tl = Timeline()
     detectors = [{"dark_count": 1}] * 2
@@ -49,14 +50,15 @@ def test_init():
 
     assert len(tl.events) == len(detectors) * 2
 
+
 def test_base_get():
     tl = Timeline()
-    photon1 = Photon("", location=1)
-    photon2 = Photon("", location=2)
-    photon3 = Photon("", location=3)
-    photon1_2 = Photon("", location=1)
-    detectors = [{}] * 2
-    bsm = make_bsm("bsm", tl, encoding_type="time_bin", detectors=detectors)
+    photon1 = Photon("", tl, location=1)
+    photon2 = Photon("", tl, location=2)
+    photon3 = Photon("", tl, location=3)
+    photon1_2 = Photon("", tl, location=1)
+    detectors = [{}] * 4
+    bsm = make_bsm("bsm", tl, encoding_type="polarization", detectors=detectors)
 
     bsm.get(photon1)
     assert len(bsm.photons) == 1
@@ -74,6 +76,7 @@ def test_base_get():
     bsm.get(photon3)
     assert len(bsm.photons) == 1
 
+
 def test_polarization_get():
     tl = Timeline()
     detectors = [{"efficiency": 1}] * 4
@@ -82,8 +85,8 @@ def test_polarization_get():
     bsm.attach(parent)
 
     # get 2 photons in orthogonal states (map to Psi+)
-    p1 = Photon("p1", location=1, quantum_state=(complex(1), complex(0)))
-    p2 = Photon("p2", location=2, quantum_state=(complex(0), complex(1)))
+    p1 = Photon("p1", tl, location=1, quantum_state=(complex(1), complex(0)))
+    p2 = Photon("p2", tl, location=2, quantum_state=(complex(0), complex(1)))
     bsm.get(p1)
     bsm.get(p2)
 
@@ -91,8 +94,8 @@ def test_polarization_get():
 
     # get 2 photons in same state (map to Phi+ / can't measure)
     tl.time = 1e6
-    p3 = Photon("p3", location=1, quantum_state=(complex(1), complex(0)))
-    p4 = Photon("p4", location=2, quantum_state=(complex(1), complex(0)))
+    p3 = Photon("p3", tl, location=1, quantum_state=(complex(1), complex(0)))
+    p4 = Photon("p4", tl, location=2, quantum_state=(complex(1), complex(0)))
     bsm.get(p3)
     bsm.get(p4)
 
@@ -133,6 +136,7 @@ def test_polarization_update():
 
     assert len(parent.results) == 2
 
+
 def test_time_bin_get():
     tl = Timeline()
     detectors = [{"efficiency": 1, "count_rate": 1e9}] * 2
@@ -142,8 +146,8 @@ def test_time_bin_get():
     detector_list = bsm.detectors
 
     # get 2 photons in orthogonal states (map to Psi+)
-    p1 = Photon("p1", encoding_type=time_bin, location=1, quantum_state=(complex(1), complex(0)))
-    p2 = Photon("p2", encoding_type=time_bin, location=2, quantum_state=(complex(0), complex(1)))
+    p1 = Photon("p1", tl, encoding_type=time_bin, location=1, quantum_state=(complex(1), complex(0)))
+    p2 = Photon("p2", tl, encoding_type=time_bin, location=2, quantum_state=(complex(0), complex(1)))
     process = Process(bsm, "get", [p1])
     event = Event(0, process)
     tl.schedule(event)
@@ -155,8 +159,8 @@ def test_time_bin_get():
     assert len(parent.results) == 1
 
     # get 2 photons in same state (map to Phi+ / can't measure)
-    p3 = Photon("p3", encoding_type=time_bin, location=1, quantum_state=(complex(1), complex(0)))
-    p4 = Photon("p4", encoding_type=time_bin, location=2, quantum_state=(complex(1), complex(0)))
+    p3 = Photon("p3", tl, encoding_type=time_bin, location=1, quantum_state=(complex(1), complex(0)))
+    p4 = Photon("p4", tl, encoding_type=time_bin, location=2, quantum_state=(complex(1), complex(0)))
     process = Process(bsm, "get", [p3])
     event = Event(1e6, process)
     tl.schedule(event)
@@ -201,14 +205,15 @@ def test_time_bin_update():
 
     assert len(parent.results) == 2
 
+
 def test_single_atom_get():
     class PhotonSendWrapper():
         def __init__(self, mem1, mem2, bsm):
             self.bsm = bsm
-            mem1.owner = self
-            mem2.owner = self
+            mem1.add_receiver(self)
+            mem2.add_receiver(self)
 
-        def send_qubit(self, dst, photon):
+        def get(self, photon, **kwargs):
             self.bsm.get(photon)
 
     tl = Timeline()
@@ -219,7 +224,7 @@ def send_qubit(self, dst, photon):
     mem_1 = Memory("mem_1", tl, fidelity=1, frequency=0, efficiency=1, coherence_time=1, wavelength=500)
     mem_2 = Memory("mem_2", tl, fidelity=1, frequency=0, efficiency=1, coherence_time=1, wavelength=500)
 
-    pw = PhotonSendWrapper(mem_1, mem_2, bsm)
+    _ = PhotonSendWrapper(mem_1, mem_2, bsm)
 
     # initially opposite states
     tl.time = 0
@@ -245,3 +250,79 @@ def send_qubit(self, dst, photon):
     assert tl.quantum_manager.get(mem_1.qstate_key) is tl.quantum_manager.get(mem_2.qstate_key)
 
 
+def test_absorptive_get():
+    from sequence.components.detector import Detector
+
+    class Measurer:
+        def __init__(self, detector):
+            self.detector = detector
+            self.generator = np.random.default_rng(1)
+
+        def get(self, photon, **kwargs):
+            res = Photon.measure(None, photon, self.generator)
+            if res:
+                self.detector.get()
+
+    class DetectorMonitor:
+        def __init__(self, detector: Detector):
+            detector.attach(self)
+            self.times = []
+
+        def trigger(self, detector, info):
+            self.times.append(info["time"])
+
+    class RandomControl:
+        def __init__(self, seed):
+            self.generator = np.random.default_rng(seed)
+
+        def get_generator(self):
+            return self.generator
+
+    NUM_TRIALS = 1000
+    PERIOD = 1e6
+
+    tl = Timeline()
+    detectors = [{"efficiency": 1}] * 2
+    bsm = make_bsm("bsm", tl, encoding_type="absorptive", detectors=detectors)
+    random_control = RandomControl(0)
+    bsm.owner = random_control
+    d0 = Detector("d0", tl, 1)
+    d1 = Detector("d1", tl, 1)
+    m0 = Measurer(d0)
+    m1 = Measurer(d1)
+    monitor0 = DetectorMonitor(d0)
+    monitor1 = DetectorMonitor(d1)
+
+    tl.init()
+    photons0 = [None, None]
+    photons1 = [None, None]
+
+    # simulate sending photon pairs
+    for i in range(NUM_TRIALS):
+        tl.time = i * PERIOD
+
+        # pair 0 (null)
+        photons0[0] = Photon("", tl, encoding_type=absorptive, location=0, use_qm=True)
+        photons0[1] = Photon("", tl, encoding_type=absorptive, location=0, use_qm=True)
+        photons0[0].is_null = True
+        photons0[1].is_null = True
+        photons0[0].entangle(photons0[1])
+        photons0[0].set_state((complex(1), complex(0), complex(0), complex(0)))
+
+        # pair 1 (not null)
+        photons1[0] = Photon("", tl, encoding_type=absorptive, location=1, use_qm=True)
+        photons1[1] = Photon("", tl, encoding_type=absorptive, location=1, use_qm=True)
+        photons1[0].entangle(photons1[1])
+        photons1[0].set_state((complex(0), complex(0), complex(0), complex(1)))
+
+        # send part of each pair to bsm
+        bsm.get(photons0[0])
+        bsm.get(photons1[0])
+
+        # detect other part
+        m0.get(photons0[1])
+        m1.get(photons1[1])
+
+    assert len(set(monitor0.times) & set(monitor1.times)) == 0
+    assert len(monitor0.times + monitor1.times) == NUM_TRIALS
+    assert abs(len(monitor0.times)/len(monitor1.times) - 1) < 0.1
diff --git a/tests/components/test_circuit.py b/tests/components/test_circuit.py
index d603306d..ad98ccdc 100644
--- a/tests/components/test_circuit.py
+++ b/tests/components/test_circuit.py
@@ -1,3 +1,5 @@
+import numpy as np
+
 from sequence.components.circuit import Circuit
 from numpy import array, array_equal, identity
 from pytest import raises
@@ -78,6 +80,13 @@ def test_s():
     assert array_equal(expect, circuit.get_unitary_matrix())
 
 
+def test_phase():
+    circuit = Circuit(1)
+    circuit.phase(0, np.pi/2)
+    expect = array([[1, 0], [0, complex(0, 1)]])
+    assert array_equal(expect, circuit.get_unitary_matrix())
+
+
 def test_measure():
     qc = Circuit(1)
     assert len(qc.measured_qubits) == 0
@@ -130,14 +139,16 @@ def test_Circuit():
 
 def test_serialization():
     CIRCUIT_SIZE = 2
-    GATES = [["h", [0]], ["x", [1]]]
+    GATES = [["h", [0], None], ["x", [1], None], ["phase", [0], np.pi]]
     MEASURES = [0]
     circuit = Circuit(2)
-    for gate_type, gate_indices in GATES:
+    for gate_type, gate_indices, args in GATES:
         if gate_type == "h":
             circuit.h(gate_indices[0])
         elif gate_type == "x":
             circuit.x(gate_indices[0])
+        elif gate_type == "phase":
+            circuit.phase(gate_indices[0], args)
         else:
             raise NotImplementedError
     for index in MEASURES:
@@ -145,8 +156,8 @@ def test_serialization():
 
     serialized_info = circuit.serialize()
     assert serialized_info["size"] == CIRCUIT_SIZE
-    serailized_gates = [{"indices": gate_indices, "name": gate_type} for
-                        gate_type, gate_indices
+    serailized_gates = [{"arg": arg, "indices": gate_indices, "name": gate_type} for
+                        gate_type, gate_indices, arg
                         in GATES]
     assert serialized_info["gates"] == serailized_gates
     assert serialized_info["measured_qubits"] == MEASURES
diff --git a/tests/components/test_detector.py b/tests/components/test_detector.py
index f886a01d..71683e54 100644
--- a/tests/components/test_detector.py
+++ b/tests/components/test_detector.py
@@ -1,12 +1,22 @@
+from math import sqrt
+import numpy as np
+
 from sequence.components.detector import *
 from sequence.components.photon import Photon
 from sequence.kernel.timeline import Timeline
-from sequence.utils.encoding import polarization, time_bin
-import numpy as np
+from sequence.utils.encoding import polarization, time_bin, absorptive
+
+SEED = 0
+
+
+def clear_qsd_detectors(qsd):
+    for d in qsd.detectors:
+        d.next_detection_time = -1
+        d.photon_counter = 0
 
 
 def create_detector(efficiency=0.9, dark_count=0, count_rate=25e6, time_resolution=150):
-    class Parent():
+    class Parent:
         def __init__(self, tl):
             self.timeline = tl
             self.log = []
@@ -14,11 +24,20 @@ def __init__(self, tl):
         def trigger(self, detector, msg):
             self.log.append((self.timeline.now(), msg['time'], detector))
 
+    class Owner:
+        def __init__(self):
+            self.generator = np.random.default_rng(SEED)
+
+        def get_generator(self):
+            return self.generator
+
     tl = Timeline()
     detector = Detector("", tl, efficiency=efficiency, dark_count=dark_count,
                         count_rate=count_rate, time_resolution=time_resolution)
     parent = Parent(tl)
+    own = Owner()
     detector.attach(parent)
+    detector.owner = own
     return detector, parent, tl
 
 
@@ -143,8 +162,7 @@ def test_QSDetectorPolarization():
         tl.time = i * 1e12 / frequency
         basis = basis_list[i]
         bit = np.random.randint(2)
-        photon = Photon(str(i),
-                        quantum_state=polarization["bases"][basis][bit])
+        photon = Photon(str(i), tl, quantum_state=polarization["bases"][basis][bit])
         qsdetector.get(photon)
 
     trigger_times = qsdetector.get_photon_times()
@@ -167,7 +185,7 @@ def test_QSDetectorTimeBin():
         tl.time = i * 1e12 / frequency
         basis = basis_list[i]
         bit = np.random.randint(2)
-        photon = Photon(str(i), encoding_type=time_bin,
+        photon = Photon(str(i), tl, encoding_type=time_bin,
                         quantum_state=time_bin["bases"][basis][bit])
         qsdetector.get(photon)
 
@@ -177,3 +195,133 @@ def test_QSDetectorTimeBin():
     trigger_times = qsdetector.get_photon_times()
     length = len(trigger_times[0] + trigger_times[1] + trigger_times[2])
     assert abs(length / 1000 - 7 / 8) < 0.1
+
+
+def test_QSDetectorFockDirect():
+    NUM_TRIALS = 1000
+    COUNT_RATE = 80e6
+    period = (1e12 / COUNT_RATE) + 1
+    src_list = ["a", "b"]
+
+    tl = Timeline()
+
+    qsd = QSDetectorFockDirect("qsd", tl, src_list)
+    [qsd.update_detector_params(i, "efficiency", 1) for i in range(2)]
+    [qsd.update_detector_params(i, "count_rate", COUNT_RATE) for i in range(2)]
+
+    tl.init()
+
+    for _ in range(1000):
+        photon = Photon("", tl, encoding_type=absorptive, use_qm=True)
+        photon.set_state((0, 1))
+        qsd.get(photon, src=src_list[0])
+        tl.time += period
+
+    times = qsd.get_photon_times()
+    assert len(times[0]) == NUM_TRIALS
+
+    tl.time = 0
+    for _ in range(1000):
+        photon = Photon("", tl, encoding_type=absorptive, use_qm=True)
+        photon.set_state((0, 1))
+        qsd.get(photon, src=src_list[1])
+        tl.time += period
+
+    times = qsd.get_photon_times()
+    assert len(times[1]) == NUM_TRIALS
+
+
+def test_QSDetectorFockInterference():
+    class RandomControl:
+        def __init__(self, seed):
+            self.generator = np.random.default_rng(seed)
+
+        def get_generator(self):
+            return self.generator
+
+    NUM_TRIALS = 1000
+    COUNT_RATE = 80e6
+    period = (1e12 / COUNT_RATE) + 1
+    psi_minus = [complex(0), complex(sqrt(1 / 2)), -complex(sqrt(1 / 2)), complex(0)]
+
+    tl = Timeline(formalism="density_matrix")
+
+    qsd = QSDetectorFockInterference("qsd", tl)
+    random_control = RandomControl(0)
+    qsd.owner = random_control
+    [qsd.update_detector_params(i, "efficiency", 1) for i in range(2)]
+    [qsd.update_detector_params(i, "count_rate", COUNT_RATE) for i in range(2)]
+
+    tl.init()
+
+    # measure unentangled
+    for _ in range(NUM_TRIALS):
+        p0 = Photon("", tl, encoding_type=absorptive, use_qm=True)
+        qsd.get(p0)
+        tl.time += period
+
+    times = qsd.get_photon_times()
+    assert abs(len(times[0]) / len(times[1])) - 1 < 0.1
+
+    # measure unentangled, two incident real photons
+    tl.time = 0
+    clear_qsd_detectors(qsd)
+    for _ in range(NUM_TRIALS):
+        p0 = Photon("", tl, encoding_type=absorptive, use_qm=True)
+        p1 = Photon("", tl, encoding_type=absorptive, use_qm=True)
+        qsd.get(p0)
+        qsd.get(p1)
+        tl.time += period
+
+    times = qsd.get_photon_times()
+    assert abs(len(times[0]) / len(times[1])) - 1 < 0.1
+
+    # measure entangled, no phase
+    tl.time = 0
+    clear_qsd_detectors(qsd)
+    for _ in range(NUM_TRIALS):
+        p0 = Photon("", tl, encoding_type=absorptive, use_qm=True)
+        p1 = Photon("", tl, encoding_type=absorptive, use_qm=True)
+        p0.is_null = True
+        p0.entangle(p1)
+        p0.set_state(psi_minus)
+        qsd.get(p0)
+        qsd.get(p1)
+        tl.time += period
+
+    times = qsd.get_photon_times()
+    assert abs(len(times[0]) / len(times[1])) - 1 < 0.1
+
+    # measure entangled, pi/2 phase
+    tl.time = 0
+    clear_qsd_detectors(qsd)
+    qsd.set_phase(np.pi/2)
+    for _ in range(NUM_TRIALS):
+        p0 = Photon("", tl, encoding_type=absorptive, use_qm=True)
+        p1 = Photon("", tl, encoding_type=absorptive, use_qm=True)
+        p0.is_null = True
+        p0.entangle(p1)
+        p0.set_state(psi_minus)
+        qsd.get(p0)
+        qsd.get(p1)
+        tl.time += period
+
+    times = qsd.get_photon_times()
+    assert len(times[1]) == NUM_TRIALS
+
+    # measure entangled, 3pi/2 phase
+    tl.time = 0
+    clear_qsd_detectors(qsd)
+    qsd.set_phase(3*np.pi/2)
+    for _ in range(NUM_TRIALS):
+        p0 = Photon("", tl, encoding_type=absorptive, use_qm=True)
+        p1 = Photon("", tl, encoding_type=absorptive, use_qm=True)
+        p0.is_null = True
+        p0.entangle(p1)
+        p0.set_state(psi_minus)
+        qsd.get(p0)
+        qsd.get(p1)
+        tl.time += period
+
+    times = qsd.get_photon_times()
+    assert len(times[0]) == NUM_TRIALS
diff --git a/tests/components/test_fiber_stretcher.py b/tests/components/test_fiber_stretcher.py
new file mode 100644
index 00000000..e1fa6b70
--- /dev/null
+++ b/tests/components/test_fiber_stretcher.py
@@ -0,0 +1,54 @@
+import numpy as np
+
+from sequence.kernel.timeline import Timeline
+from sequence.components.photon import Photon
+from sequence.utils.encoding import absorptive
+from sequence.components.circuit import Circuit
+from sequence.components.fiber_stretcher import FiberStretcher
+
+
+def test_init():
+    tl = Timeline()
+    fs = FiberStretcher("fs", tl, np.pi)
+
+    fs_circ = fs._circuit.get_unitary_matrix()
+    desired = np.array([[complex(1), complex(0)],
+                        [complex(0), complex(-1)]])
+    assert np.array_equal(fs_circ, desired)
+
+
+def test_set_phase():
+    tl = Timeline()
+    fs = FiberStretcher("fs", tl)
+    fs.set_phase(np.pi/2)
+
+    fs_circ = fs._circuit.get_unitary_matrix()
+    desired = np.array([[complex(1), complex(0)],
+                        [complex(0), complex(0, 1)]])
+    assert np.array_equal(fs_circ, desired)
+
+
+def test_get():
+    class DumbReceiver:
+        def __init__(self):
+            pass
+
+        def get(self, photon, **kwargs):
+            pass
+
+    tl = Timeline()
+    photon = Photon("", tl, encoding_type=absorptive, use_qm=True)
+    fs = FiberStretcher("fs", tl, np.pi)
+    receiver = DumbReceiver()
+    fs.add_receiver(receiver)
+
+    # set photon to + state
+    circuit = Circuit(1)
+    circuit.h(0)
+    tl.quantum_manager.run_circuit(circuit, [photon.quantum_state])
+    # send photon through fiber stretcher
+    fs.get(photon)
+
+    photon_state = tl.quantum_manager.get(photon.quantum_state).state
+    desired = np.array([complex(np.sqrt(1/2)), complex(-np.sqrt(1/2))])
+    assert np.where(np.isclose(photon_state, desired))
diff --git a/tests/components/test_interferometer.py b/tests/components/test_interferometer.py
index 9755e6a1..5dc7aa9a 100644
--- a/tests/components/test_interferometer.py
+++ b/tests/components/test_interferometer.py
@@ -1,15 +1,24 @@
+import numpy as np
+
 from sequence.components.interferometer import Interferometer
 from sequence.components.photon import Photon
 from sequence.kernel.timeline import Timeline
 from sequence.utils.encoding import time_bin
-from numpy import random
 
 
 NUM_TRIALS = int(10e3)
+SEED = 0
 
 
 def create_intf(quantum_state):
-    class Receiver():
+    class Owner:
+        def __init__(self):
+            self.generator = np.random.default_rng(SEED)
+
+        def get_generator(self):
+            return self.generator
+
+    class Receiver:
         def __init__(self, name, timeline):
             self.name = name
             self.timeline = timeline
@@ -18,31 +27,24 @@ def __init__(self, name, timeline):
         def get(self):
             self.log.append(self.timeline.now())
 
-    class FakeOwner():
-        def __init__(self):
-            self.generator = random.default_rng(0)
-
-        def get_generator(self):
-            return self.generator
-
     tl = Timeline()
     intfm = Interferometer("interferometer", tl, time_bin["bin_separation"])
-    owner = FakeOwner()
-    intfm.owner = owner
-
     d0 = Receiver("d0", tl)
     d1 = Receiver("d1", tl)
-    intfm.set_receiver(0, d0)
-    intfm.set_receiver(1, d1)
+    own = Owner()
+    intfm.add_receiver(d0)
+    intfm.add_receiver(d1)
+    intfm.owner = own
     tl.init()
+
     for i in range(NUM_TRIALS):
         tl.time = i * 1e6
-        photon = Photon(str(i), quantum_state=quantum_state)
+        photon = Photon(str(i), tl, encoding_type=time_bin, quantum_state=quantum_state)
         intfm.get(photon)
     tl.time = 0
     tl.run()
 
-    return intfm.receivers[0].log, intfm.receivers[1].log
+    return intfm._receivers[0].log, intfm._receivers[1].log
 
 
 def test_Interferometer_get():
diff --git a/tests/components/test_light_source.py b/tests/components/test_light_source.py
index 5c36b18b..81661862 100644
--- a/tests/components/test_light_source.py
+++ b/tests/components/test_light_source.py
@@ -1,51 +1,40 @@
+from numpy import random
 from sequence.components.light_source import LightSource
-from sequence.components.optical_channel import QuantumChannel
 from sequence.kernel.timeline import Timeline
-from sequence.topology.node import Node
 from sequence.utils.encoding import polarization
 
+random.seed(0)
 
-class FakeNode(Node):
-    def __init__(self, name, tl, ls):
-        Node.__init__(self, name, tl)
-        self.lightsource = ls
-        self.lightsource.owner = self
 
+class Receiver:
+    def __init__(self, timeline):
+        self.timeline = timeline
+        self.log = []
 
-def test_light_source():
-    class Receiver(Node):
-        def __init__(self, name, tl):
-            Node.__init__(self, name, tl)
-            self.log = []
+    def get(self, photon):
+        self.log.append((self.timeline.now(), photon))
 
-        def receive_qubit(self, src: str, qubit) -> None:
-            self.log.append((self.timeline.now(), src, qubit))
 
+def test_light_source():
     tl = Timeline()
     FREQ, MEAN = 1e8, 0.1
     ls = LightSource("ls", tl, frequency=FREQ, mean_photon_num=MEAN)
-    sender = FakeNode("sender", tl, ls)
-    sender.set_seed(0)
-
-    assert sender.lightsource.frequency == FREQ and sender.lightsource.mean_photon_num == MEAN
+    receiver = Receiver(tl)
+    ls.add_receiver(receiver)
 
-    receiver = Receiver("receiver", tl)
-    qc = QuantumChannel("qc", tl, distance=1e5, attenuation=0)
-    qc.set_ends(sender, receiver.name)
     state_list = []
     STATE_LEN = 1000
     for _ in range(STATE_LEN):
-        basis = sender.get_generator().integers(2)
-        bit = sender.get_generator().integers(2)
+        basis = random.randint(2)
+        bit = random.randint(2)
         state_list.append(polarization["bases"][basis][bit])
 
     tl.init()
-    ls.emit(state_list, "receiver")
+    ls.emit(state_list)
     tl.run()
 
     assert (len(receiver.log) / STATE_LEN) - MEAN < 0.1
-    for time, src, qubit in receiver.log:
+    for time, qubit in receiver.log:
         index = int(qubit.name)
         assert state_list[index] == qubit.quantum_state.state
-        assert time == index * (1e12 / FREQ) + qc.delay
-        assert src == "sender"
+        assert time == index * (1e12 / FREQ)
diff --git a/tests/components/test_memory.py b/tests/components/test_memory.py
index e76f587c..505d692c 100644
--- a/tests/components/test_memory.py
+++ b/tests/components/test_memory.py
@@ -1,32 +1,60 @@
-from typing import Dict
 import numpy as np
 import math
 
-from sequence.components.memory import Memory, MemoryArray, MemoryWithRandomCoherenceTime
+from sequence.components.memory import *
 from sequence.kernel.event import Event
 from sequence.kernel.process import Process
 from sequence.kernel.timeline import Timeline
 from sequence.entanglement_management.entanglement_protocol import EntanglementProtocol
-from sequence.topology.node import QuantumRouter
-from sequence.message import Message
 
+SEED = 0
 
-class DumbReceiver():
+
+class DumbReceiver:
     def __init__(self):
         self.photon_list = []
 
-    def send_qubit(self, dst, photon):
+    def get(self, photon, **kwargs):
         self.photon_list.append(photon)
 
 
-class DumbParent():
+class DumbParent:
     def __init__(self, memory):
         memory.attach(self)
+        memory.add_receiver(self)
+        memory.owner = self
+        self.tl = memory.timeline
         self.pop_log = []
+        self.photon_list = []
+        self.photon_arrival_times = []
+        self.generator = np.random.default_rng(SEED)
 
     def memory_expire(self, memory):
         self.pop_log.append(memory)
 
+    def get(self, photon, **kwargs):
+        self.photon_list.append(photon)
+        self.photon_arrival_times.append(self.tl.now())
+
+    def reset(self):
+        self.photon_list = []
+        self.photon_arrival_times = []
+
+    def get_generator(self):
+        return self.generator
+
+
+class Owner:
+    def __init__(self):
+        self.generator = np.random.default_rng(SEED)
+
+    def get_generator(self):
+        return self.generator
+
+
+def perfect_efficiency(_):
+    return 1
+
 
 def test_MemoryArray_init():
     tl = Timeline()
@@ -38,11 +66,11 @@ def test_MemoryArray_init():
 
 
 def test_MemoryArray_expire():
-    class FakeNode(QuantumRouter):
+    class FakeNode:
         def __init__(self, tl):
-            super().__init__("fake", tl)
+            self.timeline = tl
             self.ma = MemoryArray("ma", tl)
-            self.ma.set_node(self)
+            self.ma.owner = self
             self.is_expired = False
             self.expired_memory = -1
 
@@ -67,7 +95,7 @@ def test_Memory_update_state():
     mem.update_state(new_state)
     
     assert len(tl.quantum_manager.states) == 1
-    assert (tl.quantum_manager.get(mem.qstate_key) == np.array(new_state)).all
+    assert np.all(tl.quantum_manager.get(mem.qstate_key).state == np.array(new_state))
 
 
 def test_Memory_excite():
@@ -75,8 +103,10 @@ def test_Memory_excite():
 
     tl = Timeline()
     rec = DumbReceiver()
+    own = Owner()
     mem = Memory("mem", tl, fidelity=1, frequency=0, efficiency=1, coherence_time=-1, wavelength=500)
-    mem.owner = rec
+    mem.add_receiver(rec)
+    mem.owner = own
 
     # test with perfect efficiency
 
@@ -84,9 +114,8 @@ def test_Memory_excite():
         mem.excite()
 
     assert len(rec.photon_list) == NUM_TESTS
-    null_photons = [p for p in rec.photon_list if p.is_null]
-    null_ratio = len(null_photons) / NUM_TESTS
-    assert null_ratio == 1
+    for p in rec.photon_list:
+        assert p.loss == 0
 
     # test with imperfect efficiency
 
@@ -138,7 +167,7 @@ def expire(self, memory: Memory):
     mem.detach(parent)
     assert len(parent.pop_log) == 0 and protocol.is_expire is False
     mem.expire()
-    assert (tl.quantum_manager.get(mem.qstate_key).state == np.array([1, 0])).all  # check if collapsed to |0> state
+    assert np.all(tl.quantum_manager.get(mem.qstate_key).state == np.array([1, 0]))  # check if collapsed to |0> state
     assert mem.entangled_memory == {"node_id": None, "memo_id": None}
     assert len(parent.pop_log) == 0 and protocol.is_expire is True
 
@@ -169,7 +198,138 @@ def test_Memory__schedule_expiration():
         if event.is_invalid():
             counter += 1
     assert counter == 1
-    
+
+
+def test_Absorptive_prepare():
+    PREPARE_TIME = 100
+    tl = Timeline()
+    mem = AbsorptiveMemory("mem", tl, 1, 80e6, 1, perfect_efficiency, 100, 0, 500, 0, PREPARE_TIME)
+
+    tl.init()
+    mem.prepare()
+    assert len(tl.events) == 1
+    event = tl.events.data[0]
+    assert event.time == PREPARE_TIME
+    process = event.process
+    assert process.owner is mem
+    assert process.activation == "_prepare_AFC"
+
+    tl.run()
+    assert mem.is_prepared
+
+
+def test_Absorptive_get_all_bins():
+    PERIOD = 1
+    MODE_NUM = 100
+    tl = Timeline()
+    mem = AbsorptiveMemory("mem", tl, 1, 1e12/PERIOD, 1, perfect_efficiency, MODE_NUM, 0, 500, 0, 100)
+    mem.is_prepared = True
+    tl.init()
+
+    # get first photon
+    photon = Photon("", tl, 500)
+    mem.get(photon)
+    assert mem.absorb_start_time == 0
+    stored = mem.stored_photons[0]
+    assert stored is not None
+    assert stored["photon"] is photon
+
+    # get other photons
+    for i in range(1, MODE_NUM):
+        tl.time += PERIOD
+        photon = Photon("", tl, 500)
+        mem.get(photon)
+        stored = mem.stored_photons[i]
+        assert stored is not None
+        assert stored["photon"] is photon
+
+    assert mem.photon_counter == MODE_NUM
+
+
+def test_Absorpive_get_skip_bins():
+    PERIOD = 1
+    tl = Timeline()
+    mem = AbsorptiveMemory("mem", tl, 1, 1e12/PERIOD, 1, perfect_efficiency, 100, 0, 500, 0, 100)
+    mem.is_prepared = True
+    tl.init()
+
+    # get first photon
+    photon = Photon("", tl, 500)
+    mem.get(photon)
+
+    # get second after two periods
+    tl.time += 2 * PERIOD
+    photon = Photon("", tl, 500)
+    mem.get(photon)
+    assert mem.stored_photons[1] is None
+    stored = mem.stored_photons[2]
+    assert stored is not None
+    assert stored["photon"] is photon
+
+
+def test_Absorptive_retrieve():
+    PERIOD = 1
+    MODE_NUM = 100
+    tl = Timeline()
+    mem = AbsorptiveMemory("mem", tl, 1, 1e12/PERIOD, 1, perfect_efficiency, MODE_NUM, 0, 500, 0, 100)
+    parent = DumbParent(mem)
+    mem.is_prepared = True
+    tl.init()
+
+    mem.absorb_start_time = 0
+    photons = [None] * MODE_NUM
+    for i in range(MODE_NUM):
+        photon = Photon(str(i), tl, 500)
+        photons[i] = photon
+        mem.stored_photons[i] = {"photon": photon, "time": i}
+        mem.excited_photons.append(photon)
+
+    # retrieve photons in forward order
+    mem.retrieve()
+    assert len(tl.events) == MODE_NUM
+    tl.run()
+    assert len(parent.photon_list) == MODE_NUM
+    for i, time in enumerate(parent.photon_arrival_times):
+        assert i == time
+    for i, photon in enumerate(parent.photon_list):
+        assert photons[i] == photon
+
+    # retrieve photons in reverse order
+    mem.is_prepared = True
+    mem.is_reversed = True
+    parent.reset()
+    tl.time = 0
+    mem.retrieve()
+    assert len(tl.events) == MODE_NUM
+    tl.run()
+    assert len(parent.photon_list) == MODE_NUM
+    photon_list = parent.photon_list[:]
+    photon_list.reverse()
+    for i, time in enumerate(parent.photon_arrival_times):
+        assert i == time
+    for i, photon in enumerate(photon_list):
+        assert photons[i] == photon
+
+
+def test_Absorptive_expire():
+    tl = Timeline()
+    mem = AbsorptiveMemory("mem", tl, 1, 80e6, 1, perfect_efficiency, 100, 0, 500, 0, 100)
+    parent = DumbParent(mem)
+
+    process = Process(mem, "expire", [])
+    event = Event(1e12, process)
+    tl.schedule(event)
+    mem.expiration_event = event
+
+    mem._schedule_expiration()
+
+    counter = 0
+    for event in tl.events:
+        if event.is_invalid():
+            counter += 1
+    assert counter == 1
+
+
 def test_MemoryWithRandomCoherenceTime__schedule_expiration():
     NUM_TRIALS = 200
     coherence_period_avg = 1
@@ -182,11 +342,11 @@ def test_MemoryWithRandomCoherenceTime__schedule_expiration():
     
     times_of_expiration_calculated = [0]
     np.random.seed(2)
-    for i in range( NUM_TRIALS ):
-        times_of_expiration_calculated.append( times_of_expiration_calculated[-1]
-                                              + int(mem.coherence_time_distribution()*1e12) )
+    for i in range(NUM_TRIALS):
+        times_of_expiration_calculated.append(times_of_expiration_calculated[-1]
+                                              + int(mem.coherence_time_distribution()*1e12))
     times_of_expiration_calculated.pop(0)
-    
+
     np.random.seed(2)
     process = Process(mem, "update_state", [[complex(math.sqrt(1/2)), complex(math.sqrt(1/2))]])
     for i in range(NUM_TRIALS):
@@ -197,17 +357,17 @@ def test_MemoryWithRandomCoherenceTime__schedule_expiration():
         assert times_of_expiration_calculated[i] == tl.now()
         
     period_sum = times_of_expiration_calculated[0]
-    period_squared_sum = times_of_expiration_calculated[0]**2
-    for i in range( 1, len( times_of_expiration_calculated ) ):
-        period = times_of_expiration_calculated[i]-times_of_expiration_calculated[i-1]
+    period_squared_sum = times_of_expiration_calculated[0] ** 2
+    for i in range(1, len(times_of_expiration_calculated)):
+        period = times_of_expiration_calculated[i] - times_of_expiration_calculated[i-1]
         period_sum += period
         period_squared_sum += period*period
     
     avg_simulated = period_sum / NUM_TRIALS * 1e-12
-    stdev_simulated = np.sqrt( ( period_squared_sum - period_sum * period_sum * 1.0/NUM_TRIALS ) / NUM_TRIALS )*1e-12
+    stdev_simulated = np.sqrt((period_squared_sum - period_sum * period_sum * 1.0/NUM_TRIALS) / NUM_TRIALS) * 1e-12
 
     #check that values in series are different
     assert stdev_simulated > 0.0
     #probability of error below is less then 0.3%
-    assert abs( avg_simulated - coherence_period_avg ) < 3 * coherence_period_stdev / np.sqrt( NUM_TRIALS )
+    assert abs(avg_simulated - coherence_period_avg) < 3 * coherence_period_stdev / np.sqrt(NUM_TRIALS)
 
diff --git a/tests/components/test_mirror.py b/tests/components/test_mirror.py
index 9ffa334f..06db202a 100644
--- a/tests/components/test_mirror.py
+++ b/tests/components/test_mirror.py
@@ -15,6 +15,10 @@ def __init__(self, name, timeline, light_source):
         super().__init__(name, timeline)
         self.light_source = light_source
         self.light_source.owner = self
+        light_source.add_receiver(self)
+
+    def get(self, photon, **kwargs):
+        self.send_qubit("mid", photon)
 
 
 class MiddleNode(Node):
@@ -69,7 +73,7 @@ def test_mirror():
         state_list.append(polarization["bases"][basis][bit])
 
     tl.init()
-    sender.light_source.emit(state_list, "mid")
+    sender.light_source.emit(state_list)
     tl.run()
 
     assert abs((len(receiver.log) / STATE_LEN) - (MEAN * FIDELITY)) < 0.1
diff --git a/tests/components/test_optical_channel.py b/tests/components/test_optical_channel.py
index 38a52418..c0be381e 100644
--- a/tests/components/test_optical_channel.py
+++ b/tests/components/test_optical_channel.py
@@ -1,8 +1,11 @@
-from typing import List
+import numpy as np
+
 from sequence.components.optical_channel import *
 from sequence.kernel.timeline import Timeline
 from sequence.topology.node import Node
 
+SEED = 0
+
 
 def test_ClassicalChannel_set_ends():
     tl = Timeline()
@@ -32,9 +35,8 @@ def receive_message(self, src: str, msg: str):
     tl = Timeline()
     cc = ClassicalChannel("cc", tl, 1e3)
 
-    n1, n2 = [FakeNode(f'n{i}', tl) for i in range(1, 3)]
-    for node in [n1, n2]:
-        node.set_seed(0)
+    n1 = FakeNode('n1', tl)
+    n2 = FakeNode('n2', tl)
     cc.set_ends(n1, n2.name)
 
     args = [['1-1', n1, 5], ['1-2', n1, 5]]
@@ -60,9 +62,8 @@ def test_QuantumChannel_init():
 def test_QuantumChannel_set_ends():
     tl = Timeline()
     qc = QuantumChannel("qc", tl, attenuation=0.0002, distance=1e4)
-    end1, end2 = [Node(f'n{i}', tl) for i in range(1, 3)]
-    for node in [end1, end2]:
-        node.set_seed(0)
+    end1 = Node("end1", tl)
+    end2 = Node("end2", tl)
     assert len(end1.qchannels) == len(end2.qchannels) == 0
     qc.set_ends(end1, end2.name)
 
@@ -80,10 +81,14 @@ class FakeNode(Node):
         def __init__(self, name, tl):
             Node.__init__(self, name, tl)
             self.log = []
+            self.generator = np.random.default_rng(SEED)
 
         def receive_qubit(self, src, photon):
             self.log.append((src, self.timeline.now(), photon.name))
 
+        def get_generator(self):
+            return self.generator
+
     tl = Timeline()
     qc = QuantumChannel("qc", tl, attenuation=0.0002, distance=1e4)
     sender = FakeNode("sender", tl)
@@ -94,7 +99,7 @@ def receive_qubit(self, src, photon):
     tl.init()
 
     for i in range(1000):
-        photon = Photon(str(i))
+        photon = Photon(str(i), tl)
         qc.transmit(photon, sender)
         tl.time = tl.time + 1
 
diff --git a/tests/components/test_photon.py b/tests/components/test_photon.py
index 3878235e..75f37551 100644
--- a/tests/components/test_photon.py
+++ b/tests/components/test_photon.py
@@ -1,18 +1,22 @@
-import pytest
 import numpy as np
+
+from sequence.kernel.timeline import Timeline
 from sequence.components.photon import Photon
 
 rng = np.random.default_rng()
 
 
 def test_init():
-    photon = Photon("")
+    tl = Timeline()
+    photon = Photon("", tl)
     
     assert photon.quantum_state.state == (complex(1), complex(0))
 
+
 def test_entangle():
-    photon1 = Photon("p1")
-    photon2 = Photon("p2")
+    tl = Timeline()
+    photon1 = Photon("p1", tl)
+    photon2 = Photon("p2", tl)
     photon1.entangle(photon2)
 
     state1 = photon1.quantum_state
@@ -25,25 +29,31 @@ def test_entangle():
     assert state1.entangled_states == state2.entangled_states
     assert state1.entangled_states == [state1, state2]
 
+
 def test_set_state():
-    photon = Photon("")
+    tl = Timeline()
+    photon = Photon("", tl)
     test_state = (complex(0), complex(1))
     photon.set_state(test_state)
 
     for i, coeff in enumerate(photon.quantum_state.state):
         assert coeff == test_state[i]
 
+
 def test_measure():
-    photon1 = Photon("p1", quantum_state=(complex(1), complex(0)))
-    photon2 = Photon("p2", quantum_state=(complex(0), complex(1)))
+    tl = Timeline()
+    photon1 = Photon("p1", tl, quantum_state=(complex(1), complex(0)))
+    photon2 = Photon("p2", tl, quantum_state=(complex(0), complex(1)))
     basis = ((complex(1), complex(0)), (complex(0), complex(1)))
 
     assert Photon.measure(basis, photon1, rng) == 0
     assert Photon.measure(basis, photon2, rng) == 1
 
+
 def test_measure_multiple():
-    photon1 = Photon("p1")
-    photon2 = Photon("p2")
+    tl = Timeline()
+    photon1 = Photon("p1", tl)
+    photon2 = Photon("p2", tl)
     photon1.entangle(photon2)
 
     basis = ((complex(1), complex(0), complex(0), complex(0)),
@@ -53,8 +63,10 @@ def test_measure_multiple():
 
     assert Photon.measure_multiple(basis, [photon1, photon2], rng) == 0
 
+
 def test_add_loss():
-    photon = Photon("", encoding_type={"name": "single_atom"})
+    tl = Timeline()
+    photon = Photon("", tl, encoding_type={"name": "single_atom"})
     assert photon.loss == 0
 
     photon.add_loss(0.5)
diff --git a/tests/components/test_switch.py b/tests/components/test_switch.py
index a518e2cf..328cbdd2 100644
--- a/tests/components/test_switch.py
+++ b/tests/components/test_switch.py
@@ -8,27 +8,26 @@
 FREQ = 1e6
 
 
-def create_switch(basis_list, photons):
-    class Receiver():
+def create_switch(tl, name, basis_list, photons):
+    class Receiver:
         def __init__(self, tl):
             self.timeline = tl
             self.log = []
 
-        def get(self, photon=None):
+        def get(self, photon):
             self.log.append((self.timeline.now(), photon))
 
-    tl = Timeline()
-    sw = Switch("sw", tl)
+    sw = Switch(name, tl)
     r1 = Receiver(tl)
     r2 = Receiver(tl)
-    sw.set_detector(r1)
-    sw.set_interferometer(r2)
+    sw.add_receiver(r1)
+    sw.add_receiver(r2)
     sw.set_basis_list(basis_list, 0, FREQ)
 
     tl.init()
     for i, photon in enumerate(photons):
-        tl.time = 1e12 / FREQ * i
-        sw.get(photons[i])
+        tl.time = (1e12 / FREQ) * i
+        sw.get(photon)
     tl.time = 0
     tl.run()
 
@@ -36,25 +35,24 @@ def get(self, photon=None):
 
 
 def test_Switch_get():
+    tl = Timeline()
     # z-basis measure |e> and |l>
-    photons = [Photon('', encoding_type=time_bin, quantum_state=time_bin["bases"][0][i]) for i in range(2)]
-    log1, log2 = create_switch([0] * 2, photons)
+    photons = [Photon('', tl, encoding_type=time_bin, quantum_state=time_bin["bases"][0][i]) for i in range(2)]
+    log1, log2 = create_switch(tl, "sw1", [0] * 2, photons)
     expects = [0, 1e12 / FREQ + time_bin["bin_separation"]]
     for i, log in enumerate(log1):
-        time, photon = log
+        time = log[0]
         assert time == expects[i]
-        assert photon is None
     assert len(log2) == 0
 
     # z-basis measure |e+l> and |e-l>
-    photons = [Photon('', encoding_type=time_bin, quantum_state=time_bin["bases"][0][random.randint(2)]) for _ in
+    photons = [Photon('', tl, encoding_type=time_bin, quantum_state=time_bin["bases"][0][random.randint(2)]) for _ in
                range(2000)]
-    log1, log2 = create_switch([0] * 2000, photons)
+    log1, log2 = create_switch(tl, "sw2", [0] * 2000, photons)
     assert len(log2) == 0
     counter1 = 0
     counter2 = 0
     for time, photon in log1:
-        assert photon is None
         time = time % (1e12 / FREQ)
         if time == 0:
             counter1 += 1
@@ -65,10 +63,10 @@ def test_Switch_get():
     assert abs(counter2 / counter1 - 1) < 0.1
 
     # x-basis get photons
-    photons = [Photon('',
+    photons = [Photon('', tl,
                       encoding_type=time_bin,
                       quantum_state=time_bin["bases"][random.randint(2)][random.randint(2)]) for _ in range(2000)]
-    log1, log2 = create_switch([1] * 2000, photons)
+    log1, log2 = create_switch(tl, "sw3", [1] * 2000, photons)
     assert len(log1) == 0
     for time, photon in log1:
         time = time % (1e12 / FREQ)
diff --git a/tests/entanglement_management/test_generation.py b/tests/entanglement_management/test_generation.py
index 408a109a..bf2f7117 100644
--- a/tests/entanglement_management/test_generation.py
+++ b/tests/entanglement_management/test_generation.py
@@ -8,7 +8,7 @@
 from sequence.topology.node import Node
 
 
-class ResourceManager():
+class ResourceManager:
     def __init__(self):
         self.log = []
 
@@ -16,11 +16,24 @@ def update(self, protocol, memory, state):
         self.log.append((memory, state))
 
 
-class FakeNode(Node):
+class FakeRouter(Node):
     def __init__(self, name, tl, **kwargs):
-        Node.__init__(self, name, tl)
-        self.msg_log = []
+        super().__init__(name, tl)
         self.resource_manager = ResourceManager()
+        self.memory_array = None
+
+    def init(self):
+        self.memory_array.add_receiver(self)
+
+    def get(self, photon, **kwargs):
+        dst = kwargs["dst"]
+        self.send_qubit(dst, photon)
+
+
+class FakeBSMNode(Node):
+    def __init__(self, name, tl, **kwargs):
+        super().__init__(name, tl)
+        self.msg_log = []
 
     def receive_message(self, src: str, msg: "Message"):
         self.msg_log.append((self.timeline.now(), src, msg))
@@ -41,23 +54,21 @@ def test_generation_message():
 def test_generation_receive_message():
     tl = Timeline()
     node = Node("e1", tl)
-    m0 = FakeNode("m1", tl)
+    m0 = FakeBSMNode("m1", tl)
     qc = QuantumChannel("qc_nodem1", tl, 0, 1e3)
     qc.frequency = 1e12
     qc.set_ends(node, m0.name)
     node.memory_array = MemoryArray("memory", tl)
     node.assign_cchannel(ClassicalChannel("cc", tl, 0, delay=1), "m1")
 
-    eg = EntanglementGenerationA(node, "EG", middle="m1", other="e2",
-                                 memory=node.memory_array[0])
+    eg = EntanglementGenerationA(node, "EG", middle="m1", other="e2", memory=node.memory_array[0])
     eg.qc_delay = 1
 
     # negotiate message
-    msg = EntanglementGenerationMessage(GenerationMsgType.NEGOTIATE_ACK, "EG",
-                                        emit_time=0)
+    msg = EntanglementGenerationMessage(GenerationMsgType.NEGOTIATE_ACK, "EG", emit_time=0)
     eg.received_message("e2", msg)
     assert eg.expected_time == 1
-    assert len(tl.events.data) == 2  # emit event and start/update_memory event
+    assert len(tl.events.data) == 2  # two excites, flip state, end time
 
 
 def test_generation_pop():
@@ -81,17 +92,14 @@ def send_message(self, destination, message):
 
     # BSM result
     middle.bsm_update(m0.bsm, {'info_type': "BSM_res", 'res': 0, 'time': 100})
-
     assert len(m0.messages) == 2
     assert m0.messages[0][0] == "e0"
     assert m0.messages[1][0] == "e1"
-    assert m0.messages[0][1].msg_type \
-           == m0.messages[1][1].msg_type \
-           == GenerationMsgType.MEAS_RES
+    assert m0.messages[0][1].msg_type == m0.messages[1][1].msg_type == GenerationMsgType.MEAS_RES
 
 
 def test_generation_expire():
-    class DumbBSM():
+    class DumbBSM:
         def __init__(self):
             pass
 
@@ -99,9 +107,9 @@ def get(self, qubit):
             pass
 
     tl = Timeline(1e12)
-    e0 = Node("e0", tl)
-    e1 = Node("e1", tl)
-    m0 = FakeNode("m0", tl)
+    e0 = FakeRouter("e0", tl)
+    e1 = FakeRouter("e1", tl)
+    m0 = FakeBSMNode("m0", tl)
 
     qc0 = QuantumChannel("qc_e0m0", tl, 0, 1e3)
     qc1 = QuantumChannel("qc_e1m0", tl, 0, 1e3)
@@ -123,11 +131,8 @@ def get(self, qubit):
 
     tl.init()
 
-    protocol0 = EntanglementGenerationA(e0, "e0prot", middle="m0", other="e1",
-                                        memory=e0.memory_array[0])
-    protocol1 = EntanglementGenerationA(e1, "e1prot", middle="m0", other="e0",
-                                        memory=e1.memory_array[0])
-    protocol0.primary = True
+    protocol0 = EntanglementGenerationA(e0, "e0prot", middle="m0", other="e1", memory=e0.memory_array[0])
+    protocol1 = EntanglementGenerationA(e1, "e1prot", middle="m0", other="e0", memory=e1.memory_array[0])
     e0.protocols.append(protocol0)
     e1.protocols.append(protocol1)
     protocol0.set_others(protocol1.name, e1.name, [e1.memory_array[0].name])
@@ -138,19 +143,20 @@ def get(self, qubit):
         event = Event(0, process)
         tl.schedule(event)
 
+    tl.init()
     tl.run()
 
     assert e0.memory_array[0].expiration_event.time > 1e12
 
 
 def test_generation_run():
-    NUM_TESTS = 500
+    NUM_TESTS = 100
 
     tl = Timeline()
 
-    e0 = FakeNode("e0", tl)
-    m0 = FakeNode("m0", tl)
-    e1 = FakeNode("e1", tl)
+    e0 = FakeRouter("e0", tl)
+    m0 = FakeBSMNode("m0", tl)
+    e1 = FakeRouter("e1", tl)
     e0.set_seed(0)
     m0.set_seed(1)
     e1.set_seed(2)
@@ -169,15 +175,13 @@ def test_generation_run():
                 cc.set_ends(src, dst.name)
 
     # add hardware
-    e0.memory_array = MemoryArray("e0.memory_array", tl,
-                                  num_memories=NUM_TESTS)
+    e0.memory_array = MemoryArray("e0.memory_array", tl, num_memories=NUM_TESTS)
     e0.memory_array.owner = e0
-    e1.memory_array = MemoryArray("e1.memory_array", tl,
-                                  num_memories=NUM_TESTS)
+    e1.memory_array = MemoryArray("e1.memory_array", tl, num_memories=NUM_TESTS)
     e1.memory_array.owner = e1
     detectors = [{"efficiency": 1, "count_rate": 1e11}] * 2
-    m0.bsm = make_bsm("m0.bsm", tl, encoding_type="single_atom",
-                      detectors=detectors)
+    m0.bsm = make_bsm("m0.bsm", tl, encoding_type="single_atom", detectors=detectors)
+    m0.bsm.owner = m0
 
     # add middle protocol
     eg_m0 = EntanglementGenerationB(m0, "eg_m0", others=["e0", "e1"])
@@ -190,18 +194,14 @@ def test_generation_run():
 
     for i in range(NUM_TESTS):
         name0, name1 = [f"eg_e{j}[{i}]" for j in range(2)]
-        protocol0 = EntanglementGenerationA(e0, name0, middle="m0", other="e1",
-                                            memory=e0.memory_array[i])
+        protocol0 = EntanglementGenerationA(e0, name0, middle="m0", other="e1", memory=e0.memory_array[i])
         e0.protocols.append(protocol0)
         protocols_e0.append(protocol0)
-        protocol1 = EntanglementGenerationA(e1, name1, middle="m0", other="e0",
-                                            memory=e1.memory_array[i])
+        protocol1 = EntanglementGenerationA(e1, name1, middle="m0", other="e0", memory=e1.memory_array[i])
         e1.protocols.append(protocol1)
         protocols_e1.append(protocol1)
-        protocol0.set_others(protocol1.name, e1.name,
-                             [e1.memory_array[i].name])
-        protocol1.set_others(protocol0.name, e0.name,
-                             [e0.memory_array[i].name])
+        protocol0.set_others(protocol1.name, e1.name, [e1.memory_array[i].name])
+        protocol1.set_others(protocol0.name, e0.name, [e0.memory_array[i].name])
 
         for protocol in [protocols_e0[i], protocols_e1[i]]:
             process = Process(protocol, "start", [])
@@ -210,6 +210,8 @@ def test_generation_run():
 
     tl.run()
 
+    assert len(e0.resource_manager.log) == NUM_TESTS
+    assert len(e1.resource_manager.log) == NUM_TESTS
     empty_count = 0
     for i in range(NUM_TESTS):
         if e0.resource_manager.log[i][1] == "RAW":
@@ -228,16 +230,16 @@ def test_generation_run():
     
 
 def test_generation_fidelity_ket():
-    NUM_TESTS = 1000
+    NUM_TESTS = 100
     FIDELITY = 0.75
 
     tl = Timeline()
 
-    e0 = FakeNode("e0", tl)
+    e0 = FakeRouter("e0", tl)
+    m0 = FakeBSMNode("m0", tl)
+    e1 = FakeRouter("e1", tl)
     e0.set_seed(0)
-    m0 = FakeNode("m0", tl)
     m0.set_seed(1)
-    e1 = FakeNode("e1", tl)
     e1.set_seed(2)
 
     # add connections
@@ -254,15 +256,12 @@ def test_generation_fidelity_ket():
                 cc.set_ends(n1, n2.name)
 
     # add hardware
-    e0.memory_array = MemoryArray("e0.memory_array", tl, fidelity=FIDELITY,
-                                  num_memories=NUM_TESTS)
+    e0.memory_array = MemoryArray("e0.memory_array", tl, fidelity=FIDELITY, num_memories=NUM_TESTS)
     e0.memory_array.owner = e0
-    e1.memory_array = MemoryArray("e1.memory_array", tl, fidelity=FIDELITY,
-                                  num_memories=NUM_TESTS)
+    e1.memory_array = MemoryArray("e1.memory_array", tl, fidelity=FIDELITY, num_memories=NUM_TESTS)
     e1.memory_array.owner = e1
     detectors = [{"efficiency": 1, "count_rate": 1e11}] * 2
-    m0.bsm = make_bsm("m0.bsm", tl, encoding_type="single_atom",
-                      detectors=detectors)
+    m0.bsm = make_bsm("m0.bsm", tl, encoding_type="single_atom", detectors=detectors)
     m0.bsm.owner = m0
 
     # add middle protocol
@@ -277,18 +276,14 @@ def test_generation_fidelity_ket():
     for i in range(NUM_TESTS):
         name0 = "eg_e0[{}]".format(i)
         name1 = "eg_e1[{}]".format(i)
-        protocol0 = EntanglementGenerationA(e0, name0, middle="m0", other="e1",
-                                            memory=e0.memory_array[i])
+        protocol0 = EntanglementGenerationA(e0, name0, middle="m0", other="e1", memory=e0.memory_array[i])
         e0.protocols.append(protocol0)
         protocols_e0.append(protocol0)
-        protocol1 = EntanglementGenerationA(e1, name1, middle="m0", other="e0",
-                                            memory=e1.memory_array[i])
+        protocol1 = EntanglementGenerationA(e1, name1, middle="m0", other="e0", memory=e1.memory_array[i])
         e1.protocols.append(protocol1)
         protocols_e1.append(protocol1)
-        protocol0.set_others(protocol1.name, e1.name,
-                             [e1.memory_array[i].name])
-        protocol1.set_others(protocol0.name, e0.name,
-                             [e0.memory_array[i].name])
+        protocol0.set_others(protocol1.name, e1.name, [e1.memory_array[i].name])
+        protocol1.set_others(protocol0.name, e0.name, [e0.memory_array[i].name])
 
         process = Process(protocols_e0[i], "start", [])
         event = Event(i * 1e12, process)
@@ -297,6 +292,7 @@ def test_generation_fidelity_ket():
         event = Event(i * 1e12, process)
         tl.schedule(event)
 
+    tl.init()
     tl.run()
 
     desired = np.array([complex(np.sqrt(1 / 2)), complex(0),
@@ -310,6 +306,7 @@ def test_generation_fidelity_ket():
             if np.array_equal(desired, mem_state):
                 correct += 1
 
+    assert total > 0, "More trials needed; insufficient successes"
     ratio = correct / total
     assert abs(ratio - FIDELITY) < 0.1
 
diff --git a/tests/entanglement_management/test_purification.py b/tests/entanglement_management/test_purification.py
index 2f8e5e4f..1080e62f 100644
--- a/tests/entanglement_management/test_purification.py
+++ b/tests/entanglement_management/test_purification.py
@@ -23,7 +23,7 @@
 BELL_STATES = [phi_plus, phi_minus, psi_plus, psi_minus]
 
 
-class FakeResourceManager():
+class FakeResourceManager:
     def __init__(self, owner):
         self.log = []
 
@@ -32,6 +32,7 @@ def update(self, protocol, memory, state):
         if state == RAW:
             memory.reset()
 
+
 class FakeNode(Node):
     def __init__(self, name, tl, **kwargs):
         Node.__init__(self, name, tl)
@@ -100,8 +101,7 @@ def create_scenario(state1, state2, seed_index, fidelity=1.0):
 
     tl.run()
 
-    assert meas1.entangled_memory == meas2.entangled_memory == {
-        'node_id': None, 'memo_id': None}
+    assert meas1.entangled_memory == meas2.entangled_memory == {'node_id': None, 'memo_id': None}
 
     return tl, kept1, kept2, meas1, meas2, ep1, ep2
 
@@ -647,8 +647,7 @@ def test_BBPSSW_fidelity():
             assert a2.resource_manager.log[-1] == (kept2, ENTANGLED)
         else:
             assert kept1.fidelity == 0
-            assert kept1.entangled_memory["node_id"] == kept2.entangled_memory[
-                "node_id"] == None
+            assert kept1.entangled_memory["node_id"] == kept2.entangled_memory["node_id"] == None
             assert a1.resource_manager.log[-1] == (kept1, RAW)
             assert a2.resource_manager.log[-1] == (kept2, RAW)
 
diff --git a/tests/entanglement_management/test_swapping.py b/tests/entanglement_management/test_swapping.py
index 09fea6c5..be91d760 100644
--- a/tests/entanglement_management/test_swapping.py
+++ b/tests/entanglement_management/test_swapping.py
@@ -7,7 +7,7 @@
 from sequence.topology.node import Node
 
 
-class ResourceManager():
+class ResourceManager:
     def __init__(self):
         self.log = []
 
@@ -38,8 +38,10 @@ def receive_message(self, src: str, msg: "Message"):
 
 def correct_order(state, keys):
     if keys[0] > keys[1]:
-        return numpy.array(
-            [[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]) @ state
+        return numpy.array([[1, 0, 0, 0],
+                            [0, 0, 1, 0],
+                            [0, 1, 0, 0],
+                            [0, 0, 0, 1]]) @ state
     else:
         return state
 
@@ -75,8 +77,7 @@ def config_three_nodes_network(state1, state2, seed_index):
 
 
 def create_scenario(state1, state2, seed_index):
-    tl, nodes, memories = config_three_nodes_network(state1, state2,
-                                                     seed_index)
+    tl, nodes, memories = config_three_nodes_network(state1, state2, seed_index)
     a1, a2, a3 = nodes
     memo1, memo2, memo3, memo4 = memories
 
@@ -528,8 +529,7 @@ def test_EntanglementSwapping():
 
         es1 = EntanglementSwappingB(a1, "a1.ESb%d" % i, memo1)
         a1.protocols.append(es1)
-        es2 = EntanglementSwappingA(a2, "a2.ESa%d" % i, memo2, memo3,
-                                    success_prob=0.2)
+        es2 = EntanglementSwappingA(a2, "a2.ESa%d" % i, memo2, memo3, success_prob=0.2)
         a2.protocols.append(es2)
         es3 = EntanglementSwappingB(a3, "a3.ESb%d" % i, memo4)
         a3.protocols.append(es3)
@@ -542,12 +542,9 @@ def test_EntanglementSwapping():
         es2.start()
 
         assert memo2.fidelity == memo3.fidelity == 0
-        assert memo1.entangled_memory["node_id"] == memo4.entangled_memory[
-            "node_id"] == "a2"
-        assert memo2.entangled_memory["node_id"] == memo3.entangled_memory[
-            "node_id"] == None
-        assert memo2.entangled_memory["memo_id"] == memo3.entangled_memory[
-            "memo_id"] == None
+        assert memo1.entangled_memory["node_id"] == memo4.entangled_memory["node_id"] == "a2"
+        assert memo2.entangled_memory["node_id"] == memo3.entangled_memory["node_id"] == None
+        assert memo2.entangled_memory["memo_id"] == memo3.entangled_memory["memo_id"] == None
         assert a2.resource_manager.log[-2] == (memo2, "RAW")
         assert a2.resource_manager.log[-1] == (memo3, "RAW")
 
diff --git a/tests/kernel/test_entity.py b/tests/kernel/test_entity.py
index 6a3bc7e4..2ba8b157 100644
--- a/tests/kernel/test_entity.py
+++ b/tests/kernel/test_entity.py
@@ -4,7 +4,15 @@
 from numpy.random._generator import Generator
 
 
-class FakeOwner():
+class FakeOwnerNoGen:
+    def __init__(self):
+        pass
+
+
+class FakeOwner:
+    def __init__(self):
+        self.generator = None
+
     def get_generator(self):
         return self.generator
 
@@ -18,7 +26,7 @@ def test_get_generator():
     tl = Timeline()
 
     # owner does not have generator
-    owner = FakeOwner()
+    owner = FakeOwnerNoGen()
     foo = Foo("foo", tl)
     foo.owner = owner
     assert isinstance(foo.get_generator(), Generator)
diff --git a/tests/kernel/test_eventlist.py b/tests/kernel/test_eventlist.py
index 1958ea72..88daa349 100644
--- a/tests/kernel/test_eventlist.py
+++ b/tests/kernel/test_eventlist.py
@@ -2,6 +2,7 @@
 from sequence.kernel.eventlist import EventList
 from numpy import random
 
+
 MAX_TS = 100
 MIN_TS = 0
 
diff --git a/tests/kernel/test_quantum_manager.py b/tests/kernel/test_quantum_manager.py
index 7c8c23b8..38dac3f7 100644
--- a/tests/kernel/test_quantum_manager.py
+++ b/tests/kernel/test_quantum_manager.py
@@ -33,7 +33,7 @@ def test_qmanager_new():
 
     test_state = [complex(0), complex(1)]
     key = qm.new(test_state)
-    assert (qm.get(key).state == np.array(test_state)).all
+    assert np.all(qm.get(key).state == np.array(test_state))
 
 
 def test_qmanager_set():
@@ -41,14 +41,14 @@ def test_qmanager_set():
     key = qm.new()
     new_state = [complex(0), complex(1)]
     qm.set([key], new_state)
-    assert (qm.get(key) == np.array(new_state)).all
+    assert np.all(qm.get(key).state == np.array(new_state))
 
     key2 = qm.new()
     keys = [key, key2]
     new_state = [complex(1), complex(0), complex(0), complex(0)]
     qm.set(keys, new_state)
-    assert (qm.get(key).state == qm.get(key2).state).all
-    assert (qm.get(key).state == np.array(new_state)).all
+    assert np.all(qm.get(key).state == qm.get(key2).state)
+    assert np.all(qm.get(key).state == np.array(new_state))
 
 
 def test_qmanager_remove():
@@ -65,20 +65,20 @@ def test_qmanager_circuit():
     key = qm.new()
     circuit = DumbCircuit(1, np.array([[0, 1], [1, 0]]))
     qm.run_circuit(circuit, [key])
-    assert (qm.get(key).state == np.array([0, 1])).all
+    assert np.all(qm.get(key).state == np.array([0, 1]))
 
     # two states
     key1 = qm.new()
     key2 = qm.new()
     circuit = DumbCircuit(2, np.identity(4))
     qm.run_circuit(circuit, [key1, key2])
-    assert (qm.get(key1).state == qm.get(key2).state).all
-    assert (qm.get(key1).state == np.array([1, 0, 0, 0])).all
+    assert np.all(qm.get(key1).state == qm.get(key2).state)
+    assert np.all(qm.get(key1).state == np.array([1, 0, 0, 0]))
 
     # two states, wrong order
     qm.run_circuit(circuit, [key2, key1])
-    assert (qm.get(key1).state == qm.get(key2).state).all
-    assert (qm.get(key1).state == np.array([1, 0, 0, 0])).all
+    assert np.all(qm.get(key1).state == qm.get(key2).state)
+    assert np.all(qm.get(key1).state == np.array([1, 0, 0, 0]))
     assert qm.get(key1).keys == [key2, key1]
 
     # single state in multi-qubit system
@@ -88,7 +88,7 @@ def test_qmanager_circuit():
     qm.run_circuit(circuit1, [key1, key2])
     circuit2 = DumbCircuit(1, np.array([[0, 1], [1, 0]]))
     qm.run_circuit(circuit2, [key1])
-    assert (qm.get(key1).state == np.array([0, 0, 1, 0])).all
+    assert np.all(qm.get(key1).state == np.array([0, 0, 1, 0]))
     assert (qm.get(key1) is qm.get(key2))
 
     # extension of circuit
@@ -216,9 +216,9 @@ def test_qmanager__measure():
     
     assert abs((len(meas_0) / NUM_TESTS) - 0.5) < 0.1
     for key in meas_0:
-        assert (qm.get(key).state == np.array([1, 0])).all
+        assert np.all(qm.get(key).state == np.array([1, 0]))
     for key in meas_1:
-        assert (qm.get(key).state == np.array([0, 1])).all
+        assert np.all(qm.get(key).state == np.array([0, 1]))
 
     # single state in multi-qubit system
     meas_0 = []
@@ -238,9 +238,9 @@ def test_qmanager__measure():
 
     assert abs((len(meas_0) / NUM_TESTS) - 0.5) < 0.1
     for key in meas_0:
-        assert (qm.get(key).state == np.array([1, 0])).all
+        assert np.all(qm.get(key).state == np.array([1, 0]))
     for key in meas_1:
-        assert (qm.get(key).state == np.array([0, 1])).all
+        assert np.all(qm.get(key).state == np.array([0, 1]))
 
     # multiple state
     meas_0 = []
@@ -262,6 +262,7 @@ def test_qmanager__measure():
 
     assert abs((len(meas_0) / NUM_TESTS) - 0.5) < 0.1
 
+
 def test_qmanager__measure_density():
     NUM_TESTS = 1000
 
@@ -283,9 +284,9 @@ def test_qmanager__measure_density():
     
     assert abs((len(meas_0) / NUM_TESTS) - 0.5) < 0.1
     for key in meas_0:
-        assert (qm.get(key).state == np.array([[1, 0], [0, 0]])).all
+        assert np.all(qm.get(key).state == np.array([[1, 0], [0, 0]]))
     for key in meas_1:
-        assert (qm.get(key).state == np.array([[0, 0], [0, 1]])).all
+        assert np.all(qm.get(key).state == np.array([[0, 0], [0, 1]]))
 
     # mixed state
     meas_0 = []
diff --git a/tests/utils/test_quantum_state.py b/tests/kernel/test_quantum_state.py
similarity index 92%
rename from tests/utils/test_quantum_state.py
rename to tests/kernel/test_quantum_state.py
index 0f3153ac..68af51ec 100644
--- a/tests/utils/test_quantum_state.py
+++ b/tests/kernel/test_quantum_state.py
@@ -1,4 +1,4 @@
-from sequence.utils.quantum_state import QuantumState
+from sequence.kernel.quantum_state import FreeQuantumState
 from sequence.utils.encoding import polarization
 from math import sqrt
 from numpy.random import default_rng
@@ -7,7 +7,7 @@
 
 
 def test_measure():
-    qs = QuantumState()
+    qs = FreeQuantumState()
     states = [(complex(1), complex(0)),
               (complex(0), complex(1)),
               (complex(sqrt(1 / 2)), complex(sqrt(1 / 2))),
@@ -45,7 +45,7 @@ def test_measure():
 
 
 def test_measure_entangled():
-    qs1 = QuantumState()
+    qs1 = FreeQuantumState()
     states = [(complex(1), complex(0)),
               (complex(0), complex(1)),
               (complex(sqrt(1 / 2)), complex(sqrt(1 / 2))),
@@ -61,7 +61,7 @@ def test_measure_entangled():
         counter = 0
         for _ in range(100):
             qs1.set_state_single(s)
-            qs2 = QuantumState()
+            qs2 = FreeQuantumState()
             qs1.entangle(qs2)
             res = qs1.measure(b, rng)
             if res:
@@ -78,7 +78,7 @@ def test_measure_entangled():
         counter = 0
         for _ in range(1000):
             qs1.set_state_single(s)
-            qs2 = QuantumState()
+            qs2 = FreeQuantumState()
             qs1.entangle(qs2)
             res = qs1.measure(b, rng)
             if res:
diff --git a/tests/network_management/test_network_manager.py b/tests/network_management/test_network_manager.py
index 9be6d63e..b7490018 100644
--- a/tests/network_management/test_network_manager.py
+++ b/tests/network_management/test_network_manager.py
@@ -11,7 +11,8 @@
 class FakeNode(QuantumRouter):
     def __init__(self, name, timeline, memo_size=50):
         super().__init__(name, timeline, memo_size)
-        self.network_manager = NewNetworkManager((self))
+        memo_arr = self.get_components_by_type("MemoryArray")[0]
+        self.network_manager = NewNetworkManager(self, memo_arr.name)
         self.send_log = []
         self.receive_log = []
         self.send_out = True
diff --git a/tests/network_management/test_reservation.py b/tests/network_management/test_reservation.py
index 024c403b..706a5db1 100644
--- a/tests/network_management/test_reservation.py
+++ b/tests/network_management/test_reservation.py
@@ -1,5 +1,6 @@
 from numpy import random
 from sequence.components.optical_channel import QuantumChannel, ClassicalChannel
+from sequence.components.memory import MemoryArray
 from sequence.kernel.timeline import Timeline
 from sequence.network_management.reservation import *
 
@@ -46,7 +47,14 @@ def test_MemoryTimeCard_schedule_reservation():
 class FakeNode(QuantumRouter):
     def __init__(self, name, timeline, memo_size=50):
         super().__init__(name, timeline, memo_size)
-        self.rsvp = ResourceReservationProtocol(self, self.name + ".rsvp")
+
+        memo_arr_name = ""
+        for name in self.components.keys():
+            if type(self.components[name]) is MemoryArray:
+                memo_arr_name = name
+                break
+
+        self.rsvp = ResourceReservationProtocol(self, self.name + ".rsvp", memo_arr_name)
         self.rsvp.upper_protocols.append(self)
         self.rsvp.lower_protocols.append(self)
         self.push_log = []
@@ -68,7 +76,14 @@ def pop(self, **kwargs):
 def test_ResourceReservationProtocol_push():
     tl = Timeline()
     n1 = FakeNode("n1", tl)
-    assert len(n1.rsvp.timecards) == len(n1.memory_array)
+
+    memo_arr = None
+    for c in n1.components.values():
+        if type(c) is MemoryArray:
+            memo_arr = c
+            break
+
+    assert len(n1.rsvp.timecards) == len(memo_arr)
     n1.rsvp.push("n10", 1, 10, 1000, 0.9)
     assert n1.pop_log[0]["msg"].msg_type == RSVPMsgType.REJECT
     assert len(n1.push_log) == 0
@@ -275,14 +290,19 @@ def test_ResourceReservationProtocol_create_rules():
     for node in routers:
         assert len(node.resource_manager.rule_manager) == 0
 
+    memo_arr = None
+    for c in routers[0].components.values():
+        if type(c) is MemoryArray:
+            memo_arr = c
+            break
+
     counter = 0
-    for memory in routers[0].memory_array:
+    for memory in memo_arr:
         print(memory.entangled_memory["node_id"], memory.fidelity)
-        if memory.entangled_memory["node_id"] == "r4" \
-                and memory.fidelity >= 0.9:
+        if memory.entangled_memory["node_id"] == "r4" and memory.fidelity >= 0.9:
             counter += 1
-
     assert counter >= 0
+
     for info in routers[0].resource_manager.memory_manager:
         if info.state == "ENTANGLED" \
                 and info.remote_node == "r4" \
diff --git a/tests/resource_management/test_resource_manager.py b/tests/resource_management/test_resource_manager.py
index b6d290dd..98bc94d8 100644
--- a/tests/resource_management/test_resource_manager.py
+++ b/tests/resource_management/test_resource_manager.py
@@ -2,21 +2,24 @@
 
 import numpy
 
-numpy.random.seed(0)
-
 from sequence.components.memory import MemoryArray
+from sequence.components.bsm import SingleAtomBSM
 from sequence.kernel.timeline import Timeline
 from sequence.resource_management.resource_manager import ResourceManager, \
     ResourceManagerMessage, ResourceManagerMsgType
 from sequence.resource_management.rule_manager import Rule
 from sequence.topology.node import Node
 
+numpy.random.seed(0)
+
 
 class FakeNode(Node):
     def __init__(self, name, tl):
-        Node.__init__(self, name, tl)
-        self.memory_array = MemoryArray(name + ".MemoryArray", tl)
-        self.resource_manager = ResourceManager(self)
+        super().__init__(name, tl)
+        memo_array_name = name + ".MemoryArray"
+        memory_array = MemoryArray(memo_array_name, tl)
+        self.add_component(memory_array)
+        self.resource_manager = ResourceManager(self, memo_array_name)
         self.send_log = []
 
     def send_message(self, dst: str, msg: "Message", priority=math.inf) -> None:
@@ -25,7 +28,8 @@ def send_message(self, dst: str, msg: "Message", priority=math.inf) -> None:
     def get_idle_memory(self, info):
         pass
 
-class FakeProtocol():
+
+class FakeProtocol:
     def __init__(self, name, memories=[]):
         self.name = name
         self.other_is_setted = False
@@ -45,6 +49,38 @@ def start(self):
         self.is_started = True
 
 
+class FakeNode2(Node):
+    def __init__(self, name, tl):
+        Node.__init__(self, name, tl)
+        memo_array_name = name + ".MemoryArray"
+        memory_array = MemoryArray(memo_array_name, tl, num_memories=50)
+        self.add_component(memory_array)
+        memory_array.owner = self
+        memory_array.add_receiver(self)
+        self.resource_manager = ResourceManager(self, memo_array_name)
+
+    def get(self, photon, **kwargs):
+        dst = kwargs["dst"]
+        self.send_qubit(dst, photon)
+
+    def receive_message(self, src: str, msg: "Message") -> None:
+        if msg.receiver == "resource_manager":
+            self.resource_manager.received_message(src, msg)
+        else:
+            if msg.receiver is None:
+                matching = [p for p in self.protocols if type(p) == msg.protocol_type]
+                for p in matching:
+                    p.received_message(src, msg)
+            else:
+                for protocol in self.protocols:
+                    if protocol.name == msg.receiver:
+                        protocol.received_message(src, msg)
+                        break
+
+    def get_idle_memory(self, info):
+        pass
+
+
 def test_load():
     def fake_condition(memo_info, manager, args):
         if memo_info.state == "RAW":
@@ -58,16 +94,19 @@ def fake_action(memories, args):
     tl = Timeline()
     node = FakeNode("node", tl)
     assert len(node.resource_manager.rule_manager) == 0
+
     rule = Rule(1, fake_action, fake_condition, None, None)
     for memo_info in node.resource_manager.memory_manager:
         assert memo_info.state == "RAW"
+
     node.resource_manager.load(rule)
+    memo_array = node.resource_manager.memory_manager.memory_array
     assert len(node.resource_manager.rule_manager) == 1
     for memo_info in node.resource_manager.memory_manager:
         assert memo_info.state == "OCCUPIED"
-    assert len(node.resource_manager.waiting_protocols) == len(node.memory_array)
+    assert len(node.resource_manager.waiting_protocols) == len(memo_array)
     assert len(node.resource_manager.pending_protocols) == 0
-    assert len(rule.protocols) == len(node.memory_array)
+    assert len(rule.protocols) == len(memo_array)
 
 
 def test_update():
@@ -82,7 +121,9 @@ def fake_action(memories, args):
 
     tl = Timeline()
     node = FakeNode("node", tl)
+
     assert len(node.resource_manager.rule_manager) == 0
+
     rule = Rule(1, fake_action, fake_condition, None, None)
     node.resource_manager.load(rule)
     assert len(node.resource_manager.rule_manager) == 1
@@ -91,21 +132,23 @@ def fake_action(memories, args):
 
     protocol = FakeProtocol("protocol1")
     node.protocols.append(protocol)
-    node.memory_array[0].fidelity = 0.5
-    node.memory_array[0].detach(node.memory_array)
-    node.memory_array[0].attach(protocol)
-    node.resource_manager.update(protocol, node.memory_array[0], "ENTANGLED")
+    memo_array = node.resource_manager.memory_manager.memory_array
+    memo_array[0].fidelity = 0.5
+    memo_array[0].detach(memo_array)
+    memo_array[0].attach(protocol)
+    node.resource_manager.update(protocol, memo_array[0], "ENTANGLED")
     assert len(node.protocols) == len(rule.protocols) == 0
-    assert len(node.memory_array[0]._observers) == 1
+    assert len(memo_array[0]._observers) == 1
     assert node.resource_manager.memory_manager[0].state == "ENTANGLED"
 
     protocol = FakeProtocol("protocol2")
     node.protocols.append(protocol)
-    node.memory_array[1].fidelity = 0.9
-    node.memory_array[1].attach(protocol)
-    node.resource_manager.update(protocol, node.memory_array[1], "ENTANGLED")
+    memo_array[1].fidelity = 0.9
+    memo_array[1].detach(memo_array)
+    memo_array[1].attach(protocol)
+    node.resource_manager.update(protocol, memo_array[1], "ENTANGLED")
     assert len(node.resource_manager.waiting_protocols) == len(rule.protocols) == 1
-    assert len(node.memory_array[1]._observers) == 2
+    assert len(memo_array[1]._observers) == 1
     assert node.resource_manager.memory_manager[1].state == "OCCUPIED"
 
 
@@ -117,15 +160,15 @@ def test_send_request():
     protocol = FakeProtocol("no_send")
     resource_manager.send_request(protocol, None, None, {})
     assert len(node.send_log) == 0
-    assert protocol in resource_manager.waiting_protocols and len(
-        resource_manager.pending_protocols) == 0
+    assert protocol in resource_manager.waiting_protocols
+    assert len(resource_manager.pending_protocols) == 0
     assert protocol.own == node
     protocol = FakeProtocol("send")
     node.resource_manager.send_request(protocol, "dst_id",
                                        "req_condition_func", {})
     assert len(node.send_log) == 1
-    assert protocol in resource_manager.pending_protocols and len(
-        resource_manager.waiting_protocols) == 1
+    assert protocol in resource_manager.pending_protocols
+    len(resource_manager.waiting_protocols) == 1
     assert protocol.own == node
 
 
@@ -183,8 +226,7 @@ def false_fun(protocols, args):
                                       is_approved=False,
                                       paired_protocol="paired_protocol",
                                       paired_node="paired_node",
-                                      paired_memories=[]
-                                      )
+                                      paired_memories=[])
     resource_manager.received_message("sender", resp_msg)
     assert protocol2 not in node.protocols
     assert protocol2 not in resource_manager.pending_protocols
@@ -211,23 +253,25 @@ def test_expire():
     tl = Timeline()
     node = FakeNode("node", tl)
     tl.init()
+
     for info in node.resource_manager.memory_manager:
         info.to_occupied()
     rule = Rule(0, None, None, None, None)
+    memo_array = node.resource_manager.memory_manager.memory_array
     for i in range(6):
-        node.memory_array[i].detach(node.memory_array)
-    p1 = FakeProtocol("waiting_protocol", [node.memory_array[0]])
-    node.memory_array[0].attach(p1)
-    p2 = FakeProtocol("pending_protocol", [node.memory_array[1]])
-    node.memory_array[1].attach(p2)
-    p3 = FakeProtocol("running_protocol", [node.memory_array[2]])
-    node.memory_array[2].attach(p3)
-    p4 = FakeProtocol("other_waiting_protocol", [node.memory_array[3]])
-    node.memory_array[3].attach(p4)
-    p5 = FakeProtocol("other_pending_protocol", [node.memory_array[4]])
-    node.memory_array[4].attach(p5)
-    p6 = FakeProtocol("other_running_protocol", [node.memory_array[5]])
-    node.memory_array[5].attach(p6)
+        memo_array[i].detach(memo_array)
+    p1 = FakeProtocol("waiting_protocol", [memo_array[0]])
+    memo_array[0].attach(p1)
+    p2 = FakeProtocol("pending_protocol", [memo_array[1]])
+    memo_array[1].attach(p2)
+    p3 = FakeProtocol("running_protocol", [memo_array[2]])
+    memo_array[2].attach(p3)
+    p4 = FakeProtocol("other_waiting_protocol", [memo_array[3]])
+    memo_array[3].attach(p4)
+    p5 = FakeProtocol("other_pending_protocol", [memo_array[4]])
+    memo_array[4].attach(p5)
+    p6 = FakeProtocol("other_running_protocol", [memo_array[5]])
+    memo_array[5].attach(p6)
     for p in [p1, p2, p3]:
         p.rule = rule
         rule.protocols.append(p)
@@ -252,12 +296,12 @@ def test_expire():
     for i in range(3, 6):
         assert node.resource_manager.memory_manager[i].state == "OCCUPIED"
 
-    for i, memory in enumerate(node.memory_array):
+    for i, memory in enumerate(memo_array):
         assert len(memory._observers) == 1
         if i < 3:
-            isinstance(memory._observers.pop(), MemoryArray)
+            assert isinstance(memory._observers.pop(), MemoryArray)
         elif i < 6:
-            isinstance(memory._observers.pop(), FakeProtocol)
+            assert isinstance(memory._observers.pop(), FakeProtocol)
 
 
 def test_ResourceManager1():
@@ -265,31 +309,6 @@ def test_ResourceManager1():
     from sequence.topology.node import BSMNode
     from sequence.entanglement_management.generation import EntanglementGenerationA
 
-    class TestNode(Node):
-        def __init__(self, name, tl):
-            Node.__init__(self, name, tl)
-            self.memory_array = MemoryArray(name + ".MemoryArray", tl,
-                                            num_memories=50)
-            self.memory_array.owner = self
-            self.resource_manager = ResourceManager(self)
-
-        def receive_message(self, src: str, msg: "Message") -> None:
-            if msg.receiver == "resource_manager":
-                self.resource_manager.received_message(src, msg)
-            else:
-                if msg.receiver is None:
-                    matching = [p for p in self.protocols if type(p) == msg.protocol_type]
-                    for p in matching:
-                        p.received_message(src, msg)
-                else:
-                    for protocol in self.protocols:
-                        if protocol.name == msg.receiver:
-                            protocol.received_message(src, msg)
-                            break
-
-        def get_idle_memory(self, info):
-            pass
-
     def eg_rule_condition(memory_info, manager, args):
         if memory_info.state == "RAW":
             return [memory_info]
@@ -304,8 +323,7 @@ def eg_req_func(protocols, args):
     def eg_rule_action1(memories_info, args):
         memories = [info.memory for info in memories_info]
         memory = memories[0]
-        protocol = EntanglementGenerationA(None, "EGA." + memory.name,
-                                           "mid_node", "node2", memory)
+        protocol = EntanglementGenerationA(None, "EGA." + memory.name, "mid_node", "node2", memory)
         protocol.primary = True
         return [protocol, ["node2"], [eg_req_func], [{}]]
 
@@ -318,10 +336,16 @@ def eg_rule_action2(memories_info, args):
 
     tl = Timeline()
 
-    node1, node2 = TestNode("node1", tl), TestNode("node2", tl)
+    node1, node2 = FakeNode2("node1", tl), FakeNode2("node2", tl)
     mid_node = BSMNode("mid_node", tl, [node1.name, node2.name])
-    mid_node.bsm.detectors[0].efficiency = 1
-    mid_node.bsm.detectors[1].efficiency = 1
+
+    bsm = None
+    for c in mid_node.components.values():
+        if type(c) is SingleAtomBSM:
+            bsm = c
+            break
+    bsm.detectors[0].efficiency = 1
+    bsm.detectors[1].efficiency = 1
 
     for src in [node1, node2, mid_node]:
         for dst in [node1, node2, mid_node]:
@@ -360,30 +384,6 @@ def test_ResourceManager2():
     from sequence.topology.node import BSMNode
     from sequence.entanglement_management.generation import EntanglementGenerationA
 
-    class TestNode(Node):
-        def __init__(self, name, tl):
-            Node.__init__(self, name, tl)
-            self.memory_array = MemoryArray(name + ".MemoryArray", tl, num_memories=50)
-            self.memory_array.owner = self
-            self.resource_manager = ResourceManager(self)
-
-        def receive_message(self, src: str, msg: "Message") -> None:
-            if msg.receiver == "resource_manager":
-                self.resource_manager.received_message(src, msg)
-            else:
-                if msg.receiver is None:
-                    matching = [p for p in self.protocols if type(p) == msg.protocol_type]
-                    for p in matching:
-                        p.received_message(src, msg)
-                else:
-                    for protocol in self.protocols:
-                        if protocol.name == msg.receiver:
-                            protocol.received_message(src, msg)
-                            break
-
-        def get_idle_memory(self, info):
-            pass
-
     def eg_rule_condition(memory_info, manager, args):
         if memory_info.state == "RAW":
             return [memory_info]
@@ -398,8 +398,7 @@ def eg_req_func(protocols, args):
     def eg_rule_action1(memories_info, args):
         memories = [info.memory for info in memories_info]
         memory = memories[0]
-        protocol = EntanglementGenerationA(None, "EGA." + memory.name,
-                                           "mid_node", "node2", memory)
+        protocol = EntanglementGenerationA(None, "EGA." + memory.name, "mid_node", "node2", memory)
         protocol.primary = True
         return [protocol, ["node2"], [eg_req_func], [{}]]
 
@@ -412,10 +411,16 @@ def eg_rule_action2(memories_info, args):
 
     tl = Timeline()
 
-    node1, node2 = TestNode("node1", tl), TestNode("node2", tl)
+    node1, node2 = FakeNode2("node1", tl), FakeNode2("node2", tl)
     mid_node = BSMNode("mid_node", tl, [node1.name, node2.name])
-    mid_node.bsm.detectors[0].efficiency = 1
-    mid_node.bsm.detectors[1].efficiency = 1
+
+    bsm = None
+    for c in mid_node.components.values():
+        if type(c) is SingleAtomBSM:
+            bsm = c
+            break
+    bsm.detectors[0].efficiency = 1
+    bsm.detectors[1].efficiency = 1
 
     for src in [node1, node2, mid_node]:
         for dst in [node1, node2, mid_node]:
diff --git a/tests/resource_management/test_rule_manager.py b/tests/resource_management/test_rule_manager.py
index 7393193d..24d4d71c 100644
--- a/tests/resource_management/test_rule_manager.py
+++ b/tests/resource_management/test_rule_manager.py
@@ -25,8 +25,7 @@ def __init__(self, name):
     def fake_action(memories_info, args):
         assert args["exist"]
         if len(memories_info) == 1:
-            return FakeProtocol("protocol1"), ["req_dst1"], [
-                "req_condition1"], [{}]
+            return FakeProtocol("protocol1"), ["req_dst1"], ["req_condition1"], [{}]
         else:
             return FakeProtocol("protocol2"), [None], [None], [{}]
 
@@ -36,20 +35,18 @@ def fake_action(memories_info, args):
     rule = Rule(1, fake_action, None, action_args, None)
     rule.set_rule_manager(rule_manager)
     assert rule.priority == 1 and len(rule.protocols) == 0
-    memory = Memory("mem", tl, fidelity=1, frequency=0, efficiency=1,
-                    coherence_time=-1, wavelength=500)
+    memory = Memory("mem", tl, fidelity=1, frequency=0, efficiency=1, coherence_time=-1, wavelength=500)
     memories_info = [MemoryInfo(memory, 0)]
     assert len(memory._observers) == 0
     rule.do(memories_info)
     assert len(rule.protocols) == 1 and rule.protocols[0].name == "protocol1"
-    assert len(rule_manager.log) == 1 and rule_manager.log[0] == (
+    assert len(rule_manager.log) == 1
+    assert rule_manager.log[0] == (
         "protocol1", "req_dst1", "req_condition1", {})
     assert rule.protocols[0].rule == rule
     assert len(memory._observers) == 1
-    mem1 = Memory("1", tl, fidelity=1, frequency=0, efficiency=1,
-                  coherence_time=-1, wavelength=500)
-    mem2 = Memory("2", tl, fidelity=1, frequency=0, efficiency=1,
-                  coherence_time=-1, wavelength=500)
+    mem1 = Memory("1", tl, fidelity=1, frequency=0, efficiency=1, coherence_time=-1, wavelength=500)
+    mem2 = Memory("2", tl, fidelity=1, frequency=0, efficiency=1, coherence_time=-1, wavelength=500)
     memories_info = [MemoryInfo(mem1, 0), MemoryInfo(mem2, 1)]
     rule.do(memories_info)
     assert len(rule.protocols) == 2 and rule.protocols[1].name == "protocol2"
diff --git a/tests/topology/test_node.py b/tests/topology/test_node.py
index ceb51214..2d767822 100644
--- a/tests/topology/test_node.py
+++ b/tests/topology/test_node.py
@@ -58,7 +58,6 @@ def receive_message(self, src, msg):
 
     expect_node2_log = [(CC_DELAY + i, "node1", str(i))
                         for i in range(MSG_NUM)]
-
     for actual, expect in zip(node2.log, expect_node2_log):
         assert actual == expect
 
@@ -87,12 +86,12 @@ def receive_qubit(self, src, qubit):
     tl.init()
 
     for i in range(1000):
-        photon = Photon(str(i))
+        photon = Photon(str(i), tl)
         node1.send_qubit("node2", photon)
         tl.time += 1
 
     for i in range(1000):
-        photon = Photon(str(i))
+        photon = Photon(str(i), tl)
         node2.send_qubit("node1", photon)
         tl.time += 1
 
diff --git a/tests/topology/test_router_net_topo.py b/tests/topology/test_router_net_topo.py
index 83792684..8960ce4f 100644
--- a/tests/topology/test_router_net_topo.py
+++ b/tests/topology/test_router_net_topo.py
@@ -18,7 +18,8 @@ def test_sequential_simulation():
     routers = all_nodes[RouterNetTopo.QUANTUM_ROUTER]
     e1 = e2 = e3 = e4 = None
     for router in routers:
-        assert len(router.memory_array) == 20
+        memory_array = router.get_components_by_type("MemoryArray")[0]
+        assert len(memory_array) == 20
         assert len(router.qchannels) == 1
         assert len(router.cchannels) == 2
         if router.name == "e1":
diff --git a/tests/topology/topology.json b/tests/topology/topology.json
deleted file mode 100644
index f5c03767..00000000
--- a/tests/topology/topology.json
+++ /dev/null
@@ -1,48 +0,0 @@
-{
-    "nodes": [
-        {
-            "name": "alice",
-            "type": "QKDNode"
-        },
-        {
-            "name": "bob",
-            "type": "QKDNode"
-        },
-        {
-            "name": "e1",
-            "type": "QuantumRouter"
-        },
-        {
-            "name": "e2",
-            "type": "QuantumRouter"
-        }
-    ],
-    "qchannels": [
-        {
-            "node1": "alice",
-            "node2": "bob",
-            "attenuation": 1e-5,
-            "distance": 3e3
-        }
-    ],
-    "qconnections": [
-        {
-            "node1": "e1",
-            "node2": "e2",
-            "attenuation": 1e-5,
-            "distance": 5e3
-        }
-    ],
-    "cconnections": [
-        {
-            "node1": "alice",
-            "node2": "bob",
-            "distance": 3e3
-        },
-        {
-            "node1": "e1",
-            "node2": "e2",
-            "distance": 5e3
-        }
-    ]
-}