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, aSparsePauliOp, aSparseObservable, 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, aSparsePauliOp, aSparseObservable, 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
runcall.
- Returns:
the circuits, jobs and shots the run takes, and a breakdown by job.
- Return type:
- class QCut.RunEstimate(breakdown: dict[str, ~QCut.execution.estimate.JobEstimate]=<factory>, exact: bool = True)[source]#
Bases:
objectWhat running an experiment will cost, before anything is submitted.
- breakdown: dict[str, JobEstimate]#
- exact: bool = True#
- property jobs: int#
How many
runcalls 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#
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
shotstimes 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
runcall. Use it for whatever the target takes per run, since QCut sets onlyshotsitself.
- Returns:
a handle on the submitted experiment, whose
QCutJob.result()returns whatrun_experiments()does.- Return type:
- 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 byQCutJob.result(). The counts come back as[group][observable][subcircuit], each key"xxx yy"wherexxxis the end-of-circuit measurements on themeasregister andyythe QPD basis measurements on theqpd_measregister.- 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
runcall.
- Returns:
the raw counts, carrying the experiment they came from so that
QCut.estimate_expectation_values()can be called on them alone- Return type:
- class QCut.QCutJob(experiment, backend, shots: int, max_batch_size: int, run_options: dict | None, num_waves: int = 1)[source]#
Bases:
objectA submitted experiment, handed back before any of its results are in.
Shaped like a qiskit job:
job_ids()andstatus()can be read at once, andresult()waits for everything and returns the sameRawResultQCut.run_experiments()does. An experiment is usually severalruncalls, so the per-batch view is there too, instatuses()andpartial_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 fromresult().- __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
runcall.
- property run_options: dict#
What was passed on to every
runcall, 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_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.
- status() JobStatus[source]#
One status for the whole experiment.
DONEonly once every wave has been submitted and collected and every batch is done. A batch failing, or the waves stopping on an error, makes itERROR.
- 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
Noneto wait as long as it takes.wait (float) – seconds between polls.
- Raises:
JobTimeoutError – the experiment was not finished within
timeout.
- 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
Noneto 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:
- 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:
NamedTupleOne
runcall 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,
Noneif 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:
objectSpread 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