#
# Copyright (c) 2017, Stephanie Wehner and Axel Dahlberg
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. All advertising materials mentioning features or use of this software
# must display the following acknowledgement:
# This product includes software developed by Stephanie Wehner, QuTech.
# 4. Neither the name of the QuTech organization nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ''AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import logging
import random
from collections import deque
from twisted.spread import pb
from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks, DeferredLock, Deferred, DeferredList
from twisted.internet.task import deferLater
from twisted.internet.error import ConnectionRefusedError, CannotListenError
from twisted.spread.pb import RemoteError
from simulaqron.virtNode.basics import quantumError, noQubitError, virtNetError
from simulaqron.virtNode.quantum import simulatedQubit
from simulaqron.general.hostConfig import socketsConfig
from simulaqron.settings import simulaqron_settings
if simulaqron_settings.backend == "qutip":
from simulaqron.virtNode.qutipSimulator import qutipEngine
elif simulaqron_settings.backend == "projectq":
from simulaqron.virtNode.projectQSimulator import projectQEngine
elif simulaqron_settings.backend == "stabilizer":
from simulaqron.virtNode.stabilizerSimulator import stabilizerEngine
else:
raise quantumError("Unknown backend {}".format(simulaqron_settings.backend))
######
#
# backEnd - starts the local virtual node and connects to the other virtual nodes
# forming the quantum network
#
[docs]class backEnd(object):
def __init__(self, name, virtualFile, network_name="default"):
"""
Initialize. This will read the configuration file and populate the name,hostname,port information with the
information found in the configuration file for the given name.
"""
# Read the configuration file
try:
self.config = socketsConfig(virtualFile, network_name=network_name, config_type="vnode")
self.myID = self.config.hostDict[name]
except KeyError as e:
logging.error("LOCAL {}: No such name in the configuration file {}: {}".format(name, virtualFile, e))
raise e
except Exception as e:
logging.error("LOCAL {}: Error reading the configuration file {}: {}".format(name, virtualFile, e))
raise e
[docs] def start(self, maxQubits=simulaqron_settings.max_qubits, maxRegisters=simulaqron_settings.max_registers):
"""
Start listening to requests from other nodes.
Arguments
maxQubits maximum qubits in the default register
"""
try:
logging.debug("VIRTUAL NODE %s: Starting on port %d", self.myID.name, self.myID.port)
node = virtualNode(self.myID, self.config, maxQubits=maxQubits, maxRegisters=maxRegisters)
reactor.listenTCP(self.myID.port, pb.PBServerFactory(node))
logging.debug("VIRTUAL NODE %s: running reactor.", self.myID.name)
reactor.run()
except CannotListenError:
logging.error("LOCAL {}: CQC server address ({}) is already in use.".format(self.myID.name, self.myID.port))
return
except Exception as e:
logging.error(
"LOCAL {}: Critical error when starting local virtual node server: {}".format(self.myID.name, e)
)
return
#######
#
# virtualNode - this is the virtual quantum node. It keeps track of registers simulated here, qubits
# virtually available at this node, etc
#
[docs]class virtualNode(pb.Root):
def __init__(self, ID, config, maxQubits=simulaqron_settings.max_qubits,
maxRegisters=simulaqron_settings.max_registers):
"""
Initialize storing also our own name, hostname and port.
Arguments:
ID host identifier of this node
maxQubits maximum number of qubits to use in the default engine (default 10)
maxRegister maximum number of registers
"""
try:
# Store our own host identifiers and configuration
self.myID = ID
self.myID.root = self
self.config = config
# Set max nr of registers and virtual qubits
self.maxRegs = maxRegisters
self.maxQubits = maxQubits
# List of connections
self.conn = {}
# Number of registers _created_ at this node
# this may not equal the numbers of registers virtually carried
self.numRegs = 0
# Counter for used register numbers
self._next_reg_num = 0
# Set up the dictionary of registers
self.registers = {}
# Initialize the list of qubits at this node
self.virtQubits = []
self.simQubits = []
# Set up connections to the neighouring nodes in the network
self.connectNet()
# Global lock: needs to be acquire whenever we want to manipulate more than one
# qubit object
self._lock = DeferredLock()
# Time until retry
self._delay = 1
# Maximum number of attempts at getting locks
self.maxAttempts = 300
# List of qubit received to be polled by CQC
self.cqcRecv = {}
# List of halves of epr-pairs received to be polled by CQC
self.cqcRecvEpr = {}
except Exception as e:
logging.error("VIRTUAL NODE {}: Critical error when initializing virtNode: {}".format(ID.name, e))
raise e
[docs] def reraise_remote_error(self, remote_err):
"""
This is a function re-raises the error thrown remotely
:param remote_err: :obj:`twisted.spread.pb.RemoteError`
:return: class
"""
# Get name of remote error
error_name = remote_err.remoteType.split(b".")[-1].decode()
# Get class of remote error
error_class = eval(error_name)
raise error_class(str(remote_err))
[docs] def connectNet(self):
"""
Initialize the connections to the other virtual nodes in the network according to the available
configuration.
"""
try:
for key in self.config.hostDict:
node = self.config.hostDict[key]
if node.name != self.myID.name:
self.connect_to_node(node)
else:
self.conn[node.name] = node
except Exception as e:
logging.error(
"VIRTUAL NODE {}: Critical error when connection network of virtual nodes: {}".format(self.myID.name, e)
)
raise e
[docs] def remote_check_connections(self):
"""
Checks if all connections are up. (Just checks if the number of
connections equal the number of nodes in config-file)
"""
return len(self.conn) == len(self.config.hostDict)
[docs] @inlineCallbacks
def get_connection(self, name):
"""
Returns the connection specified by 'name'. If no such connection is
up yet but name is in the configuration file, wait and try again.
"""
if name in self.conn:
return self.conn[name]
else:
try:
logging.debug(
"VIRTUAL NODE {}: Connection to {} not up yet, need to wait...".format(self.myID.name, name)
)
conn_to_return = yield deferLater(reactor, simulaqron_settings.conn_retry_time, self.get_connection,
name)
return conn_to_return
except Exception as e:
raise e
[docs] def connect_to_node(self, node):
"""
Connects to other node. If node not up yet, waits for CONF_WAIT_TIME seconds.
"""
logging.debug("VIRTUAL NODE {}: Trying to connect to node {}.".format(self.myID.name, node.name))
node.factory = pb.PBClientFactory()
reactor.connectTCP(node.hostname, node.port, node.factory)
defer = node.factory.getRootObject()
defer.addCallback(self.handle_connection, node)
defer.addErrback(self.handle_connection_error, node)
[docs] def handle_connection(self, obj, node):
"""
Callback obtaining twisted root object when connection to the node given by the node details 'node'.
"""
try:
logging.debug("VIRTUAL NODE %s: New connection to %s.", self.myID.name, node.name)
# Retrieve the root object: virtualNode on the remote
node.root = obj
# Add this node to the local connections
self.conn[node.name] = node
except Exception as e:
logging.error(
"VIRTUAL NODE {}: Critical error when handling connection to node {}: {}".format(
self.myID.name, node.name, e
)
)
raise e
[docs] def handle_connection_error(self, reason, node):
"""
Handles errors from trying to connect to other node.
If a ConnectionRefusedError is raised another try will be made after CONF_WAIT_TIME seconds.
CONF_WAIT_TIME is set in 'settings.py'.
Any other error is raised again.
"""
try:
reason.raiseException()
except ConnectionRefusedError:
logging.debug("VIRTUAL NODE {}: Could not connect to {}, trying again...".format(self.myID.name, node.name))
reactor.callLater(simulaqron_settings.conn_retry_time, self.connect_to_node, node)
except Exception as e:
logging.error(
"VIRTUAL NODE {}: Critical error when connection to local virtual node: {}".format(self.myID.name, e)
)
reactor.stop()
[docs] def get_virtual_id(self):
"""
This is a crude and horrible cludge to generate unique IDs for virtual qubits.
"""
# Loop through the firt k numbers where k is the number of virtual qubits + 1
# Note that this is guaranteed to find a an index which is not yet used
for j in range(len(self.virtQubits) + 1):
used = 0
for q in self.virtQubits:
if q.num == j:
used = 1
if used == 0:
return j
[docs] def get_sim_id(self):
"""
Similarly, this is a crude and horrible cludge to generate unique IDs for simulated qubits.
"""
# Loop through the firt k numbers where k is the number of virtual qubits + 1
# Note that this is guaranteed to find a an index which is not yet used
for j in range(len(self.simQubits) + 1):
used = 0
for q in self.simQubits:
if q.simNum == j:
used = 1
if used == 0:
return j
def _q_num_to_obj(self, num):
"""
Given the simulation number of a qubit simulated here, return the corresponding object.
"""
for q in self.simQubits:
if q.simNum == num:
return q
return None
[docs] def remote_isLocked(self):
return self._lock.locked
@inlineCallbacks
def _get_global_lock(self):
logging.debug("VIRTUAL NODE %s: Local GETTING LOCK", self.myID.name)
try:
yield self._lock.acquire()
except Exception as e:
raise e
logging.debug("VIRTUAL NODE %s: Local GOT LOCK", self.myID.name)
[docs] @inlineCallbacks
def remote_get_global_lock(self):
logging.debug("VIRTUAL NODE %s: Remote GETTING LOCK", self.myID.name)
try:
yield self._lock.acquire()
except Exception as e:
raise e
logging.debug("VIRTUAL NODE %s: Remote GOT LOCK", self.myID.name)
@inlineCallbacks
def _release_global_lock(self):
logging.debug("VIRTUAL NODE %s: Local RELEASE LOCK", self.myID.name)
if self._lock.locked:
try:
yield self._lock.release()
except Exception as e:
raise e
[docs] @inlineCallbacks
def remote_release_global_lock(self):
logging.debug("VIRTUAL NODE %s: Remote RELEASE LOCK", self.myID.name)
if self._lock.locked:
try:
yield self._lock.release()
except Exception as e:
raise e
@inlineCallbacks
def _lock_reg_qubits(self, qubit):
"""
Acquire the lock on all qubits in the same register as the local sim qubit qubit.
"""
for q in self.simQubits:
if q.register == qubit.register:
try:
yield q.lock()
except Exception as err:
raise err
[docs] @inlineCallbacks
def remote_lock_reg_qubits(self, qubitNum):
"""
Acquire the lock on all qubits in the same register as qubitNum.
"""
try:
yield self._lock_reg_qubits(self._q_num_to_obj(qubitNum))
except Exception as err:
raise err
@inlineCallbacks
def _unlock_reg_qubits(self, qubit):
"""
Release the lock on all qubits in the same register as qubit.
"""
for q in self.simQubits:
if q.register == qubit.register:
if q._lock.locked:
try:
yield q.unlock()
except Exception as err:
raise err
[docs] @inlineCallbacks
def remote_unlock_reg_qubits(self, qubitNum):
"""
Release the lock on all qubits in the same register as qubitNum.
"""
try:
yield self._unlock_reg_qubits(self._q_num_to_obj(qubitNum))
except Exception as err:
raise err
[docs] def remote_add_register(self, maxQubits=10):
"""
Adds a new register to the node..
Arguments:
maxQubits maximum number of qubits to use in the default engine
"""
# TODO We have to methods that do the same thing, should deprecate one of them
return self.remote_new_register(maxQubits=maxQubits)
[docs] def get_new_reg_num(self):
"""
Returns an unused register number.
"""
reg_num = self._next_reg_num
self._next_reg_num += 1
return reg_num
[docs] def remote_new_register(self, maxQubits=10):
"""
Initialize a local register. Right now, this simple creates a register according to the simple engine backend
using qubit.
Arguments:
maxQubits maximum number of qubits to use in the default engine (default 10)
"""
try:
# Make sure that reg numbers are assigned correctly
self._get_global_lock()
if self.numRegs >= self.maxRegs:
logging.error("%s: Maximum number of registers reached.", self.myID.name)
raise quantumError("Maximum number of registers reached.")
self.numRegs = self.numRegs + 1
regNum = self.get_new_reg_num()
if simulaqron_settings.backend == "qutip":
newReg = qutipEngine(self.myID, regNum, maxQubits)
elif simulaqron_settings.backend == "projectq":
newReg = projectQEngine(self.myID, regNum, maxQubits)
elif simulaqron_settings.backend == "stabilizer":
newReg = stabilizerEngine(self.myID, regNum, maxQubits)
else:
raise quantumError("Unknown backend {}".format(simulaqron_settings.backend))
self.registers[regNum] = newReg
logging.debug("VIRTUAL NODE %s: Initializing new simulated register.", self.myID.name)
except Exception as e:
logging.error("VIRTUAL NODE {}: Critical error when getting new register: {}".format(self.myID.name, e))
raise e
finally:
self._release_global_lock()
return newReg
[docs] def remote_delete_register(self, reg):
"""
Removes the register from the node.
Happens if the last qubit in the register is measured out.
"""
# Get register number
regnum = reg.num
# Remove register
self.registers.pop(regnum)
self.numRegs -= 1
[docs] @inlineCallbacks
def remote_new_qubit(self, ignore_max_qubits=False):
"""
Create a new qubit in the default local register.
:param ignore_max_qubits: bool
Used to ignore the check if max virtual qubits is reached. This is used when creating EPR pairs
to be able to temporarily create a qubit.
"""
logging.debug("VIRTUAL NODE %s: Request to create new qubit.", self.myID.name)
try:
# Get a lock to assure IDs are assigned correctly and maxQubits is consitently checked
try:
yield self._get_global_lock()
except Exception as err:
raise err
if (len(self.virtQubits) >= self.maxQubits) and (not ignore_max_qubits):
logging.error("VIRTUAL NODE %s: Maximum number of virtual qubits reached.", self.myID.name)
raise noQubitError("Max virtual qubits reached")
else:
# Qubit in the simulation backend, initialized to |0>
simNum = self.get_sim_id()
# Create a new register
newReg = self.remote_add_register()
# simQubit = simulatedQubit(self.myID, self.defaultReg, simNum)
simQubit = simulatedQubit(self.myID, newReg, simNum)
try:
simQubit.make_fresh()
except noQubitError as err:
logging.error("VIRTUAL NODE %s: Max qubits for register reached.", self.myID.name)
raise err
self.simQubits.append(simQubit)
# Virtual qubit
newNum = self.get_virtual_id()
newQubit = virtualQubit(self.myID, self.myID, simQubit, newNum)
self.virtQubits.append(newQubit)
finally:
self._release_global_lock()
return newQubit
[docs] @inlineCallbacks
def remote_new_qubit_inreg(self, reg):
"""
Create a new qubit in the specified register reg.
"""
# Only allow if the register is local
if reg.simNode != self.myID:
raise quantumError("Can only create qubits registers simulated locally by this node.")
try:
# Get a lock to assure IDs are assigned correctly and maxQubits is consitently checked
try:
yield self._get_global_lock()
except Exception as err:
raise err
if len(self.virtQubits) >= self.maxQubits:
logging.error("VIRTUAL NODE %s: Maximum number of virtual qubits reached.", self.myID.name)
raise noQubitError("Max virtual qubits reached")
else:
# Qubit in the local simulation backend, initialized to |0>
simNum = self.get_sim_id()
simQubit = simulatedQubit(self.myID, reg, simNum)
try:
simQubit.make_fresh()
except noQubitError as err:
logging.error("VIRTUAL NODE %s: Max qubits for register reached.", self.myID.name)
raise err
self.simQubits.append(simQubit)
# Virtual qubit
newNum = self.get_virtual_id()
newQubit = virtualQubit(self.myID, self.myID, simQubit, newNum)
self.virtQubits.append(newQubit)
finally:
self._release_global_lock()
return newQubit
[docs] @inlineCallbacks
def remote_cqc_send_qubit(self, num, targetName, app_id, remote_app_id):
"""
Send interface for CQC to add the qubit to the remote nodes received list for an application.
Arguments:
num number of virtual qubit to send
targetName name of the node to send to
app_id application asking to have this qubit delivered
remote_app_id application ID to deliver the qubit to
"""
logging.debug("VIRTUAL NODE %s: request to send qubit %d to %s", self.myID.name, num, targetName)
virtQubit = self.remote_get_virtual_ref(num)
try:
newVirtNum = yield self.remote_send_qubit(virtQubit, targetName)
except Exception as err:
raise err
# Lookup host ID of node
try:
if not (targetName in self.config.hostDict):
raise virtNetError(
"Trying to get conncetion to virtual node {}, but this is not in configuration file".format(
targetName
)
)
remoteNode = yield self.get_connection(targetName)
except Exception as err:
raise err
# Ask to add to list
try:
yield remoteNode.root.callRemote("cqc_add_recv_list", self.myID.name, app_id, remote_app_id, newVirtNum)
except RemoteError as remote_err:
self.reraise_remote_error(remote_err)
except Exception as err:
raise err
[docs] def remote_cqc_add_recv_list(self, fromName, from_app_id, to_app_id, new_virt_num):
"""
Add an item to the received list for use in CQC.
"""
if not (to_app_id in self.cqcRecv):
self.cqcRecv[to_app_id] = deque([])
self.cqcRecv[to_app_id].append(QubitCQC(fromName, self.myID.name, from_app_id, to_app_id, new_virt_num))
logging.debug("VIRTUAL NODE %s: Added a qubit for app id %d to recv list", self.myID.name, to_app_id)
[docs] def remote_cqc_get_recv(self, to_app_id):
"""
Retrieve the next qubit with the given app ID form the received list.
"""
logging.debug(
"VIRTUAL NODE %s: Trying to retrieve qubit for app id %d from recv list", self.myID.name, to_app_id
)
# Get the list corresponding to the specified application ID
if not (to_app_id in self.cqcRecv):
return None
qQueue = self.cqcRecv[to_app_id]
if not qQueue:
return None
# Retrieve the first element on that list (first in, first out)
qc = qQueue.popleft()
if not qc:
return None
logging.debug("VIRTUAL NODE %s: Returning qubit for app id %d from recv list", self.myID.name, to_app_id)
return self.remote_get_virtual_ref(qc.virt_num)
[docs] @inlineCallbacks
def remote_cqc_send_epr_half(self, num, targetName, app_id, remote_app_id, rawEntInfo):
"""
Send interface for CQC to add the qubit to the remote nodes received list for an application.
Arguments:
num number of virtual qubit to send
targetName name of the node to send to
app_id application asking to have this qubit delivered
remote_app_id application ID to deliver the qubit to
entInfo entanglement information
"""
qubit = self.remote_get_virtual_ref(num)
try:
newVirtNum = yield self.remote_send_qubit(qubit, targetName)
except Exception as err:
raise err
# Lookup host ID of node
try:
if not (targetName in self.config.hostDict):
raise virtNetError(
"Trying to get conncetion to virtual node {}, but this is not in configuration file".format(
targetName
)
)
remoteNode = yield self.get_connection(targetName)
except Exception as e:
raise e
# Ask to add to list
try:
yield remoteNode.root.callRemote(
"cqc_add_epr_list", self.myID.name, app_id, remote_app_id, newVirtNum, rawEntInfo
)
except RemoteError as remote_err:
self.reraise_remote_error(remote_err)
except Exception as err:
raise err
[docs] def remote_cqc_add_epr_list(self, fromName, from_app_id, to_app_id, new_virt_num, rawEntInfo):
"""
Add an item to the epr list for use in CQC.
"""
if not (to_app_id in self.cqcRecvEpr):
self.cqcRecvEpr[to_app_id] = deque([])
self.cqcRecvEpr[to_app_id].append(
QubitCQC(fromName, self.myID.name, from_app_id, to_app_id, new_virt_num, rawEntInfo=rawEntInfo)
)
logging.debug("VIRTUAL NODE %s: Added a qubit for app id %d to epr list", self.myID.name, to_app_id)
[docs] def remote_cqc_get_epr_recv(self, to_app_id):
"""
Retrieve the next qubit (half of an EPR-pair) with the given app ID from the received list.
"""
try:
logging.debug(
"VIRTUAL NODE %s: Trying to retrieve qubit for app id %d from epr list", self.myID.name, to_app_id
)
# Get the list corresponding to the specified application ID
if not (to_app_id in self.cqcRecvEpr):
return None
qQueue = self.cqcRecvEpr[to_app_id]
if not qQueue:
return None
# Retrieve the first element on that list (first in, first out)
qc = qQueue.popleft()
if not qc:
return None
logging.debug("VIRTUAL NODE %s: Returning qubit for app id %d from epr list", self.myID.name, to_app_id)
return self.remote_get_virtual_ref(qc.virt_num), qc.rawEntInfo
except Exception as e:
raise e
[docs] @inlineCallbacks
def remote_send_qubit(self, qubit, targetName):
"""
Sends the qubit to the specified target node. This creates a new virtual qubit object at the remote node
with the right qubit and backend details.
Arguments
qubit virtual qubit to be sent
targetName target ndoe to place qubit at (host object)
"""
logging.debug("VIRTUAL NODE %s: Request to send qubit sim Num %d to %s.", self.myID.name, qubit.num, targetName)
if qubit.active != 1:
logging.debug("VIRTUAL NODE %s: Attempt to manipulate qubit no longer at this node.", self.myID.name)
return
# Lookup host id of node
try:
if not (targetName in self.config.hostDict):
raise virtNetError(
"Trying to get conncetion to virtual node {}, but this is not in configuration file".format(
targetName
)
)
remoteNode = yield self.get_connection(targetName)
except Exception as e:
raise e
try:
# Get lock to prevent access to qubits between sending and manipulating local list
self._get_global_lock()
# Check whether we are just the virtual, or also the simulating node
if qubit.virtNode == qubit.simNode:
logging.debug("VIRTUAL NODE %s: Sending qubit simulated locally", self.myID.name)
# We are both the virtual as well as the simulating node
# Pass a reference to our locally simulated qubit object to the remote node
try:
newNum = yield remoteNode.root.callRemote("add_qubit", self.myID.name, qubit.simQubit)
except RemoteError as remote_err:
self.reraise_remote_error(remote_err)
except Exception as err:
raise err
else:
logging.debug(
"VIRTUAL NODE %s: Sending qubit simulated remotely at %s", self.myID.name, qubit.simNode.name
)
# We are only the virtual node, not the simulating one. In this case, we need to ask
# the actual simulating node to do the transfer for us. Due to the pecularities of Twisted PB
# we need to do this by the simulated qubit number
try:
simQubitNum = yield qubit.simQubit.callRemote("get_sim_number")
except RemoteError as remote_err:
self.reraise_remote_error(remote_err)
except Exception as err:
raise err
try:
newNum = yield qubit.simNode.root.callRemote("transfer_qubit", simQubitNum, targetName)
except RemoteError as remote_err:
self.reraise_remote_error(remote_err)
except Exception as err:
raise err
# We gave it away so mark as inactive
qubit.active = 0
# Remove the qubit from the local virtual list. Note it remains in the simulated
# list, since we continue to simulate this qubit if we did so before.
self.virtQubits.remove(qubit)
except Exception as err:
raise err
finally:
self._release_global_lock()
return newNum
[docs] @inlineCallbacks
def remote_transfer_qubit(self, simQubitNum, targetName):
"""
Transfer the qubit to the destination node if we are the simulating node. The reason why we cannot
do this directly is that Twisted PB does not allow objects to be passed between connecting nodes.
Only between the creator of the object and its immediate connections.
Arguments
simQubitNum simulated qubit number to be sent
targetName target node to place qubit at (host object)
"""
logging.debug("VIRTUAL NODE %s: Request to transfer qubit to %s.", self.myID.name, targetName)
# Convert the number into the right local object
simQubit = self._q_num_to_obj(simQubitNum)
# Lookup host id of node
try:
if not (targetName in self.config.hostDict):
raise virtNetError(
"Trying to get conncetion to virtual node {}, but this is not in configuration file".format(
targetName
)
)
remoteNode = yield self.get_connection(targetName)
except Exception as e:
raise e
# Check if we are both the destination node and simulating node
if self.myID.name == targetName:
try:
newNum = yield remoteNode.root.remote_add_qubit(self.myID.name, simQubit)
except Exception as err:
raise err
else:
try:
newNum = yield remoteNode.root.callRemote("add_qubit", self.myID.name, simQubit)
except RemoteError as remote_err:
self.reraise_remote_error(remote_err)
except Exception as err:
raise err
return newNum
[docs] @inlineCallbacks
def remote_add_qubit(self, name, simQubit):
"""
Add a qubit to the local virtual node.
Arguments
name name of the node simulating this qubit
simQubit simulated qubit reference in the backend we're adding
"""
logging.debug("VIRTUAL NODE %s: Request to add qubit from %s.", self.myID.name, name)
# Get the details of the remote node
try:
if not (name in self.config.hostDict):
raise virtNetError(
"Trying to get conncetion to virtual node {}, but this is not in configuration file".format(name)
)
nb = yield self.get_connection(name)
except Exception as e:
raise e
try:
# Get a lock to make sure IDs are assigned correctly
self._get_global_lock()
if len(self.virtQubits) >= self.maxQubits:
raise noQubitError("Max virtual qubits reached")
# Generate a new virtual qubit object for the qubit now at this node
newNum = self.get_virtual_id()
newQubit = virtualQubit(self.myID, nb, simQubit, newNum)
# Add to local list
self.virtQubits.append(newQubit)
except Exception as err:
raise err
finally:
self._release_global_lock()
return newNum
[docs] def remote_get_virtual_ref(self, num):
"""
Return a virual qubit object for the given number.
Arguments
num number of the virtual qubit
"""
for q in self.virtQubits:
if q.num == num:
return q
return None
[docs] def remote_remove_sim_qubit_num(self, delNum):
"""
Removes the simulated qubit delQubit from the node and also from the underlying engine. Relies on this qubit
having been locked.
Arguments
delNum simID of the simulated qubit to delete
"""
self._remove_sim_qubit(self._q_num_to_obj(delNum))
@inlineCallbacks
def _remove_sim_qubit(self, delQubit):
"""
Removes the simulated qubit object.
Arguments
delQubit simulated qubit object to delete
"""
# Caution: Only qubits simulated at this node can be removed
if delQubit not in self.simQubits:
logging.error("VIRTUAL NODE %s: Attempt to delete qubit not simulated at this node.", self.myID.name)
raise quantumError("%s: Cannot delete qubits we don't simulate.")
#
delNum = delQubit.num
delRegister = delQubit.register
try:
# We need to manipulate multiple qubits, get global lock
yield self._get_global_lock()
# Lock all relevant qubits first
for q in self.simQubits:
if q.register == delRegister:
yield q.lock()
# First we remove the physical qubit from the register
delRegister.remove_qubit(delNum)
# Check if this was the last qubit
if delRegister.activeQubits == 0:
self.remote_delete_register(delRegister)
else:
# When removing a qubit, we need to update the positions of the qubits in
# the underlying physical register
# in all relevant qubit objects.
for q in self.simQubits:
# If they are in the same engine, and update is required
if q.register == delRegister:
if q.num > delNum:
q.num = q.num - 1
# Remove the qubit form the list of simulated qubits
self.simQubits.remove(delQubit)
except Exception as e:
logging.error("VIRTUAL NODE {}: Cannot remove sim qubit - {}".format(self.myID.name, e))
finally:
# Release all relevant qubits again
for q in self.simQubits:
if q.register == delRegister:
q.unlock()
# Release the global multi qubit lock
self._release_global_lock()
[docs] def remote_merge_regs(self, num1, num2):
"""
Merges the two local quantum registers. Note that these register may simulate virtual qubits across different
network nodes. This will ignore maxQubits and simply create one large register allowing twice maxQubits qubits.
Arguments
num1 number of the first qubit
num2 number of the second qubit
"""
# Lookup the qubit objects corresponding to these numbers
for q in self.simQubits:
if q.simNum == num1:
q1 = q
elif q.simNum == num2:
q2 = q
self.local_merge_regs(q1, q2)
[docs] def local_merge_regs(self, qubit1, qubit2):
"""
Merges the two local quantum registers. Note that these register may simulate virtual qubits across different
network nodes. This will ignore maxQubits and simply create one large register allowing twice maxQubits qubits.
Arguments
qubit1 qubit1 in reg1, called from remote having access to only qubits
qubit2 qubit2 in reg2
"""
logging.debug(
"VIRTUAL NODE %s: Request to merge local register for qubits simNum %d and simNum %d.",
self.myID.name,
qubit1.simNum,
qubit2.simNum,
)
# This should only be called if locks are acquired
assert qubit1._lock.locked
assert qubit2._lock.locked
assert self._lock.locked
logging.debug("VIRTUAL NODE %s: Request to merge LOCKS PRESENT", self.myID.name)
reg1 = qubit1.register
reg2 = qubit2.register
# Check if there's anything to do at all
if reg1 == reg2:
logging.debug("VIRTUAL NODE %s: Request to merge local register: not required", self.myID.name)
return
logging.debug("VIRTUAL NODE %s: Request to merge local register: need merge", self.myID.name)
# Allow reg 1 to absorb reg 2
reg1.maxQubits = reg1.maxQubits + reg2.activeQubits
# For relabelling qubit numbers get the offset
offset = reg1.activeQubits
# Add reg2 to reg1
reg1.absorb(reg2)
# Update the simulated qubit numbering and register
for q in self.simQubits:
if q.register == reg2:
logging.debug("VIRTUAL NODE %s: Updating register %d to %d.", self.myID.name, q.num, q.num + offset)
q.register = reg1
q.num = q.num + offset
# reg2.reset()
self.remote_delete_register(reg2)
[docs] @inlineCallbacks
def remote_merge_from(self, simNodeName, simQubitNum, localReg):
"""
Bring a remote register to this node.
Arguments
simNodeName name of the node who simulates right now
simQubitNum simulation number of qubit whose register we will merge
localReg local register to merge with
"""
logging.debug("VIRTUAL NODE %s: Merging from %s", self.myID.name, simNodeName)
# This should only be called if lock is acquired
assert self._lock.locked
logging.debug("VIRTUAL NODE %s: Merging from %s LOCKS PRESENT", self.myID.name, simNodeName)
# Lookup the local connection for this simulating node
try:
if not (simNodeName in self.config.hostDict):
raise virtNetError(
"Trying to get conncetion to virtual node {}, but this is not in configuration file".format(
simNodeName
)
)
simNode = yield self.get_connection(simNodeName)
except Exception as e:
raise e
# Fetch the details of the remote register and qubit, and remove sim qubits at node
try:
(R, I, activeQ, oldRegNum, oldQubitNum) = yield simNode.root.callRemote("get_register_del", simQubitNum)
except RemoteError as remote_err:
self.reraise_remote_error(remote_err)
except Exception as err:
raise err
# Get numbering offset from previous register: append at end
offset = localReg.activeQubits
# Allow localReg to absorb the remote register
localReg.maxQubits = localReg.maxQubits + activeQ
localReg.absorb_parts(R, I, activeQ)
# Collect mappings between numbers and objects for updating the virtual qubits
newD = {}
# Make new qubit objects
for k in range(activeQ):
simNum = self.get_sim_id()
newQubit = simulatedQubit(self.myID, localReg, simNum, offset + k)
self.simQubits.append(newQubit)
newD[k] = newQubit
# Issue an update call to all nodes to update their virtual qubits if necessary
# for name in self.conn:
for name in self.config.hostDict:
if name != self.myID.name:
try:
nb = yield self.get_connection(name)
except Exception as err:
raise err
try:
yield nb.root.callRemote("update_virtual_merge", self.myID.name, simNodeName, oldRegNum, newD)
except RemoteError as remote_err:
self.reraise_remote_error(remote_err)
except Exception as err:
raise err
# Locally, we might also already have virtual qubits which were in the remote simulated
# register. Update them as well
logging.debug("VIRTUAL NODE %s: Updating local virtual qubits.", self.myID.name)
try:
yield self.remote_update_virtual_merge(self.myID.name, simNodeName, oldRegNum, newD)
except Exception as err:
raise err
# Return the qubit object corresponding to the new physical qubit
return newD[oldQubitNum]
[docs] @inlineCallbacks
def remote_update_virtual_merge(self, newSimNodeName, oldSimNodeName, oldRegNum, newD):
"""
Update the virtual qubits to the new simulating node, if applicable. This is extremely
inefficient due to not keeping register information in virtualQubit.
Arguments
newSimNodeName new node simulating this qubit
oldSimNodeName old node simulating the qubit
oldReg old register
newD dictionary mapping qubit numbers to qubit objects at the new simulating node
"""
logging.debug("VIRTUAL NODE %s: Request to update local virtual qubits.", self.myID.name)
# If this is a third node (not involved in the two qubit gate, but carrying virtual qubits
# which were in the simulated register), then they will now be updated. We remark that this function
# can only be called from the _simulating node_ now handing over simulation to someone else. Both the simulating
# node and the new simulating node are globally locked so there should be no conflicts here in updating:
# a third node that may wish to do a 2 qubit gate between the qubits to be updated needs to wait.
# Lookup the local connections for the given node names
try:
if not (newSimNodeName in self.config.hostDict):
raise virtNetError(
"Trying to get conncetion to virtual node {}, but this is not in configuration file".format(
newSimNodeName
)
)
if not (oldSimNodeName in self.config.hostDict):
raise virtNetError(
"Trying to get conncetion to virtual node {}, but this is not in configuration file".format(
oldSimNodeName
)
)
newSimNode = yield self.get_connection(newSimNodeName)
oldSimNode = yield self.get_connection(oldSimNodeName)
except Exception as e:
raise e
for q in self.virtQubits:
if q.virtNode == q.simNode and q.simNode == oldSimNode:
logging.debug("VIRTUAL NODE %s: Simulating node update.", self.myID.name)
# We previously simulated this qubit ourselves
givenReg = q.simQubit.register.num
givenNum = q.simQubit.num
elif q.simNode == oldSimNode:
logging.debug("VIRTUAL NODE %s: Previously remote simulator node update.", self.myID.name)
# We had the virtual qubit but it was simulated elsewhere
try:
(givenNum, givenReg) = yield q.simQubit.callRemote("get_numbers")
except RemoteError as remote_err:
self.reraise_remote_error(remote_err)
except Exception as err:
raise err
# Check if this qubit needs updating
if q.simNode == oldSimNode and givenReg == oldRegNum:
logging.debug(
"VIRTUAL NODE %s: Updating virtual qubit %d, previously %s now %s",
self.myID.name,
q.num,
oldSimNode.name,
newSimNode.name,
)
q.simNode = newSimNode
q.simQubit = newD[givenNum]
[docs] @inlineCallbacks
def remote_get_register_RI(self, qubit):
"""
Return the real and imaginary part of the (possibly remote) simulated register which
contains this virtual qubit.
"""
if isinstance(qubit, virtualQubit):
realM, imagM = yield qubit.remote_get_register_RI()
else:
realM, imagM = yield qubit.callRemote("get_register_RI")
return realM, imagM
[docs] def remote_get_register(self, qubit):
"""
Return the value of of a locally simulated register which contains this virtual qubit.
"""
(realM, imagM) = qubit.simQubit.register.get_register_RI()
activeQ = qubit.simQubit.register.activeQubits
oldRegNum = qubit.simQubit.register.num
oldQubitNum = qubit.simQubit.num
return (realM, imagM, activeQ, oldRegNum, oldQubitNum)
[docs] def remote_get_register_del(self, qubitNum):
"""
Return the value of of a locally simulated register, and remove the simulated qubits from this node.
Caution: virtual qubits not updated.
"""
assert self._lock.locked
# Locate the qubit object for this ID
gotQ = None
for q in self.simQubits:
if q.simNum == qubitNum:
gotQ = q
# If nothing is found, return
if gotQ is None:
logging.debug("VIRTUAL NODE %s: No simulated qubit with ID %d.", qubitNum)
return ([], [], 0, 0, 0)
(realM, imagM) = gotQ.register.get_register_RI()
activeQ = gotQ.register.activeQubits
oldRegNum = gotQ.register.num
oldQubitNum = gotQ.num
delRegister = gotQ.register
# Remove all simulated qubits and the register
# Need to iterate of simQubits in reverse, otherwise wrong elements are removed
for q in reversed(self.simQubits):
if q.register.num == oldRegNum:
self.simQubits.remove(q)
# gotQ.register.activeQubits -= 1
self.remote_delete_register(delRegister)
return (realM, imagM, activeQ, oldRegNum, oldQubitNum)
[docs] @inlineCallbacks
def remote_get_multiple_qubits(self, qList):
"""
Return the state of multiple qubits virtually located at this node. This will fail if the qubits
are not in the same register or thus also simulating node.
Arguments
qList list of virtual qubits of which to retrieve the state
"""
localSim = False
remoteSim = False
# Check whether we are the simulating node.
for q in qList:
if q.simNode == q.virtNode:
localSim = True
elif q.simNode != q.virtNode:
remoteSim = True
# Check whether two nodes are the simulator, for now we simply fail in this case
if localSim and remoteSim:
logging.error(
"VIRTUAL NODE %s: Getting multiple qubits from multiple simulators is currently not supported.",
self.myID.name,
)
return ([0], [0])
if localSim:
# Qubits are local, simply retrieve from the simulation
nums = []
for q in qList:
nums.append(q.simQubit.simNum)
logging.debug("VIRTUAL NODE %s: Looking for simulated qubits. %s", self.myID.name, nums)
(R, I) = self.remote_get_state(nums)
else:
# Qubits are located elsewhere.
nums = []
for q in qList:
try:
(num, name) = yield q.simQubit.callRemote("get_details")
except RemoteError as remote_err:
self.reraise_remote_error(remote_err)
except Exception as err:
raise err
nums.append(num)
try:
(R, I) = yield qList[0].simNode.root.callRemote("get_state", nums)
except RemoteError as remote_err:
self.reraise_remote_error(remote_err)
except Exception as err:
raise err
return (R, I)
[docs] def remote_get_state(self, simNumList):
"""
Return the state of multiple qubits corresponding to the IDs in simNumList.
"""
# Convert simulation numbers to register and real number in register
traceList = []
foundOne = False
prev = None
for n in simNumList:
for q in self.simQubits:
if q.simNum == n:
if foundOne is True and prev.register != q.register:
logging.error(
"VIRTUAL NODE %s: Getting multiple qubits from different registers not supported.",
self.myID.name,
)
return ([], [])
prev = q
foundOne = True
traceList.append(q.num)
if not foundOne:
logging.error("VIRTUAL NODE %s: No such qubits found.", self.myID.name)
return
traceList.sort()
(realM, imagM) = prev.register.get_qubits_RI(traceList)
return (realM, imagM)
#######
#
# virtualQubit - a qubit that is virtually carried at this node. It may be simulated elsewhere
# but in the simulation it is located at this particular virtualNode.
#
# This is given out as a reference object to users who ask for a "local" qubit
#
#
[docs]class virtualQubit(pb.Referenceable):
def __init__(self, virtNode, simNode, simQubit, num):
"""
Creates a virtual qubit object simulated in the specified simulation register backend
Arguments
virtNode node where this qubit is virtually located
simNode node where this qubit is simulated
simQubit reference to the underlying qubit object (may be remote)
num number ID among the virtual qubits
"""
# Node where this qubit is virtually located
self.virtNode = virtNode
# Node where this qubit is being simulated
self.simNode = simNode
# Underlying qubit object for simulation
self.simQubit = simQubit
# Qubit active at this node. The client may retain a reference to this object,
# which will cause python to keep it, while it has actually be transferred to
# another node. We do not allow operations on a qubit that is now virtually elsewhere.
self.active = 1
# Our number at this virtual node. Note that this has nothing to do
# with the number of the qubits in the register
self.num = num
@inlineCallbacks
def _single_gate(self, name, *args):
"""
Apply the single gate function to the underlying qubit. This is an internal method used by all the other
single qubit calls, which will perform the correct local or remote method calls as applicable after
performing the necessary locking.
Arguments
name name of the method corresponding to the name. For example: name = apply_X
param parameters for gates such as rotations (axis,angle)
"""
if self.active != 1:
logging.error("VIRTUAL NODE %s: Attempt to manipulate qubits no longer at this node.", self.virtNode.name)
return False
# Construct the name of the method to call if the qubit is locally simulated
# in which case we (ironically) need to append the prefix remote which is automatically
# added if the method is called from remote by Twisted
localName = "".join(["remote_", name])
# Check whether the qubit is local or remote. Due to remote register merges, this may change
# while we try and get a lock. For this reason, we have to wait until we have a lock on an _active_
# qubit before proceeding. If it is no longer active when we get the lock, then it has been
# moved elsewhere in the meantime and we need to wait for the remote message to update the virtual
# qubit object in the background.
waiting = True
outcome = False
while waiting:
if self.virtNode == self.simNode:
if not self.simQubit.isLocked():
try:
yield self.simQubit.lock()
if self.simQubit.active:
getattr(self.simQubit, localName)(*args)
waiting = False
outcome = True
except Exception as e:
logging.error("VIRTUAL NODE {}: Cannot apply {} - {}".format(self.virtNode.name, e, name))
waiting = False
finally:
self.simQubit.unlock()
else:
try:
isLocked = yield self.simQubit.callRemote("isLocked")
except RemoteError as remote_err:
self.virtNode.root.reraise_remote_error(remote_err)
except Exception as err:
raise err
if not isLocked:
try:
yield self.simQubit.callRemote("lock")
active = yield self.simQubit.callRemote("isActive")
if active:
logging.debug(
"VIRTUAL NODE %s: Calling %s remotely to apply %s.",
self.virtNode.name,
self.simNode.name,
name,
)
yield self.simQubit.callRemote(name, *args)
waiting = False
outcome = True
except Exception as e:
logging.error("VIRTUAL NODE %s: Cannot apply %s - %s", e, name)
waiting = False
finally:
yield self.simQubit.callRemote("unlock")
# If we did not get a lock on an active qubit, wait for update and try again
if waiting:
try:
yield deferLater(reactor, self.virtNode.root._delay, lambda: None)
except Exception as err:
raise err
return outcome
[docs] @inlineCallbacks
def remote_apply_X(self):
"""
Apply X gate to itself by passing it onto the underlying register.
"""
try:
success = yield self._single_gate("apply_X")
return success
except Exception as err:
raise err
[docs] @inlineCallbacks
def remote_apply_Y(self):
"""
Apply Y gate.
"""
try:
success = yield self._single_gate("apply_Y")
return success
except Exception as err:
raise err
[docs] @inlineCallbacks
def remote_apply_Z(self):
"""
Apply Z gate.
"""
try:
success = yield self._single_gate("apply_Z")
return success
except Exception as err:
raise err
[docs] @inlineCallbacks
def remote_apply_H(self):
"""
Apply H gate.
"""
try:
success = yield self._single_gate("apply_H")
return success
except Exception as err:
raise err
[docs] @inlineCallbacks
def remote_apply_K(self):
"""
Apply K gate - taking computational basis to Y eigenbasis.
"""
try:
success = yield self._single_gate("apply_K")
return success
except Exception as err:
raise err
[docs] @inlineCallbacks
def remote_apply_T(self):
"""
Apply T gate.
"""
try:
success = yield self._single_gate("apply_T")
return success
except Exception as err:
raise err
[docs] @inlineCallbacks
def remote_apply_rotation(self, n, a):
"""
Apply rotation around axis n with angle a.
Arguments:
n A tuple of three numbers specifying the rotation axis, e.g n=(1,0,0)
a The rotation angle in radians.
"""
try:
success = yield self._single_gate("apply_rotation", n, a)
return success
except Exception as err:
raise err
[docs] @inlineCallbacks
def remote_measure(self, inplace=False):
"""
Measure the qubit in the standard basis. If inplace=False, this does delete the qubit from the simulation.
Returns the measurement outcome.
"""
if self.active != 1:
logging.error("VIRTUAL NODE %s: Attempt to manipulate qubits no longer at this node.", self.virtNode.name)
return
# Check whether the qubit is local or remote. Due to remote register merges, this may change
# while we try and get a lock. For this reason, we have to wait until we have a lock on an _active_
# qubit before proceeding.
waiting = True
outcome = None
while waiting:
if self.virtNode == self.simNode:
if not self.simQubit.isLocked():
try:
yield self.simQubit.lock()
if self.simQubit.active:
logging.debug("VIRTUAL NODE %s: Measuring local qubit", self.virtNode.name)
outcome = self.simQubit.remote_measure_inplace()
if not inplace:
self.virtNode.root._remove_sim_qubit(self.simQubit)
# Delete from virtual qubits
self.virtNode.root.virtQubits.remove(self)
waiting = False
except Exception as e:
logging.error(
"VIRTUAL NODE {}: Cannot remove local qubit. Error: {}".format(self.virtNode.name, e)
)
waiting = False
finally:
self.simQubit.unlock()
else:
try:
isLocked = yield self.simQubit.callRemote("isLocked")
except RemoteError as remote_err:
self.virtNode.root.reraise_remote_error(remote_err)
except Exception as err:
raise err
if not isLocked:
try:
yield self.simQubit.callRemote("lock")
active = yield self.simQubit.callRemote("isActive")
if active:
logging.debug(
"VIRTUAL NODE %s: Measuring remote qubit at %s.", self.virtNode.name, self.simNode.name
)
outcome = yield self.simQubit.callRemote("measure_inplace")
if not inplace:
num = yield self.simQubit.callRemote("get_sim_number")
yield self.simNode.root.callRemote("remove_sim_qubit_num", num)
# Delete from virtual qubits
self.virtNode.root.virtQubits.remove(self)
waiting = False
except Exception as e:
logging.error(
"VIRTUAL NODE {}: Cannot remove remote qubit. Error: {}".format(self.virtNode.name, e)
)
waiting = False
finally:
yield self.simQubit.callRemote("unlock")
# If we did not get a lock on an active qubit, wait for update and try again
if waiting:
try:
yield deferLater(reactor, self.virtNode.root._delay, lambda: None)
except Exception as err:
raise err
return outcome
def _lock_nodes(self, target):
"""
Wrapper to acquire the global register lock on both nodes that involve the qubits, and local node.
Arguments
target virtual qubit of the target qubit
"""
lockedLocal = False
lockedRemoteTarget = False
# Lock qubits nodes
if self.simNode == self.virtNode:
# first qubit is locally simulated
def1 = self.simNode.root._get_global_lock()
lockedLocal = True
else:
# first qubit is remote
def1 = self.simNode.root.callRemote("get_global_lock")
# If target is a different node
if target.simNode != self.simNode:
if target.simNode == target.virtNode:
# target qubit is local
def2 = target.simNode.root._get_global_lock()
lockedLocal = True
else:
# target qubit is remote
def2 = target.simNode.root.callRemote("get_global_lock")
lockedRemoteTarget = True
if not lockedLocal:
def0 = self.virtNode.root._get_global_lock()
if lockedRemoteTarget:
return DeferredList([def0, def1, def2], fireOnOneCallback=False, consumeErrors=True)
else:
return DeferredList([def0, def1], fireOnOneCallback=False, consumeErrors=True)
else:
if lockedRemoteTarget:
return DeferredList([def1, def2], fireOnOneCallback=False, consumeErrors=True)
else:
return DeferredList([def1], fireOnOneCallback=False, consumeErrors=True)
@inlineCallbacks
def _unlock_nodes(self, q1simNode, q1virtNode, q2simNode, q2virtNode):
"""
Wrapper to acquire the global register lock on both nodes that involve the qubits. This takes different
arguments as lock nodes since we wish to call it with the _original_ simulated and target nodes from
which we got the lock - not the updated ones.
Arguments
q1simNode original simulating node of the first qubit
q1virtNode original virtual node of the first qubit
q2simNode original simulating node of the second qubit
q2virtNode original virtual node of the second qubit
"""
try:
# Release qubit node locks
if q1simNode == q1virtNode:
# first qubit was locally simulated
yield self.simNode.root._release_global_lock()
else:
# first qubit was remote
yield q1simNode.root.callRemote("release_global_lock")
# If target was a different node
if q1simNode != q2simNode:
if q2simNode == q2virtNode:
# target qubit was local
yield q2simNode.root._release_global_lock()
else:
# target qubit was remote
yield q2simNode.root.callRemote("release_global_lock")
# Release local node (may be the same as above)
self.virtNode.root._release_global_lock()
except RemoteError as remote_err:
self.virtNode.root.reraise_remote_error(remote_err)
except Exception as err:
raise err
@inlineCallbacks
def _lock_inreg(self, qubit):
"""
Lock all qubits in the same register as the virtual qubit qubit.
"""
try:
if qubit.simNode == qubit.virtNode:
yield qubit.simNode.root._lock_reg_qubits(qubit.simQubit)
else:
simNum = yield qubit.simQubit.callRemote("get_sim_number")
yield qubit.simNode.root.callRemote("lock_reg_qubits", simNum)
except RemoteError as remote_err:
self.virtNode.root.reraise_remote_error(remote_err)
except Exception as err:
raise err
@inlineCallbacks
def _unlock_inreg(self, qubit):
"""
Lock all qubits in the same register as the virtual qubit qubit.
"""
try:
if qubit.simNode == qubit.virtNode:
yield qubit.simNode.root._unlock_reg_qubits(qubit.simQubit)
else:
simNum = yield qubit.simQubit.callRemote("get_sim_number")
yield qubit.simNode.root.callRemote("unlock_reg_qubits", simNum)
except RemoteError as remote_err:
self.virtNode.root.reraise_remote_error(remote_err)
except Exception as err:
raise err
[docs] @inlineCallbacks
def remote_cnot_onto(self, target):
"""
Performs a CNOT operation with this qubit as control, and the other qubit as target.
Arguments
target the virtual qubit to use as the target of the CNOT
"""
try:
success = yield self._two_qubit_gate(target, "cnot_onto")
return success
except Exception as err:
raise err
[docs] @inlineCallbacks
def remote_cphase_onto(self, target):
"""
Performs a CPHASE operation with this qubit as control, and the other qubit as target.
Arguments
target the virtual qubit to use as the target of the CPHASE
"""
try:
success = yield self._two_qubit_gate(target, "cphase_onto")
return success
except Exception as err:
raise err
@inlineCallbacks
def _two_qubit_gate(self, target, name):
"""
Perform a two qubit gate including all the required locking.
Arguments
target second virtual qubit (beyond self which is the first)
name name of the gate to perform
"""
if self.active != 1 or target.active != 1:
logging.error("VIRTUAL NODE %s: Attempt to manipulate qubits no longer at this node.", self.virtNode.name)
return
localName = "".join(["remote_", name])
logging.debug(
"VIRTUAL NODE %s: Doing 2 qubit gate name %s and local call %s", self.virtNode.name, name, localName
)
# Before we proceed, we need to acquire the gobal locks of the nodes holding the
# registers of both qubits. We wrap this in a timeout with random repeat since there is
# otherwise the possibility of a deadlock if two nodes compete for the _two_ locks
waiting = True
attempts = 0
try:
while waiting and attempts <= self.virtNode.root.maxAttempts:
# Set up the timeout at a random time between 1s and 4s later
timeoutD = Deferred()
timeup = reactor.callLater(random.uniform(1, 4), timeoutD.callback, None)
# Check if self simNode is locked
if self.simNode == self.virtNode:
self_isLocked = self.simNode.root._lock.locked
else:
self_isLocked = yield self.simNode.root.callRemote("isLocked")
# Check if other simNode is locked
if target.simNode == target.virtNode:
other_isLocked = target.simNode.root._lock.locked
else:
other_isLocked = yield target.simNode.root.callRemote("isLocked")
if self_isLocked:
logging.debug(
"VIRTUAL NODE {}: This SimNode {} already locked. Need to wait.".format(
self.virtNode.name, self.simNode.name
)
)
yield timeoutD
attempts += 1
elif other_isLocked:
logging.debug(
"VIRTUAL NODE {}: Other SimNode {} already locked. Need to wait.".format(
self.virtNode.name, target.simNode.name
)
)
yield timeoutD
attempts += 1
else:
# Set up the lock acquisition
lockD = self._lock_nodes(target)
try:
# Yield on both of them
gotLock, timeoutRes = yield DeferredList(
[lockD, timeoutD], fireOnOneCallback=True, fireOnOneErrback=True, consumeErrors=True
)
except Exception as e:
logging.debug("VIRTUAL NODE %s: Cannot get lock %s", self.virtNode.name, e)
yield self._unlock_nodes(self.simNode, self.virtNode, target.simNode, target.virtNode)
timeup.cancel()
return
else:
if timeoutD.called:
logging.debug("VIRTUAL NODE %s: Timing out getting locks.", self.virtNode.name)
lockD.cancel()
yield self._unlock_nodes(self.simNode, self.virtNode, target.simNode, target.virtNode)
attempts = attempts + 1
elif lockD.called:
waiting = False
timeup.cancel()
except RemoteError as remote_err:
self.virtNode.root.reraise_remote_error(remote_err)
except Exception as err:
raise err
# We have now acquired the two relevant global node locks. If more than one qubit is locked, all code
# will first acquire the global lock, so this should be safe from deadlocks now, so we will not timeout
try:
yield self._lock_inreg(self)
yield self._lock_inreg(target)
except Exception as err:
raise err
# When merging registers, we may need to update the virtual qubits. Remember the original ones so we can
# send appropriate unlocks below. (note this assignment must be done after the locks are acquired)
q1simNode = self.simNode
q1virtNode = self.virtNode
q2simNode = target.simNode
q2virtNode = target.virtNode
# Todo a 2 qubit gate, both qubits must be in the same simulated register. We will merge
# registers if this is not already the case.
try:
if self.simNode == target.simNode:
# Both qubits are simulated at the same node
if self.simNode == self.virtNode:
# Both qubits are both locally simulated, check whether they are in the same register
if self.simQubit.register == target.simQubit.register:
# They are even in the same register, just do the gate
getattr(self.simQubit, localName)(target.simQubit.num)
else:
logging.debug("VIRTUAL NODE %s: 2qubit command demands register merge.", self.virtNode.name)
# Both are local but not in the same register
self.simNode.root.local_merge_regs(self.simQubit, target.simQubit)
# After the merge, just do the gate
getattr(self.simQubit, localName)(target.simQubit.num)
else:
# Both are remotely simulated
logging.debug("VIRTUAL NODE %s: 2qubit command demands remote register merge.", self.virtNode.name)
# Fetch the details of the two simulated qubits from remote
(fNum, fNode) = yield self.simQubit.callRemote("get_details")
(tNum, tNode) = yield target.simQubit.callRemote("get_details")
# Sanity check: we really have the right simulating node
if fNode != self.simNode.name or tNode != target.simNode.name:
logging.error("VIRTUAL NODE %s: Inconsistent simulation. Cannot merge.", self.myID.name)
raise quantumError("Inconsistent simulation")
# Merge the remote register according to the simulation IDs of the qubits
yield self.simNode.root.callRemote("merge_regs", fNum, tNum)
# Get the number of the target in the new register
targetNum = yield target.simQubit.callRemote("get_number")
# Execute the 2 qubit gate
yield self.simQubit.callRemote(name, targetNum)
logging.debug(
"VIRTUAL NODE %s: Remote 2qubit command to %s.", self.virtNode.name, target.simNode.name
)
else:
# They are simulated at two different nodes
if self.simNode == self.virtNode:
# We are the locally simulating node of the first qubit, merge all to us
logging.debug(
"VIRTUAL NODE %s: 2qubit command demands merge from remote target sim %s to us.",
self.simNode.name,
target.simNode.name,
)
(fNum, fNode) = yield target.simQubit.callRemote("get_details")
if fNode != target.simNode.name:
logging.error("VIRTUAL NODE %s: Inconsistent simulation. Cannot merge.", self.myID.name)
raise quantumError("Inconsistent simulation.")
target.simQubit = yield self.simNode.root.remote_merge_from(
target.simNode.name, fNum, self.simQubit.register
)
# Get the number of the target in the new register
targetNum = target.simQubit.num
# Execute the 2 qubit gate
getattr(self.simQubit, localName)(targetNum)
elif target.simNode == target.virtNode:
# We are the locally simulating node of the target qubit, merge all to us
logging.debug(
"VIRTUAL NODE %s: 2qubit command demands merge from remote sim %s to us.",
target.simNode.name,
self.simNode.name,
)
(fNum, fNode) = yield self.simQubit.callRemote("get_details")
if fNode != self.simNode.name:
logging.error("VIRTUAL NODE %s: Inconsistent simulation. Cannot merge.", self.myID.name)
raise quantumError("Inconsistent simulation.")
self.simQubit = yield target.simNode.root.remote_merge_from(
self.simNode.name, fNum, target.simQubit.register
)
# Get the number of the target in the new register
targetNum = target.simQubit.num
# Execute the 2 qubit gate
getattr(self.simQubit, localName)(targetNum)
else:
# Both qubits are remotely simulated - we will pull both registers to become one local register
logging.debug(
"VIRTUAL NODE %s: 2qubit command demands total remote merge from %s and %s.",
self.virtNode.name,
target.simNode.name,
self.simNode.name,
)
# Create a new local register
newLocalReg = self.virtNode.root.remote_add_register()
# Fetch the detail of the two registers from remote
(fNum, fNode) = yield self.simQubit.callRemote("get_details")
if fNode != self.simNode.name:
logging.error("VIRTUAL NODE %s: Inconsistent simulation. Cannot merge.", self.myID.name)
raise quantumError("Inconsistent simulation.")
(tNum, tNode) = yield target.simQubit.callRemote("get_details")
if tNode != target.simNode.name:
logging.error("VIRTUAL NODE %s: Inconsistent simulation. Cannot merge.", self.myID.name)
raise quantumError("Inconsistent simulation.")
# Pull the remote registers to this node
self.simQubit = yield self.virtNode.root.remote_merge_from(self.simNode.name, fNum, newLocalReg)
target.simQubit = yield target.virtNode.root.remote_merge_from(
target.simNode.name, tNum, newLocalReg
)
# Get the number of the target in the new register
targetNum = target.simQubit.num
# Finally, execute the two qubit gate
logging.debug("RUN GATE")
getattr(self.simQubit, localName)(targetNum)
except RemoteError as remote_err:
self.virtNode.root.reraise_remote_error(remote_err)
except Exception as e:
logging.error("VIRTUAL NODE %s: Cannot perform two qubit gate %s:", self.virtNode.name, e)
raise e
finally:
# We need to release all the locks, no matter what happened
yield self._unlock_inreg(self)
yield self._unlock_inreg(target)
yield self._unlock_nodes(q1simNode, q1virtNode, q2simNode, q2virtNode)
return True
[docs] @inlineCallbacks
def remote_get_number(self):
"""
Returns the number of this qubit in whatever local register it is in. Not useful for the client,
but convenient for debugging.
"""
if self.active != 1:
logging.error("VIRTUAL NODE %s: Attempt to manipulate qubits no longer at this node.", self.virtNode.name)
if self.virtNode == self.simNode:
num = self.simQubit.num
else:
try:
num = yield self.simQubit.callRemote("get_number")
except ConnectionError:
logging.error("VIRTUAL NODE %s: Connection failed: cannot get qubit number.")
return
return num
[docs] def remote_get_virt_num(self):
"""
Returns the number of the virtual qubit.
"""
return self.num
[docs] def remote_get_virtNode(self):
"""
Returns the virtNode of this virtual qubit
"""
return self.virtNode.name
[docs] def remote_get_simNode(self):
"""
Returns the simNode of this virtual qubit
"""
return self.simNode.name
[docs] @inlineCallbacks
def remote_get_qubit(self):
"""
Returns the state of this qubit in real and imaginary parts separated. This is required
single Twisted cannot natively transfer complex valued objects.
"""
if self.active != 1:
logging.error("VIRTUAL NODE %s: Attempt to manipulate qubits no longer at this node.", self.virtNode.name)
if self.virtNode == self.simNode:
(R, I) = self.simQubit.remote_get_qubit()
else:
try:
try:
(R, I) = yield self.simQubit.callRemote("get_qubit")
except RemoteError as remote_err:
self.virtNode.root.reraise_remote_error(remote_err)
except ConnectionError:
logging.error("VIRTUAL NODE %s: Connection failed: cannot get qubit number.")
except Exception as err:
raise err
return (R, I)
[docs] @inlineCallbacks
def remote_get_register_RI(self):
if self.simNode == self.virtNode:
realM, imagM = self.simQubit.register.get_register_RI()
else:
realM, imagM = yield self.simQubit.callRemote("get_register_RI")
return realM, imagM
############################################
#
# Keeping track of received qubits for CQC
[docs]class QubitCQC:
def __init__(self, fromName, toName, from_app_id, to_app_id, new_virt_num, rawEntInfo=None):
self.fromName = fromName
self.toName = toName
self.from_app_id = from_app_id
self.to_app_id = to_app_id
self.virt_num = new_virt_num
self.rawEntInfo = rawEntInfo