Experiments and execution#

See Running experiments for how they fit together.

Building experiments#

QCut.get_experiment_circuits(cut_circuit: CutCircuit, observables: str | Pauli | SparsePauliOp | SparseObservable | Mapping[str | Pauli, float] | _Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | complex | bytes | _NestedSequence[complex | bytes | str] | None = None, qubits: list[int] | None = None) → CutExperiment[source]#

Generate experiment circuits by inserting QPD operations on measure/initialize/cutCZ nodes.

Parameters:
  • cut_circuit (CutCircuit) – The cut circuit to generate experiment circuits for.

  • observables – the observables to estimate, taken the way qiskit’s estimator takes them – a label, a Pauli, a SparsePauliOp, a SparseObservable, a {label: coefficient} mapping, or any nested sequence of those. The expectation values come back shaped like what is given here.

  • qubits (list[int]) – The qubits to measure.

  • provided. (One of observables or qubits must be)

Returns:

An object containing the generated experiment circuits and related information.

Return type:

CutExperiment

QCut.ObservablesLike#

What observables can be given as, the same as Qiskit’s estimator takes: a Pauli label, a Pauli, a SparsePauliOp, a SparseObservable, a {label: coefficient} mapping, or any nested sequence of those.

What a run costs#

QCut.estimate_run(cut_experiment: CutExperiment, shots: int = 4096, backend=None, max_batch_size: int = 100) → RunEstimate[source]#

Work out what submit_experiments() will cost, without submitting anything.

Exact for an experiment that runs in one go. One whose wire cuts communicate runs in waves, and every wave after the first spends its shots in proportion to what the one before it measured, so only that first wave is known in advance. The rest is worked out as if every group got an equal share. That gives the most circuits a wave can submit, since a group whose label never came up submits none, and the fewest jobs it can take, since an uneven share puts circuits wanting very different shot counts in jobs of their own.

Parameters:
  • cut_experiment (CutExperiment) – the experiment circuits that would be run.

  • shots (int) – shots per circuit, as passed to submit_experiments().

  • backend – the backend or sampler that would run it, read only for the most shots it takes in one job.

  • max_batch_size (int) – maximum number of circuits submitted per run call.

Returns:

the circuits, jobs and shots the run takes, and a breakdown by job.

Return type:

RunEstimate

class QCut.RunEstimate(breakdown: dict[str, ~QCut.execution.estimate.JobEstimate]=<factory>, exact: bool = True)[source]#

Bases: object

What running an experiment will cost, before anything is submitted.

breakdown: dict[str, JobEstimate]#
exact: bool = True#
property jobs: int#

How many run calls the experiment takes.

property circuits: int#

How many circuits are submitted, over every job.

property shots: int#

How many shots the experiment spends in total.

__init__(breakdown: dict[str, ~QCut.execution.estimate.JobEstimate]=<factory>, exact: bool = True) → None#
class QCut.JobEstimate(circuits: int, shots: int)[source]#

Bases: object

One run call: how many circuits it carries, at how many shots each.

circuits: int#
shots: int#
__init__(circuits: int, shots: int) → None#

Submitting#

QCut.submit_experiments(cut_experiment: CutExperiment, shots: int = 4096, backend=None, max_batch_size: int = 100, run_options: dict | None = None) → QCutJob[source]#

Submit experiment circuits and return without waiting for their results.

Every batch is submitted before this returns, and the returned job collects them when its QCutJob.result() is asked for. An experiment whose wire cuts communicate runs in waves: the first is submitted before this returns, and the job submits the rest itself as each wave before them comes back.

Parameters:
  • cut_experiment (CutExperiment) – the experiment circuits to run

  • shots (int) – number of shots per circuit run (optional). Communicating wire cuts spend it differently: the waves split a total of shots times the number of groups per subcircuit between them, in proportion to how often each label came up, so individual circuits run at very different counts and only that total is fixed. See _submit_communicating().

  • backend – backend or V2 sampler used for running the circuits. A sampler is run through its own interface and its results are read per register, so the circuits must already be in its target’s basis.

  • max_batch_size (int) – maximum number of circuits submitted per backend.run call. Larger batches reduce per-job overhead on real hardware. A target that batches on its own account, as e.g. fiqci-ems does, is given this size too, so it does not split a batch QCut has already sized.

  • run_options (dict) – passed on to every run call. Use it for whatever the target takes per run, since QCut sets only shots itself.

Returns:

a handle on the submitted experiment, whose QCutJob.result() returns what run_experiments() does.

Return type:

QCutJob

QCut.run_experiments(cut_experiment: CutExperiment, shots: int = 4096, backend=None, max_batch_size: int = 100, run_options: dict | None = None) → RawResult[source]#

Run experiment circuits and wait for their results.

The same as submit_experiments() followed by QCutJob.result(). The counts come back as [group][observable][subcircuit], each key "xxx yy" where xxx is the end-of-circuit measurements on the meas register and yy the QPD basis measurements on the qpd_meas register.

Parameters:
  • cut_experiment (CutExperiment) – the experiment circuits to run

  • shots (int) – number of shots per circuit run (optional), as in submit_experiments().

  • backend – backend or V2 sampler used for running the circuits.

  • max_batch_size (int) – maximum number of circuits submitted per backend.run call, as in submit_experiments().

  • run_options (dict) – passed on to every run call.

Returns:

the raw counts, carrying the experiment they came from so that QCut.estimate_expectation_values() can be called on them alone

Return type:

RawResult

class QCut.QCutJob(experiment, backend, shots: int, max_batch_size: int, run_options: dict | None, num_waves: int = 1)[source]#

Bases: object

A submitted experiment, handed back before any of its results are in.

Shaped like a qiskit job: job_ids() and status() can be read at once, and result() waits for everything and returns the same RawResult QCut.run_experiments() does. An experiment is usually several run calls, so the per-batch view is there too, in statuses() and partial_results().

An experiment whose wire cuts communicate runs in waves, and every wave after the first spends its shots on what the one before it measured. A thread of the job’s own collects each wave and submits the next as soon as it can, so such a job comes back as quickly as any other and does not sit idle waiting to be polled. Its job ids and batches grow as the waves go out, and it reports itself running until the last one is in.

Submission is not atomic: a target can refuse a batch after accepting the ones before it. Raising there would lose the jobs already queued, so the job is handed back regardless, reports ERROR, and raises the refusal from result().

__init__(experiment, backend, shots: int, max_batch_size: int, run_options: dict | None, num_waves: int = 1) → None[source]#
property experiment#

The experiment that was submitted.

property backend#

The backend or sampler it was submitted to.

property shots: int#

Shots per circuit, as given. What the estimator divides by.

property max_batch_size: int#

The most circuits submitted in one run call.

property run_options: dict#

What was passed on to every run call, as it was at submission.

property num_waves: int#

How many waves it runs in. One unless its wire cuts communicate.

property wave: int#

The wave most recently submitted, counting from zero.

property num_circuits: int#

How many circuits have been submitted so far.

property total_shots: int#

How many shots have been submitted so far, over every circuit.

Matches QCut.estimate_run() exactly for an experiment that runs in one go. One whose wire cuts communicate decides its later waves’ shots from what the earlier ones measured, so this is what it actually spent.

job_id() → str | None[source]#

The first backend job’s id, None if nothing was submitted.

job_ids() → list[str | None][source]#

Every backend job’s id, in submission order.

A batch that was never submitted has None. A target that splits a batch into several jobs of its own, as fiqci-ems does, contributes all of them.

statuses() → list[JobStatus][source]#

Each batch’s current status, in submission order.

status() → JobStatus[source]#

One status for the whole experiment.

DONE only once every wave has been submitted and collected and every batch is done. A batch failing, or the waves stopping on an error, makes it ERROR.

done() → bool[source]#

Whether the experiment finished successfully.

running() → bool[source]#

Whether any of it is running, or waves remain to submit.

cancelled() → bool[source]#

Whether the experiment was cancelled.

in_final_state() → bool[source]#

Whether the experiment is done, failed or cancelled.

wait_for_final_state(timeout: float | None = None, wait: float = 5.0) → None[source]#

Poll until the experiment reaches a final state.

Parameters:
  • timeout (float) – seconds to wait before giving up, or None to wait as long as it takes.

  • wait (float) – seconds between polls.

Raises:

JobTimeoutError – the experiment was not finished within timeout.

cancel() → None[source]#

Cancel every backend job not yet finished, and submit no further waves.

partial_results() → list[PartialBatch][source]#

Each batch as it stands, with its result once it is done.

The slots of the experiment only fill in once every batch is in, so this is the place to look at what came back while the rest is still running.

result(timeout: float | None = None) → RawResult[source]#

Wait for every batch and return the raw results, computed once and kept.

Parameters:

timeout (float) – seconds to wait before giving up, or None to wait as long as it takes.

Returns:

the raw counts, carrying the experiment they came from, exactly as QCut.run_experiments() returns them.

Return type:

RawResult

Raises:
  • JobTimeoutError – the experiment was not finished within timeout.

  • QCutError – the experiment was cancelled.

  • Exception – whatever a refused batch, a failed job or a failed wave raised.

class QCut.PartialBatch(index: int, wave: int, job_id: str | None, status: JobStatus, circuits: int, shots: int, result: object | None)[source]#

Bases: NamedTuple

One run call of a submitted experiment, as it stands.

index: int#

Position of the batch in submission order.

wave: int#

The wave it belongs to. Zero unless the experiment’s wire cuts communicate.

job_id: str | None#

The backend’s id for it, None if it was never submitted.

status: JobStatus#

Its current JobStatus.

circuits: int#

How many circuits it carries.

shots: int#

The shots each of them runs at.

result: object | None#

What the backend returned, once the batch is done, else None.

class QCut.ParallelBackend(backends, optimization_level: int = 3, transpile_options: dict | None = None, use_iqm_transpiler: bool = True)[source]#

Bases: object

Spread an experiment’s jobs over several backends, giving each batch to the least loaded.

QCut submits every batch of a wave before collecting any of it, so handing successive batches to different backends leaves them all queued at once. Each batch is transpiled for the backend about to run it, so the backends do not have to be alike, and only one batch is ever held in its transpiled form. The experiment itself stays as it was built.

Pass it where a backend goes:

backend = ParallelBackend([first, second])
job = ck.submit_experiments(experiment, shots=4096, backend=backend)

A batch goes only to a backend wide enough for it, so a fleet of different sizes works. The pieces that fit anywhere are shared, and the widest go to the backends that can hold them. Among those that fit, a batch goes to the one given the fewest shots so far, the first listed on a tie.

Pass untranspiled cut circuits and experiments: each batch is transpiled for the device it lands on.

backends#

the backends, in the order given.

Type:

list

submitted#

how many circuits each has been given, which is what to read to see how the work was shared.

Type:

list[int]

submitted_shots#

how many shots each has been given, which is what the work is shared by.

Type:

list[int]

__init__(backends, optimization_level: int = 3, transpile_options: dict | None = None, use_iqm_transpiler: bool = True)[source]#

Init.

Parameters:
  • backends – the backends to run on, two or more.

  • optimization_level (int) – optimization level for transpilation (0-3).

  • transpile_options (dict) – arguments passed to the transpiler.

  • use_iqm_transpiler (bool) – whether an IQM backend may use IQM’s transpiler.

Raises:

QCutError – fewer than two backends were given.

property max_shots: int | None#

The most shots every one of them takes, so a batch fits wherever it lands.

run(circuits, shots: int = 1024, **options)[source]#

Transpile this batch for the least loaded backend that fits, and submit it.

Parameters:
  • circuits – the batch, as QCut sized it.

  • shots (int) – what to run them at. Passed on unchanged: it is what the estimator divides by, and the waves allocate it themselves.

  • **options – passed on to the backend.

Returns:

The backend’s own job, so the results are read exactly as they would be had the backend been given the batch directly.

Raises:

QCutError – no backend is wide enough for the batch.

The whole sequence in one call#

QCut.run(circuit: QuantumCircuit, observables: str | Pauli | SparsePauliOp | SparseObservable | Mapping[str | Pauli, float] | _Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | complex | bytes | _NestedSequence[complex | bytes | str] | None = None, backend=None, max_batch_size: int = 100, options: CutOptions | None = None, shots: int = 4096, qubits: list[int] | None = None, run_options: dict | None = None) → ndarray | QuasiProbabilities[source]#

Run the whole circuit knitting sequence with one function call.

Parameters:
  • circuit (QuantumCircuit) – circuit with cut experiments

  • observables – the observables to estimate, taken as qiskit’s estimator takes them. See get_experiment_circuits().

  • backend – backend or V2 sampler to run the experiment circuits on. A device with a coupling map gets the subcircuits transpiled for it first. A sampler is not transpiled for, so it must take the circuits as built.

  • max_batch_size (int) – maximum number of circuits submitted per backend.run call (optional)

  • options (CutOptions) – configuration for the run (optional)

  • shots (int) – number of shots per circuit run, as in submit_experiments() (optional)

  • qubits (list[int]) – the qubits to reconstruct a distribution over, instead of estimating observables (optional)

  • run_options (dict) – passed on to the backend or sampler, as in submit_experiments() (optional)

  • provided. (One of observables or qubits must be)

Returns:

one expectation value per observable, shaped like the observables given.A single observable comes back as a zero-dimensional array or, when given qubits, the reconstructed distribution over them.

Return type:

np.ndarray | QuasiProbabilities

QCut.run_cut_circuit(cut_circuit: CutCircuit, observables: str | Pauli | SparsePauliOp | SparseObservable | Mapping[str | Pauli, float] | _Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | complex | bytes | _NestedSequence[complex | bytes | str] | None = None, backend=None, max_batch_size: int = 100, options: CutOptions | None = None, shots: int = 4096, qubits: list[int] | None = None, run_options: dict | None = None) → ndarray | QuasiProbabilities[source]#

After splitting the circuit run the rest of the circuit knitting sequence.

Parameters:
  • cut_circuit (CutCircuit) – the split circuit, carrying its placeholder operations and cut locations

  • observables – the observables to estimate, taken as qiskit’s estimator takes them. See get_experiment_circuits().

  • backend – backend or V2 sampler to run the experiment circuits on. A device with a coupling map gets the subcircuits transpiled for it first. A sampler is not transpiled for, so it must take the circuits as built.

  • max_batch_size (int) – maximum number of circuits submitted per backend.run call (optional)

  • options (CutOptions) – configuration, overriding what the CutCircuit carries (optional)

  • shots (int) – number of shots per circuit run, as in submit_experiments() (optional)

  • qubits (list[int]) – the qubits to reconstruct a distribution over, instead of estimating observables (optional)

  • run_options (dict) – passed on to the backend or sampler, as in submit_experiments() (optional)

  • provided. (One of observables or qubits must be)

Returns:

one expectation value per observable, shaped like the observables given. A single observable comes back as a zero-dimensional array or, when given qubits, the reconstructed distribution over them.

Return type:

np.ndarray | QuasiProbabilities