Skip to content

Simulation

qsimlab.sim

Simulation: simulate, plan, requests and results.

A request says what you want from a circuit; simulate picks (or is told) the engine and returns a result object holding numpy arrays.

>>> import numpy as np
>>> from qsimlab import Circuit, simulate, statevector, amplitudes, samples, expectation
>>> ghz = Circuit(3).h(0).cx(0, 1).cx(1, 2)
>>> np.round(simulate(ghz, amplitudes(["000", "111", 5])).amplitudes, 4)
array([0.7071+0.j, 0.7071+0.j, 0.    +0.j])
>>> simulate(ghz, expectation(["Z0 Z1", "X0 X1 X2", "Z0"])).values.round(6)
array([1., 1., 0.])
>>> r = simulate(ghz, samples(8), seed=7)
>>> r.bits.shape, bool((r.bits == r.bits[:, :1]).all())
((8, 3), True)

Request dataclass

Request()

Base class of the four request types.

StatevectorRequest dataclass

StatevectorRequest()

Bases: Request

The full state vector (2**n amplitudes, little-endian, global phase included).

AmplitudesRequest dataclass

AmplitudesRequest(bitstrings: Tuple[BitstringLike, ...])

Bases: Request

<x|ψ> for chosen basis states (ints, or bitstrings with qubit 0 rightmost).

SamplesRequest dataclass

SamplesRequest(shots: int)

Bases: Request

shots measurement records (one column per measurement, program order).

ExpectationRequest dataclass

ExpectationRequest(paulis: Tuple[str, ...])

Bases: Request

<ψ|P|ψ> for each Pauli string P ("X0 Z3" or dense "XIZ", qubit 0 rightmost).

Budget dataclass

Budget(memory: Union[int, str, None] = None)

Resource limits for a simulation.

memory: largest register any engine may allocate, as bytes or a string like "4GiB" / "512MB". None = the engine's hard cap (32 GiB).

>>> Budget(memory="1GiB").memory_bytes
1073741824

NoiseModel dataclass

NoiseModel(
    p1: float = 0.0,
    p2: float = 0.0,
    readout: float = 0.0,
    reset: float = 0.0,
)

Circuit-level noise applied on top of the circuit's own noise channels (samples only).

  • p1: single-qubit depolarizing after every 1-qubit gate;
  • p2: two-qubit depolarizing after every 2-qubit gate;
  • readout: probability each measurement result is reported flipped;
  • reset: probability each reset leaves |1>.

Explanation dataclass

Explanation(
    engine: Optional[str],
    ranked: List[Tuple[str, float]],
    plan_seconds: float,
    cached: bool,
    features: Dict[str, Any],
    notes: List[str],
)

What the planner decided and why (simulate(..., explain=True) or plan).

ranked lists (engine, predicted_seconds) best first; predictions come from cost models fitted on an M1 Pro (one thread), so read them as relative.

Result dataclass

Result(
    engine: str,
    components: List[Tuple[int, int, str]],
    seed: int,
    precision: str,
    wall_time: float,
    explanation: Optional[Explanation] = None,
)

Fields shared by every result.

  • engine: the engine that ran ("pipeline" if components used different ones);
  • components: [(num_qubits, num_gates, engine)] per simulated part;
  • seed: the seed actually used (pass it back for bit-identical samples);
  • precision: "f64" or "f32", what was actually used;
  • wall_time: seconds spent in the engine;
  • explanation: an Explanation if explain=True.

StatevectorResult dataclass

StatevectorResult(
    engine: str,
    components: List[Tuple[int, int, str]],
    seed: int,
    precision: str,
    wall_time: float,
    explanation: Optional[Explanation] = None,
    state: ndarray = None,
)

Bases: Result

state: complex ndarray of length 2**n; state[i] is the amplitude of basis index i.

probabilities

probabilities() -> ndarray

|state|**2.

AmplitudesResult dataclass

AmplitudesResult(
    engine: str,
    components: List[Tuple[int, int, str]],
    seed: int,
    precision: str,
    wall_time: float,
    explanation: Optional[Explanation] = None,
    amplitudes: ndarray = None,
    bitstrings: Tuple[BitstringLike, ...] = (),
)

Bases: Result

amplitudes[k] = <bitstrings[k]|ψ> (complex128).

SamplesResult dataclass

SamplesResult(
    engine: str,
    components: List[Tuple[int, int, str]],
    seed: int,
    precision: str,
    wall_time: float,
    explanation: Optional[Explanation] = None,
    bits: ndarray = None,
    measured_qubits: Tuple[int, ...] = (),
)

Bases: Result

bits: uint8 ndarray[shots, m], column k = measurement k (program order); measured_qubits[k] is the qubit measurement k read.

counts

counts(
    *, as_int: bool = False
) -> Dict[Union[str, int], int]

Histogram of records. Keys are bitstrings with measurement 0 rightmost (Qiskit order), or ints (bit k = measurement k) with as_int=True.

>>> from qsimlab import Circuit, simulate, samples
>>> simulate(Circuit(2).x(0).measure_all(), samples(5)).counts()
{'01': 5}

probabilities

probabilities() -> Dict[str, float]

Empirical probabilities, same keys as counts.

parity

parity(columns: Sequence[int]) -> ndarray

XOR of the given measurement columns per shot (uint8 ndarray[shots]).

ExpectationResult dataclass

ExpectationResult(
    engine: str,
    components: List[Tuple[int, int, str]],
    seed: int,
    precision: str,
    wall_time: float,
    explanation: Optional[Explanation] = None,
    values: ndarray = None,
    paulis: Tuple[str, ...] = (),
)

Bases: Result

values[k] = <ψ|paulis[k]|ψ> (float64).

statevector

statevector() -> StatevectorRequest

Request the full state vector (complex ndarray of length 2**n).

amplitudes

amplitudes(
    bitstrings: Union[
        BitstringLike, Iterable[BitstringLike]
    ],
) -> AmplitudesRequest

Request <x|ψ> for basis states x: ints (bit q = qubit q) or '0'/'1' strings whose rightmost character is qubit 0. Works far beyond state-vector sizes when the planner finds a cheaper engine (sparse, MPS, HSF path sums).

samples

samples(shots: int) -> SamplesRequest

Request shots measurement records. Noise channels, resets, mid-circuit measurements and conditionals are simulated exactly, shot by shot or batched.

expectation

expectation(
    paulis: Union[str, Iterable[str]],
) -> ExpectationRequest

Request expectation values of Pauli strings (see API.md §5 for the syntax).

simulate

simulate(
    circuit: Circuit,
    request: Request,
    *,
    engine: Engine = "auto",
    precision: Precision = "f64",
    seed: Optional[int] = None,
    budget: Union[Budget, int, str, None] = None,
    threads: Optional[int] = None,
    noise: Optional[NoiseModel] = None,
    repeat: Optional[bool] = None,
    explain: bool = False,
) -> Result

Simulates circuit for request and returns the matching result object.

Parameters:

Name Type Description Default
engine Engine

"auto" (default): compile passes + Planner v2 choose the cheapest exact engine per connected component. Or force one of ENGINES; a forced engine that cannot do the job raises instead of falling back.

'auto'
precision Precision

"f64" or "f32" (honoured by dense state-vector paths; see result.precision).

'f64'
seed Optional[int]

RNG seed for sampling; None draws one (reported as result.seed).

None
budget Union[Budget, int, str, None]

Memory cap: Budget, bytes, or a string like "8GiB".

None
threads Optional[int]

Worker threads for this call (default: qsimlab.get_num_threads).

None
noise Optional[NoiseModel]

A NoiseModel applied on top of the circuit's channels (samples only).

None
repeat Optional[bool]

Run the repeat-detection pass (default: on iff the circuit was built with Circuit.repeat or parsed from Stim with REPEAT).

None
explain bool

Attach the planner's Explanation to the result.

False

Examples:

>>> from qsimlab import Circuit, simulate, expectation
>>> r = simulate(Circuit(1).ry(0, 0.5), expectation("Z0"), explain=True)
>>> round(float(r.values[0]), 6), r.explanation is not None
(0.877583, True)

plan

plan(
    circuit: Circuit,
    request: Request,
    *,
    budget: Union[Budget, int, str, None] = None,
) -> Explanation

The planner's prediction for request without running anything.

>>> from qsimlab import Circuit, plan, samples
>>> e = plan(Circuit(30).h(0).cx(0, 1).measure_all(), samples(1000))
>>> e.engine is not None and len(e.ranked) >= 1
True