Skip to content

Latest commit

 

History

History
executable file
·
606 lines (508 loc) · 22 KB

multi_gate.rst

File metadata and controls

executable file
·
606 lines (508 loc) · 22 KB

Multiple-Gate Circuit

This example solves a logic-circuit problem on a D-Wave quantum computer. It expands on the discussion in the and example about the effect of minor-embedding on performance, demonstrating different approaches to embedding.

Circuit with 7 logic gates, 4 inputs (a, b, c, d), and 1 output (z). The circuit implements function z = \overline{b} (ac + ad + \overline{c}\overline{d}).

Circuit with 7 logic gates, 4 inputs (a, b, c, d), and 1 output (z). The circuit implements function $z = \overline{b} (ac + ad + \overline{c}\overline{d})$.

Example Requirements

To run the code in this example, the following is required.

  • The requisite information for problem submission through SAPI, as described in sapi_access
  • Ocean tools dimod </docs_dimod/sdk_index> and dwave-system </docs_system/sdk_index>. For the optional graphics, you will also need problem-inspector </docs_inspector/sdk_index>.

Formulating the Problem as a CSP

Other examples (not and and) show how a Boolean gate is represented as a constraint satisfaction problem (CSP) on a quantum computer. This example aggregates the resulting binary quadratic models (BQM) to do the same for multiple gates that constitute a circuit.

Small-Circuit Problem

To start with, solve the circuit with 7 logic gates shown above.

The code below represents standard Boolean gates with BQMs provided by dimod </docs_dimod/sdk_index> BQM generators, represents the Boolean NOT operation by inverting the coefficients of the relevant variables, and sums the gate-level BQMs. The resulting aggregate BQM has its lowest value for variable assignments that satisfy all the constraints representing the circuit's Boolean gates.

>>> from dimod.generators import and_gate, or_gate ... >>> bqm_gate2 = or_gate('b', 'c', 'out2') >>> bqm_gate3 = and_gate('a', 'b', 'out3') >>> bqm_gate3.flip_variable('b') >>> bqm_gate4 = or_gate('out2', 'd', 'out4') >>> bqm_gate5 = and_gate('out3', 'out4', 'out5') >>> bqm_gate7 = or_gate('out5', 'out4', 'z') >>> bqm_gate7.flip_variable('out4') ... >>> bqm = bqm_gate2 + bqm_gate3 + bqm_gate4 + bqm_gate5 + bqm_gate7 >>> print(bqm.num_variables) 9

This circuit is small enough to solve with a brute-force algorithm such as that used by the ~dimod.reference.samplers.ExactSolver class. Solve and print all feasible solutions.

>>> from dimod import ExactSolver, drop_variables ... >>> solutions = ExactSolver().sample(bqm).lowest() >>> out_fields = [key for key in list(next(solutions.data(['sample'])))[0].keys() if 'out' in key] >>> print(drop_variables(solutions, out_fields)) a b c d z energy num_oc. 0 0 0 0 1 0 0.0 1 1 0 0 1 1 0 0.0 1 2 0 1 1 1 0 0.0 1 3 0 1 0 1 0 0.0 1 4 1 1 0 1 0 0.0 1 5 1 1 1 1 0 0.0 1 6 1 1 1 0 0 0.0 1 7 1 1 0 0 0 0.0 1 8 0 1 0 0 0 0.0 1 9 0 1 1 0 0 0.0 1 10 0 0 1 0 0 0.0 1 11 1 0 0 1 1 0.0 1 12 1 0 1 1 1 0.0 1 13 1 0 1 0 1 0.0 1 14 1 0 0 0 1 0.0 1 15 0 0 0 0 1 0.0 1 ['BINARY', 16 rows, 16 samples, 5 variables]

Brute-force algorithms are not effective for large problems.

Large-Circuit Problem

In the figure below, the 7-gates circuit solved above is replicated with the outputs connected through a series of XOR gates.

Multiple replications of the 7-gates circuit (z = \overline{b} (ac + ad + \overline{c}\overline{d})) connected by XOR gates.

Multiple replications of the 7-gates circuit ($z = \overline{b} (ac + ad + \overline{c}\overline{d})$) connected by XOR gates.

The circuit_bqm function defined below replicates, for a specified number of circuits, the BQM developed above and connects the outputs through a cascade of XOR gates.

>>> from dimod import BinaryQuadraticModel, quicksum >>> from dimod.generators import and_gate, or_gate, xor_gate ... >>> def circuit_bqm(n: int = 3) -> BinaryQuadraticModel: ... "Create a BQM for n replications of the 7-gate circuit." ... ... if n < 2: ... raise ValueError("n must be at least 2") ... ... bqm_gate2 = [or_gate(f"b{c}", f"c{c}", f"out2{c}") for c in range(n)] ... bqm_gate3 = [and_gate(f"a{c}", f"b{c}", f"out3{c}") for c in range(n)] ... [bqm.flip_variable("b{}".format(indx)) for indx, bqm in enumerate(bqm_gate3)] ... bqm_gate4 = [or_gate(f"out2{c}", f"d{c}", f"out4{c}") for c in range(n)] ... bqm_gate5 = [and_gate(f"out3{c}", f"out4{c}", f"out5{c}") for c in range(n)] ... bqm_gate7 = [or_gate(f"out5{c}", f"out4{c}", f"z{c}") for c in range(n)] ... [bqm.flip_variable("out4{}".format(indx)) for indx, bqm in enumerate(bqm_gate7)] ... bqm_z = [xor_gate("z_0", "z_1", "zz0", "aux0")] + [ ... xor_gate(f"z{c}", f"zz{c-2}", f"zz{c-1}", f"aux{c-1}") for c in range(2, n)] ... return quicksum(bqm_gate2 + bqm_gate3 + bqm_gate4 + bqm_gate5 + bqm_gate7 + bqm_z)

Instantiate a BQM for six replications. The resulting circuit has 64 variables.

>>> bqm = circuit_bqm(6) >>> print(bqm.num_variables) 64

Minor-Embedding and Sampling

The and example used the ~dwave.system.composites.EmbeddingComposite composite to minor-embed its unstructured problem onto the topology of a quantum computer's qubits and couplers. That composite uses the minorminer algorithm <sdk_index_minorminer> to find a minor embedding for each problem you submit.

For some applications, the submitted problems might be related or limited in size in such a way that you can find a common minor embedding for the entire set of problems. For instance, you might wish to submit the circuit of this example multiple times with various configurations of fixed inputs (inputs set to zero or one).

One approach is to pre-calculate a minor embedding for a clique (a complete graph), a "clique embedding", of a size at least equal to the maximum number of variables (nodes) expected in your problem. Because any BQM of n variables maps to a subgraph of an n--node clique1, you can then use the clique embedding by assigning to all unused nodes and edges a value of zero.

Configure a quantum computer as a sampler using both the ~dwave.system.composites.EmbeddingComposite composed sampler, for standard embedding, and the ~dwave.system.samplers.DWaveCliqueSampler composed sampler, for clique embedding.

>>> from dwave.system import DWaveSampler, EmbeddingComposite, DWaveCliqueSampler ... >>> sampler1 = EmbeddingComposite(DWaveSampler(solver=dict(topology__type='pegasus'))) >>> sampler2 = DWaveCliqueSampler(solver=dict(name=sampler1.child.solver.name)) # doctest: +SKIP

Performance Comparison: Embedding Time

The ~dwave.system.samplers.DWaveCliqueSampler composed sampler reduces the overhead of repeatedly finding minor embeddings for applications that submit a sequence of problems that are all sub-graphs of a clique that can be embedded on the QPU sampling the problems.

The table below shows the minor-embedding times2 for a series of random problems of increasing size3. Some interesting differences are bolded.

Note

The times below are for a particular development environment. Embedding times can be expected to vary for different CPUs, operating systems, etc as well as varying across problems and executions of the heuristic algorithm.

Minor-Embedding: Embedding Times
Problem Size: Nodes (Edges/Node) ~dwave.system.composites.EmbeddingComposite Time [sec] ~dwave.system.samplers.DWaveCliqueSampler Time [sec] Notes
10 (5) 0.06 152.7 ~dwave.system.samplers.DWaveCliqueSampler calculates all clique embeddings for the QPU.
20 (10) 0.41 0.11
30 (15) 1.39 0.11
40 (20) 3.88 0.13
50 (25) 9.7 0.14
60 (30) 17.91 0.16
70 (35) 22.02 0.19
80 (40) 73.74 0.25
90 (45) 65.05 0.22
100 (50) 28.92 0.25 For this execution, ~dwave.system.composites.EmbeddingComposite found a good embedding quickly; other executions might not.

You can see that while the first submission is slow for the ~dwave.system.samplers.DWaveCliqueSampler composed sampler, subsequent submissions are fast. For the ~dwave.system.composites.EmbeddingComposite composed sampler, the time depends on the size and complexity of each problem, can vary between submissions of the same problem, and each submission incurs the cost of finding an embedding anew.

For example, to submit a problem with two replications of the $z = \overline{b} (ac + ad + \overline{c}\overline{d})$ circuit, which has 20 variables (and 34 interactions), you can expect an embedding time of less than a second.

>>> bqm1 = circuit_bqm(2) >>> bqm2 = bqm1.copy(bqm1) >>> bqm1.fix_variables([('b_0', 0), ('b_1', 0), ('a_0', 1)]) >>> bqm2.fix_variables([('b_0', 1), ('b_1', 1), ('a_0', 0)]) ... >>> bqms = [bqm1, bqm2] >>> times = {key: [] for key in samplers.keys()} # doctest: +SKIP >>> for name, sampler in samplers.items(): # doctest: +SKIP ... for i, bqm in enumerate(bqms): ... times[name].append(time.time()) ... sampler.sample(bqm, num_reads=5000) ... times[name][i] = time.time() - times[name][i] >>> times # doctest: +SKIP {'sampler1': [0.43480920791625977, 0.381976842880249], 'sampler2': [0.12912440299987793, 0.12194108963012695]}

If, however, you wish to submit a problem with ten replications repeatedly, with some subset of the variables fixed, the overhead cost of embedding can be minutes.

Performance Comparison: Solution Quality

The ~dwave.system.composites.EmbeddingComposite composed sampler attempts to find a good embedding for any given problem while the ~dwave.system.samplers.DWaveCliqueSampler reuses a clique embedding found once. Typically the former results in an embedding with shorter chains than the latter, with the difference in length increasing for larger problems. Chain length can significantly affect solution quality.

The table below compares, for the two embedding methods, the ratio of ground states (solutions for which the value of the BQM is zero, meaning no constraint is violated or all assignments of values to the problem's variables match valid states of the circuit represented by the BQM) to total samples returned from the quantum computer when minor-embedding a sequence of problems of increasing size.

The results are for one particular code4 execution on problems that replicate the $z = \overline{b} (ac + ad + \overline{c}\overline{d})$ circuit between two to ten times.

Minor-Embedding: Ground-State Ratio Across Samplers
Problem Size: Circuit Replications ~dwave.system.composites.EmbeddingComposite Ground-State Ratio [%] ~dwave.system.samplers.DWaveCliqueSampler Ground-State Ratio [%]
2 69.6 74.4
3 52.6 25.2
4 40.5 16.8
5 29.1 3.3
6 20.9 2.9
7 16.3 0.2
8 14.4 0.0
9 8.9 0.0
10 7.2 0.3

You can see that for small problems, using a clique embedding can produce high-quality solutions, and might be advantageously used when submitting a sequence of related problems.

Visualizing the Minor Embedding

Ocean's problem-inspector </docs_inspector/sdk_index> can illuminate differences in solution quality between alternative minor embeddings. You can use it to visualize sample sets returned from a quantum computer in your browser.

>>> import dwave.inspector >>> bqm = circuit_bqm(2) >>> sampleset1 = sampler1.sample(bqm, num_reads=5000) >>> dwave.inspector.show(sampleset1) # doctest: +SKIP

The figure below, constituted of snapshots from the problem inspector for submissions to the two samplers defined above, shows that for the BQM representing two replications of the $z = \overline{b} (ac + ad + \overline{c}\overline{d})$ circuit, with its 20 variables and 34 edges, the clique embedding uses chains of length two to represent nodes that the standard embedding represented with single qubits.

Graph of two replications of the z = \overline{b} (ac + ad + \overline{c}\overline{d}) circuit (top centre), a standard embedding (bottom-left), and a clique-embedding (bottom-right).

Graph of two replications of the $z = \overline{b} (ac + ad + \overline{c}\overline{d})$ circuit (top centre), a standard embedding (bottom-left), and a clique-embedding (bottom-right).

Short chains of a few qubits generally enable good quality solutions: as shown in the table above, for a two-replications circuit the ratio of ground states is similar for both embedding methods.

The BQM representing ten replications of the $z = \overline{b} (ac + ad + \overline{c}\overline{d})$ circuit has over 100 variables. Typically, minor embeddings for dense graphs require longer chains than those of sparse graphs; for this problem the clique embedding needed chains of up to 11 qubits versus the standard embedding's chains of 2 qubits. Depending on the problem, such chains may degrade the solution, as is the case here.

>>> bqm = circuit_bqm(10) >>> for sampler in samplers.values(): # doctest: +SKIP ... sampleset = sampler.sample(bqm, num_reads=5000, return_embedding=True) ... chain_len = max(len(x) for x in sampleset.info["embedding_context"]["embedding"].values()) ... print("Maximum chain length: {}".format(chain_len)) # doctest: +SKIP ... dwave.inspector.show(sampleset) Maximum chain length: 2 Maximum chain length: 11

Graph of ten replications of the z = \overline{b} (ac + ad + \overline{c}\overline{d}) circuit (top centre), an embedding (bottom-left), a clique-embedding (bottom-right).

Graph of ten replications of the $z = \overline{b} (ac + ad + \overline{c}\overline{d})$ circuit (top centre), an embedding (bottom-left), a clique-embedding (bottom-right).

The inspector_graph_partitioning example demonstrates how the problem inspector can be used to analyze your problem submissions.

Performance Comparison: Embedding Executions

Algorithmic minor embedding is heuristic: solution results can vary significantly based on the minor embedding found.

The table below compares, for a single random ~dimod.generators.ran_r problem of a given size, the ratio of ground states to total samples returned from the quantum computer over several executions5 of minor embedding. The outlier results are bolded.

For each run, the ~dwave.system.composites.EmbeddingComposite composed sampler finds and returns a minor embedding for the problem. The returned embedding is then reused twice by a ~dwave.system.composites.FixedEmbeddingComposite composed sampler.

Minor-Embedding: Ground-State Ratio Across Executions
Execution Ground-State Ratio [%] Chain Length
1 10.1, 10.1, 8.9 4
2 25.8, 29.1, 28.4 4
3 24.9, 24.4, 27.9 3
4 14.0, 11.0, 13.6 3
5 38.5, 36.4, 34.5 4
6 15.4, 14.6, 14.5 4
7 43.8, 44.2, 45.1 3
8 15.9, 17.2, 15.3 4
9 31.1, 34.1, 27.1 3
10 15.3, 11.5, 12.1 3

You can see that each set of three submission of the problem using the same embedding have minor variations in the ground-state ratios. However, the variation across minor-embeddings of the same problem can be significant. Consequently, for some applications it can be beneficial to run the minor-embedding multiple times and select the best embedding.


  1. The figure below illustrates such subgraphs. Two random ~networkx.generators.random_graphs.random_regular_graph graphs with four and three nodes, respectively, each with two edges per node, are plotted on a background of a 5-node clique.

    Two subgraphs of a 5-node clique. The clique is plotted in yellow and the random sub-graphs in blue (four nodes) and red (three nodes).

  2. The times are approximate: the code measures the blocking time when submitting problems, of which minor-embedding is the major element.

  3. The code below can take several minutes to run. Uncommenting the print statement lets you view the execution's progress. Note that if you have already used ~dwave.system.samplers.DWaveCliqueSampler, your cache might need to be cleared with the minorminer.busclique.busgraph_cache.clear_all_caches function to measure the initial calculation time.

    >>> import time >>> import networkx as nx ... >>> samplers = {"sampler1": sampler1, "sampler2": sampler2} # doctest: +SKIP >>> times = {key: [] for key in samplers.keys()} # doctest: +SKIP >>> for name, sampler in samplers.items(): # doctest: +SKIP ... for i in range(10): ... nodes = 10 + 10*i ... edges = 5 + 5*i ... G = nx.random_regular_graph(n=nodes, d=edges) ... # print("Submitting problem of {} nodes and {} edges to sampler {}".format(nodes, edges, name)) ... times[name].append(time.time()) ... sampler.sample_ising({}, {edge: 1 for edge in G.edges}) ... times[name][i] = time.time() - times[name][i]

  4. The code below can take minutes to run. Uncommenting the print statements lets you view the execution's progress.

    >>> samplers = {"sampler1": sampler1, "sampler2": sampler2} # doctest: +SKIP >>> samplesets = {key: [] for key in samplers.keys()} # doctest: +SKIP >>> for num_circuits in range(2, 11): ... bqm = circuit_bqm(num_circuits) ... for name, sampler in samplers.items(): # doctest: +SKIP ... # print("Submitting problem of {} circuits to sampler {}".format(num_circuits, name)) ... sampleset = sampler.sample(bqm, num_reads=5000) ... if sampleset.first.energy > 0: ... samplesets[name].append(0) ... else: ... samplesets[name].append(sum(sampleset.lowest().record.num_occurrences))

  5. The code below can take minutes to run. Uncommenting the print statements lets you view the execution's progress.

    >>> from dimod.generators import ran_r >>> from dwave.system import DWaveSampler, EmbeddingComposite, FixedEmbeddingComposite ... >>> problem_size = 20 >>> num_problems = 10 >>> submission_repeats = 2 >>> qpu = DWaveSampler(solver=dict(topology__type='pegasus')) >>> sampler1 = EmbeddingComposite(qpu) >>> bqm = ran_r(10, problem_size) ... >>> samplesets = {} >>> chain_len = [] >>> for runs in range(num_problems): ... samplesets[runs] = [] ... print("Run {}".format(runs)) ... sampleset = sampler1.sample(bqm, num_reads=5000, return_embedding=True) ... if sampleset.first.energy > 0: ... samplesets[runs].append(0) ... else: ... samplesets[runs].append(sum(sampleset.lowest().record.num_occurrences)) ... embedding = sampleset.info["embedding_context"]["embedding"] # doctest: +SKIP ... chain_len.append(max(len(x) for x in embedding.values())) # doctest: +SKIP ... sampler2 = FixedEmbeddingComposite(qpu, embedding=embedding) # doctest: +SKIP ... ... for i in range(submission_repeats): ... print("tSubmitting {}--{}".format(runs, i)) ... sampleset = sampler2.sample(bqm, num_reads=5000, return_embedding=True) # doctest: +SKIP ... if sampleset.first.energy > 0: # doctest: +SKIP ... samplesets[runs].append(0) ... else: ... samplesets[runs].append(sum(sampleset.lowest().record.num_occurrences))