def test_row_identity(self): C41 = dnx.chimera_graph(4, 1) coord = dnx.chimera_coordinates(4, 1, 4) labels = dnx.canonical_chimera_labeling(C41) labels = {v: coord.chimera_to_linear(labels[v]) for v in labels} G = nx.relabel_nodes(C41, labels, copy=True) self.assertTrue(nx.is_isomorphic(G, C41))
def test_3x3_identity(self): C33 = dnx.chimera_graph(3, 3) coord = dnx.chimera_coordinates(3, 3, 4) labels = dnx.canonical_chimera_labeling(C33) labels = {v: coord.chimera_to_linear(labels[v]) for v in labels} G = nx.relabel_nodes(C33, labels, copy=True) self.assertTrue(nx.is_isomorphic(G, C33))
def __init__(self, guest: Graph, host: ChimeraGraph): if host.faulty_nodes or host.faulty_edges: raise NotImplementedError( "Chimera graphs with faults are not supported by these algorithms") self.guest = guest self.host = host m, l = host.params dnx_coords = dnx.chimera_coordinates(m, t=l) self.linear_to_chimera = dnx_coords.linear_to_chimera self.chimera_to_linear = dnx_coords.chimera_to_linear self.forward_embed = None
def test_tile_identity(self): C1 = dnx.chimera_graph(1) coord = dnx.chimera_coordinates(1, 1, 4) labels = dnx.canonical_chimera_labeling(C1) labels = {v: coord.chimera_to_linear(labels[v]) for v in labels} G = nx.relabel_nodes(C1, labels, copy=True) self.assertTrue(nx.is_isomorphic(G, C1)) self.assertEqual(set(G), set(C1))
def test_bqm_tile_identity(self): J = {e: -1 for e in dnx.chimera_graph(1).edges} C1bqm = dimod.BinaryQuadraticModel.from_ising({}, J) coord = dnx.chimera_coordinates(1, 1, 4) labels = dnx.canonical_chimera_labeling(C1bqm) labels = {v: coord.chimera_to_linear(labels[v]) for v in labels} bqm = C1bqm.relabel_variables(labels, inplace=False) self.assertEqual(bqm, C1bqm)
def test_reversed(self): C33 = nx.OrderedGraph() C33.add_nodes_from(reversed(range(3 * 3 * 4))) C33.add_edges_from(dnx.chimera_graph(3, 3, 4).edges) coord = dnx.chimera_coordinates(3, 3, 4) labels = dnx.canonical_chimera_labeling(C33) labels = {v: coord.chimera_to_linear(labels[v]) for v in labels} G = nx.relabel_nodes(C33, labels, copy=True) self.assertTrue(nx.is_isomorphic(G, C33))
def double_triangle_clique(chimera_graph: ChimeraGraph) -> Dict[int, List[int]]: """ Performs a double-sided triangle embedding in a similar fashion as described by the PSSA paper. 'Graph Minors from Simulated Annealing for Annealing Machines with Sparse Connectivity' Reference: Y. Sugie, Y. Yoshida, N. Mertig, T. Takemoto, H. Teramoto, A. Nakamura, I. Takigawa, S.-i. Minato, M.Yamaoka, and T. Komatsuzaki, Minor-embedding heuristics for large-scale annealing processors with sparse hardware graphs of up to 102,400 nodes, 2020. arXiv:2004.03819 [quant-ph]: Args: chimera_graph: Chimera host to transform. Returns: an embedding. !NOTE: The vertex sets are represented as lists but the ordering matters. The nodes should be ordered according to the chain formed on the hardware hardware. """ m, l = chimera_graph.params to_linear = dnx.chimera_coordinates(m, t=l).chimera_to_linear # Embed the upper triangular top_embed = [[] for _ in range(m * l)] for i in range(m * l): cell, unit = i // l, i % l # Add the nodes above diagonal cell for j in range(cell): top_embed[i].append(to_linear((j, cell, 0, unit))) # Add the two nodes in the diagonal cell top_embed[i].extend((to_linear( (cell, cell, 0, unit)), to_linear((cell, cell, 1, unit)))) # Add the nodes to right of diagonal cell for j in range(cell + 1, m): top_embed[i].append(to_linear((cell, j, 1, unit))) # Embed the lower triangular bot_embed = [[] for _ in range((m - 1) * l)] for i in range((m - 1) * l): cell, unit = i // l, i % l # Add the nodes to left of diagonal cell for j in range(cell): bot_embed[i].append(to_linear((cell + 1, j, 1, unit))) # Add the two nodes in the diagonal cell bot_embed[i].extend((to_linear( (cell + 1, cell, 1, unit)), to_linear((cell + 1, cell, 0, unit)))) # Add the nodes below diagonal cell for j in range(cell + 1, m - 1): bot_embed[i].append(to_linear((j + 1, cell, 0, unit))) combined = top_embed + bot_embed return {i: combined[i] for i in range(len(combined))}
def _initialize_weights_chimera(chimera_graph, size, draw_inter_weight, draw_intra_weight, draw_other_weight): c_coor = dwave.chimera_coordinates(size) for _from, _to in chimera_graph.edges: _from_nice = c_coor.linear_to_chimera(_from) _to_nice = c_coor.linear_to_chimera(_to) if in_same_chimera_tile(_from_nice, _to_nice): # edge from one side to the other (internal edge) if not on_same_side(_from_nice, _to_nice): chimera_graph.add_edge(_from, _to, weight=draw_intra_weight()) else: # odd couplers chimera_graph.add_edge(_from, _to, weight=draw_other_weight()) else: chimera_graph.add_edge(_from, _to, weight=draw_inter_weight())
def __init__(self, *args, **kwargs): super(TestBusclique, self).__init__(*args, **kwargs) self.c16 = dnx.chimera_graph(16) self.p16 = dnx.pegasus_graph(16) self.c16c = dnx.chimera_graph(16, coordinates=True, data=False) self.c428 = dnx.chimera_graph(4, n=2, t=8) self.c248 = dnx.chimera_graph(2, n=4, t=8) self.c42A = dnx.chimera_graph(4, n=2, t=9) c4c_0 = subgraph_node_yield(dnx.chimera_graph(4, coordinates=True), .95) p4c_0 = subgraph_node_yield(dnx.pegasus_graph(4, coordinates=True), .95) c4c = [ c4c_0, subgraph_edge_yield(c4c_0, .95), subgraph_edge_yield_few_bad(c4c_0, .95, 6) ] p4c = [ p4c_0, subgraph_edge_yield(p4c_0, .95), subgraph_edge_yield_few_bad(p4c_0, .95, 6) ] p4coords = dnx.pegasus_coordinates(4) c4coords = dnx.chimera_coordinates(4, 4, 4) c4 = [ relabel(c, c4coords.iter_chimera_to_linear, c4coords.iter_chimera_to_linear_pairs, 4) for c in c4c ] p4 = [ relabel(p, p4coords.iter_pegasus_to_linear, p4coords.iter_pegasus_to_linear_pairs, 4) for p in p4c ] p4n = [ relabel(p, p4coords.iter_pegasus_to_nice, p4coords.iter_pegasus_to_nice_pairs, 4, nice_coordinates=True) for p in p4c ] self.c4, self.c4_nd, self.c4_d = list(zip(c4, c4c)) self.p4, self.p4_nd, self.p4_d = list(zip(p4, p4c, p4n))
def bipartite_with_faults(chimera_graph: ChimeraGraph): """ Create a bipartite template embedding which allows for faulty nodes and edges. Args: chimera_graph: Chimera host to transform. Returns: Tuple (left, right) for embedding in each respective partition. """ def append_nonempty(super, sub): if sub: super.append(sub) m, l = chimera_graph.params faulty = chimera_graph.faulty_nodes to_linear = dnx.chimera_coordinates(m, t=l).chimera_to_linear h_embed = [] for i in range(m * l): chain = [] cell, unit = i // l, i % l for j in range(m): ln = to_linear((cell, j, 1, unit)) if ln in faulty: append_nonempty(h_embed, chain) chain = [] else: chain.append(ln) append_nonempty(h_embed, chain) v_embed = [] for i in range(m * l): chain = [] cell, unit = i // l, i % l for j in range(m): ln = to_linear((j, cell, 0, unit)) if ln in faulty: append_nonempty(v_embed, chain) chain = [] else: chain.append(ln) append_nonempty(v_embed, chain) return h_embed, v_embed
def test_coordinate_generator(self): G = dnx.chimera_graph(4, 2, coordinates=True, data=True) H = dnx.chimera_graph(4, 2, coordinates=False, data=True) self.assertTrue(nx.is_isomorphic(G, H)) Gnodes = set(G.nodes) Glabels = set(n['linear_index'] for n in G.nodes.values()) Hnodes = set(H.nodes) Hlabels = set(n['chimera_index'] for n in H.nodes.values()) self.assertEqual(Gnodes, Hlabels) self.assertEqual(Hnodes, Glabels) coords = dnx.chimera_coordinates(2, 1) f = nx.relabel_nodes(g, coords.chimera_to_linear, copy=True) self.assertEqual(set(f.edges), set(h.edges)) e = nx.relabel_nodes(h, coords.linear_to_chimera, copy=True) self.assertEqual(set(e.edges), set(g.edges))
def test_construction_string_labels(self): C22 = dnx.chimera_graph(2, 2, 3) coord = dnx.chimera_coordinates(2, 2, 3) alpha = 'abcdefghijklmnopqrstuvwxyz' bqm = dimod.BinaryQuadraticModel.empty(dimod.BINARY) for u, v in reversed(list(C22.edges)): bqm.add_interaction(alpha[u], alpha[v], 1) assert len(bqm.quadratic) == len(C22.edges) assert len(bqm) == len(C22) labels = dnx.canonical_chimera_labeling(bqm) labels = {v: alpha[coord.chimera_to_linear(labels[v])] for v in labels} bqm2 = bqm.relabel_variables(labels, inplace=False) self.assertEqual(bqm, bqm2)
def overlap_clique(chimera_graph: ChimeraGraph): """ Returns a clique overlap template embedding as described in 'Template-based minor embedding for adiabatic quantum optimization'. Reference: T. Serra, T. Huang, A. Raghunathan, and D. Bergman, Template-based minor embedding for adiabatic quantum optimization, 2019. arXiv:1910.02179 [cs.DS]. Args: chimera_graph: Chimera host to transform. Returns: an embedding. """ m, l = chimera_graph.params to_linear = dnx.chimera_coordinates(m, t=l).chimera_to_linear # Embed the clique major top_embed = [[] for _ in range(m * l)] for i in range(m * l): cell, unit = i // l, i % l # Add the nodes above diagonal cell for j in range(cell): top_embed[i].append(to_linear((j, cell, 0, unit))) # Add the two nodes in the diagonal cell top_embed[i].extend((to_linear( (cell, cell, 0, unit)), to_linear((cell, cell, 1, unit)))) # Add the entire row for j in range(0, m): top_embed[i].append(to_linear((cell, j, 1, unit))) # Embed the clique minor bot_embed = [[] for _ in range((m - 1) * l)] for i in range((m - 1) * l): cell, unit = i // l, i % l for j in range(cell, m - 1): bot_embed[i].append(to_linear((j + 1, cell, 0, unit))) combined = top_embed + bot_embed return {i: combined[i] for i in range(len(combined))}
def test_graph_relabeling(self): def graph_equal(g, h): self.assertEqual(set(g), set(h)) self.assertEqual(set(map(tuple, map(sorted, g.edges))), set(map(tuple, map(sorted, g.edges)))) for v, d in g.nodes(data=True): self.assertEqual(h.nodes[v], d) coords = dnx.chimera_coordinates(3) for data in True, False: c3l = dnx.chimera_graph(3, data=data) c3c = dnx.chimera_graph(3, data=data, coordinates=True) graph_equal(c3l, coords.graph_to_linear(c3c)) graph_equal(c3l, coords.graph_to_linear(c3l)) graph_equal(c3c, coords.graph_to_chimera(c3l)) graph_equal(c3c, coords.graph_to_chimera(c3c)) h = dnx.chimera_graph(2) del h.graph['labels'] with self.assertRaises(ValueError): coords.graph_to_linear(h) with self.assertRaises(ValueError): coords.graph_to_chimera(h)
def test_nonsquare_coordinate_generator(self): #issue 149 found an issue with non-square generators -- let's be extra careful here for (m, n) in [(2, 4), (4, 2)]: G = dnx.chimera_graph(m, n, coordinates=True, data=True) H = dnx.chimera_graph(m, n, coordinates=False, data=True) self.assertTrue(nx.is_isomorphic(G, H)) Gnodes = set(G.nodes) Glabels = set(q['linear_index'] for q in G.nodes.values()) Hnodes = set(H.nodes) Hlabels = set(q['chimera_index'] for q in H.nodes.values()) self.assertEqual(Gnodes, Hlabels) self.assertEqual(Hnodes, Glabels) coords = dnx.chimera_coordinates(m, n) F = nx.relabel_nodes(G, coords.chimera_to_linear, copy=True) self.assertEqual(set(map(frozenset, F.edges)), set(map(frozenset, H.edges))) E = nx.relabel_nodes(H, coords.linear_to_chimera, copy=True) self.assertEqual(set(map(frozenset, E.edges)), set(map(frozenset, G.edges)))
def _lookup_intersection_coordinates(G): """For a dwave_networkx graph G, this returns a dictionary mapping the lattice points to sets of vertices of G. For Chimera, Pegasus and Zephyr, each lattice point corresponds to the 2 qubits intersecting at that point. """ graph_data = G.graph family = graph_data.get("family") data_key = None intersection_points = defaultdict(set) if family == "chimera": shore = graph_data.get("tile") collect_intersection_points = _chimera_all_intersection_points if graph_data["labels"] == "coordinate": def get_coords(v): return v elif graph_data["data"]: data_key = "chimera_index" else: coords = dnx.chimera_coordinates(graph_data['rows'], n=graph_data['columns'], t=shore) get_coords = coords.linear_to_chimera elif family == "pegasus": shore = [ graph_data['vertical_offsets'], graph_data['horizontal_offsets'] ] collect_intersection_points = _pegasus_all_intersection_points if graph_data["labels"] == "coordinate": def get_coords(v): return v elif graph_data["data"]: data_key = "pegasus_index" else: coords = dnx.pegasus_coordinates(graph_data['rows']) if graph_data['labels'] == 'int': get_coords = coords.linear_to_pegasus elif graph_data['labels'] == 'nice': get_coords = coords.nice_to_pegasus elif family == "zephyr": shore = graph_data.get("tile") collect_intersection_points = _zephyr_all_intersection_points if graph_data["labels"] == "coordinate": def get_coords(v): return v elif graph_data["data"]: data_key = "zephyr_index" else: coords = dnx.zephyr_coordinates(graph_data['rows'], t=shore) get_coords = coords.linear_to_zephyr if data_key is None: for v in G: collect_intersection_points(intersection_points, shore, v, *get_coords(v)) else: for v, d in G.nodes(data=True): collect_intersection_points(intersection_points, shore, v, *d[data_key]) return intersection_points
def quadripartite_with_faults(chimera_graph: ChimeraGraph): """ Create a quadripartite template embedding which allows for faulty nodes and edges. Args: chimera_graph: Chimera host to transform. Returns: Tuple (U1, U2, U3, U4) for embedding in each respective partition. """ def append_nonempty(super, sub): if sub: super.append(sub) m, l = chimera_graph.params faulty = chimera_graph.faulty_nodes to_linear = dnx.chimera_coordinates(m, t=l).chimera_to_linear U1, U4 = [], [] for i in range(m * l): chain1, chain4 = [], [] cell, unit = i // l, i % l for j in range(m): ln = to_linear((cell, j, 1, unit)) if ln in faulty: if i < m * l / 2: append_nonempty(U1, chain1) chain1 = [] else: append_nonempty(U4, chain4) chain4 = [] else: if i < m * l / 2: chain1.append(ln) else: chain4.append(ln) append_nonempty(U1, chain1) append_nonempty(U4, chain4) U2, U3 = [], [] for i in range(m * l): chain2, chain3 = [], [] cell, unit = i // l, i % l for j in range(m): ln = to_linear((j, cell, 0, unit)) if ln in faulty: if j < m / 2: append_nonempty(U2, chain2) chain2 = [] else: append_nonempty(U3, chain3) chain3 = [] else: if j < m / 2: chain2.append(ln) else: chain3.append(ln) append_nonempty(U2, chain2) append_nonempty(U3, chain3) return U1, U2, U3, U4
def make_origin_embeddings(qpu_sampler=None, lattice_type=None): """Creates optimal embeddings for a lattice. The embeddings created are compatible with the topology and shape of a specified ``qpu_sampler``. Args: qpu_sampler (:class:`dimod.Sampler`, optional): Quantum sampler such as a D-Wave system. If not specified, the :class:`~dwave.system.samplers.DWaveSampler` sampler class is used to select a QPU solver with a topology compatible with the specified ``lattice_type`` (e.g. an Advantage system for a 'pegasus' lattice type). lattice_type (str, optional, default=qpu_sampler.properties['topology']['type']): Options are: * "cubic" Embeddings compatible with the schemes arXiv:2009.12479 and arXiv:2003.00133 are created for a ``qpu_sampler`` of topology type either 'pegasus' or 'chimera'. * "pegasus" Embeddings are chain length one (minimal and native). If ``qpu_sampler`` topology type is 'pegasus', maximum scale subgraphs are embedded using the ``nice_coordinates`` vector labeling scheme for variables. * "chimera" Embeddings are chain length one (minimal and native). If ``qpu_sampler`` topology type is 'chimera', maximum scale chimera subgraphs are embedded using the chimera vector labeling scheme for variables. Returns: A list of embeddings. Each embedding is a dictionary, mapping geometric problem keys to sets of qubits (chains) compatible with the ``qpu_sampler``. Examples: This example creates a list of three cubic lattice embeddings compatible with the default online system. These three embeddings are related by rotation of the lattice: for a Pegasus P16 system the embeddings are for lattices of size (15,15,12), (12,15,15) and (15,12,15) respectively. >>> from dwave.system.samplers import DWaveSampler # doctest: +SKIP >>> sampler = DWaveSampler() # doctest: +SKIP >>> embeddings = make_origin_embeddings(qpu_sampler=sampler, ... lattice_type='cubic') # doctest: +SKIP """ if qpu_sampler is None: if lattice_type == 'pegasus' or lattice_type == 'chimera': qpu_sampler = DWaveSampler(solver={'topology__type': lattice_type}) else: qpu_sampler = DWaveSampler() qpu_type = qpu_sampler.properties['topology']['type'] if lattice_type is None: lattice_type = qpu_type qpu_shape = qpu_sampler.properties['topology']['shape'] target = nx.Graph() target.add_edges_from(qpu_sampler.edgelist) if qpu_type == lattice_type: # Fully yielded fully utilized native topology problem. # This method is also easily adapted to work for any chain-length 1 # embedding origin_embedding = {q: [q] for q in qpu_sampler.properties['qubits']} if lattice_type == 'pegasus': # Trimming to nice_coordinate supported embeddings is not a unique, # options, it has some advantages and some disadvantages: proposed_source = dnx.pegasus_graph(qpu_shape[0], nice_coordinates=True) proposed_source = nx.relabel_nodes( proposed_source, { q: dnx.pegasus_coordinates(qpu_shape[0]).nice_to_linear(q) for q in proposed_source.nodes() }) lin_to_vec = dnx.pegasus_coordinates(qpu_shape[0]).linear_to_nice elif lattice_type == 'chimera': proposed_source = dnx.chimera_graph(qpu_shape[0], qpu_shape[1], qpu_shape[2]) lin_to_vec = dnx.chimera_coordinates( qpu_shape[0]).linear_to_chimera else: raise ValueError( f'Unsupported native processor topology {qpu_type}. ' 'Support for Zephyr and other topologies is straightforward to ' 'add subject to standard dwave_networkx library tool availability.' ) elif lattice_type == 'cubic': if qpu_type == 'pegasus': vec_to_lin = dnx.pegasus_coordinates( qpu_shape[0]).pegasus_to_linear L = qpu_shape[0] - 1 dimensions = [L, L, 12] # See arXiv:2003.00133 origin_embedding = { (x, y, z): [ vec_to_lin((0, x, z + 4, y)), vec_to_lin((1, y + 1, 7 - z, x)) ] for x in range(L) for y in range(L) for z in range(8) if target.has_edge(vec_to_lin(( 0, x, z + 4, y)), vec_to_lin((1, y + 1, 7 - z, x))) } origin_embedding.update({ (x, y, z): [ vec_to_lin((0, x + 1, z - 8, y)), vec_to_lin((1, y, 19 - z, x)) ] for x in range(L) for y in range(L) for z in range(8, 12) if target.has_edge(vec_to_lin(( 0, x + 1, z - 8, y)), vec_to_lin((1, y, 19 - z, x))) }) elif qpu_type == 'chimera': vec_to_lin = dnx.chimera_coordinates( qpu_shape[0], qpu_shape[1], qpu_shape[2]).chimera_to_linear L = qpu_shape[0] // 2 dimensions = [L, L, 8] # See arxiv:2009.12479, one choice amongst many origin_embedding = { (x, y, z): [ vec_to_lin(coord) for coord in [(2 * x + 1, 2 * y, 0, z), (2 * x, 2 * y, 0, z), (2 * x, 2 * y, 1, z), (2 * x, 2 * y + 1, 1, z)] ] for x in range(L) for y in range(L) for z in range(4) if target.has_edge(vec_to_lin(( 2 * x + 1, 2 * y, 0, z)), vec_to_lin((2 * x, 2 * y, 0, z))) and target.has_edge(vec_to_lin(( 2 * x, 2 * y, 0, z)), vec_to_lin((2 * x, 2 * y, 1, z))) and target.has_edge(vec_to_lin(( 2 * x, 2 * y, 1, z)), vec_to_lin((2 * x, 2 * y + 1, 1, z))) } origin_embedding.update({ (x, y, 4 + z): [ vec_to_lin(coord) for coord in [(2 * x + 1, 2 * y, 1, z), (2 * x + 1, 2 * y + 1, 1, z), (2 * x + 1, 2 * y + 1, 0, z), (2 * x, 2 * y + 1, 0, z)] ] for x in range(L) for y in range(L) for z in range(4) if target.has_edge(vec_to_lin(( 2 * x + 1, 2 * y, 1, z)), vec_to_lin((2 * x + 1, 2 * y + 1, 1, z))) and target.has_edge(vec_to_lin(( 2 * x + 1, 2 * y + 1, 1, z)), vec_to_lin((2 * x + 1, 2 * y + 1, 0, z))) and target. has_edge(vec_to_lin((2 * x + 1, 2 * y + 1, 0, z)), vec_to_lin((2 * x, 2 * y + 1, 0, z))) }) else: raise ValueError(f'Unsupported qpu_sampler topology {qpu_type} ' 'for cubic lattice solver') proposed_source = _make_cubic_lattice(dimensions) else: raise ValueError('Unsupported combination of lattice_type ' 'and qpu_sampler topology') origin_embedding = _yield_limited_origin_embedding(origin_embedding, proposed_source, target) if qpu_type == lattice_type: # Convert keys to standard vector scheme: origin_embedding = { lin_to_vec(node): origin_embedding[node] for node in origin_embedding } # We can propose additional embeddings. Or we can use symmetries of the # target graph (automorphisms), to create additional embedding options. # This is important in the cubic case, because the subregion shape and # embedding features are asymmetric in the x, y and z directions. # Various symmetries can be exploited in all lattices. origin_embeddings = [origin_embedding] if lattice_type == 'cubic': # A rotation is sufficient for demonstration purposes: origin_embeddings.append({(key[2], key[0], key[1]): value for key, value in origin_embedding.items()}) origin_embeddings.append({(key[1], key[2], key[0]): value for key, value in origin_embedding.items()}) elif lattice_type == 'pegasus': # A horizontal to vertical flip is sufficient for demonstration purposes: # Flip north-east to south-west axis (see draw_pegasus): L = qpu_shape[0] origin_embeddings.append({(key[0], L - 2 - key[2], L - 2 - key[1], 1 - key[3], 3 - key[4]): value for key, value in origin_embedding.items()}) else: # A horizontal to vertical flip is sufficient for demonstration purposes: origin_embeddings.append({(key[1], key[0], 1 - key[2], key[3]): value for key, value in origin_embedding.items()}) return origin_embeddings