def update(self, Q, from_default=True, **kwargs): """ Updates the attributes of a given qnode :param qnode: Node to be updated :param kwargs: Keyword arguements for updating the qnode :return: None """ if from_default is True: # Set attributes of node to default class_name = self.__class__ dummy_node = class_name(Q, name=self.name) for arg in self.__dict__: setattr(self, arg, dummy_node.__dict__[arg]) # For all keyword arguments, check if it exists in the Qnode attributes. If it does, update it. args_to_cull = [] for arg in kwargs: if arg in self.__dict__: setattr(self, arg, kwargs[arg]) args_to_cull.append(arg) # Remove the kwargs that were used for arg in args_to_cull: kwargs.pop(arg) # Update node.costs with remaining kwargs for item in kwargs: print(kwargs) cost_vector = QNET.make_cost_vector(Q, **kwargs) self.costs = cost_vector # Update node.memory with remaining kwargs memory_vector = QNET.make_memory_vector(Q, **kwargs) self.memory = memory_vector
def add_memory_qchan(self, edge=None, **kwargs): """ Add a temporal Qchan to the graph to show time-connection of nodes with quantum memory. The only difference with add_qchan() is that if either of the node types are Satellites, the airCosts will be not be updated since the memory costs are inherent properties of the nodes themselves and hence, are time independent. :param list edge: Array-like object of two qnodes to be connected :param float kwargs: Other costs or qualifying attributes :return: None """ # Assert edge is valid assert (edge != None), "\'edge\' must be an array-like object of two qnodes" assert (len(edge) == 2 ), "\'edge\' must be an array-like object of two qnodes" u = self.getNode(str(edge[0])) v = self.getNode(str(edge[1])) if u is None: u = QNET.Ground(self, name=str(edge[0])) self.add_node(u) if v is None: v = QNET.Ground(self, name=str(edge[1])) self.add_node(v) cost_vector = QNET.make_cost_vector(self, **kwargs) self.add_edge(u, v, **cost_vector)
def __init__(self, Q, name=None, coords=None, isMemory=False, **kwargs): """ Qnode Initialization :param Q: The Qnet graph intended for the node. :param name: The name of the Qnode. :param coords: Cartesian coordinates. Usage: [x,y,z] :param isMemory: Boolean to describe if the node has quantum memory or not :param kwargs: Costs that are valid for the Qnet graph Q. """ if name is None: name = "None" if coords is None: coords = [0]*3 else: assert (len(coords) == 3), "Usage: [x, y, z]" # Initialize cost vector cost_vector = QNET.make_cost_vector(Q, **kwargs) # Initialize memory cost vector memory_vector = QNET.make_memory_vector(Q, **kwargs) self.name = name self.coords = coords self.costs = cost_vector self.memory = memory_vector self.isMemory = isMemory
def best_costs(P=None, head=None, tail=None): # Default cost_array if None in [P, head, tail]: return {'e': 0, 'f': 0} else: e = QNET.best_path_cost(P, head, tail, cost_type='e') f = QNET.best_path_cost(P, head, tail, cost_type='f') cost_vector = {'e': e, 'f': f} return cost_vector
def add_qchan(self, edge=None, key=None, **kwargs): """ Add a Qchan to the graph. If either of the node types are Satellites, the costs of the edge will be automatically calculated with the Node class method "airCost" Parameters ---------- edge: (qnode, qnode) A pair of nodes to be connected. Order does not matter. key: int Optional A key specifying a specific edge kwargs: Key word arguments for the edge cost Returns ------- None Warnings -------- Additional costs specified in **kwargs that are not in self.cost_vector will not be added. This is because such costs do not have an associated range or additive conversion method. """ # Assert edge is valid assert (edge != None), "\'edge\' must be an array-like object of two qnodes" assert (len(edge) == 2 ), "\'edge\' must be an array-like object of two qnodes" u = self.getNode(str(edge[0])) v = self.getNode(str(edge[1])) if u is None: u = QNET.Ground(self, name=str(edge[0])) self.add_node(u) if v is None: v = QNET.Ground(self, name=str(edge[1])) self.add_node(v) # If either of the nodes are satellites, get air costs if isinstance(u, QNET.Satellite): e, f = u.airCost(v) kwargs.update({'e': e, 'f': f}) elif isinstance(v, QNET.Satellite): e, f = v.airCost(u) kwargs.update({'e': e, 'f': f}) # Pass it through make_cost_vector to ensure that cost_vector = QNET.make_cost_vector(self, **kwargs) self.add_edge(u, v, key=key, **cost_vector)
def picky_path(Q, node_list, cost_type): """ Given a list of nodes for the shortest path in cost_type, this function returns a QNET.Path object specifying the shortest path. Since Qnet is a multi-graph, a list of nodes is insufficient to specify the true shortest path. What picky_path does is find the edges in the list of nodes that minimizes cost_type and then create a QNET.Path object with this information contained. Parameters ---------- Q: Qnet() node_list: [Qnode()] list of nodes in the shortest path cost_type: str Any valid cost from the cost vector Returns ------- Path() """ edge_keys = [] path_len = len(node_list) i = 0 while i < path_len - 1: cur = node_list[i] nxt = node_list[i + 1] edge_num = Q.number_of_edges(cur, nxt) edge_list = [Q.edges[cur, nxt, j] for j in range(edge_num)] seq = [e[cost_type] for e in edge_list] key = seq.index(max(seq)) edge_keys.append(key) i += 1 return QNET.Path(Q, node_list, edge_keys)
def plot_paths(Q, tMax, dt): """ Plot the costs of all simple paths over time along with the cost from simple_purify :param Q: Qnet Graph :param tMax: Maximum time :param dt: Size of time increment :return: None """ # Get Time Array time_arr = QNET.getTimeArr(tMax, dt) # Plot the costs of every simple path over time path_dict = sim_all_simple(Q, 'A', 'B', tMax, dt) for path in path_dict: for cost in Q.cost_vector.keys(): a = [] for d in path_dict[path]: a.append(d[cost]) plt.plot(time_arr, a, label=f"{str(path)} ({cost})") # Purified costs over time: pur_arr = sim_protocol(Q, "A", "B", QNET.simple_purify, tMax, dt) plot_cv(time_arr, pur_arr, label="Path Purification") plt.xlabel('Time') plt.ylabel("Path Costs") plt.title("Network Path Costs Over Time Between Nodes \"A\" and \"B\"") plt.legend() plt.show()
def sim_all_simple(G, source, target, tMax, dt, cost_type=None): """ Get the cost arrays for all simple paths over time :param G: Qnet Graph :type G: Qnet() :param source: Qnode :param target: Qnode :param tMax: Timespan :type tMax: float :param dt: Time interval :type dt: float :param cost_type: string, optional :return: Dictionary of paths to a list of cost arrays over time """ C = copy.deepcopy(G) # get source and target from names source = C.getNode(source) target = C.getNode(target) # Create a generator of all simple paths simplePathGen = nx.algorithms.simple_paths.all_simple_paths( C, source, target) # Unpack paths from generator into array as QNET paths path_arr = [] for path in simplePathGen: path_arr.append(QNET.Path(C, path)) # Assign each path to an empty cost array path_dict = {path: [] for path in path_arr} # Initialize array size size_arr = len(np.arange(0, tMax, dt)) i = 0 while i < size_arr: j = 0 while j < len(path_arr): # Get the cost of each path and append it to respective array if cost_type is None: # Fetch all costs in cost vector cost = path_arr[j].cost_vector else: # Fetch specified cost cost = path_arr[j].cost_vector[cost_type] path_dict[path_arr[j]].append(cost) j += 1 C.update(dt) i += 1 for path in path_arr: for node in path.node_array: if isinstance(node, QNET.Satellite): if node.cartesian is False: node.setTime() return path_dict
def purify(Q, source, target): """ This function performs a multi-path entanglement purification between a source and target node using all available paths. :param Q: Qnet Graph :param source: Name of source node :param target: Name of target node If none, will purify all possible paths :param string, optional, method: The method used to do the purification. Supported options: "edge_disjoint", "node_disjoint", "total_disjoint", "greedy". edge_disjoint: No intersecting edges node_disjoint: No intersecting nodes total_disjoint: No intersecting edges or nodes Other inputs produce a ValueError :return: float """ # TODO: Implement a threshold attribute so user doesn't have to iterate through all paths def fidTransform(F1, F2): return (F1 * F2) / (F1 * F2 + (1 - F1) * (1 - F2)) # Get paths for Graph u = Q.getNode(source) v = Q.getNode(target) # TODO: Find a better way of producing disjoint paths generator = nx.node_disjoint_paths(Q, u, v) # Get p values for each path f_arr = [] for path in generator: new_path = QNET.Path(Q, path) # check if path is valid if new_path.is_valid() == True: f = new_path.cost_vector['f'] f_arr.append(f) else: pass assert (len(f_arr) != 0), f"No path exists from {source} to {target}" # Initialize purified fidelity as the max fidelity value pure_cost = max(f_arr) f_arr.remove(pure_cost) # Purify fidelities together # TODO: Depreciate this code while len(f_arr) != 0: pmax = max(f_arr) pure_cost = fidTransform(pure_cost, pmax) f_arr.remove(pmax) return pure_cost
def multidim_lattice(dim, size, e, f, periodic=False): dim = [size] * dim G = nx.grid_graph(dim, periodic) Q = QNET.Qnet() for edge in G.edges(): u = edge[0] v = edge[1] Q.add_qchan(edge=(u, v), e=e, f=f) return Q
def square_lattice(m, n, efficiency, fidelity): G = nx.grid_2d_graph(m, n, periodic=False, create_using=None) Q = QNET.Qnet() for node in G.nodes(): Q.add_qnode(name=str(node), qnode_type="Ground", coords=(node[0], node[1], 0)) for edge in G.edges(): u = edge[0] v = edge[1] Q.add_qchan(edge=(u, v), e=efficiency, f=fidelity) return Q
def add_qnode(self, name=None, qnode_type=None, coords=None, **kwargs): """ Initialize a qnode of some type and add it to the graph In the event where a node of the same name is initialised, update the old node with new parameters Parameters ---------- name: str Node name qnode_type: str {'Ground', "Satellite', 'Swapper'}, optional Qnode subclass (The default is None, which initializes a Qnode of the default type) kwargs Keyword arguments for qnode initialization For details, consult the documentation for the Node class Returns ------- None Raises ------ AssertionError If the qnode_type is invalid """ # Check if node already exists in graph. If yes, update old node old_node = self.getNode(name) if old_node is not None: old_node.update(self, name=name, coords=coords, **kwargs) # Else, add new node else: # If qnode_type is none, initialize a node of the default type if qnode_type is None: new_node = QNET.Qnode(self, name=name, coords=coords, **kwargs) # Else initialize a node of the specified type else: # Check if type is valid assert ( qnode_type in typeDict), f"Unsupported qnode type: \'{qnode_type}\'" new_node = typeDict[qnode_type](self, name=name, coords=coords, **kwargs) self.add_node(new_node)
def data_method(Q, pairs): """ An arbitrary method for measuring the effectiveness of a graph For all given communication pairs, find the best path between them in terms of fidelity. Average the cost vectors, and return the result. Parameters ---------- Q: Qnet pairs: list Returns ------- dict Cost vector """ # If no path exists, return minimum values for efficiency and fidelity if Q is None or pairs is None: return {"e":0, "f":0.5} df = pd.DataFrame() for index, pair in enumerate(pairs): # Get shortest path between pairs in fidelity u, v = pair best_path = QNET.best_path(Q, u, v, 'f') # Get cost vector, and forget the additive costs cost_vector = best_path.cost_vector cost_vector = QNET.cv_strip_add(cost_vector) # Add the costs to the DataFrame df = df.append(pd.Series(cost_vector), ignore_index=True) # Convert data frame into single cost vector meanie = df.mean() return meanie.to_dict()
def sim_optimal_cost(G, source_name, target_name, cost_type, tMax, dt): """ Calculate the costs of the lowest cost path from "source" to "target" over time. :param G: Qnet Graph :param string source_name: Name of source node :param string target_name: Name of target node :param string cost_type: The type of cost to optimise over. Choose from {'loss', 'fid'} :param float tMax: Time period :param float dt: Time increment :return: Optimal loss array """ C = copy.deepcopy(G) u = C.getNode(source_name) v = C.getNode(target_name) # Initialize arrays cost_arr = [] size = len(np.arange(0, tMax, dt)) # Get optimal path cost and append it to costArr i = 0 while i < size: cost = QNET.best_path_cost(C, source_name, target_name, cost_type) cost_arr.append(cost) # Update network C.update(dt) i += 1 """ simplePathGen = nx.algorithms.simple_paths.all_simple_paths(C, u, v) pathArr = [] for path in simplePathGen: pathArr.append(QNET.Path(C, path)) for path in pathArr: for node in path.node_array: if isinstance(node, QNET.Satellite): if node.cartesian is False: node.setTime() """ return cost_arr
def posPlot(Q, u, v, tMax, dt): """ Plot the distance between two nodes over time :param Q: QNet Graph :param u: Name of Qnode :param v: Name of Qnode :param tMax: Maximum time :param dt: Size of timestep :return: None """ C = copy.deepcopy(Q) u = C.getNode(u) v = C.getNode(v) posArr = [] sizeArr = len(np.arange(0, tMax, dt)) i = 0 while i < sizeArr: if isinstance(u, QNET.Satellite): dist = u.distance(v) elif isinstance(v, QNET.Satellite): dist = v.distance(u) else: assert (False) posArr.append(dist) C.update(dt) i += 1 if isinstance(u, QNET.Satellite): u.setTime() if isinstance(v, QNET.Satellite): v.setTime() timeArr = QNET.getTimeArr(tMax, dt) plt.plot(timeArr, posArr) plt.xlabel("Time (in s)") plt.ylabel("Distance (in 10^3 km)") plt.title(f"Distance between {u.name} and {v.name}") plt.show()
def hexagonal_lattice(m, n, efficiency, fidelity, periodic=False, with_positions=True): """ Returns the m by n triangular lattice Qnet Parameters ---------- m n efficiency fidelity periodic with_positions create_using Returns ------- Qnet """ G = nx.generators.hexagonal_lattice_graph(m, n, periodic=periodic, with_positions=with_positions) Q = QNET.Qnet() for edge in G.edges(): u = edge[0] v = edge[1] Q.add_qchan(edge=(u, v), e=efficiency, f=fidelity) if with_positions is True: for node_name, position in list(G.nodes(data="pos")): # Update Qnet nodes with positions Q.add_qnode(name=str(node_name), coords=[position[0], position[1], 0]) return Q
def get_cost_vector(self): # List of cost vectors for all elements in the path: element_cvs = [] # Get cost vectors of edges path_len = len(self.node_array) i = 0 while i < path_len - 1: cur = self.node_array[i] nxt = self.node_array[i + 1] # TODO: Add key key here to make it more clear edge_data = self.G.get_edge_data(cur, nxt, self.edge_keys[i]) element_cvs.append(edge_data) i += 1 # Get cost vectors of nodes for node in self.node_array: element_cvs.append(node.costs) # Cost vectors have identical keys # Sum all key elements together, convert additive costs to correct value, update dictionary def sum_cost_vectors(cv_list): counter = collections.Counter() for d in cv_list: counter.update(d) return dict(counter) new_cv = sum_cost_vectors(element_cvs) # Convert additive costs into regular costs: for cost_type in new_cv: if cost_type.startswith("add_"): # Get additive cost cost = new_cv[cost_type] # Remove prefix of cost_type cost_type = QNET.remove_prefix(cost_type, "add_") # Convert additive cost to normal, add it to new_cv new_cv[cost_type] = self.G.conversions[cost_type][1](cost) return new_cv
def sim_path(G, path, tMax, dt, cost_type=None): """ Return an array of path costs over time Parameters ---------- G: Qnet Graph path: An array of node names tMax: float Timespan of the simulation dt: Time increment cost_type: string, optional (The default value is None, which returns a list of cost vectors for the path over time) Returns ------- List of floats or list of dicts """ if cost_type is not None: assert cost_type in G.cost_vector C = copy.deepcopy(G) path = QNET.Path(C, path) cost_array = [] i = 0 size_arr = len(np.arange(0, tMax, dt)) while i < size_arr: if cost_type is not None: cost_array.append(path.cost_vector[cost_type]) else: cost_array.append(path.cost_vector) C.update(dt) path.update() i += 1 return cost_array
def simple_purify(Q=None, head=None, tail=None, threshold=None): """ A simple purification algorithm that works as follows: 1. Find the best path between source and target and save the cost 2. Remove the edges of this path from the graph 3. Find the next best path 4. Purify the paths together 5. Repeat steps 3 and 4 until either we hit the threshold number or there are no more paths between source and target :param Q: :param source: Source Node :param target: Target Node :param: threshold: Maximum number of paths to purify before returning cost vector :return: cost vector :param: dict """ if threshold is not None: assert isinstance(threshold, int) assert threshold > 0 # Default cost_array if None in [Q, head, tail]: return {'e': 0, 'f': 0} # Make copy of graph C = copy.deepcopy(Q) # Get source and target head = C.getNode(head) tail = C.getNode(tail) # Find the best path in terms of fidelity, path = QNET.best_path(C, head, tail, 'f') # pur_f = path.cost('f') pur_f = path.cost_vector['f'] # pur_e = path.cost('e') pur_e = path.cost_vector['e'] path.remove_edges() path_counter = 1 # Purify paths against eachother until either no path exists or threshold is reached while nx.has_path(C, head, tail) is True: if threshold is not None: if path_counter > threshold: break path = QNET.best_path(C, head, tail, 'f') # new_f = path.cost('f') new_f = path.cost_vector['f'] # new_e = path.cost('e') new_e = path.cost_vector['e'] # Efficiency is weakest-link. Update pur_e to whatever the lowest path efficiency is. if new_e < pur_e: pur_e = new_e pur_f = QNET.fidTransform(pur_f, new_f) path.remove_edges() path_counter += 1 # Each path purification requires 2*(n-1) bell projections, where n is the number of bell pairs # Assume that projections are done with a PBS with e = 1/2 pur_e = pur_e * 0.5**(2 * path_counter - 1) return {'e': pur_e, 'f': pur_f}
def altLinSatGen(n, spacing, e=1, f=1, startTime=0, Line1='', Line2='', *args): ''' Constructs a linear chain with end Qnodes A and B, and alternating Swapper and Ground nodes in the middle. A satellite is connected to end nodes A and B, and all swapper nodes. Parameters ---------- n : int Total number of nodes in the chain, including end nodes A and B. spacing : Array of float Distance between two consecutive nodes in [x,y,z] format. eVal : float Efficiency of a single channel in the regular lattice. The default is 1. pxVal : float Probability of state not undergoing X-flip in the regular lattice. The default is 0. pyVal : float Probability of state not undergoing Y-flip in the regular lattice. The default is 0. pzVal : float Probability of state not undergoing Z-flip in the regular lattice. The default is 0. startTime : int Time in seconds from now (current time) starting which the satellite should be tracked. The default is 0. Line1 : String Line 1 of satellite TLE. The deault is ''. Line2 : String Line 2 of satellite TLE. The deault is ''. Returns ------- Q : QNET Graph A linear chain of length n consisting of alternating Bell pair generator and swappers. End nodes A and B, and all swapper nodes are connected to a satellite. ''' Q = QNET.Qnet() if n > 0: firstNode = QNET.Qnode(Q, name='A', coords=spacing) Q.add_node(firstNode) previousNode = QNET.Qnode(Q) currentNode = QNET.Qnode(Q) ChannelList = [] if n > 2: for i in range(n - 2): GroundNode = QNET.Ground(Q, name=('G' + str(i + 1)), coords=np.multiply(i + 2, spacing)) SwapNode = QNET.Swapper(Q, name=('T' + str(i + 1)), coords=np.multiply(i + 2, spacing)) if n > 3: if (i) % 2 == 0: currentNode = SwapNode else: currentNode = GroundNode else: currentNode = GroundNode Q.add_node(currentNode) if i > 0: ChannelList.append({ 'edge': (previousNode.name, currentNode.name), 'e': e, 'f': f }) previousNode = currentNode if n > 1: lastNode = QNET.Qnode(Q, name='B', coords=np.multiply(n, spacing)) Q.add_node(lastNode) ChannelList.append({ 'edge': (firstNode.name, list(Q.nodes)[1].name), 'e': e, 'f': f }) ChannelList.append({ 'edge': (list(Q.nodes)[n - 2].name, lastNode.name), 'e': e, 'f': f }) S = QNET.Satellite(Q, name='S', t=startTime, line1=Line1, line2=Line2, cartesian=False) Q.add_node(S) nodeList = Q.nodes() for node in nodeList: if type(node) != QNET.Ground and type(node) != QNET.Satellite: ChannelList.append({'edge': (node.name, S.name)}) Q.add_qchans_from(ChannelList) return Q
def regularLatticeGen(x, y, xspacing, yspacing=[0, 0, 0], e=1, f=1): ''' Constructs a 2D Regular Lattice with end Qnodes A and B, and alternating Swapper and Ground nodes in the middle. Parameters ---------- x : int Number of columns in the regular lattice. y : int Number of rows in the regular lattice. xspacing : 3x1 Array of float Distance between two consecutive columns in 2D regular lattice in [x,0,0] format. yspacing : 3x1 Array of float Distance between two consecutive rows in 2D regular lattice in [0,y,0] format. The default is [0,0,0]. eVal : float Efficiency of a single channel in the regular lattice. The default is 1. pxVal : float Probability of state not undergoing X-flip in the regular lattice. The default is 0. pyVal : float Probability of state not undergoing Y-flip in the regular lattice. The default is 0. pzVal : float Probability of state not undergoing Z-flip in the regular lattice. The default is 0. Returns ------- Q : QNET Graph A y*x 2D regular lattice. ''' assert (x > 0) assert (y > 0) if yspacing == [0, 0, 0]: yspacing[1] = xspacing[0] Q = QNET.Qnet() firstNode = QNET.Qnode(Q, name='A', coords=[0, 0, 0]) lastNode = QNET.Qnode(Q, name='B', coords=np.add(np.multiply(x - 1, xspacing), np.multiply(y - 1, yspacing))) previousNode = QNET.Qnode(Q) currentNode = QNET.Qnode(Q) ChannelList = [] # Constructing the nodes in the Regular Lattice for i in range(x): for j in range(y): GroundNode = QNET.Ground(Q, name=('G(' + str(i + 1) + ',' + str(j + 1) + ')'), coords=np.add(np.multiply(i, xspacing), np.multiply(j, yspacing))) SwapNode = QNET.Swapper(Q, name=('T(' + str(i + 1) + ',' + str(j + 1) + ')'), coords=np.add(np.multiply(i, xspacing), np.multiply(j, yspacing))) # Swap nodes at odd positions and Ground nodes at even positions in a Regular Lattice if x >= 1 and y >= 1: if (i + j) % 2 == 0: currentNode = GroundNode else: currentNode = SwapNode # Linear chain of length 3 exception if (x == 3 and y == 1) or (x == 1 and y == 3): currentNode = GroundNode # 2*2 2D lattice exception if x == 2 and y == 2: currentNode = GroundNode # Initialising the first node as A if i == 0 and j == 0: currentNode = firstNode # Initialising the last node as B if i == (x - 1) and j == (y - 1): currentNode = lastNode Q.add_node(currentNode) previousNode = currentNode # Constructing the vertical edges in the Regular Lattice for i in range(x): previousNode = list(Q.nodes)[i * y] for j in range(y - 1): currentNode = list(Q.nodes)[j + 1 + (i * y)] ChannelList.append({ 'edge': (previousNode.name, currentNode.name), 'e': e, 'f': f }) previousNode = currentNode # Constructing the horizontal edges in the Regular Lattice for j in range(y): previousNode = list(Q.nodes)[j] for i in range(x - 1): currentNode = list(Q.nodes)[j + (i + 1) * y] ChannelList.append({ 'edge': (previousNode.name, currentNode.name), 'e': e, 'f': f }) previousNode = currentNode Q.add_qchans_from(ChannelList) # Q.print_qchans() return Q
def simple_swap(Q, source, dest): """ Given a linear graph or subgraph, this function calculates the efficiency from head to tail if all valid swapper nodes are used. Currently, both this quantity and the normal efficiency of the path are returned In the future, if it's more efficient not to use any swappers, the normal efficiency of the path will be returned. :param Q: Linear Qnet Graph :return float: no_swap and swap efficiencies of the graph Example: A - G1 - T - G2 - T - G3 - B net_eff = min ( e[A-G1-T], e[T-G2], e[T-G3-B] ) * e[T]**2 Where e[A-G-T] is taken to mean the efficiency of the path [A-G-T] and e[T] is taken to mean the efficiency cost of performing a swap with the node T. """ # Check that path exists from A to B assert (nx.has_path(Q, source, dest)) # Create generator of paths from A to B. If no generator exists raise exception path_gen = nx.all_simple_paths(Q, source, dest) assert (path_gen != None ), f"No valid path exists between {source.name} and {dest.name}." # Unpack generator. If more than one path exists, raise exception path_count = 0 for obj in path_gen: path = QNET.Path(Q, obj) path_count += 1 assert (path_count == 1 ), "More than one path exists, linear reduction cannot be done." # Does a Swapper with swapping potential exist? swap_candidate = None # Does ground exist before a swapper? front_ground = False # Local efficiency of a Ground - Swapper - Ground configuration (or equivalent) local_eff = 1 # list of swapper nodes used in the swapping process swap_list = [] # Total efficiency of path net_eff = 1 # Current node cur = source # Counter i = 0 while cur != dest: # If current node is a Ground and we haven't seen a Swapper yet, set front_ground to True if isinstance(cur, QNET.Ground) and swap_candidate is None: # Add node eff to local local_eff *= cur.costs['e'] front_ground = True # If Current node is a Swapper and we've seen a ground node previously, we now have a candidate for a swapper # that could be used. elif isinstance(cur, QNET.Swapper) and front_ground is True: swap_switch = True if swap_candidate == None: swap_candidate = cur # If we had another swap candidate previously, update swap_candidate to be better efficiency option elif swap_candidate.swap_prob < cur.swap_prob: swap_candidate = cur local_eff *= cur.costs['e'] # If current node is a Ground and we've seen a Swapper, we perform a swap with the local resources # (Front-Ground, Swapper, Back-Ground) and update net_eff. elif isinstance(cur, QNET.Ground) and swap_candidate is not None: # Add node eff to local local_eff *= cur.costs['e'] # Update net_eff to local_eff if net_eff is smaller if local_eff < net_eff: net_eff = local_eff # Add the swapper we were considering to the list of swappers used swap_list.append(swap_candidate) # Reset parameters to prepare for the next swap local_eff = 1 swap_candidate = None front_ground = False # Otherwise include the node cost in the local efficiency else: local_eff *= cur.costs['e'] # Increment counter and get the edge efficiency i += 1 edge_costs = Q.get_edge_data(cur, path.node_array[i]) # Update local_eff with edge efficiency local_eff *= edge_costs['e'] # Move to next node cur = path.node_array[i] # If the new node is the destination if cur == dest: # Append node costs from dest local_eff *= cur.costs['e'] # Update net_eff to local_eff if net_eff is smaller # This also handles the case where no valid swapper was encountered if local_eff < net_eff: net_eff = local_eff # Multiply net_eff with the costs of all swappers used. for swapper in swap_list: net_eff *= swapper.costs['e'] """Calculate cost of path without any swapping""" # Cost of path without swapping no_swap = 1 # Length of path path_len = len(path.node_array) i = 0 # Sum all edge efficiencies while (i < path_len - 1): cur = path.node_array[i] nxt = path.node_array[i + 1] edgeData = path.G.get_edge_data(cur, nxt) no_swap *= edgeData['e'] i += 1 # Sum all node efficiencies except for swappers for node in path.node_array: no_swap *= node.costs['e'] # DEBUG: For now, return both swap and no swap: return (no_swap, net_eff) """
def temporalGen(Q, dt, n, startLayer=0, endLayer=None): """ Creates a temporal extension of given graph. Creates a temporal extension of given graph to simulate the effect of quantum memory. Each layer(slice) of this temporal graph is an updated/evolved version of the previous layer by dt time. n such layers of these time updated graphs are created and only those nodes with quantum memory (node.isMemory == True) are connected in the temporal dimension. However, out of all these n time-updated layers of graph Q, we might only want to connect a few in the temporal dimension. The arguments startLayer and endLayer correspond to the number of layer between which all the graphs are connected. Example: If startLayer=1 and endLayer=3, only layers from 1 to 3 are connected in temporal dimension. t=0 ------- t=1 ------- startLayer=1 | | | | t=2 ------- | | | | t=3 ------- endLayer=3 : t=n-1 ------- Parameters ---------- Q : QNET Graph The spatial graph which is to be utilised to make a spatio-temporal graph. dt : float Time steps by which each layer/slice/graph is to be updated. n : int Number of such layers/slices/graphs to be made. startLayer : int The number corresponding to first layer starting which the layers are connected in temporal dimension. Default value is 0 i.e. the first layer itself. endLayer : int The number corresponding to last layer at which the layers' connection in temporal dimension ends. Default value is n-1 i.e. the last layer itself. Returns ------- finalGraph : QNET Graph The temporal extension (including both connected and unconnected layers) of given graph Q. """ if endLayer == None: endLayer = n - 1 ## Create a list of time-updated graph layers ## G = [] new_graph = copy.deepcopy(Q) layer_num = 0 for i in range(0, n): C = copy.deepcopy(new_graph) dummy_graph = copy.deepcopy(new_graph) dummy_graph.updateName(layer_num) G.append(dummy_graph) C.update(dt) layer_num = layer_num + 1 new_graph = copy.deepcopy(C) ## Join these time-updated graph layers ## assert (0 <= startLayer <= n - 1), f"Out of range -- 0 <= startLayer <= n-1 " assert (0 <= endLayer <= n - 1), f"Out of range -- 0 <= endLayer <= n-1 " assert (startLayer <= endLayer), f"startLayer <= endLayer <= n-1 " finalGraph = QNET.Qnet() for layer_num in range(0, len(G)): finalGraph = nx.compose(finalGraph, G[layer_num]) #print(finalGraph) for node in Q.nodes(): for layer_num in range(startLayer + 1, endLayer + 1): if node.isMemory and layer_num > 0: u = finalGraph.getNode(str(layer_num - 1) + node.name) v = finalGraph.getNode(str(layer_num) + node.name) finalGraph.add_memory_qchan(edge=[u, v], e=u.memory['mem_e'], f=u.memory['mem_f']) return finalGraph
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Created on Mon Apr 6 13:44:28 2020 Small static tripartite graph without node costs @author: hudson """ import networkx as nx import QNET X = QNET.Qnet() nbunch = [{ 'name': 'A', 'coords': [0, 50, 0] }, { 'name': 'B', 'coords': [200, 50, 0] }, { 'name': 'G1', 'qnode_type': 'Ground', 'coords': [100, 20, 0] }, { 'name': 'G2', 'qnode_type': 'Ground', 'coords': [100, 40, 0] }, { 'name': 'G3',
def altLinGen(n, spacing, e=1, f=1): ''' Constructs a linear chain with end Qnodes A and B, and alternating Swapper and Ground nodes in the middle. Parameters ---------- n : int Total number of nodes in the chain, including end nodes A and B. spacing : Array of float Distance between two consecutive nodes in [x,y,z] format. e : float Efficiency of a single channel in the regular lattice. The default is 1. Returns ------- Q : QNET Graph A linear chain of length n consisting of alternating Bell pair generator and swappers. ''' Q = QNET.Qnet() if n > 0: firstNode = QNET.Qnode(Q, name='A', coords=spacing) Q.add_node(firstNode) previousNode = QNET.Qnode(Q) currentNode = QNET.Qnode(Q) ChannelList = [] if n > 2: for i in range(n - 2): GroundNode = QNET.Ground(Q, name=('G' + str(i + 1)), coords=np.multiply(i + 2, spacing)) SwapNode = QNET.Swapper(Q, name=('T' + str(i + 1)), coords=np.multiply(i + 2, spacing)) if n > 3: if (i) % 2 == 0: currentNode = SwapNode else: currentNode = GroundNode else: currentNode = GroundNode Q.add_node(currentNode) if i > 0: ChannelList.append({ 'edge': (previousNode.name, currentNode.name), 'e': e, 'f': f }) previousNode = currentNode if n > 1: lastNode = QNET.Qnode(Q, name='B', coords=np.multiply(n, spacing)) Q.add_node(lastNode) ChannelList.append({ 'edge': (firstNode.name, list(Q.nodes)[1].name), 'e': e, 'f': f }) ChannelList.append({ 'edge': (list(Q.nodes)[n - 2].name, lastNode.name), 'e': e, 'f': f }) Q.add_qchans_from(ChannelList) return Q