Chapter 3: Entanglement Management

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

  • how to use BBPSSW (BBPSSW purification protocol) to improve the fidelity of entanglement

  • how to use EntanglementSwappingA and EntanglementSwappingB (swapping protocol) to extend the distance of entanglement

Example: Use EntanglementGenerationA and BSMNode to generate entanglement

eg_topo

The above figure shows the network topology used in this example. The network includes three nodes: one BSMNode node and two EntangleGenNode nodes. The BSMNode node comes from SeQUeNCe. We will build the custom node EntangleGenNode that inherits the Node class from SeQUeNCe. Two quantum channels connect the BSMNode with the two EntangleGenNode. Classical channels and nodes create a complete classical graph, which is not shown in the figure.

BSMNode includes:

  • Hardware: two detectors in a bell state measurement device (BSM) to record the arrival time of photons.

  • Software: the EntanglementGenerationB protocol to collect the arrival time of photons and notify the EntanglementGenerationA protocols on the other nodes.

EntangleGenNode includes:

  • Hardware: one quantum memory in the |+⟩ 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. 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:

from sequence.topology.node import Node
from sequence.components.memory import Memory
from sequence.entanglement_management.generation import EntanglementGenerationA


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)

        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 get(self, photon, **kwargs):
        self.send_qubit(kwargs['dst'], photon)

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 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:

  1. the node that holds the protocol instance

  2. the identity (name) of the protocol instance

  3. the name of the BSMNode involved in entanglement generation

  4. the name of the remote EntangleGenNode involved in entanglement generation

  5. the memory used for generating entanglement

Q&A

Q: Why is the SimpleManager necessary?

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?

A: The implementation of EntanglementGenerationA assumes it has been placed in the Node.protocols list when it starts.

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.

from sequence.kernel.timeline import Timeline
from sequence.topology.node import BSMNode
from sequence.components.optical_channel import QuantumChannel, ClassicalChannel


tl = Timeline()

node1 = EntangleGenNode('node1', tl)
node2 = EntangleGenNode('node2', tl)
bsm_node = BSMNode('bsm_node', tl, ['node1', 'node2'])
node1.set_seed(0)
node2.set_seed(1)
bsm_node.set_seed(2)

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)
qc1.set_ends(node1, bsm_node.name)
qc2.set_ends(node2, bsm_node.name)

nodes = [node1, node2, bsm_node]

for i in range(3):
    for j in range(3):
        cc= ClassicalChannel('cc_%s_%s'%(nodes[i].name, nodes[j].name), tl, 1000, 1e8)
        cc.set_ends(nodes[i], nodes[j].name)

Step 3: Configure and Start the EntanglementGenerationA Protocol

First, we will use create_protocol to create the instance of the protocol on the node. Before we start the protocol, we need to pair the protocols on the two nodes. The function pair_protocol defined here uses two EntangleGenNode as the input and will pair all necessary protocols. The protocols in EntangleGenNode.protocols are paired with the set_others method.

Now, the protocols are ready to start generating entanglement and we can start our experiment.

from sequence.entanglement_management.entanglement_protocol import EntanglementProtocol


def pair_protocol(node1: Node, node2: Node):
    p1 = node1.protocols[0]
    p2 = node2.protocols[0]
    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.resource_manager.create_protocol('bsm_node', 'node2')
node2.resource_manager.create_protocol('bsm_node', 'node1')
pair_protocol(node1, node2)

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.run()

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"

The start method starts the protocol. The run mehtod starts the simulation. Note that the start method must be called after the timeline init method. After the simulation, we can observe two possible states of memory based on the result of entanglement generation. If the protocol generates entanglement successfully, the Memory.entangled_memory will present information about the entangled memory. The fidelity of entanglements equal 0.9, as set in the constructor function of Memory. If the protocol fails, the fidelity of entanglement is 0.

Step 4: Try to Generate Entanglement Multiple Times

The mechanism of the Barrett-Kok generation protocol can achieve at most 50% success rate. We can, however, set the protocol to try multiple times and observe the success rate of protocol. We will use the Memory.reset() method to reset the state of quantum memories before restarting protocols.

tl.init()
for i in range(1000):
    tl.time = tl.now() + 1e11
    node1.resource_manager.create_protocol('bsm_node', 'node2')
    node2.resource_manager.create_protocol('bsm_node', 'node1')
    pair_protocol(node1, node2)

    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)

Example: Use BBPSSW to improve the fidelity of entanglement

EP_topo

The above figure shows the network topology of this example. The network is composed of two PurifyNode nodes and one ClassicalChannel. Two pairs of entangled memories are located at two nodes. The BBPSSW purification protocol will consume one entanglement to improve the fidelity of the other entanglement.

Step 1: Customized Node

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 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 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.

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)
        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)

The constructor function of BBPSSW requires four arguments:

  1. The node that holds the protocol instance

  2. The identity of the protocol instance

  3. The memory used as the kept_memo

  4. the memory used as the meas_memo

Step 2: Create Network

We can now use the code below to create the simulated network.

tl = Timeline()

node1 = PurifyNode('node1', tl)
node2 = PurifyNode('node2', tl)
node1.set_seed(0)
node2.set_seed(1)

cc0 = ClassicalChannel('cc0', tl, 1000, 1e9)
cc1 = ClassicalChannel('cc1', tl, 1000, 1e9)
cc0.set_ends(node1, node2.name)
cc1.set_ends(node2, node1.name)

Step 3: Manually Set Entanglement States

To avoid unnecessary modules and operations, we will manually modify the memories to create an entangled state. First, we use the Memory.reset() to reset the state of memory. Then, we assign the identity of the node and memory to which we are entangled in Memory.entangled_memory (implemented as a dictionary {'node_id': str, 'memo_id': str}). Finally, we set the fidelity of entanglement.

def entangle_memory(memo1: Memory, memo2: Memory, fidelity: float):
    memo1.reset()
    memo2.reset()

    memo1.entangled_memory['node_id'] = memo2.owner.name
    memo1.entangled_memory['memo_id'] = memo2.name
    memo2.entangled_memory['node_id'] = memo1.owner.name
    memo2.entangled_memory['memo_id'] = memo1.name

    memo1.fidelity = memo2.fidelity = fidelity


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

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]
    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.resource_manager.create_protocol()
node2.resource_manager.create_protocol()

pair_protocol(node1, node2)

tl.init()
node1.protocols[0].start()
node2.protocols[0].start()
tl.run()

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(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'

After the simulation, the first print statement produces one of two possible outputs. The first output (the first comment) shows a successful purification operation. The second output (the second comment) shows the failure of purification. Note that the entanglement fields and fidelity will be reset by the resource manager, discussed in the next chapter. The success rate of purificaiton depends on the fidelity of entanglement. Entanglements with higher fidelities have a higher success rate.

Note: The BBPSSW protocol assumes the fidelity of the two entangled pairs are the same.

Note: You can inherit the BBPSSW class and overwrite BBPSSW.success_probability(F: float) and BBPSSW.improved_fidelity(F: float) to customize the success probability and fidelity improvement of the purification protocol.

Step 5: Try to Purify Entanglement Multiple Times

We can run the purification protocol multiple times to observe the state of memory with different purification results.

tl.init()
for i in range(10):
    entangle_memory(kept_memo_1, kept_memo_2, 0.9)
    entangle_memory(meas_memo_1, meas_memo_2, 0.9)

    node1.resource_manager.create_protocol()
    node2.resource_manager.create_protocol()

    pair_protocol(node1, node2)

    node1.protocols[0].start()
    node2.protocols[0].start()
    tl.run()

    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

ES_topo

The above figure shows the network topology of this example. The network consists of two SwapNodeB and one SwapNodeA connected by classical channels.

The SwapNodeB node has:

  • Hardware: one quantum memory entangled with one memory on SwapNodeA

  • Software: EntanglementSwappingB swapping protocol

The SwapNodeA node has:

  • Hardware: two entangled memories

  • Software: EntanglementSwappingA swapping protocol

The swapping protocols on the three nodes use these two pairs of entangled memories to generate the entanglement between two SwapNodeB nodes. After the swapping protocol, the two memories on SwapNodeA are no longer entangled with the memories on each SwapNodeB.

Step 1: Customized Node

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.

class SwapNodeA(Node):
    def __init__(self, name: str, tl: Timeline):
        super().__init__(name, tl)
        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)

The code for SwapNodeB is identical to SwapNodeA but with only one memory:

class SwapNodeB(Node):
    def __init__(self, name: str, tl: Timeline):
        super().__init__(name, tl)
        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.

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):
        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

We create the three nodes and connect them with classical channels.

tl = Timeline()

left_node = SwapNodeB('left', tl)
right_node = SwapNodeB('right', tl)
mid_node = SwapNodeA('mid', tl)
left_node.set_seed(0)
right_node.set_seed(1)
mid_node.set_seed(2)

nodes = [left_node, right_node, mid_node]

for i in range(3):
    for j in range(3):
        cc = ClassicalChannel('cc_%s_%s' % (nodes[i].name, nodes[j].name), tl, 1000, 1e9)
        cc.set_ends(nodes[i], nodes[j].name)

Step 3: Manually Set Entanglement State and Start Protocol

We will reuse the code for entangle_memory from the previous examples to configure the states of hardware and software. The pair_protocol function is slightly more complicated, as there are more nodes to deal with. Because we set the success probability to 1, we can guaruntee a successful result after running the simulation. The fidelity of entanglement after swapping will be 0.9*0.9*0.99=0.8019.

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.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.resource_manager.create_protocol()

pair_protocol(left_node, right_node, mid_node)

tl.init()
for node in nodes:
    node.protocols[0].start()
tl.run()

print(left_memo.entangled_memory)
# {'node_id': 'right', 'memo_id': 'right.memo'}

print(mid_left_memo.entangled_memory)
# {'node_id': None, 'memo_id': None}

print(mid_right_memo.entangled_memory)
# {'node_id': None, 'memo_id': None}

print(right_memo.entangled_memory)
# {'node_id': 'left', 'memo_id': 'left.memo'}

print(left_memo.fidelity)
# 0.8019000000000001