mimiqcircuits.backends.backend

Abstract base classes for simulator backends.

This module defines the contract every simulator must satisfy:

See Implementing a Custom Backend for a step-by-step guide.

Classes

Backend()

Abstract base class for any simulator backend.

LocalBackend()

Base class for simulators that run in the local process.

LossState(n_or_flags)

Per-qubit present/lost register, a peer to the quantum, classical and complex registers of a State.

RNGs(shot, noise, trajectory, pass_)

Four independent RNG streams, one per simulation stage.

RemoteBackend()

Base class for simulators that run on a remote service.

State()

Composite simulation state held by a backend.

class mimiqcircuits.backends.backend.RNGs(shot, noise, trajectory, pass_)[source]

Bases: object

Four independent RNG streams, one per simulation stage.

Keeping the streams separate means seeding one stage (for example sampling) does not affect the determinism of another (for example noise selection), even when the same execution touches both.

shot

Sampling shots from the final state.

Type:

random.Random

noise

Kraus and mixed-unitary noise sampling.

Type:

random.Random

trajectory

Monte Carlo trajectory selection.

Type:

random.Random

pass_

Randomised compilation passes (e.g. simulated-annealing reorderers).

Type:

random.Random

Construct from a single int seed via :py:meth:`from_seed`.
shot: Random
noise: Random
trajectory: Random
pass_: Random
static from_seed(master)[source]

Derive four streams from a single master seed.

XOR-tags the seed bits per stream — cheap, deterministic, and stable across Python versions, which matters because the downstream backends must reproduce results bit-for-bit from the same input.

static from_generator(gen)[source]

Draw a master seed from gen and forward to from_seed().

__init__(shot, noise, trajectory, pass_)
class mimiqcircuits.backends.backend.State[source]

Bases: ABC

Composite simulation state held by a backend.

A state bundles three registers:

  • the quantum register (whatever representation the backend uses; state vector, MPS, tensor network, …);

  • the classical-bit register written by measurements and IfStatement outcomes;

  • the complex-valued register written by non-destructive observations (Amplitude, ExpectationValue, BondDim, …).

Required surface for every subclass:

Optional surface:

  • expectation() — backend overrides when it can compute ⟨ψ|O|ψ⟩ non-destructively (gated by the "expectation_state" capability).

  • reset() — backend overrides when it can rebuild itself in place to the zero state.

abstract property num_qubits: int

Number of qubits in the quantum register.

abstract property num_bits: int

Number of classical bits in the classical register.

abstract property num_zvars: int

Number of complex slots in the z-register.

abstract amplitude(bs)[source]
abstract sample(nsamples, rng=None, *, seed=None)[source]

Sample nsamples measurement outcomes from the state.

Exactly one source of randomness is consumed:

  • rng (positional or keyword): a random.Random whose getrandbits(63) produces the seed forwarded to the simulator’s PRNG.

  • seed (keyword): an int that seeds the simulator’s default PRNG directly.

  • neither: the simulator draws fresh cryptographic entropy.

Passing both rng and seed must raise TypeError. The two arguments are mutually exclusive.

expectation(op, *qubits)[source]

Compute ⟨ψ|op|ψ⟩ on this state.

Most backends implement expectation on the Backend rather than the State (so they can route through the simulator’s compile / evolve machinery). State-level expectation is provided as a convenience hook for backends whose quantum register supports it directly.

abstract property classical_bits
abstract property complex_values
reset()[source]
get_loss_state()[source]

Return the per-qubit loss register (see LossState).

Backends that opt into the shared runtime-loss driver (LocalBackend.uses_loss_driver()) embed one and clear it in reset().

class mimiqcircuits.backends.backend.LossState(n_or_flags)[source]

Bases: object

Per-qubit present/lost register, a peer to the quantum, classical and complex registers of a State. Entry q is True when qubit q has leaked out of the system.

The register holds no loss policy: only the shared runtime-loss driver (LocalBackend.evolve_with_loss()) mutates it.

__init__(n_or_flags)[source]
is_lost(q)[source]
mark_lost(q)[source]
mark_present(q)[source]
any_lost(qubits)[source]
all_lost(qubits)[source]
reset()[source]
as_map()[source]

Lost-qubit map consumed by lossmodel_rewrite(); only lost qubits get an entry since the rewrite reads it via get(.., False).

class mimiqcircuits.backends.backend.Backend[source]

Bases: ABC

Abstract base class for any simulator backend.

A concrete backend must:

Pick the right base for your simulator:

  • LocalBackend — the simulator runs in-process and you can hand it a State it mutates with each instruction.

  • RemoteBackend — the simulator runs elsewhere (cloud service, queued executor) and your wrapper submits jobs and polls for results.

See Implementing a Custom Backend.

abstract property name: str
abstract property version: str
abstract capabilities()[source]
limits()[source]
topology()[source]
can_handle(circuit)[source]

Default admission check against limits() and the backend’s advertised capabilities().

Backends with richer admission criteria (bond-dimension estimates, hardware connectivity, gate-set whitelists) should override.

Rejects (returns Inadmissible) when:

  • the circuit exceeds one of max_qubits / max_classical_bits / max_zvars declared by limits();

  • the circuit contains a loss-bearing operation (Loss) but the backend does not advertise the "loss" capability.

Returns Admissible otherwise.

stochastic_kind(op_or_instruction)[source]

Classify how this backend resolves op_or_instruction — StochasticKind.Deterministic, StochasticKind.TrajectorySampleable, or StochasticKind.RuntimeOnly.

The default delegates to default_stochastic_kind() (mix-unitary Kraus is TS; non-mix-unitary Kraus, mid-circuit Measure, and Loss are RT; everything else Deterministic). Backends that handle a specific op type differently override this method.

Backend-dependent: a MixedUnitary is TrajectorySampleable for an MPS-style sampler but could be RuntimeOnly for a backend that resolves it state-dependently. The classification lives on the backend, not on the op.

default_passes()[source]

The pipeline this backend wants applied to a circuit before evolution. Empty by default — backends override.

This is the whole of a backend’s involvement in circuit preparation: it declares the passes it expects, and execute() adjusts that declaration to match what the caller asked for. A backend never sees the flags; when one asks for a pass, the backend builds it through fuse_pass(), reorder_pass(), canonical_decompose_pass() or remove_swaps_pass().

fuse_pass(*, threshold=None)[source]

The pass execute(fuse=True) or fuse_threshold=n installs.

Defaults to FusePass gated on threshold qubits (None means 0).

reorder_pass(method=None, *, seed=None)[source]

The pass execute(reorderqubits=...) installs.

method names the requested search, or is None for reorderqubits=True, which leaves the choice to the backend. There is no generic local reorder pass, so the default raises NotImplementedError.

canonical_decompose_pass()[source]

The pass execute(canonicaldecompose=True) installs. Defaults to CanonicalDecomposePass.

remove_swaps_pass()[source]

The pass execute(remove_swaps=True) installs.

There is no generic local remove-swaps pass, so the default raises NotImplementedError.

simplify_pass()[source]

The pass execute(simplify=True) installs. Defaults to SimplifyPass with DEFAULT_SIMPLIFY_PASSES.

accepts_pass(p)[source]
delegates_pass(p)[source]
abstract execute(circuit, *, nsamples=1000, seed=None, rng=None, passes=None, fuse=None, canonicaldecompose=None, reorderqubits=None, remove_swaps=None, simplify=None, reorderqubits_seed=None, callback=None, param_grid=None, strict_pass_order=True)[source]

Run circuit on this backend and return QCSResults.

Pass circuit as a single Circuit or a list of circuits — a list returns a list of results in the same shape. On a local backend circuit can also be a SweepExperiment, which runs the circuit once per point of its table and returns one result per point.

seed and rng are mutually exclusive sources of randomness; pass at most one. With neither, the backend draws fresh entropy. To request specific amplitudes, push Amplitude instructions into circuit and read the resulting results.zstates.

The flags — fuse, canonicaldecompose, reorderqubits, remove_swaps, simplify — adjust the backend’s default_passes() rather than replacing them. Each speaks about one kind of pass: None leaves that pass as the backend declared it, False drops it, and any other value replaces it with the one the flag describes — or adds that one, in the position it belongs, when the backend declared none of that kind. Everything the flags say nothing about stands.

So fuse=False is “your pipeline, without the fusion”. Local backends also take fuse_threshold: fuse_threshold=18 is “your pipeline, fusing from 18 qubits up”. To prepare a circuit with nothing at all, set every flag to False — a pass no flag names (container flattening, say) survives even that. A flag of the wrong type, such as fuse=0, raises TypeError.

simplify=True simplifies the circuit before evolution with the backend’s simplify_pass(), keeping its unitary exactly: inverse pairs cancel, rotations merge, and one- and two-qubit blocks are rewritten with fewer gates. It is off unless asked for.

The flags have a fixed order among themselves — remove swaps, simplify, canonical decompose, fuse, reorder — which is where a flag puts its pass when the backend declared none of that kind: before the first pass that runs later than it does, and after any pass no flag names.

passes are appended after that, as given; nothing is deduplicated, so an entry of the same kind as a pass already there simply runs too. reorderqubits_seed configures reordering rather than requesting it.

param_grid runs a parameter sweep on a local backend: a list of dicts, each mapping the circuit’s symbols (or their names) to values, such as [{theta: 0.1}, {theta: 0.2}]. Every dict is substituted into circuit with evaluate() and the result runs as its own execution, with the other arguments unchanged. The call then returns a list of results, one per dict and in the same order (with a list of circuits, one such list per circuit). Each point gets its own seed, derived from seed or rng, so the points are statistically independent and a sweep repeated with the same seed gives the same results. Every dict must bind all the free symbols of the circuit, or that point raises an error. An empty list runs the circuit once, as if it was not given. Remote backends raise NotImplementedError.

expectation(state, op, *qubits)[source]

Compute ⟨ψ|op|ψ⟩ on state non-destructively.

qubits is the list of qubit indices op acts on (0-based). Backends override when they advertise the "expectation_state" capability; the default raises NotImplementedError so undeclared backends fail loudly rather than silently degrading.

class mimiqcircuits.backends.backend.LocalBackend[source]

Bases: Backend

Base class for simulators that run in the local process.

Subclasses implement a handful of primitives and inherit a full execute() driver. Once the hooks are wired up, routing (sampling-vs-trajectory), the final-block projection circuit, loss-sampling pre-pass, and amplitude lookups all work without any per-backend duplication.

Required hooks:

  • build_state() — allocate a fresh zero state.

  • compile() — turn a Circuit into a CompiledCircuit (a backend-specific lowered form).

  • evolve() — apply the compiled circuit to a state and return the mutated state plus a typed Fidelity.

Optional hooks (sensible defaults provided):

  • prepare_trajectory() — refresh the compiled artifact once per Monte Carlo trajectory (default: identity).

  • recompile_per_trajectory() — predicate; default returns True iff the circuit contains a mixed-unitary Kraus channel.

  • bind() — substitute parameters into a parametric compile artifact (default: re-compile after substitution).

abstract build_state(nq, nb=0, nz=0, **kwargs)[source]
abstract compile(circuit)[source]
prepare_trajectory(compiled, rng)[source]

Refresh compiled for one Monte Carlo trajectory.

Override when the compiled artifact contains stochastic elements (sampled mixed-unitary channels, sampled Kraus branches, …) that must be redrawn per trajectory. The default leaves compiled unchanged, which is correct for fully deterministic compilation.

abstract evolve(state, compiled, *, rng=None, callback=None, stopped=None)[source]
recompile_per_trajectory(circuit)[source]

Return True iff compile() should be re-run for every trajectory. The default fires on any mixed-unitary krauschannel: those backends sample a branch at compile time, so the compiled artifact has to be regenerated per trajectory to expose a fresh sample. Backends that own their per-trajectory sampling internally (inside prepare_trajectory() or evolve()) should override to return False.

bind(compiled, params)[source]

Substitute params into a parametric compiled circuit.

The default implementation substitutes the symbols in the source circuit and re-runs compile(). This is correct but pays the full compile cost at every parameter point. Override when your backend can re-bind a pre-compiled artifact in-place (slot maps, pre-baked gate templates, …).

optimize(experiment, initparams=None, *, return_history=False, **kwargs)[source]

Minimize a cost read from a parametric circuit by running it on this backend.

At every step the optimizer proposes values for the circuit’s symbols, the circuit is evaluated with them and run with execute(), and the cost is the real part of the mean of the z-register zregister over the samples. Push the quantity to minimize into that register, for example with push_expval().

Call it with an OptimizationExperiment, a list of them, or a circuit and a dict of initial values for all its symbols plus the experiment’s fields as keywords:

  • optimizer: "BFGS", "LBFGS", "CG", "NELDERMEAD", "NEWTON", "COBYLA" (default) or "CMAES". All but "CMAES" come from scipy.optimize.minimize(); "CMAES" needs the cmaes extra (pip install 'mimiqcircuits[cmaes]').

  • maxiters: iteration limit, or None for the optimizer’s own.

  • zregister: index of the z-register holding the cost (default 0).

  • label: a name for the experiment.

With an experiment these come from it, and passing them again is an error. return_history=True also keeps every evaluated point. Every other keyword goes to execute() on each evaluation: nsamples, seed, fuse, passes and so on. With a fixed seed every evaluation draws the same random numbers, so a noisy cost does not jump between otherwise equal points.

Returns an OptimizationResults with the lowest-cost OptimizationRun and, with return_history=True, every evaluated run in order. A list of experiments returns a list of results in the same order.

Raises ValueError for an unknown optimizer or a zregister the circuit does not have.

Example

from symengine import symbols
import mimiqcircuits as mc

theta = symbols("theta")
c = mc.Circuit().push(mc.GateRY(theta), 0)
# The cost is <Z> on qubit 0, written to z-register 0.
c.push(mc.ExpectationValue(mc.GateZ()), 0, 0)

res = backend.optimize(c, {theta: 0.1}, optimizer="NELDERMEAD",
                       nsamples=1, seed=1)
res.get_best().cost  # close to -1, reached at theta = pi
compile_progress(circuit, progress)[source]

Compile circuit while optionally emitting a compression stage. The default ignores progress and defers to compile(); MPS-class backends whose compilation fuses gates into tensors override this to open a “compression” stage and step it per fused gate. Keeping the hook separate from compile() leaves the required signature untouched.

uses_loss_driver()[source]

Whether execute() resolves runtime LossyOperator loss through evolve_with_loss(). Opt in by returning True and implementing apply_instruction() and sample_kraus().

apply_instruction(state, inst, *, rng=None)[source]

Apply a single (loss-free) instruction to state and return the fidelity factor it contributes (1.0 for an exact application). The per-step primitive the runtime-loss driver calls; no loss logic.

sample_kraus(state, channel, targets, *, rng=None)[source]

Sample one branch of a Kraus channel against state, apply it, and return (fidelity, fired_operator) where fired_operator is the original branch (tag preserving, so a LossyOperator arrives intact). The driver maps the fired operator back to the qubits that leaked.

apply_segment(state, insts, *, rng=None)[source]

Apply a contiguous run of loss-free instructions and return the product of their fidelity factors.

The runtime-loss driver hands this the maximal run of instructions between two loss events; every instruction acts only on present qubits, so a compressing or fusing backend (MPS/MPO) may override this to compress the whole run at once — that all-present guarantee is the contract it relies on. The default applies one instruction at a time via apply_instruction(), which suits a non-compressing backend.

evolve_with_loss(state, circuit, lossmodel, *, rng=None, callback=None, stopped=None)[source]

Evolve circuit while resolving qubit loss at runtime.

Loss rides in the state’s LossState register and is interpreted exactly as the offline lower_losses() does, so the two agree shot for shot. The circuit is walked as alternating [all-present run, loss event]: each run is handed to apply_segment() (which a compressing backend fuses) before the event after it is resolved, so a lost-qubit op can never enter a fused run. Returns (state, fidelity) where fidelity is the product of the per-step fidelity factors.

execute(circuit, *, nsamples=1000, seed=None, rng=None, passes=None, fuse=None, fuse_threshold=None, canonicaldecompose=None, reorderqubits=None, remove_swaps=None, simplify=None, reorderqubits_seed=None, callback=None, param_grid=None, strict_pass_order=True, stopped=None, num_qubits=None, progress=False)[source]

Run circuit on this backend and return QCSResults.

Pass circuit as a single Circuit or a list of circuits — a list returns a list of results in the same shape. On a local backend circuit can also be a SweepExperiment, which runs the circuit once per point of its table and returns one result per point.

seed and rng are mutually exclusive sources of randomness; pass at most one. With neither, the backend draws fresh entropy. To request specific amplitudes, push Amplitude instructions into circuit and read the resulting results.zstates.

The flags — fuse, canonicaldecompose, reorderqubits, remove_swaps, simplify — adjust the backend’s default_passes() rather than replacing them. Each speaks about one kind of pass: None leaves that pass as the backend declared it, False drops it, and any other value replaces it with the one the flag describes — or adds that one, in the position it belongs, when the backend declared none of that kind. Everything the flags say nothing about stands.

So fuse=False is “your pipeline, without the fusion”. Local backends also take fuse_threshold: fuse_threshold=18 is “your pipeline, fusing from 18 qubits up”. To prepare a circuit with nothing at all, set every flag to False — a pass no flag names (container flattening, say) survives even that. A flag of the wrong type, such as fuse=0, raises TypeError.

simplify=True simplifies the circuit before evolution with the backend’s simplify_pass(), keeping its unitary exactly: inverse pairs cancel, rotations merge, and one- and two-qubit blocks are rewritten with fewer gates. It is off unless asked for.

The flags have a fixed order among themselves — remove swaps, simplify, canonical decompose, fuse, reorder — which is where a flag puts its pass when the backend declared none of that kind: before the first pass that runs later than it does, and after any pass no flag names.

passes are appended after that, as given; nothing is deduplicated, so an entry of the same kind as a pass already there simply runs too. reorderqubits_seed configures reordering rather than requesting it.

param_grid runs a parameter sweep on a local backend: a list of dicts, each mapping the circuit’s symbols (or their names) to values, such as [{theta: 0.1}, {theta: 0.2}]. Every dict is substituted into circuit with evaluate() and the result runs as its own execution, with the other arguments unchanged. The call then returns a list of results, one per dict and in the same order (with a list of circuits, one such list per circuit). Each point gets its own seed, derived from seed or rng, so the points are statistically independent and a sweep repeated with the same seed gives the same results. Every dict must bind all the free symbols of the circuit, or that point raises an error. An empty list runs the circuit once, as if it was not given. Remote backends raise NotImplementedError.

class mimiqcircuits.backends.backend.RemoteBackend[source]

Bases: Backend

Base class for simulators that run on a remote service.

Subclasses implement submit(), which dispatches the request and returns a job handle. The inherited execute() calls submit(), then blocks on the returned job.wait().

abstract submit(circuits, nsamples, **kwargs)[source]

Send the request and return a job handle.

The job handle must expose a wait() method that blocks until results are available and returns mimiqcircuits.QCSResults (or a list when circuits was a list).

execute(circuit, *, nsamples=1000, seed=None, rng=None, passes=None, callback=None, param_grid=None, strict_pass_order=True)[source]

Run circuit on this backend and return QCSResults.

Pass circuit as a single Circuit or a list of circuits — a list returns a list of results in the same shape. On a local backend circuit can also be a SweepExperiment, which runs the circuit once per point of its table and returns one result per point.

seed and rng are mutually exclusive sources of randomness; pass at most one. With neither, the backend draws fresh entropy. To request specific amplitudes, push Amplitude instructions into circuit and read the resulting results.zstates.

The flags — fuse, canonicaldecompose, reorderqubits, remove_swaps, simplify — adjust the backend’s default_passes() rather than replacing them. Each speaks about one kind of pass: None leaves that pass as the backend declared it, False drops it, and any other value replaces it with the one the flag describes — or adds that one, in the position it belongs, when the backend declared none of that kind. Everything the flags say nothing about stands.

So fuse=False is “your pipeline, without the fusion”. Local backends also take fuse_threshold: fuse_threshold=18 is “your pipeline, fusing from 18 qubits up”. To prepare a circuit with nothing at all, set every flag to False — a pass no flag names (container flattening, say) survives even that. A flag of the wrong type, such as fuse=0, raises TypeError.

simplify=True simplifies the circuit before evolution with the backend’s simplify_pass(), keeping its unitary exactly: inverse pairs cancel, rotations merge, and one- and two-qubit blocks are rewritten with fewer gates. It is off unless asked for.

The flags have a fixed order among themselves — remove swaps, simplify, canonical decompose, fuse, reorder — which is where a flag puts its pass when the backend declared none of that kind: before the first pass that runs later than it does, and after any pass no flag names.

passes are appended after that, as given; nothing is deduplicated, so an entry of the same kind as a pass already there simply runs too. reorderqubits_seed configures reordering rather than requesting it.

param_grid runs a parameter sweep on a local backend: a list of dicts, each mapping the circuit’s symbols (or their names) to values, such as [{theta: 0.1}, {theta: 0.2}]. Every dict is substituted into circuit with evaluate() and the result runs as its own execution, with the other arguments unchanged. The call then returns a list of results, one per dict and in the same order (with a list of circuits, one such list per circuit). Each point gets its own seed, derived from seed or rng, so the points are statistically independent and a sweep repeated with the same seed gives the same results. Every dict must bind all the free symbols of the circuit, or that point raises an error. An empty list runs the circuit once, as if it was not given. Remote backends raise NotImplementedError.