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()returnsdict[BitString, int], counting each classical state.results.histzvars()returnsdict[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)