Skip to content

Circuit Execution

execute() runs a mimiqcircuits circuit on the MPS engine and returns a QCSResults.

from mimiqcircuits import Circuit, GateH, GateCX
from tensorweaver import execute

c = Circuit()
c.push(GateH(), 0)
c.push(GateCX(), 0, 1)

results = execute(c, nsamples=1000, bonddim=64)
print(results.histogram())
print(results.fidelities[0])
print(results.timings)

execute() builds a fresh simulator per call. To reuse one configuration across many circuits, construct a TwSimulator instead and call its execute() method. The configuration keywords below map onto the constructor; nsamples, seed, and num_qubits stay per call.

Execution modes

The mode is chosen from the circuit's content, and it decides what nsamples means. Concepts covers why.

Sampling mode Trajectory mode
Chosen when Circuit is unitary apart from measurements that nothing depends on afterwards Circuit contains a measurement whose qubit is reused, a Reset, an IfStatement, or a noise channel
Evolutions One nsamples
nsamples means Bitstrings drawn from the final state Independent runs of the full circuit
results.fidelities One entry nsamples entries

A measurement does not by itself force trajectory mode. Measurements that nothing later depends on are stripped before evolution and reapplied when samples are mapped onto classical bits, so the run stays in the cheap mode.

from mimiqcircuits import Circuit, GateH, GateCX, Measure
from tensorweaver import execute

c = Circuit()
c.push(GateH(), 0)
c.push(GateCX(), 0, 1)
c.push(Measure(), 0, 0)
c.push(Measure(), 1, 1)

results = execute(c, nsamples=1000, bonddim=64)
print(len(results.fidelities))   # 1: sampling mode, evolved once
print(results.histogram())       # e.g. {bs"00": 498, bs"11": 502}

Reusing a measured qubit makes each shot follow its own history, so each shot needs its own evolution:

c = Circuit()
c.push(GateH(), 0)
c.push(Measure(), 0, 0)   # qubit 0 is used again below
c.push(GateH(), 0)
c.push(Measure(), 0, 1)

results = execute(c, nsamples=100, bonddim=64)
print(len(results.fidelities))   # 100: one per trajectory

Trajectory mode needs explicit measurements

In sampling mode every qubit is projected when the final state is sampled, so results.histogram() is populated even with no Measure in the circuit. In trajectory mode cstates come from the classical register, so a circuit with no Measure yields no classical states and an empty histogram. Push the measurements you want to read.

Amplitudes in trajectory mode

bitstrings=, and an in-circuit Amplitude(...), report the last trajectory's state, not an ensemble average. An amplitude is a pure-state quantity and a trajectory ensemble is a mixed state. To get ensemble statistics, aggregate over runs yourself.

Parameters

circuit and nsamples are positional; everything else is keyword-only.

Accuracy and cost

Parameter Default When to change it
bonddim 256 The main accuracy/cost dial. Raise until your observable stops moving; lower for speed on lightly entangled circuits. Cost per two-qubit gate grows like bonddim³.
sv_cutoff -1.0 (means 1e-7) Rarely. Lower it towards 1e-10 when you need tighter accuracy at a bonddim you are not saturating; raising it trades accuracy for speed. Any negative value selects the library default.
algorithm "vmpoa" "vmpoa" and "vmpob" are variational and most accurate at a given bonddim; "dmpo" is faster per application and less accurate. Switch to "dmpo" only when a bonddim sweep shows the extra accuracy is not buying anything.
entdim 16 Caps the bond dimension of the operator that gates accumulate into. Raise for deep, highly entangling segments so more gates batch per application. A single gate whose operator rank exceeds entdim raises RuntimeError.
post_compress True Leave on. Off defers the sweep that restores canonical form, which is faster on circuits well below bonddim but lets later truncations compare the wrong singular values. Turn it off only after measuring that your circuits are unaffected.
max_bond_dim None Deprecated alias for bonddim. Overrides bonddim when set. Use bonddim.

Layout

Parameter Default When to change it
reorderqubits True True (same as "greedy") suits most circuits. Use "sa" (simulated annealing) or "multilevel" when greedy leaves a high bond dimension and the layout search is worth more time. False when you need the MPS chain to keep your qubit numbering, or when the interaction graph is genuinely all-to-all so nothing can be gained.
reorderqubits_alpha 3.0 Exponent on the distance penalty in the layout cost, C(π) = Σ \|π(i) − π(j)\|^α · w(i,j). Raise it to punish long-range pairs harder, lower it to spread interactions more evenly.
num_qubits None Set it when the circuit does not touch every qubit of the register you want simulated. Inferred from the circuit otherwise.

Reordering is transparent: samples, amplitudes, and z-variables come back in your original qubit labelling.

Sampling and observation

Parameter Default When to change it
nsamples 1000 Shots in sampling mode, full evolutions in trajectory mode. Cost is flat in sampling mode and linear in trajectory mode.
seed 42 Set it for reproducibility. In trajectory mode, trajectory i uses seed + i.
bitstrings None Pass a list of BitString to read specific amplitudes out of results.amplitudes.

Circuit preparation

Parameter Default When to change it
traversal "sequential" Order gates are walked while they batch into operators. See Circuit traversal order.
fuse False Fuses runs of gates into GateCustom blocks before evolution. Distinct from, and usually redundant with, the operator batching applied during evolution.
fuse_threshold 0 Minimum qubit count for fuse to act. Smaller circuits pass through unfused.
canonicaldecompose False Decompose to the canonical basis before evolution.
buffer 0 Prepares up to buffer operators on a background thread while evolution applies the current one. Results are identical to 0. A depth of 2 to 4 is enough, since only one or two operators are in flight.

Engine

Parameter Default When to change it
usemkl False Selects the BLAS engine. See BLAS engine selection.

Bitstring / qubit ordering

TensorWeaver uses little-endian by qubit index: bitstring position i is qubit i. This is the opposite of Qiskit's convention, so bitstrings from results.cstates and results.histogram(), and those passed via bitstrings=, appear reversed compared to equivalent Qiskit output.

Return value: QCSResults

Field Type Contents
cstates list[BitString] Classical states, one per sample or trajectory. In trajectory mode these come from the circuit's Measure instructions.
fidelities list[float] Truncation fidelity per evolution. One entry in sampling mode, nsamples in trajectory mode. A lower bound: see reading the fidelity.
avggateerrors list[float] The same information expressed as an average error per two-qubit gate.
amplitudes dict[BitString, complex] Amplitudes for each bitstring passed to bitstrings=.
zstates list[list[complex]] Z-register values per evolution, written by z-variable operations.
timings dict[str, float] Wall-clock seconds: "total", "compile" (decomposition and layout), "apply" (evolution), "sample".
simulator str "TensorWeaver".
version str Version of the installed mimiq-tensorweaver package.

Two methods are worth knowing:

  • results.histogram() returns dict[BitString, int], counting each classical state.
  • results.histzvars() returns dict[tuple, int], counting each z-state.

Computing amplitudes

from mimiqcircuits import Circuit, GateH, GateCX, BitString
from tensorweaver import execute

c = Circuit()
c.push(GateH(), 0)
c.push(GateCX(), 0, 1)

bs00, bs11 = BitString([0, 0]), BitString([1, 1])
results = execute(c, nsamples=1, bonddim=64, bitstrings=[bs00, bs11])

print(results.amplitudes[bs00])   # ~0.7071+0j
print(results.amplitudes[bs11])   # ~0.7071+0j

Circuit traversal order (traversal)

A circuit is more than a flat list. Two instructions are ordered relative to each other only when they share a wire (a qubit, a classical bit, or a z-variable). traversal chooses how that freedom is used while gates batch into operators.

Value Order
"sequential" (default) The circuit's insertion order.
"bfs" Breadth-first topological order: independent gates group into layers.
"dfs" Depth-first topological order: one dependency chain is followed to its end before backtracking.

Reordering is confined to the gates between instructions that read or change the state. Measurement, reset, noise channels, observables (Amplitude, BondDim, SchmidtRank, VonNeumannEntropy, ExpectationValue), barriers, and conditionals stay where they are and act as synchronisation points.

All three produce the same simulated state. They differ in how gates batch, which changes the intermediate bond dimension and therefore runtime and memory. "bfs" tends to pack each operator with more mutually commuting gates, which helps on circuits with wide, shallow layers. "sequential" matches a literal reading of the circuit.

from mimiqcircuits import Circuit, GateH, GateCX
from tensorweaver import TwSimulator

circuit = Circuit()
for q in range(8):
    circuit.push(GateH(), q)
for q in range(0, 8, 2):
    circuit.push(GateCX(), q, q + 1)

sim = TwSimulator(bonddim=128, traversal="bfs")
results = sim.execute(circuit, nsamples=1000, seed=42)

BLAS engine selection (usemkl)

The dense linear algebra underneath the MPS runs on a BLAS/LAPACK library. Engines are separate native modules, selected per simulator:

usemkl Engine Availability
False (default) OpenBLAS Vendored into the wheel; works from a plain pip install with no system BLAS.
True Intel MKL Needs the MKL runtime on the system.
from mimiqcircuits import Circuit, GateH, GateCX
from tensorweaver import TwSimulator, execute

circuit = Circuit()
circuit.push(GateH(), 0)
circuit.push(GateCX(), 0, 1)

sim = TwSimulator(usemkl=True)                           # whole simulator
results = sim.execute(circuit, nsamples=1000)

results = execute(circuit, nsamples=1000, usemkl=True)   # one call

Which engines a wheel contains depends on the platform: Linux ships both, macOS arm64 ships OpenBLAS only (MKL is x86-only), and Windows ships MKL only. On Windows usemkl=False has no effect, because MKL is the only engine there.

Why MKL needs a system runtime

OpenBLAS is an ordinary shared library, so it is bundled into the wheel. MKL is not: libmkl_rt is a dispatcher that loads its backing libraries by name at call time, so they never appear in the wheel's import table and cannot be vendored. Install the runtime with any of:

conda install mkl
pip install mkl            # then add its lib dir to LD_LIBRARY_PATH
apt-get install libmkl-rt  # Debian/Ubuntu, non-free
# or source Intel oneAPI's setvars.sh

Requesting usemkl=True without a usable runtime raises an ImportError explaining how to install it, or, on a platform with no MKL build at all, saying so instead. The check runs up front in a short subprocess, so you get a Python exception rather than MKL aborting the process mid-operation.

Engines do not share objects

Objects are engine-specific

The engines are independent native modules. They can run side by side in one process, but their objects do not interoperate: an MPS, MPO, Rng, or TwState from one engine cannot be passed to a simulator or method backed by the other, and attempting it raises TypeError. Move a state across by serialising it:

from tensorweaver import TwSimulator

sim_ob  = TwSimulator(usemkl=False)
sim_mkl = TwSimulator(usemkl=True)

state = sim_ob.build_state(3)
blob = state.mps.serialize()                      # protobuf bytes, a copy
mps_mkl = sim_mkl._core.PyMps.deserialize(blob)   # usable by the MKL engine

Both engines compute the same results, so a workflow normally stays on one engine end to end.

TensorWeaverBasis

TensorWeaverBasis defines which operations run natively, without decomposition.

Terminal operations

Operation Handling
GateRX(θ), GateRY(θ), GateRZ(λ) Applied directly.
GateU(θ, φ, λ, γ) Applied directly.
GateID Skipped.
GateCX, GateCZ, GateISWAP Applied directly.
Any two-qubit unitary Applied from its matrix.
Control(n, 1Q-gate) Applied as a multi-controlled gate, for any number of controls.
Measure Collapses the state, stores the outcome in the classical register.
Reset, MeasureReset Measure, then a conditional X back to |0⟩.
IfStatement(op, bs) Applies op when the classical bits match bs. Terminal when op is.
Amplitude, BondDim, SchmidtRank, VonNeumannEntropy, ExpectationValue Read the state into the z-register.
Any krauschannel Mixed-unitary or general Kraus channel, on one or two qubits.
Classical logic (Not, And, Or, Xor, ParityCheck, SetBit, ...) Applied to the classical register.
Z-register arithmetic (Add, Multiply, Pow) Combines z-variables.
Barrier A fusion boundary for the qubits it names; no effect on the state.
Annotations (Detector, loss markers, ...) No simulation semantics.

Decomposed operations

Everything else goes through the canonical rewrite, calling each gate's _decompose(). For example GateH becomes GateU(π/2, 0, π) and GateSWAP becomes three GateCX.

Multi-controlled gates

Control(n, 1Q-gate) is terminal for any number of controls: the operator is built directly rather than decomposed into a CX ladder. Toffoli (CCX), C3X, and higher-order controlled gates all take this path.

Z-variable operations

Z-variable operations observe the state without changing it, writing into the circuit's z-register. They never push a circuit into trajectory mode.

Operation Arguments Stores
Amplitude(bs) A BitString Complex amplitude ⟨bs\|ψ⟩
BondDim() A bond index Bond dimension, in the .real part
SchmidtRank() A bond index Schmidt rank above a 1e-12 cutoff, in the .real part
VonNeumannEntropy() A bond index Entanglement entropy in bits, in the .real part
ExpectationValue(op) 1 or 2 qubits, matching op Complex ⟨ψ\|op\|ψ⟩

BondDim, SchmidtRank, and VonNeumannEntropy take a bond index, naming the bipartition {0..k-1} | {k..n-1}, not a qubit. Qubit reordering keeps those cuts intact and leaves the index alone.

from mimiqcircuits import (
    Circuit, GateH, GateCX, GateZ,
    Amplitude, BondDim, SchmidtRank, VonNeumannEntropy,
    ExpectationValue, BitString,
)
from tensorweaver import execute

c = Circuit()
c.push(GateH(), 0)
c.push(GateCX(), 0, 1)

c.push(Amplitude(BitString([0, 0])), 0)      # -> z0
c.push(BondDim(), 1, 1)                      # bond 1 -> z1
c.push(SchmidtRank(), 1, 2)                  # bond 1 -> z2
c.push(VonNeumannEntropy(), 1, 3)            # bond 1 -> z3
c.push(ExpectationValue(GateZ()), 0, 4)      # -> z4

results = execute(c, nsamples=1, num_qubits=2, bonddim=64)
zs = results.zstates[0]
print(f"amplitude |00>     {zs[0]}")
print(f"bond dim at cut 1  {zs[1].real:.0f}")
print(f"Schmidt rank       {zs[2].real:.0f}")
print(f"entropy (bits)     {zs[3].real:.4f}")
print(f"<Z> on qubit 0     {zs[4].real:.4f}")

Noise channels

Any krauschannel from mimiqcircuits on one or two qubits is applied directly. Mixed-unitary channels such as PauliX, Depolarizing, and PauliNoise are detected through ismixedunitary() and sample one unitary per shot. General channels such as AmplitudeDamping use the full operator-sum form.

Noise selects trajectory mode, so nsamples becomes the number of independent evolutions, and the ensemble over them reproduces the mixed state. Push the measurements you want to read: in trajectory mode the histogram comes from the classical register.

from mimiqcircuits import Circuit, GateH, GateCX, PauliX, Measure
from tensorweaver import execute

c = Circuit()
c.push(GateH(), 0)
c.push(GateCX(), 0, 1)
c.push(PauliX(0.05), 0)     # 5% bit-flip on qubit 0
c.push(PauliX(0.05), 1)     # 5% bit-flip on qubit 1
c.push(Measure(), 0, 0)
c.push(Measure(), 1, 1)

results = execute(c, nsamples=500, bonddim=64)
print(results.histogram())   # weight leaks onto 01 and 10

Conditional operations

IfStatement(op, bitstring) applies op only when the classical register matches. It selects trajectory mode.

from mimiqcircuits import Circuit, GateH, GateX, Measure, IfStatement, BitString
from tensorweaver import execute

c = Circuit()
c.push(GateH(), 0)
c.push(Measure(), 0, 0)
c.push(IfStatement(GateX(), BitString("1")), 1, 0)   # X on qubit 1 if bit 0 is set
c.push(Measure(), 1, 1)

results = execute(c, nsamples=200, num_qubits=2, bonddim=64)
print(results.histogram())   # only 00 and 11 appear

Using the basis directly

TensorWeaverBasis plugs into the mimiqcircuits decomposition API, which is useful for inspecting what a circuit lowers to before running it.

from mimiqcircuits import Circuit, GateH
from mimiqcircuits.decomposition import decompose, eachdecomposed
from tensorweaver.basis import TensorWeaverBasis

c = Circuit()
c.push(GateH(), 0)

decomposed = decompose(c, TensorWeaverBasis())

for inst in eachdecomposed(c, TensorWeaverBasis()):   # streaming variant
    print(inst)