Using dwave-gate#

The Leap quantum cloud service provides access to a simulator that enables you to test gate-model circuits intended to be executed on a dual-rail quantum processing unit (QPU). You describe your circuits using the dwave-gate package’s quantum circuit description language (QCDL), described here.

Onboarding for Beta Testers#

Important

Features for real-time control, which are being phased into dual-rail quantum computing systems, are already available on the simulator in the Leap service for prototyping and learning.

To construct QCDL programs and submit to the dual-rail simulator in the Leap service you need the following:

  1. A Leap account that has been invited to beta test the dual-rail simulator.

  2. A development environment with the Ocean SDK.

New Users#

If you are already using Ocean software for an existing Leap service account, see the Previous Users section for working with another project.

If you have accepted an invitation to the Leap service for the first time to use the dual-rail simulator, the following documentation gets you started with submitting your programs:

  • The Leap Service section.

    This section describes the Leap service: the dashboard where you can see your access to solvers such as the dual-rail simulator, the API token you need to submit programs to the simulator, whitelisting information if required by your organization, and more.

  • The Get Started with Ocean Software section.

    This section explains how to start using the Ocean SDK, which lets you write QCDL programs and submit them to the simulator.

Note

Installing the SDK is recommended. If you chose to install only the dwave-gate package, see the installation instructions here.

Previous Users#

To submit programs to the dual-rail simulator in the Leap service, you must accept the emailed invitation to a new project. You use the API token from this project to access and send jobs to the simulator.

The Authorizing Access to the Leap Service section describes how to work with multiple projects (see the “Multiple Leap Projects” tab).

The following are two simple ways to use the beta-tester project’s API token from your existing development environment.

  • Add a section to your dwave.conf file.

    You can see your dwave.conf file using the methods described in the Configuration or using the D-Wave CLI section.

    For example, add a beta section, which is used for your beta testing:

    [defaults]
    token = ABC-123456789123456789123456789
    
    [beta]
    token = BETA-123456789123456789123456789
    

    You can then set profile="beta" to use the beta-tester project’s API token when accessing the simulator.

    >>> from dwave.gate.leap import LeapQCDLSimulator
    ...
    >>> simulator = LeapQCDLSimulator(profile="beta")
    

    You can use the following D-Wave CLI commands to authorize Ocean software to access the Leap service and Configure a beta profile,:

    $ dwave auth login
    $ dwave config create --profile beta --auto-token --project "Beta Testing"
    

    where Beta Testing should be replaced with the project name as displayed in the Leap service. The commands locate your existing configuration file, or create one if needed, then create a new profile called beta, and ask you for your SAPI token to create the new profile.

  • Set the DWAVE_API_TOKEN environment variable.

    You can set this environment variable for a Unix operating system with a Bash command such as, export DWAVE_API_TOKEN="BETA-123456789123456789123456789", for example, or for a Windows system with a command such as set DWAVE_API_TOKEN=BETA-123456789123456789123456789.

    Remember to delete that environment variable when you return to your work on your previous project.

QCDL: Basic#

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.

Program Entry Point#

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.

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 Leap service, as described in the Submitting Programs section.

The print_qcdl() function can visualize this structure as readable text, and if run in a Jupyter notebook, as a display object.

from dwave.gate.utils.display import print_qcdl

print_qcdl(qcdl_program)

The code above displays the following QCDL program.

begin quantum
    h([q0], q0)
    cx([q0, q1], q0, q1)
    measure([q0], q0, log=True)
    measure([q1], q1, log=True)
end quantum

If print_qcdl() displays poorly, you can output a string by setting the function’s to_Display=False parameter.

See also

qcdl() decorator

Gates#

The gates dwave-gate supports match the method names in a Qiskit QuantumCircuit (e.g., h(), sx(), rz(), cz(), etc).

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 the Qiskit 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 FixedPointRegister is passed as the angle for a gate, its value must be in units of π (see the Classical Registers & Arithmetic section for more information).

Barrier#

When you submit your QCDL to a QPU in the Leap service, a transpiler rewrites the circuit to use the QPU’s supported basis gates and topology, as described in the 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 barrier() instruction signals to the transpiler to not combine gates across your barrier.[2]

For example, if you do not set barrier() instructions on a randomized benchmarking circuit, where the net mathematical effect is an identity operation, transpilation collapses your QCDL.

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)

Classical Registers & Arithmetic#

QCDL supports integer and fixed-point registers with the Register and 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 conditional statements within complex real-time classical-quantum logic.

When you store a 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 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.

Supported Operations#

Operation

Operators

Operands

Notes

Register assignment

\(<<=\)

Register, FixedPointRegister

QCDL uses \(<<=\) for register assignment, a syntax similar to the R language, because Python does not allow the assignment operator \(=\) to be overridden.

Standard arithmetic

\(+, -, *\)

Register, FixedPointRegister, literal

Operations between Register and FixedPointRegister classes are not supported

Bitwise operations

\(\&, |\), ^

Register, literal

Right-shift by \(1\)

Register

Comparison

\(==, !=, <, >, <=, >=\)

Register, FixedPointRegister, literal

Operations between Register and FixedPointRegister classes are not supported

Arbitrary functions

Interpolation table

Register, FixedPointRegister

Generated by the arbitrary_function() function

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 carry the value they ended with from one shot to the next. Typically, for most registers, you prefer each shot to be independent, and so should re-assign your 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 Mirroring section provides more information.

  • If you pass a FixedPointRegister object to a gate as an angle, use units of π instead of radians. For example, a value of \(1\) is equivalent to π.

Measurements#

A logical measure() operation on a dual-rail gate-model quantum computer 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 tags property is the recommended way to organize measurement data.

Measurement outcomes are handled in three different ways:

  1. If the measure() function has its log parameter set to true (log=True, the default) the outcome is appended to the array associated with the qubit on which it was measured. The Result class returns this data to you in a 3D array (per tag argument of the measure() operation) with shape (number of measurements per shot, number of shots, number of qubits), along with the arrays from the other qubits. You can retrieve this data structure using the get_memory() method. For circuits with a deterministic number of measurements per shot consistent for all qubits, you can convert this data structure a counts dictionary with the get_counts() method.

  2. The outcome may be saved to a register. Even if the register is defined on multiple qubits, only the register copy on the measured qubit is assigned. You can return this data with append_table_row() method (see the Result Records section).

  3. Each qubit implicitly stores its most recent measurement outcome and this value may be used in conditional statements.

from dwave.gate.qcdl.operations import measure

@qcdl(1)
def measurement_example1(q0):
    measure(q0)
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 get_counts() method returns all data, including erasures. To return only results without the *, thereby post-selecting on the detected errors, set the method’s post_select argument to true.

Mid-Circuit Erasure Detection#

You can non-destructively inspect a dual-rail qubit to detect if it is out of the code space (“leaked”) with the mced() operation. If the test is positive, the qubit is declared erased.

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)

Result Records#

Results are a Python dictionary where keys are set by the append_table_row() method and values are tables formatted as a Polars DataFrame and returned from the Leap service in a Result class.

The 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.

If your QCDL uses the append_table_row() method, the Result output contains records that you may retrieve with the records property.

from dwave.gate.qcdl import qcdl
from dwave.gate.leap import LeapQCDLSimulator

# Create a QCDL program:
@qcdl(1)
def results_record_example(q0):
    r = q0.Register(name="some_classical_data")
    r <<= 13
    q0.append_table_row(r, table_name="my_table")

simulated_example = results_record_example()

# Submit the QCDL to a simulator:
simulator = LeapQCDLSimulator()

future = simulator.run(
    simulated_example,
    qpu='DRsim_21qubits',
    noise_model=True,
    shots=500,
    label="SDK Examples - Results Record Job Submission")

# View the returned records:
result = future.result().result

record_as_a_dataframe = result.records["q0"]["my_table"]

The result is a Polars DataFrame containing 1 column named some_classical_data with 500 rows, each of which have a value of \(13\).

Yield Handling#

A significant feature of the simulator in the Leap service is that it flags detected errors by returning * as a third measurement outcome in addition to \(0\) and \(1\), as described in the Measurements section. Tools such as Qiskit do not handle these values so tools such as the dwave-qiskit-plugin remove individual shots containing a * when passing information. Consequently, fewer shots are likely to be returned than the number of shots you requested.[3]

The YieldHandling class provides a general way of handling result distributions. It supports options for renormalizing distributions, ignoring erasures, and others.

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)

Alternatively, the get_counts() method supports a post_select argument.

Initialize and Reset#

At the beginning of every shot, the QPU initializes all of the qubits used by your circuit. You can also explicitly use the initialize() operation in your QCDL.

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.

from dwave.gate.qcdl import qcdl
from dwave.gate.qcdl.operations import initialize

@qcdl(1)
def reset_example(q0):
    q0.reset()

Transpilation#

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 Submitting Programs section).

Basis Gates

Description

Availability

sx(), x()

Single qubit rotation around the X-axis by π/2 and π respectively.

All operational qubits.

rz()

Single qubit, parameterizable rotation around the Z-axis.

All operational qubits.

cz()

Two qubit rotation by π/2 around the ZZ-axis.

Connected, operational qubits.

Transpiler Constraints and Considerations#

  • 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 measure() instruction.

QCDL: Advanced#

Procedures#

QCDL supports procedures on qubits. A procedure is a subroutine that is called from another procedure (the 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.

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.

Scope#

The 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 mirror the same register across qubits.

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

Synchronization#

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 sync() instruction. You may apply this instruction to any number of qubits to indicate that all operations before the sync() instruction must be completed before any operations after the instruction are started.

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 x() on q1 is scheduled to start after the x() on q0 has completed.

Synchronization Considerations#

  • Compilation inserts implicit sync() instructions before and after all multi-qubit operations such as gates, procedures, and control-flow operations (including shots).

  • The sync() instruction is not sensitive to the ordering of the qubits.

  • An explicit sync() instruction in the program is treated as a 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 sync() instruction does not affect execution order of operations.

Consider carefully the positioning of any sync() and cautiously validate when executing on a QPU (for example, by using a append_table_row() instruction).

Conditionals#

Programs may execute operations subject to a condition. You accomplish this in two discrete steps:

  1. Use classical logic to compute one bit of information and store it in a register. This is the branch condition.

  2. 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 (or the default idle) is executed.

QCDL supports several ways of setting a branch condition and branching from an instruction.

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 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 y() gate is executed in the case of a \(0\) or a * instead of the default idle.

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)

Supported Condition Values#

Condition

Description

Notes

q{N}

The \(N\)th 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 Classical Registers & Arithmetic section for details.

Signal

Specify q{N}.signal to branch off of the bit that q{N} is currently signaling.

See the Signals for Branch Conditions section for details.

None

You can separate the steps of evaluating the branch condition and branching by assigning the branch condition before the statement (e.g., If()), with that branch condition persisting if None is specified. The branch condition might be set, for example, by a preceding all_to_all() or one_to_all() call, which places a signal from one qubit onto the branch condition of each recipient qubit.

See the Signals for Branch Conditions section.

True or False

Python Boolean that deterministically selects a branch taken by all qubits.

For troubleshooting.

Guidelines for Using Conditionals#

  • You can nest conditional statements arbitrarily deep.

  • Place the If() statement and its true and false branches in the same procedure.

  • Use the 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 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.

Advanced Examples#

This example detects and resets a qubit if it has been erased.

from dwave.gate.qcdl import qcdl
from dwave.gate.qcdl.operations import mced

@qcdl(1)
def detect_erasure_example(q0):
    erased = q0.Register(name="erased")
    erased <<= 0
    mced(q0, register=erased)
    with q0.If(erased == 1):
        q0.reset()

This example conditions on a classical register.

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.

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

Signals for Branch Conditions#

A register is associated with a qubit. Your QCDL must ensure the information in any qubit’s register is visible to all qubits (mirror the information) in order, for example, to select the same branch to execute for a conditional statement.

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.

Basic Signal#

For any qubit, you can set the signal property (the signal) to a Boolean value for use, in realtime, as a branch condition by other qubits.

The following example results in the bitstring being either \(11\) or \(00\) (assuming no noise). The sync() operation prevents the receiver qubit conditioning off of the signal value before the sender qubit sets it after its measurement. The Conditionals section describes the condition value used in the If statement.

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(signal=send_register == 1)
    sender.sync(receiver)
    with receiver.If(sender.signal):
        x(receiver)
    measure(receiver)

Note

If more than one qubit is branching off a signal, it is likely more efficient to use the one_to_all() method.

one_to_all Signal#

The one_to_all() method signals a Boolean value from one qubit to a set of other qubits that can use it as a branch condition 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 Conditionals section describes the condition value used in the If statement.

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)

all_to_all Signal#

The more-general all_to_all() method signals a Boolean value from all qubits to a set of participating qubits to use as a branch condition.

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 Conditionals section describes the condition value used in the If statement here and in subsequent examples.

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
    measure(q0, register=q0.Register(name=name))

    # 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\).

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 Control Flow section describes QCDL control-flow methods.

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.

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="|")

Control Flow#

QCDL programs support several control-flow mechanisms.[4]

Method

Purpose

Repeat()

Repeats the body of the context manager for a specified number of iterations.

While()

Repeats the body of the context manager as long as the condition is true.

DoWhile()

Unconditionally executes a first iteration of the context manager and then, similar to the 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.

For()

Repeats the body of the context manager as long as the condition is true, similar to the While() method, but, similarly to a C-style loop, also provides some convenience mechanisms for initializing and updating a register.

Break()

Breaks out of a loop structure. Useful for preventing infinite loops.

Continue()

Skips the remainder of the body of the context manager and jumps to the conditional.

Label()/Goto()

A Goto() instruction unconditionally jumps to the location marked by the corresponding Label() instruction.

Return()

Exits a procedure early.

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 Synchronization section. These control-flow operations are recommended only in a Scope() that includes all of the qubits.

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 Repeat() instruction could be implemented.

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

Mirroring#

A register is associated with a qubit. When you create a Register object for the qubits of a Scope class, it is implemented as a collection of registers for the qubits in the scope. And when you assign that Register to a measure() or 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 Scope or Register object (mirror the information). This is needed, for example, to select the same branch to execute for a conditional statement.

Mirroring requires an extra communication step to ensure that registers associated with all other qubits of the Register object are also updated.

For simplicity, you can use the following two cooperative techniques to implement mirroring.

  1. Execute classical calculations redundantly when possible; for example, use a scope to instantiate registers for all qubits or, for each qubit, give its register the same name and execute the same operations.

  2. Communicate non indentical information (see the Signals for Branch Conditions section). With the Scope class, the only information you must update across registers at runtime are (non-deterministic) measurement/MCED results.

Guidance on Mirroring#

  • Expressions between registers in different scopes are not supported.

  • Use the mirror parameter in the mced() or 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 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.

QPU Simulator#

the Leap service provides a Monte Carlo simulator of QCDL programs. This is 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.

The simulator in the Leap service supports two modes of simulations. The following table compares these two simulation modes.

Characteristic

Statevector Simulation

Dual-Rail Erasure Simulation

Noise model.

Solver parameter noise_model set to False.

Useful during initial testing of QCDL programs before introducing noise.

Solver parameter noise_model set to True.

This simulation is useful for exploring the impact of erasures on QCDL programs. It operates by randomly applying Pauli errors, leakages, and seepages after quantum gates and idles.

Runtime.

Scales as \(O(s*g*2^n)\) where \(n\) is the number of qubits, \(s\) the number of shots, and \(g\) the number of gates.

Slower but same scaling.

Supported gates.

All gates available in Qiskit (no transpilation required).

Subset of gates (transpilation required).

Support for errors.

No support. (Returns \(0\) for mced, signifying no leak.)

Supports the mced instruction to detect if the qubit has been erased, and the leak and seep instructions to simulate leakage and seepage errors.

Submitting Programs#

The QPU simulator in the Leap service is intended to simulate gate-model quantum computers by executing programs formulated as QCDL.

The following documentation describes how to work with the Leap service:

Descriptions of the supported parameters and simulator properties are provided in the Simulator Parameters and Simulator Properties sections.

Example Submission#

The example below submits the following Bell state QCDL program.

from dwave.gate.qcdl import qcdl
from dwave.gate.qcdl.operations import cx, h, measure

@qcdl(2)
def bell_program(q0, q1):
    h(q0)
    cx(q0, q1)
    measure(q0)
    measure(q1)

simulator_job_submission = bell_program()

Submit the program above to a simulator for a dual-rail QPU with 21 qubits, DRsim_21qubits, in the Leap service.

>>> from dwave.gate.leap import LeapQCDLSimulator
...
>>> simulator = LeapQCDLSimulator()
>>> future = simulator.run(
...     simulator_job_submission,
...     qpu='DRsim_21qubits',
...     noise_model=True,
...     shots=500,
...     label="SDK Examples - Bell-Program Job Submission")
>>> result = future.result().result

Example Results#

Results are returned as a Result class, that also includes information such as execution time and provides methods for analyzing the measurements.

The returned result is a 3D array of (measurements per shot, shots, qubits). For the previous example, one measurement is taken per shot, for 500 shots, on two qubits.

>>> print(result.get_memory().shape)
(1, 500, 2)

For one particular execution of the program above, the following counts are returned.

>>> print(result.get_counts())
[{'11': 203, '*1': 14, '00': 230, '0*': 22, '*0': 19, '1*': 10, '**': 2}]

The execution time on the simulator (excluding any queuing time, for example) for that job submission is about a tenth of a second.

>>> print(result.run_time)
0.093493

See the Results section for information about the returned results and supported methods. The Result Records section describes how you store and retrieve records of measurements.

Simulator Parameters#

The examples in this section submit the QCDL program defined in the Example Submission section.

noise_model#

Boolean flag that applies a noise model.

  • noise_model=True: Apply a noise model.

  • noise_model=False: Do not apply a noise model (simulate an ideal QPU, as described in the QPU Simulator section).

The default value is specified by the default_noise_model property.

This example applies a noise model for the program submitted to the simulator.

>>> from dwave.gate.leap import LeapQCDLSimulator
...
>>> simulator = LeapQCDLSimulator()
>>> future = simulator.run(
...     simulator_job_submission,
...     noise_model=True)
>>> result = future.result().result

qpu#

The QPU to simulate, formatted as a string.

The supported_qpu_strings property lists the supported values. The default QPU to simulate is specified by the default_qpu property.

This example submits a QCDL program to a dual-rail QPU simulator with 21 qubits, DRsim_21qubits .

>>> from dwave.gate.leap import LeapQCDLSimulator
...
>>> simulator = LeapQCDLSimulator()
>>> future = simulator.run(
...     simulator_job_submission,
...     qpu='DRsim_21qubits')
>>> result = future.result().result

repeat_until_shots_requested#

Boolean flag to run the circuit repeatedly until a target number of non-erased measurements are accumulated.

Running a circuit might return, under noisy conditions, measurements that are declared to be “erased”, as described in the Measurements section. To try to achieve the required number of non-erasure measurements indicted by the shots parameter, as counted after post-selection to remove “splats” (see the Result Records section), you can select to repeatedly run the circuit. With yield defined as the percentage of shots without erasures, the required number of executions (and runtime) is proportional to the value of the shots parameter and the reciprocal of the yield, and grows exponentially with increased noise.

  • repeat_until_shots_requested=True: Repeatedly run the circuit until the requested number of non-erasure measurements, indicated by the shots parameter, is accumulated with post_select=True. Under noisy conditions the circuit might be executed a greater number of times than set by the shots parameter.

  • repeat_until_shots_requested=False: Run the circuit the number of times set by the shots parameter. Under noisy conditions, fewer non-erasure measurements than indicated by the shots parameter might be accumulated with post_select=True.

The default value is set by the default_repeat_until_shots_requested property.

If runtime exceeds the value you specified in the time_limit parameter (or the default value of the default_time_limit_s property), execution terminates.

This example repeatedly executes the circuit, under noisy conditions, to accumulate 10 non-erasure measurements.

>>> from dwave.gate.leap import LeapQCDLSimulator
...
>>> simulator = LeapQCDLSimulator()
>>> future = simulator.run(
...     simulator_job_submission,
...     shots=10,
...     noise_model=True,
...     repeat_until_shots_requested=True)
>>> result = future.result().result

The sum of non-splat states in the returned results is the requested number of shots:

>>> print(sum(result.get_counts(post_select=True)[0].values()))
10

shots#

The number of measurements to run, formatted as an integer.

Your QCDL program is executed once for each requested measurement.

The specified value must not exceed the value of the maximum_shots property. Execution time is limited by the value you specified in the time_limit parameter (or the default value of the default_time_limit_s property).

The default value is to measure the number of times specified by the default_shots property.

This example executes the circuit 1000 times.

>>> from dwave.gate.leap import LeapQCDLSimulator
...
>>> simulator = LeapQCDLSimulator()
>>> future = simulator.run(
...     simulator_job_submission,
...     shots=1000)
>>> result = future.result().result

time_limit#

Specifies the maximum runtime, in seconds, the solver is allowed to work on the given program. Can be a float or integer.

The specified time must be between the values of the maximum_time_limit_s and minimum_time_limit_s properties.

The default runtime limit is specified by the default_time_limit_s property.

This example sets a maximum runtime of 10 minutes.

>>> from dwave.gate.leap import LeapQCDLSimulator
...
>>> simulator = LeapQCDLSimulator()
>>> future = simulator.run(
...     simulator_job_submission,
...     time_limit=10*60)
>>> result = future.result().result

transpile#

Boolean flag to rewrite the submitted QCDL circuit to use the QPU’s supported basis gates and topology, as described in the Transpilation section.

  • transpile=True: Transpile the circuit.

  • transpile=False: Run the circuit exactly as specified in the submitted QCDL or return an error.

The default value is specified by the default_transpile property.

This example requires that the QCDL circuit be submitted as written to the simulator.

>>> from dwave.gate.leap import LeapQCDLSimulator
...
>>> simulator = LeapQCDLSimulator()
>>> future = simulator.run(
...     simulator_job_submission,
...     noise_model=True,
...     transpile=False)
>>> result = future.result().result

Simulator Properties#

category#

Type of solver, as a string.

  • software-gate: Gate-model simulator.

>>> from dwave.gate.leap import LeapQCDLSimulator
...
>>> simulator = LeapQCDLSimulator()
>>> simulator.properties["category"]
'software-gate'

default_noise_model#

Default setting for the application of a noise model, as a Boolean.

  • True: A noise model is applied.

  • False: Simulates an ideal QPU, as described in the QPU Simulator section.

>>> from dwave.gate.leap import LeapQCDLSimulator
...
>>> simulator = LeapQCDLSimulator()
>>> simulator.properties["default_noise_model"]
False

default_qpu#

Default selection of the QPU to simulate, as a string.

Supported QPUs are listed in the supported_qpu_strings property.

>>> from dwave.gate.leap import LeapQCDLSimulator
...
>>> simulator = LeapQCDLSimulator()
>>> simulator.properties["default_qpu"]
'DRsim_21qubits'

default_repeat_until_shots_requested#

Default setting, as a Boolean, for rerunning the circuit until the requested number of measurements is accumulated, where the accumulated measurements do not include erasures (see the Result Records section).

  • True: Repeatedly rerun the circuit.

  • False: Run the circuit the number of times set by the shots parameter.

>>> from dwave.gate.leap import LeapQCDLSimulator
...
>>> simulator = LeapQCDLSimulator()
>>> simulator.properties["default_repeat_until_shots_requested"]
False

default_shots#

Default setting for the number of measurements to run (times to execute your QCDL circuit), as an integer. With dual-rail QPUs, a measurement result can be a “splat” (see the Measurements section).

>>> from dwave.gate.leap import LeapQCDLSimulator
...
>>> simulator = LeapQCDLSimulator()
>>> simulator.properties["default_shots"]
1000

default_time_limit_s#

Default maximum runtime, in seconds, the solver is allowed to work on the given program, as a float.

>>> from dwave.gate.leap import LeapQCDLSimulator
...
>>> simulator = LeapQCDLSimulator()
>>> simulator.properties["default_time_limit_s"]
2700

default_transpile#

Default setting, as a Boolean, for transpiling the submitted QCDL program.

  • True: Transpile the program.

  • False: Run the circuit exactly as specified in the submitted QCDL or return an error.

>>> from dwave.gate.leap import LeapQCDLSimulator
...
>>> simulator = LeapQCDLSimulator()
>>> simulator.properties["default_transpile"]
True

maximum_num_qubits#

Maximum number of qubits for QCDL circuits, as an integer.

Note

Transpilation can add and remove qubits in your QCDL.

>>> from dwave.gate.leap import LeapQCDLSimulator
...
>>> simulator = LeapQCDLSimulator()
>>> simulator.properties["maximum_num_qubits"]
21

maximum_shots#

Maximum value of the shots you can specify, as an integer.

>>> from dwave.gate.leap import LeapQCDLSimulator
...
>>> simulator = LeapQCDLSimulator()
>>> simulator.properties["maximum_shots"]
1000000

maximum_time_limit_s#

Maximum time, in seconds as a float, that your submitted circuit can run.

This value limits the range of values you can set on the time_limit parameter.

>>> from dwave.gate.leap import LeapQCDLSimulator
...
>>> simulator = LeapQCDLSimulator()
>>> simulator.properties["maximum_time_limit_s"]
2700

minimum_shots#

Minimum number of times the circuit can be executed, as an integer.

>>> from dwave.gate.leap import LeapQCDLSimulator
...
>>> simulator = LeapQCDLSimulator()
>>> simulator.properties["minimum_shots"]
1

minimum_time_limit_s#

Minimum time, in seconds as a float, you can specify for the runtime limit (the time_limit parameter) on your submitted circuit.

>>> from dwave.gate.leap import LeapQCDLSimulator
...
>>> simulator = LeapQCDLSimulator()
>>> simulator.properties["minimum_time_limit_s"]
1

quota_conversion_rate#

Rate at which user or project quota is consumed for the solver as a ratio to QPU solver usage. Different solver types may consume quota at different rates.

Time is deducted from your quota according to:

\[\frac{num\_seconds}{quota\_conversion\_rate}\]

See the Solver Usage Charges section for more information.

>>> from dwave.gate.leap import LeapQCDLSimulator
...
>>> simulator = LeapQCDLSimulator()
>>> simulator.properties["quota_conversion_rate"]
1

supported_qpu_strings#

Names of supported simulators, as a list of strings.

Available QPUs are the following:

  • DRsim_17qubits: Dual-rail QPU with 17 qubits.

  • DRsim_21qubits: Dual-rail QPU with 21 qubits.

>>> from dwave.gate.leap import LeapQCDLSimulator
...
>>> simulator = LeapQCDLSimulator()
>>> simulator.properties["supported_qpu_strings"]
['DRsim_17qubits', 'DRsim_21qubits']