.. todo:: some intro content
You use quantum circuit description language (QCDL) to write programs that run operations on D-Wave's gate-model quantum processing units (QPUs) and simulators.
QCDL is an embedded Domain Specific Language (DSL) that uses Python as the host language. If you have some experience coding in Python, you can understand the structure of QCDL programs.
The Ocean SDK supports generating these programs, as illustrated in the following examples.
You use the @qcdl
Python decorator[1] to
mark the entry point to your
QCDL circuit. This decorator converts an otherwise standard Python function into
one that generates QCDL programs when executed.
The decorator can optionally indicate the number of qubits in the program.
This example creates a Bell state.
.. testcode::
from dwave.gate.qcdl import qcdl
from dwave.gate.qcdl.operations import cx, h, measure
@qcdl(2)
def main(q0, q1):
h(q0)
cx(q0, q1)
measure(q0)
measure(q1)
qcdl_program = main()
In the code above, the @qcdl decorator specifies that the entry point
accepts two qubits, the arguments q0 and q1 of main(). The decorated
function main() returns a
Pydantic model
that you can submit to a compiler or simulator in the |cloud|_ service, as described
in the :ref:`qcdl_submitting_programs` section.
The :func:`~dwave.gate.qcdl.print_qcdl` function can visualize this dict structure as readable text, and if run in a Jupyter notebook, as a display object.
.. testcode::
from dwave.gate.qcdl import print_qcdl
print_qcdl(qcdl_program)
The code above displays the following QCDL program.
.. testoutput::
:skipif: True
:options: +NORMALIZE_WHITESPACE
begin quantum
h([q0], q0)
cx([q0, q1], q0, q1)
measure([q0], q0, log=True)
measure([q1], q1, log=True)
end quantum
If :func:`~dwave.gate.qcdl.print_qcdl` displays poorly, you can instead output a
string by print_qcdl(qcdl_program, to_Display=False).
| [1] | Python decorators are described in the Decorators section of the Wikipedia article on Python syntax and multiple internet tutorials. |
.. seealso::
:func:`~dwave.gate.qcdl.qcdl` decorator
The gates dwave-gate supports match the method names in a Qiskit
QuantumCircuit (e.g., :func:`~dwave.gate.qcdl.operations.h`,
:func:`~dwave.gate.qcdl.operations.sx`, :func:`~dwave.gate.qcdl.operations.rz`,
:func:`~dwave.gate.qcdl.operations.cz`, etc).
.. testcode::
from dwave.gate.qcdl import qcdl
from dwave.gate.qcdl.operations import h, cz
@qcdl(2)
def simple_gate_example(q0, q1):
h(q0)
cz(control_qubit=q0, target_qubit=q1)
In this example, cz(control_qubit=q0, target_qubit=q1) is similar to a
Qiskit the method call cz(q0, q1).
Note
For gates that take angles as an argument, dwave-gate lists qubits
before angles, whereas Qiskit follows the reverse order.
Note
For parameterizable gates, angles are in units of radians when passed as literals. When a :class:`~dwave.gate.qcdl.registers.FixedPointRegister` is passed as the angle for a gate, its value must be in units of π (see the :ref:`qcdl_basic_registers_arithmetic` section for more information).
When you submit your QCDL to the |cloud|_ service, a transpiler rewrites the circuit to use the QPU's supported basis gates and topology, as described in the :ref:`qcdl_basic_transpilation` section. For most algorithms, any implementation is acceptable but if you are studying fidelity or yield characterization, you can prevent the transpiler from combining certain gates. The :func:`~dwave.gate.qcdl.operations.barrier` instruction signals to the transpiler to not combine gates across your barrier.[2]
For example, if you do not set :func:`~dwave.gate.qcdl.operations.barrier` instructions on a randomized benchmarking circuit, where the net mathematical effect is an identity operation, transpilation collapses your QCDL.
.. testcode::
from dwave.gate.qcdl import qcdl
from dwave.gate.qcdl.operations import barrier, x
@qcdl(1)
def barrier_example(q0):
x(q0)
barrier(q0)
x(q0)
Note
A :func:`~dwave.gate.qcdl.operations.barrier` instruction does not necessarily imply a :meth:`~dwave.gate.qcdl.QCDLModuleContainer.sync` (see the :ref:`qcdl_advanced_synchronization` section).
| [2] | When the transpiler is not used, the :func:`~dwave.gate.qcdl.operations.barrier` instruction might affect some circuit modifications. |
QCDL supports integer and fixed-point registers with the :class:`~dwave.gate.qcdl.registers.Register` and :class:`~dwave.gate.qcdl.registers.FixedPointRegister` classes. You can use these registers for simple classical expressions: negation, addition, subtraction, multiplication, AND, OR, XOR, right-shift, and all six comparisons.
You can use the outputs of these calculations for :ref:`conditional statements <qcdl_advanced_conditionals>` within complex real-time classical-quantum logic.
When you store a :class:`~dwave.gate.qcdl.operations.measure` result to a register,
numerical values 0 and 1 are used to represent the logical
projective measurements and -1 for a * state (a measurement that was
out of the code space and declared "erased"). For more information, see the
:class:`~dwave.gate.qcdl.LogicalOutcomeToInteger` class.
Tip
The simulator is able to detect register overflow or underflow problems. For
this among many other reasons, you should validate programs with the
simulator before running your application on the QPU. Configure simulator
option use_registers=True to either warn or raise an exception on such
conditions.
.. testcode::
from dwave.gate.qcdl import qcdl, Scope
@qcdl(2)
def register_example(q0, q1):
sc = Scope(q0, q1) # Scope facilitates control flow
r1 = sc.Register(2, name="r1") # naming facilitates debugging
r2 = sc.Register()
r2 <<= 1 # set r2 to 1
r2 <<= 2 * r1 # set r2 to 2*r1 = 4
Attention!
Registers are not implicitly re-assigned with every shot. Instead, they'll carry the value they ended with from one shot to the next. Since for most registers users want each shot to be independent, they should re-assign their registers before using them.
Note
- A register is associated with a qubit. For QCDL programs with some complexity, your program must ensure the information in any qubit's register is visible to other qubits. The :ref:`qcdl_advanced_registers_mirroring` section provides more information.
- If you pass a :class:`~dwave.gate.qcdl.registers.FixedPointRegister` object to a gate as an angle, use units of π instead of radians. For example, a value of 1 is equivalent to π.
A logical :func:`~dwave.gate.qcdl.operations.measure` operation on a dual-rail system produces one of three outcomes:
- 0, 1 represent logical projective measurements.
*(which has numerical representation -1), sometimes informally referred to as a "splat", represents that a measured qubit was determined to be out of the code space and is thereby declared to be "erased".
You may place these "end of the line"-measurement instructions anywhere in your program. You can measure qubits multiple times in a given shot (usually resetting the qubit(s) in between).
Tip
Using the :attr:`~dwave.gate.Result.tags` property is the recommended way to organize measurement data.
Measurement outcomes are handled in three different ways:
.. todo:: Update below for Ocean
- If
log=True(the default) the outcome is appended to the array associated with the qubit on which it was measured. Along with the arrays from the other qubits, this data is returned to you in a 3D array (pertag) with shape "number of measurements per shot, number of shots, number of qubits". This data structure may be retrieved usingResult.get_memory. For circuits with a deterministic number of measurements per shot consistent for all qubits, this data structure may be converted into a counts dictionary withResult.get_counts(get_countscallsget_memory). - The outcome may be saved to a register. When doing so, even if the register
is defined on multiple qubits, only the register copy on the qubit measured
is assigned. This data could be returned with
append_table_row(see the :ref:`qcdl_basic_result_records` section). - Each qubit implicitly remembers its most recent measurement outcome and this value may be used in conditional statements.
.. testcode::
from dwave.gate.qcdl.operations import measure
@qcdl(1)
def measurement_example1(q0):
measure(q0)
.. testcode::
from dwave.gate.qcdl import qcdl
from dwave.gate.qcdl.operations import measure
@qcdl(1)
def measurement_example2(q0):
register = q0.Register()
measure(q0, register=register, log=False)
Warning
The 3D array of logged measurements is unlikely to be useful if measurement data is generated non-deterministically. Unless there is a deterministic number of measurements per shot, you cannot relate measurement outcomes with the generating instruction.
By default, the :meth:`~dwave.gate.Result.get_counts` method returns all data,
including erasures. To return only results without the *, thereby
post-selecting on the detected errors, use the post_select=True flag.
You can non-destructively inspect a dual-rail qubit to detect if it is out of the code space ("leaked") with the :func:`~dwave.gate.qcdl.operations.mced` operation. If the test is positive, the qubit is declared erased.
.. testcode::
from dwave.gate.qcdl import qcdl
from dwave.gate.qcdl.operations import mced
@qcdl(1)
def mced_example(q0):
register = q0.Register()
mced(q0, register=register)
Results are a Python dictionary where keys are set by the :meth:`~dwave.gate.qcdl.QCDLModuleContainer.append_table_row` method and values are tables formatted as a Polars DataFrame.
The :meth:`~dwave.gate.qcdl.QCDLModuleContainer.append_table_row` method retrieves the values of registers in runtime. When you invoke the method, register data is written to a set of tables that your application can retrieve. In addition to using this functionality in algorithms, you can use it for troubleshooting, as though it were a cross between a print statement and a breakpoint.
.. todo:: update for Ocean
If your QCDL uses the :meth:`~dwave.gate.qcdl.QCDLModuleContainer.append_table_row` method, the :class:`~dwave.gate.Result` output contains records that you may retrieve with :meth:`~Result.get_records` method.
.. testcode::
:skipif: True
import pandas as pd
from dwave.gate.qcdl import qcdl
from aqumen import Aqumen # Replace with Leap service's class
@qcdl(1)
def main(q0):
r = q0.Register(name="some_classical_data")
r <<= 13
q0.append_table_row(r, table_name="my_table")
aq = Aqumen("simulator", simulate=True)
results = await aq.execute(program=main(), shots=10)
df : pl.DataFrame = res.get_records()["q0"]["my_table"]
The result is a DataFrame containing 1 column named some_classical_data
with 10 rows, each of which have a value of 13.
The :class:`~dwave.gate.YieldHandling` class provides a general way of handling result distributions. It supports options for renormalizing distributions, ignoring erasures, and others.
.. testcode::
:skipif: True
from dwave.gate.results import YieldHandling
half_splats = {"00": 100, "0*": 100}
assert YieldHandling.only_post_selected_counts.apply(half_splats) == ({"00": 100}, 0.5)
A significant feature of the D-wave simulator is that it flags detected errors
by returning * as a third measurement outcome in addition to 0 and
1, as described in the :ref:`qcdl_basic_measurements` section. Qiskit
does not handle these values so you must remove individual shots containing a
* when passing information to Qiskit. Consequently, fewer shots are likely
to be returned than the number of shots you requested.[3]
.. todo:: update for Ocean
.. testcode::
:skipif: True
from dwave.gate.results import YieldHandling
provider = AqumenProvider(yield_handling=YieldHandling.renormalize_distribution)
simulator_noisy_backend = provider.simulator_noisy
shots = 1000
job: AqumenJob = simulator_noisy_backend.run(qc, shots=shots)
result: AqumenQiskitResult = job.result()
# no splats here!
counts: dict[str, float] = result.get_counts()
assert abs(sum(counts.values()) - shots) < 1e-8
The code above divides the values in the counts dict by the yield, trading
statistical accuracy for convenience.
Alternatively, a YieldHandling option may be passed to get_counts.
| [3] | If an application you use, for example, in computing statistical errors, is not robust to results containing few shots than requested, you can use the :class:`~dwave.gate.YieldHandling` class as a workaround temporarily and with caution. |
At the beginning of every shot, The QPU initializes all of the qubits used by your circuit. You can also explicitly use this operation in your QCDL.
.. testcode::
from dwave.gate.qcdl import qcdl
from dwave.gate.qcdl.operations import initialize
@qcdl(4)
def initialize_example(q0, q1, q2, q3):
initialize(q0, q1, q2, q3)
The operation is more effective on the QPU than any you are able to implement otherwise in QCDL code.
You can also reset qubits individually.
.. testcode::
from dwave.gate.qcdl import qcdl
from dwave.gate.qcdl.operations import initialize
@qcdl(1)
def reset_example(q0):
q0.reset()
While QCDL programs support any single- or two-qubit gate that is supported by a Qiskit QuantumCircuit, D-Wave QPUs (and simulator noise models) do not support all gates. The set of quantum gates that are compatible with a QPU is called its basis gates. A QCDL program must be transpiled to replace any unsupported gates with these basis gates.
Transpilation handles the change of gates for you. You may use any gates you wish to in your QCDL, knowing that the operations executed on the solver might differ in this way from your code. However, if you want your program executed verbatim (or an error raised), you can configure compilation and simulation to not transpile (see the :ref:`qcdl_submitting_programs` section).
| Basis Gates | Description | Availability | Notes |
|---|---|---|---|
| :func:`~dwave.gate.qcdl.operations.sx`, :func:`~dwave.gate.qcdl.operations.x` | Single qubit rotation around the X-axis by π/2 and π respectively. | All operational qubits. | |
| :func:`~dwave.gate.qcdl.operations.rz` | Single qubit, parameterizable rotation around the Z-axis. | All operational qubits. | |
| :func:`~dwave.gate.qcdl.operations.cz` | Two qubit rotation by π/2 around the ZZ-axis. | Connected, operational qubits. |
- Transpilation is a non-deterministic optimization algorithm based on Qiskit. Optimality is not guaranteed.
- Depending on topology and your circuit, the transpiler may add qubits and gates to the executed program that were not in your QCDL. You might be able to prevent this through careful placement of input gates.
- Returned logged measurements are organized according to the name of the qubit used in the :func:`~dwave.gate.qcdl.operations.measure` instruction.
QCDL supports procedures on qubits. A procedure is a subroutine that is
called from another procedure (the :ref:`entrypoint <qcdl_basic_entrypoint>`,
marked with the @qcdl decorator, is the outermost procedure).
Procedures are useful for:
- Potentially conserving instruction memory on the QPU
- Organizing code for visualization purposes
- Constraining transpilation
A procedure is marked with the @procedure decorator.
.. testcode::
from dwave.gate.qcdl import procedure, qcdl
from dwave.gate.qcdl.operations import rx, ry
@procedure
def my_procedure(qa, qb, increment):
rx(qa, increment)
ry(qb, increment)
@qcdl(2)
def procedure_example(q0, q1):
for _ in range(10):
my_procedure(q0, q1, 0.3)
my_procedure(q1, q0, 0.5)
A QCDL program calls the procedure just as it would any other Python function.
In the preceding example, if you remove the @procedure decorator, the
program inlines all the gates into the main procedure.
The :class:`~dwave.gate.qcdl.Scope` class enables you to define a set of operations you can consistently reuse on multiple qubits, which is especially beneficial for for classical and control-flow instructions.
This class is a client-side convenience feature used to generate qubit-level instructions---it is not represented in the generated QCDL. You may declare any number of scopes with arbitrary overlaps.
The example below defines a scope containing all the qubits used in the
program. At the end of each shot, all qubits have 1 in their register
if q0 is measured to be 1. This is a good example for how one might
:ref:`mirror <qcdl_advanced_registers_mirroring>` the same register across
qubits.
.. testcode::
from dwave.gate.qcdl import qcdl, Scope
from dwave.gate.qcdl.operations import h, measure
@qcdl(3)
def main(q0, q1, q2):
sc = Scope(q0, q1, q2)
is_1 = sc.Register()
is_1 <<= 0
h(q0)
measure(q0)
with sc.If(condition=q0):
is_1 += 1
In order to run on a QPU, quantum programs must have all of their gate operations scheduled, with the start time of each instruction precisely determined relative to the preceding instruction. Typically, you leave that to the compiler. For some programs, however, you might need to ensure that certain operations execute sequentially instead of concurrently; for example, to complete a measurement on one qubit before another qubit uses that measurement as a condition.
You can explicitly control such scheduling with the :meth:`~dwave.gate.qcdl.QCDLModuleContainer.sync` instruction. You may apply this instruction to any number of qubits to indicate that all operations before the :meth:`~dwave.gate.qcdl.QCDLModuleContainer.sync` instruction must be completed before any operations after the instruction are started.
.. testcode::
from dwave.gate.qcdl import qcdl
from dwave.gate.qcdl.operations import x
@qcdl(2)
def sync_example(q0, q1):
x(q0)
q0.sync(q1)
x(q1)
The example above ensures that the :func:`~dwave.gate.qcdl.operations.x` on q1
is scheduled to start after the :func:`~dwave.gate.qcdl.operations.x` on q0 has
completed.
- Compilation inserts implicit :meth:`~dwave.gate.qcdl.QCDLModuleContainer.sync` instructions before and after all multi-qubit operations such as gates, procedures, and control-flow operations (including shots).
- The :meth:`~dwave.gate.qcdl.QCDLModuleContainer.sync` instruction is not sensitive to the ordering of the qubits.
- An explicit :meth:`~dwave.gate.qcdl.QCDLModuleContainer.sync` instruction in the program is treated as a :func:`~dwave.gate.qcdl.operations.barrier` instruction by the transpiler.
- Scheduling is performed at compile-time and there is no support for "runtime" re-synchronization. If qubits are ever desynchronized, the output is meaningless. This means that non-deterministic operations must include all qubits in your program.
- Conditional statements themselves are deterministically scheduled by ensuring that an idle is inserted into either the true or false branch so that both branches are exactly the same duration.
Attention!
The simulator does not model runtime concurrency; it simply executes instructions sequentially regardless of which qubit the instruction uses. Therefore the :meth:`~dwave.gate.qcdl.QCDLModuleContainer.sync` instruction does not affect execution order of operations.
Consider carefully the positioning of any :meth:`~dwave.gate.qcdl.QCDLModuleContainer.sync` and cautiously validate when executing on a QPU (for example, by using a :meth:`~dwave.gate.qcdl.QCDLModuleContainer.append_table_row` instruction).
Programs may execute operations subject to a condition. You accomplish this in two discrete steps:
- Use classical logic to compute one bit of information and store it in a register. This is the branch condition.
- Create a true branch statement, and optionally a false branch statement. If the branch condition evaluates to 1, your true branch is executed; otherwise, the false branch is executed.
QCDL supports several ways of setting a branch condition and branching from an instruction.
.. testcode::
from dwave.gate.qcdl import qcdl
from dwave.gate.qcdl.operations import measure, x
@qcdl(2)
def branch_example1(q0, q1):
measure(q1)
with q0.If(condition=q1):
x(q0)
In the preceding example, the :func:`~dwave.gate.qcdl.operations.x` gate is executed
if the most recent measurement of q1 was a 1. Here, the unspecified
false branch---executed if the most recent measurement of q1 was a 0
or a *---is an idle of equal duration to the true branch.
The next example specifies a false branch. A :func:`~dwave.gate.qcdl.operations.y`
gate is executed in the case of a 0 or a * instead of the default
idle.
.. testcode::
from dwave.gate.qcdl import qcdl
from dwave.gate.qcdl.operations import measure, x, y
@qcdl(2)
def branch_example2(q0, q1):
measure(q1)
with q0.If(condition=q1) as Else:
x(q0)
with Else():
y(q0)
| Condition | Description | Notes |
|---|---|---|
q{N} |
The Nth qubit's most recent measurement. Branches if the last measurement for the qubit is 1. | Volatile and may change at the next measurement. |
| Expressions | A classical expression evaluated at runtime (e.g. reg2 < 5). |
See the :ref:`qcdl_basic_registers_arithmetic` section for details. |
| Signal | Specify q{N}.signal to branch off of the bit that q{N} is
currently signaling. |
See the :ref:`qcdl_advanced_signals` section for details. |
None |
You can separate the steps of evaluating the branch condition and
branching by assigning the branch condition before the (e.g.,
:meth:`~dwave.gate.qcdl.QCDLModuleContainer.If`) statement, with
that branch condition persisting if None is specified. The
branch condition might be set, for example, by a preceding
:meth:`~dwave.gate.qcdl.QCDLModuleContainer.all_to_all` or
:meth:`~dwave.gate.qcdl.QCDLModule.one_to_all` call, which
places a signal from one qubit onto the branch condition of each
recipient qubit. |
See the :ref:`qcdl_advanced_signals` section. |
True or False |
Python Boolean that deterministically selects a branch taken by all qubits. | For troubleshooting. |
- You can nest conditional statements arbitrarily deep.
- Place the :meth:`~dwave.gate.qcdl.QCDLModuleContainer.If` statement and its true and false branches in the same procedure.
- Use the :class:`~dwave.gate.qcdl.Scope` class to include an arbitrary number of qubits in a condition. If your condition value is an expression, take care that it evaluates to the same outcome for all qubits.
- Compilation does not guarantee that a qubit has been measured before it is used in a conditional. Use with caution.
- Since a condition can be a Boolean, if you do not intend that, be careful that your Python code does not inadvertently cast the condition to a Boolean. (You may find the output of :func:`~dwave.gate.qcdl.print_qcdl` helpful for this.)
- Your true and false branches must not contain operations on qubits that are not a part of the conditional branch.
This example detects and resets a qubit if it has been erased.
.. testcode::
from dwave.gate.qcdl import qcdl
from dwave.gate.qcdl.operations import mced
@qcdl(1)
def detect_erasure_example(q):
erased = q.Register(name="erased")
erased <<= 0
mced(q, register=erased)
with q.If(erased == 1):
q.reset()
This example conditions on a classical register.
.. testcode::
from dwave.gate.qcdl import qcdl, Scope
from dwave.gate.qcdl.operations import mced, x
@qcdl(2)
def classical_condition_example(q0, q1):
sc = Scope(q0, q1)
c0 = sc.Register(2, name="c0")
c1 = sc.Register()
# operations that update these registers
with q1.If( c0 | c1 == 1 ):
x(q1)
This example updates all registers with the outcome of a particular measurement.
.. testcode::
from dwave.gate.qcdl import qcdl, Scope
from dwave.gate.qcdl.operations import mced, x
@qcdl(2)
def classical_condition_example(q0, q1):
sc = Scope(q0, q1)
register = sc.Register(name="register")
measure(q0)
with sc.If(q0):
register += 1
A :ref:`register <qcdl_basic_registers_arithmetic>` is associated with a qubit. Your QCDL must ensure the information in any qubit's register is visible to all qubits (:ref:`mirror <qcdl_advanced_registers_mirroring>` the information) in order, for example, to select the same branch to execute for a :ref:`conditional statement <qcdl_advanced_conditionals>`.
This requires that you pass some information from the memory associated with one qubit to that of another, in particular results of a measurement on one qubit that condition operations on other qubits.
For any qubit, you can set the :attr:`~dwave.gate.qcdl.QCDLModule.signal` property (the signal) to a Boolean value for use, in realtime, as a :ref:`branch condition <qcdl_advanced_conditionals>` by other qubits.
The following example results in the bitstring being either 11 or
00 (assuming no noise). The
:meth:`~dwave.gate.qcdl.QCDLModuleContainer.sync` operation prevents the
receiver qubit conditioning off of the signal value before the sender
qubit sets it after its measurement. The :ref:`qcdl_advanced_conditionals`
section describes the condition value used in the If statement.
.. testcode::
from dwave.gate.qcdl import qcdl
from dwave.gate.qcdl.operations import h, measure, x
@qcdl(2)
def signal_example(q0, q1):
receiver = q1
sender = q0
h(q0)
send_register = q0.Register()
measure(q0, register=send_register)
sender.master(extf=send_register == 1)
sender.sync(receiver)
with receiver.If(sender.extf):
x(receiver)
measure(receiver)
Note
If more than one qubit is branching off a signal, it is likely more efficient to use the :meth:`~dwave.gate.qcdl.QCDLModule.one_to_all` method.
The :meth:`~dwave.gate.qcdl.QCDLModule.one_to_all` method signals a Boolean value from one qubit to a set of other qubits that can use it as a :ref:`branch condition <qcdl_advanced_conditionals>` to conditionally execute a branch of operations.
You accomplish this by specifying the following: (1) The origin qubit, (2) an expression for setting the Boolean value, and (3) the set of qubits that use the signal for a branch condition
This example results in the bitstring being either 000 or 111 (absent
noise). The :ref:`qcdl_advanced_conditionals` section describes the condition
value used in the If statement.
.. testcode::
from dwave.gate.qcdl import qcdl
from dwave.gate.qcdl.operations import h, measure, x
@qcdl(3)
def one_to_all_example(q0, q1, q2):
h(q0)
send_register = q0.Register()
measure(q0, register=send_register)
sc = Scope(q1, q2)
q0.one_to_all(sc.qcdl_modules, send_register == 1)
with sc.If(None):
x(q1)
x(q2)
measure(q1)
measure(q2)
The more-general :meth:`~dwave.gate.qcdl.QCDLModuleContainer.all_to_all` method signals a Boolean value from all qubits to a set of participating qubits to use as a :ref:`branch condition <qcdl_advanced_conditionals>`.
You accomplish this by specifying the following: (1) A set of qubits that all contribute one bit to the signal, (2) a reduction operator used to compute a Boolean value from those bits. The resulting Boolean value conditions all participating qubits.
This example results in the bitstring being either 000 or 111
(absent noise). The :ref:`qcdl_advanced_conditionals` section describes the
condition value used in the If statement here and in subsequent examples.
.. testcode::
from dwave.gate.qcdl import qcdl
from dwave.gate.qcdl.operations import h, measure, x
@qcdl(3)
def all_to_all_example(q0, q1, q2):
sc = Scope(q0, q1, q2)
h(q0)
name = "bit"
# all qubits have a copy of the same register:
send_register = sc.Register(name=name)
# set the register on q0 to 0 or 1; alias=True reuses the memory
# send_register already allocated instead of redeclaring it
measure(q0, register=q0.Register(name=name, alias=True))
# if any of the copies of the register are equal to 1, then all
# will receive a condition of True.
sc.all_to_all(send_register == 1, reduce_op="|")
with sc.If(None):
x(q1)
x(q2)
measure(q1)
measure(q2)
In the next example, if any register has value 1, update all the registers to be 1, otherwise, set them to 0.
.. testcode::
from dwave.gate.qcdl import qcdl, Scope
@qcdl(3)
def all_to_all_example2(q0, q1, q2):
sc = Scope(q0, q1, q2)
register = sc.Register(name="register")
# operations
sc.all_to_all(register == 1, reduce_op="|")
with sc.If(None) as Else:
register <<= 1
with Else():
register <<= 0
The next example loops until a qubit has been erased. The :ref:`qcdl_advanced_control_flow` section describes QCDL control-flow methods.
.. testcode::
from dwave.gate.qcdl import qcdl, Scope
from dwave.gate.qcdl.operations import mced
@qcdl(2)
def all_to_all_example3(q0, q1):
sc = Scope(q0, q1)
erased = sc.Register(name="erased")
with sc.DoWhile(None):
for q in [q0, q1]:
mced(q, register=erased)
sc.all_to_all(erased == 0, reduce_op="&")
The next example demonstrates an active reset, looping until all qubits are in a \ket 0 state.
.. todo:: The next example needs to be fixed
.. testcode::
:skipif: True
from dwave.gate.qcdl import qcdl, Register, Scope
from dwave.gate.qcdl.operations import mced, measure, x
@qcdl(2)
def all_to_all_example4(q0, q1):
name = "in_0"
sc = Scope(q0, q1)
in_0 = sc.Register(name=name)
with sc.DoWhile(None):
in_0 <<= 1
for q in sc.qubits:
measure(q)
with q.If(q):
x(q)
Register(q, name=name) <<= 0
# if any were not in 0, iterate again
sc.all_to_all(send=in_0==0, reduce_op="|")
QCDL programs support several control-flow mechanisms.[4]
| Method | Purpose |
|---|---|
| :meth:`~dwave.gate.qcdl.QCDLModuleContainer.Repeat` | Repeats the body of the context manager for a specified number of iterations. |
| :meth:`~dwave.gate.qcdl.QCDLModuleContainer.While` | Repeats the body of the context manager as long as the condition is true. |
| :meth:`~dwave.gate.qcdl.QCDLModuleContainer.DoWhile` | Unconditionally executes a first iteration of the context manager and then, similar to the :meth:`~dwave.gate.qcdl.QCDLModuleContainer.While` method, repeats the body of the context manager as long as the condition is true. Useful if the condition is evaluated only within the loop. |
| :meth:`~dwave.gate.qcdl.QCDLModuleContainer.For` | Repeats the body of the context manager as long as the condition is true, similar to the :meth:`~dwave.gate.qcdl.QCDLModuleContainer.While` method, but, similarly to a C-style loop, also provides some convenience mechanisms for initializing and updating a register. |
| :meth:`~dwave.gate.qcdl.QCDLModuleContainer.Break` | Breaks out of a loop structure. Useful for preventing infinite loops. |
| :meth:`~dwave.gate.qcdl.QCDLModuleContainer.Continue` | Skips the remainder of the body of the context manager and jumps to the conditional. |
| :meth:`~dwave.gate.qcdl.QCDLModuleContainer.Label`/:meth:`~dwave.gate.qcdl.QCDLModuleContainer.Goto` | A :meth:`~dwave.gate.qcdl.QCDLModuleContainer.Goto` instruction unconditionally jumps to the location marked by the corresponding :meth:`~dwave.gate.qcdl.QCDLModuleContainer.Label` instruction. |
| :meth:`~dwave.gate.qcdl.QCDLModuleContainer.Return` | Exits a procedure early. |
| [4] | These are all abstractions built on top of an underlying goto and label mechanism. |
Attention!
If your QCDL lets only a subset of qubit branches execute a jump, these powerful control-flow expressions risk desychronizing operations on qubits, as noted in the :ref:`qcdl_advanced_synchronization` section. These control-flow operations are recommended only in a :meth:`~dwave.gate.qcdl.Scope` that includes all of the qubits.
.. testcode::
from dwave.gate.qcdl import procedure, qcdl, Scope
from dwave.gate.qcdl.operations import rx, ry
@procedure
def rotate(q0, q1, increment):
rx(q0, increment)
ry(q1, increment)
@qcdl(2)
def repeat_example(q0, q1):
sc = Scope(q0, q1)
num_iterations = 5
with sc.Repeat(num_iterations):
rotate(q0, q0, 0.1)
The following is also an example for how a :meth:`~dwave.gate.qcdl.QCDLModuleContainer.Repeat` instruction could be implemented.
.. testcode::
from dwave.gate.qcdl import procedure, qcdl, Scope
@qcdl(2)
def dowhile_example(q0, q1):
sc = Scope(q0, q1)
counter = sc.Register(name="counter")
num_iterations = 5
counter <<= num_iterations
with sc.DoWhile(counter > 0):
counter -= 1
A register is associated with a qubit. When you create a :class:`~dwave.gate.qcdl.registers.Register` object for the qubits of a :class:`~dwave.gate.qcdl.Scope` class, it is implemented as a collection of registers for the qubits in the scope. And when you assign that :class:`~dwave.gate.qcdl.registers.Register` to a :func:`dwave.gate.qcdl.operations.measure` or :func:`dwave.gate.qcdl.operations.mced` operation, only the register associated with the measured qubit is updated (the measurement outcome is immediately written to that register).
Your QCDL must ensure the information in any qubit's register is visible to all qubits in the :class:`~dwave.gate.qcdl.Scope` or :class:`~dwave.gate.qcdl.registers.Register` object (mirror the information). This is needed, for example, to select the same branch to execute for a :ref:`conditional statement <qcdl_advanced_conditionals>`.
Mirroring requires an extra communication step to ensure that registers associated with all other qubits of the :class:`~dwave.gate.qcdl.registers.Register` object are also updated.
For simplicity, you can use the following two cooperative techniques to implement mirroring.
- Execute classical calculations redundantly when possible; for example, use a :ref:`scope <qcdl_advanced_scope>` to instantiate registers for all qubits or, for each qubit, give its register the same name and execute the same operations.
- Communicate non indentical information (see the :ref:`qcdl_advanced_signals` section). With the :class:`~dwave.gate.qcdl.Scope` class, the only information you must update across registers at runtime are (non-deterministic) measurement/MCED results.
- Expressions between registers in different scopes are not supported.
- Use the
mirrorparameter in the :func:`~dwave.gate.qcdl.operations.mced` or :func:`~dwave.gate.qcdl.operations.measure` operations to propagate measurements among registers. - Always use the same scope for conditional statements and register instantiation. A good practice is to instantiate one :class:`~dwave.gate.qcdl.Scope` object at the start of your QCDL that contains all qubits and use that scope for registers and loops, making other scopes only for small tasks.
- It is technically possible to compose a conditional expression using registers such that some qubits go to a true branch and others to a false branch. This is not recommended and the simulator raises an exception.
.. seealso::
:func:`~dwave.gate.qcdl.implementations.mirror_bool_register` and
:func:`~dwave.gate.qcdl.implementations.mirror_measurement_register`
functions
.. todo:: add references to places such as
https://docs.dwavequantum.com/en/latest/quantum_research/index_get_started.html
.. todo:: update for Ocean
Ocean software provides a Monte Carlo simulator of QCDL programs. This is an ideal simulator built on top of Qiskit's AerStatevector.
This simulator closely models the classical and quantum operation of the QPU with varying approximations. It instantiates a "state" representing both classical and quantum components of the hardware and then executes your QCDL instructions one at a time to update that state. As a Monte Carlo simulator, it is significantly slower than a "sampling" simulator and scales linearly with the number of shots; however, its operation is embarrassingly parallel.
Tip
Accuracy bears a simulation cost and error handling increases circuit complexity. It is advisable to start circuit development against the ideal simulator and then introduce error modeling in a controlled way. For example, start by simulating full-precision registers before simulating reduced-precision registers; start with ideal quantum operations before introducing erasures.
The simulator in the |cloud|_ service supports two modes of simulations:
Statevector Simulation
This simulation is useful during initial testing of QCDL programs before introducing noise.
Dual-Rail Erasure Simulation
This simulation is useful for exploring the impact of erasures on QCDL programs. It represents the quantum state as a Statevector with an array of booleans (which mark whether a qubit has leaked or not). It operates by randomly applying Pauli errors, leakages, and seepages after quantum gates and idles.
The following table compares these two simulation modes.
| Characteristic | Statevector Simulation | Dual-Rail Erasure Simulation |
|---|---|---|
| Run time | Scales as O(2^n) where n is the number of qubits. | Slightly slower than statevector simulation but same scaling. |
| Topology | No restrictions. | Depends on the noise model and the coupling map, options which model the selected QPU by default. |
| Depends on the noise model but which will represent the selected QPU by default. | All basis gates available in Qiskit (no transpilation require). | Subset of basis gates (transpilation required). |
| Support for errors | No support. | Supports the mced instruction to detect if the qubit has been
erased, and the leak and seep instructions to simulate
leakage and seepage errors. |
.. todo:: Update the rest once I can test in Leap
| Option | Description | Type | Default |
|---|---|---|---|
transpile |
To run the circuit verbatim (error if incompatible), set
transpile=False. |
bool | True |
timeout |
For faster feedback, use a lower timeout (in seconds) when troubleshooting circuits with loops. | float | 45 minutes |
noise_model |
If noise_model=None, no noise model is used; otherwise, specify
the name of the noise model to use. |
None, "conservative_c8", "simple_default_model" |
"conservative_c8" |
use_registers |
If true, the bit-width restrictions are simulated; otherwise, the classical calculations are done in full precision. | bool | False |
For troubleshooting when jobs are starting and completing, configure logging, which Python does not turn on by default. There are many configuration options for logging with various tradeoffs, the main one being signal-to-noise.
D-Wave recommends a simple logging configuration that adds the following lines
of code once near the beginning of the highest level .py or .ipynb
(for Jupyter notebooks) files. This code prints job IDs next to reported errors.
.. testcode::
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
)
The codes sets typical
log attributes.
Typically, INFO level is sufficient but not too verbose for production use.
You may consider using WARNING in standard output (stdout) and
separately saving INFO level to a .log file.