This is the API reference for the SwiftQiskit core library (Sources/SwiftQiskit/) — the
product you get from import SwiftQiskit. The shared, UI-free-Core-preserving
SwiftQiskitViews module (Sources/SwiftQiskitViews/) is covered in its own section below;
it does not cover the remaining playground helper module (Playgrounds.playground/Sources/),
see PLAYGROUNDSUPPORT.md for that.
SwiftQiskit is v0.7.1 — the API is unstable and correctness is prioritized over performance. Keep this document in sync with the source when the public API changes.
- Qubit indexing: qubit 0 is the most-significant (leftmost) bit. This applies to
QuantumCircuitgate methods,CNOTGate.matrix(qubits:control:target:),⊗(tensor product — the left-hand operand occupies the high-order bits), and binary state labels likeKet("01"). - Angles are in radians, and — following Qiskit — appear as the first argument on
parameterized circuit methods:
p(theta, qubit),rx(theta, qubit), etc. - Invariants are enforced with
precondition, not thrown errors: passing a mismatched matrix dimension, an out-of-range qubit index, or an empty state traps at runtime rather than failing gracefully. Check preconditions before calling. - Value types throughout, except
QuantumCircuit, which is afinal class(reference semantics — copies alias the same circuit).
import SwiftQiskit
let circuit = QuantumCircuit(qubits: 2)
circuit.h(0)
circuit.cx(0, 1)
let state = circuit.run()
print(state)
// |0⟩: 0.7071067811865475 <- unpadded label; this is |00⟩
// |1⟩: 0.0 <- this is |01⟩
// |10⟩: 0.0
// |11⟩: 0.7071067811865475
let result = circuit.measure(shots: 1000)
print(result.sortedCounts)
// [(state: "00", count: 487), (state: "11", count: 513)] -- counts vary run to runSources/SwiftQiskit/Math/Complex.swift — a value-type complex number.
public struct Complex: Equatable, Hashable {
public var real: Double
public var imag: Double
public init(_ real: Double = 0.0, _ imag: Double = 0.0)
}| Member | Notes |
|---|---|
.zero, .one, .i |
Constants Complex(0,0), Complex(1,0), Complex(0,1). |
magnitude |
|z|. |
magnitudeSquared |
|z|² — used internally to avoid a sqrt where only the square is needed (e.g. probabilities). |
conjugate |
z̄. |
+, -, *, / |
Standard complex arithmetic. / traps via precondition on division by zero. |
*(Complex, Double), *(Double, Complex) |
Scalar multiplication, either operand order. |
description |
Formats as "a", "bi", or "a + bi" / "a - bi". |
Gaps to know about: there is no unary - and no exp/polar constructor — write
Complex(-1) for negation and Complex(cos(theta), sin(theta)) for e^{iθ} (see how the
gate files do it).
Sources/SwiftQiskit/Math/Matrix.swift (plus adjoint, added in Dirac.swift) — a
row-major, dense complex matrix.
public struct Matrix: Equatable, Hashable {
public let rows: Int
public let cols: Int
public init(rows: Int, cols: Int, repeating value: Complex = .zero)
public init(_ data: [[Complex]])
public subscript(row: Int, col: Int) -> Complex { get set }
}init(_ data:)traps on an empty array or ragged rows.subscripttraps on out-of-range indices.
| Operation | Signature | Notes |
|---|---|---|
| Matrix × matrix | static func * (Matrix, Matrix) -> Matrix |
Traps if lhs.cols != rhs.rows. |
| Matrix × vector | func multiply(by vector: [Complex]) -> [Complex] |
Traps if cols != vector.count. |
| Addition / subtraction | static func + / - |
Entrywise; traps on a dimension mismatch. |
| Scalar multiply | static func * (Matrix, Complex), (Complex, Matrix), (Matrix, Double), (Double, Matrix) |
All four orders/types are supported. |
| Identity | static func identity(size: Int) -> Matrix |
|
| Tensor (Kronecker) product | func tensor(_ other: Matrix) -> Matrix, operator ⊗ |
(rows·other.rows) × (cols·other.cols); any dimensions are valid, not just square. ⊗ is declared here as infix operator ⊗ : MultiplicationPrecedence. |
| Adjoint (conjugate transpose) | var adjoint: Matrix, postfix † |
Declared in Dirac.swift. |
| Unitarity check | func isUnitary(tolerance: Double = 1e-10) -> Bool |
true iff square and U†U ≈ I entrywise within tolerance; false for any non-square matrix. Replaces the hand M†M ≈ I check otherwise repeated at every call site (see the Equatable gotcha below). |
| Trace | var trace: Complex |
Σᵢ self[i,i]. Traps if the matrix isn't square. |
| Permutation matrix | static func permutation(size: Int, image: (Int) -> Int) -> Matrix |
Builds the size×size permutation sending column i to row image(i) (i.e. |image(i)⟩ ← |i⟩). Traps unless image is a genuine bijection on 0..<size — every row must be hit by exactly one column — so the constructor itself is the unitarity check a hand-rolled permutation loop would otherwise verify separately. |
| Matrix exponential | func expm(terms: Int = 20) -> Matrix |
e^A via scaling-and-squaring: halves A until its ∞-norm (max absolute row sum) is ≤ 0.5, sums a terms-term Taylor series there, then squares the result back up the same number of times. Traps if the matrix isn't square. No implicit -i — for e^(-iθH), scale H by Complex(0, -theta) yourself first. |
| Description | description |
One bracketed row per line. |
Gotcha: Equatable/Hashable compare Complex entries exactly, so two matrices that
are mathematically equal but differ by floating-point rounding will compare unequal with
==. Use isUnitary(tolerance:) or an approximate comparison in tests (see
MatrixArithmeticTests.swift for the pattern used in this repo).
Sources/SwiftQiskit/Quantum/StateVector.swift, extended in Dirac.swift — a normalized
vector of complex amplitudes representing an n-qubit pure state (2ⁿ amplitudes).
public struct StateVector: Equatable {
public private(set) var amplitudes: [Complex]
public init(_ amplitudes: [Complex]) // auto-normalizes; traps on an empty or all-zero input
public init(qubits: Int) // |0...0⟩; traps if qubits <= 0
}
public typealias Ket = StateVector| Member | Notes |
|---|---|
dimension |
2ⁿ, i.e. amplitudes.count. |
probabilities |
[Double] of |αᵢ|², one per basis state. |
subscript(index:) |
Amplitude at a basis index; traps out of range. |
normalize() |
Mutating; rescales to unit norm. Skips the rescale (and the rounding error it would add) when the norm is already within 1e-12 of 1, which keeps the dagger a true involution: (ψ†)† == ψ exactly. Traps if the norm is zero. |
apply(_ matrix: Matrix) |
Mutating: |ψ'⟩ = U|ψ⟩, then re-normalizes. Any matrix of the right dimension is accepted and silently rescaled to a unit vector — a non-unitary U will not be rejected, it will just change the probabilities in ways U alone wouldn't predict. |
measure() -> Int |
Mutating: samples a basis index from probabilities and collapses self to that basis state (all other amplitudes become .zero). Not idempotent — calling it twice is a different measurement, not a re-read. |
marginalProbabilities(over qubits: [Int]) -> [String: Double] |
Sums out every qubit not listed. The result's keys are the bits of qubits, in the order given (not necessarily ascending index), zero-padded to qubits.count characters — every one of the 2^k possible keys is present, including zero-probability ones. Traps if qubits is empty, has a duplicate, or has an out-of-range index. |
tensor(_ other:) -> StateVector, operator ⊗ |
Combines two registers; self occupies the high-order bits. |
description |
One `" |
Ket("01") // basis ket from a binary label (qubit 0 = leftmost bit)
StateVector.zero // |0⟩
StateVector.one // |1⟩
StateVector.plus // |+⟩ = (|0⟩ + |1⟩)/√2
StateVector.minus // |−⟩ = (|0⟩ − |1⟩)/√2
StateVector.plusI // |i⟩ = (|0⟩ + i|1⟩)/√2
StateVector.minusI // |−i⟩ = (|0⟩ − i|1⟩)/√2Ket(_ label:) traps if the label contains anything but 0/1 or is empty.
Sources/SwiftQiskit/Quantum/Dirac.swift — bra-ket notation built on top of StateVector/Matrix.
public struct Bra: Equatable {
public private(set) var amplitudes: [Complex] // stored already conjugated
public init(_ ket: StateVector) // ⟨ψ| = (|ψ⟩)†
public init(_ label: String) // e.g. Bra("01") = ⟨01|
public var ket: StateVector { get } // |ψ⟩ = (⟨ψ|)†
public var dimension: Int { get }
}postfix operator †
public postfix func † (ket: StateVector) -> Bra // ⟨ψ| = (|ψ⟩)†
public postfix func † (bra: Bra) -> StateVector // |ψ⟩ = (⟨ψ|)†
public postfix func † (matrix: Matrix) -> Matrix // U† (same as .adjoint)| Expression | Type | Notes |
|---|---|---|
Bra * StateVector |
Complex |
Inner product ⟨φ|ψ⟩. Traps on a dimension mismatch. |
Bra * Matrix |
Bra |
⟨ψ|U — row vector times matrix. Enables expectation values: psi† * U * psi. |
StateVector * Bra |
Matrix |
Outer product |ψ⟩⟨φ|. |
Bra ⊗ Bra |
Bra |
Combines two bras; self is the high-order register. |
StateVector ⊗ Bra, Bra ⊗ StateVector |
Matrix |
Mixed tensor product — both reduce to the outer product |a⟩⟨b|. |
Worked example — a Pauli-Z expectation value:
let psi = StateVector.plus
let z = psi† * PauliZGate.matrix * psi // ⟨ψ|Z|ψ⟩, a Complex (real part ≈ 0 for |+⟩)public extension StateVector {
func expectation(_ observable: Matrix) -> Double // ⟨ψ|A|ψ⟩.real
}A named one-liner wrapping the worked example above: psi.expectation(z) instead of
(psi† * z * psi).real. Assumes observable is Hermitian (any genuine observable — Pauli
matrices, projectors, real linear combinations of these) — the imaginary part is discarded
rather than checked, so a non-Hermitian matrix won't trap, it will just quietly lose
information. Traps (via the underlying Bra products) on a dimension mismatch.
Sources/SwiftQiskit/Gates/*.swift — each fixed gate is a public enum exposing a static
matrix: Matrix; parameterized gates expose a static matrix(theta:) -> Matrix instead.
Follow this pattern when adding a new gate.
| Gate | Type | Circuit method | Notes |
|---|---|---|---|
HadamardGate.matrix |
fixed | h(qubit) |
1/√2 · [[1, 1], [1, -1]]. |
PauliXGate.matrix |
fixed | x(qubit) |
[[0, 1], [1, 0]]. |
PauliYGate.matrix |
fixed | y(qubit) |
[[0, -i], [i, 0]]. |
PauliZGate.matrix |
fixed | z(qubit) |
[[1, 0], [0, -1]]. |
SGate.matrix |
fixed | s(qubit) |
P(π/2), exact entries [[1,0],[0,i]]. |
SDaggerGate.matrix |
fixed | sdg(qubit) |
SGate.matrix.adjoint. |
TGate.matrix |
fixed | t(qubit) |
PhaseGate.matrix(theta: .pi/4). |
TDaggerGate.matrix |
fixed | tdg(qubit) |
TGate.matrix.adjoint. |
PhaseGate.matrix(theta:) |
parameterized | p(theta, qubit) |
[[1,0],[0, e^{iθ}]]. |
RXGate.matrix(theta:) |
parameterized | rx(theta, qubit) |
Rotation about X: exp(-iθX/2). |
RYGate.matrix(theta:) |
parameterized | ry(theta, qubit) |
Rotation about Y: exp(-iθY/2), all-real entries. |
RZGate.matrix(theta:) |
parameterized | rz(theta, qubit) |
Rotation about Z: exp(-iθZ/2); equals P(θ) up to the global phase e^{-iθ/2}. |
CNOTGate.matrix |
fixed, 2-qubit | cx(control, target)* |
The plain 4×4 CNOT (control = qubit 0, target = qubit 1). |
CNOTGate.matrix(qubits:control:target:) |
general, n-qubit | cx(control, target) |
The full 2ⁿ×2ⁿ CNOT for any distinct control/target pair on an n-qubit register, built as a basis-state permutation. Traps if qubits < 2, if either index is out of range, or if control == target. |
RZZGate.matrix(theta:) |
parameterized, 2-qubit | rzz(theta, q0, q1)* |
exp(-iθ·Z⊗Z/2) on adjacent qubits; equals cx(0,1); rz(θ,1); cx(0,1) exactly. |
RXXGate.matrix(theta:) |
parameterized, 2-qubit | rxx(theta, q0, q1)* |
exp(-iθ·X⊗X/2) on adjacent qubits. |
RYYGate.matrix(theta:) |
parameterized, 2-qubit | ryy(theta, q0, q1)* |
exp(-iθ·Y⊗Y/2) on adjacent qubits. |
ToffoliGate.matrix |
fixed, 3-qubit | ccx(control1, control2, target)* |
The plain 8×8 Toffoli (controls = qubits 0/1, target = qubit 2). |
ToffoliGate.matrix(qubits:control1:control2:target:) |
general, n-qubit | ccx(control1, control2, target) |
The full 2ⁿ×2ⁿ Toffoli for any three distinct qubits, built via Matrix.permutation: flips target iff both controls are 1. Symmetric in its two controls. Traps if qubits < 3, if any index is out of range, or if the three indices aren't distinct. |
MultiControlledXGate.matrix(qubits:controls:target:) |
general, n-qubit | mcx(controls, target) |
Flips target iff every qubit in controls is 1, built via Matrix.permutation. controls may be empty (an unconditional flip, equivalent to x); one control is equivalent to cx, two to ccx. Traps if target is out of range, any control is out of range or duplicated, or controls contains target. |
ControlledZGate.matrix / matrix(qubits:control:target:) |
fixed 2-qubit / general | cz(control, target) |
CZ, diag(1,1,1,−1); symmetric in its two qubits. Equals h(t); cx(c,t); h(t). |
MultiControlledZGate.matrix(qubits:controls:target:) |
general, n-qubit | mcz(controls, target) |
Negates every basis state where all controls and the target are 1. controls may be empty (equivalent to z); one control is cz, two is CCZ. |
ControlledYGate.matrix / matrix(qubits:control:target:) |
fixed 2-qubit / general | cy(control, target) |
CY, applies Pauli-Y to the target when the control is 1. Equals sdg(t); cx(c,t); s(t). |
MultiControlledYGate.matrix(qubits:controls:target:) |
general, n-qubit | mcy(controls, target) |
Applies Pauli-Y to target iff every qubit in controls is 1. controls may be empty (equivalent to y); one control is cy. |
SwapGate.matrix / matrix(qubits:q0:q1:) |
fixed 2-qubit / general | swap(q0, q1) |
Exchanges two qubits, built via Matrix.permutation. Equals three alternating cx gates. |
* QuantumCircuit.cx/ccx always call the general CNOTGate.matrix(qubits:control:target:)/
ToffoliGate.matrix(qubits:control1:control2:target:) forms, not the fixed-size ones — the
fixed forms are exposed separately for direct use as standalone gates. rzz/rxx/ryy work
on any distinct pair of qubits on an n-qubit circuit (not just adjacent ones) — see
pauliRotation below.
Rotation gates satisfy RA(2π) = -I (a full turn is minus identity) and
RA(π) = -i·A up to that same global phase, for the corresponding Pauli matrix A.
Note: the library has no built-in CZ or SWAP gate. Build these with apply(_:) and a
hand-constructed permutation or product of existing gates (several playground pages — e.g.
11GroverExample, 13Teleportation — show the pattern).
Sources/SwiftQiskit/Circuit/QuantumCircuit.swift — records gate operations as full 2ⁿ×2ⁿ
matrices and replays them on demand.
public final class QuantumCircuit {
public let qubits: Int
public init(qubits: Int) // traps if qubits <= 0 or > 30 (16 B × 2ⁿ of amplitudes)
public func apply(_ matrix: Matrix) // full-dimension gate; traps on dimension mismatch
}Because it's a final class, assigning or passing a QuantumCircuit shares the same
underlying operation list — it does not copy.
func h(_ qubit: Int)
func x(_ qubit: Int)
func y(_ qubit: Int)
func z(_ qubit: Int)
func s(_ qubit: Int)
func sdg(_ qubit: Int)
func t(_ qubit: Int)
func tdg(_ qubit: Int)
func p(_ theta: Double, _ qubit: Int)
func rx(_ theta: Double, _ qubit: Int)
func ry(_ theta: Double, _ qubit: Int)
func rz(_ theta: Double, _ qubit: Int)
func cx(_ control: Int, _ target: Int)
func ccx(_ control1: Int, _ control2: Int, _ target: Int)
func mcx(_ controls: [Int], _ target: Int)
func cz(_ control: Int, _ target: Int)
func mcz(_ controls: [Int], _ target: Int)
func cy(_ control: Int, _ target: Int)
func mcy(_ controls: [Int], _ target: Int)
func swap(_ q0: Int, _ q1: Int)
func rzz(_ theta: Double, _ q0: Int, _ q1: Int)
func rxx(_ theta: Double, _ q0: Int, _ q1: Int)
func ryy(_ theta: Double, _ q0: Int, _ q1: Int)
func pauliRotation(_ pauli: String, theta: Double)
func rotateToZ(_ basis: PauliBasis, _ qubit: Int)
func evolve(_ hamiltonian: Hamiltonian, time: Double, steps: Int, order: Int = 1)
func increment(register: [Int], controlledBy control: Int? = nil)
func decrement(register: [Int], controlledBy control: Int? = nil)Single-qubit gates are embedded across the full register via Matrix.tensor(_:)
(the file-private embedSingleQubitGate), so calling h(1) on a 3-qubit circuit builds and
applies I ⊗ H ⊗ I under the hood.
pauliRotation(_:theta:) applies exp(-iθ·P/2) for an arbitrary Pauli string (one character
per qubit, I/X/Y/Z only, indexed the same way as Ket("01") — character i acts on
qubit i). Traps if pauli.count != qubits or the string contains any other character.
Built entirely from existing gate methods: a basis change into Z on every non-I qubit
(h for X, sdg;h for Y), a CNOT staircase computing the parity of every active qubit into
the last one, a single rz there, the staircase undone, then the basis change undone. An
all-I string applies only the global phase e^{-iθ/2}·I (via apply(_:)), matching
Matrix.expm() on the same generator exactly rather than dropping the phase.
rzz/rxx/ryy(theta, q0, q1) are thin wrappers building the two-character Pauli string
and calling pauliRotation — they work on any distinct pair of qubits, not just adjacent
ones (traps if q0 == q1 or either is out of range).
rotateToZ(_ basis: PauliBasis, _ qubit: Int) appends the rotation that turns a subsequent
Z-basis measurement of qubit into a measurement in basis: h for .x, sdg;h for .y,
nothing for .z. pauliRotation's own basis-change step is built on this (and its
private inverse, rotateFromZ).
evolve(_ hamiltonian:time:steps:order:) appends steps repetitions of a Trotterized
product formula for exp(-i·hamiltonian·time), one pauliRotation call per term. order: 1 (default) applies every term in the order hamiltonian.terms lists them, each scaled by
the full step dt = time/steps; order: 2 is a second-order (Strang/Suzuki) step — every
term but the last at half dt, the last term at full dt, then every term but the last
again at half dt in reverse. There is no automatic grouping into commuting layers — the
caller controls layering by ordering hamiltonian.terms; a caller who wants a grouped order
reorders first via Hamiltonian.commutingGroups() (below) and flattens the result into a
fresh Hamiltonian. (Unlike measureExpectation(of: Hamiltonian, shots:), which groups
automatically — reordering here would change already-pinned finite-step Trotter error
numbers, so it stays opt-in.) Traps if hamiltonian.qubits != qubits, steps <= 0, or
order isn't 1 or 2. Hamiltonian.trotterCircuit(time:steps:order:) (below) is a
QuantumCircuit(qubits:) + evolve(...) convenience.
increment(register:controlledBy:)/decrement(register:controlledBy:) implement
ripple-carry ±1 on register — a binary number stored most-significant qubit first
(register[0]), matching Core's qubit-0-is-MSB convention — via mcx: bit k flips iff
every less-significant bit already in register (and control, if given) is 1.
increment applies most-significant to least-significant so every flip's controls are read
before they're themselves touched; decrement is the exact inverse (each mcx is
self-inverse, so the reverse-order sequence inverts the composite unitary). register need
not be contiguous or in index order. Traps if register is empty, has a duplicate or
out-of-range qubit, or control is out of range or one of the register qubits.
| Method | Signature | Notes |
|---|---|---|
run() |
() -> StateVector |
Builds a fresh StateVector(qubits: qubits) (i.e. |0…0⟩) and replays every recorded operation. Gates are recorded when called (no 2ⁿ×2ⁿ matrix is built) and only applied here, each in place on the amplitudes in O(2ⁿ) time (parallel across cores for ≥ 2¹⁴ amplitudes) — calling run() twice gives the same result each time. A matrix passed to apply(_:) still takes the dense/permutation path. |
runAndMeasure() |
() -> Int |
run() then a single measure() on the result — collapses that local copy, not any circuit state (the circuit itself has no persistent state to collapse). |
measure(shots:) |
(Int) -> SimulationResult |
Traps if shots <= 0. Runs the circuit once to get the final probability distribution, then draws shots independent samples from it — it does not replay the whole circuit per shot, since a full measurement of a pure state never changes the probabilities of the underlying state that produced it. |
measure(shots:basis:) |
(Int, [PauliBasis]) -> SimulationResult |
Traps if basis.count != qubits. Appends each qubit's rotateToZ rotation to a copy of the recorded operations and measures that copy — the receiver's own operation list is untouched, so the same circuit can be measured in different bases without rebuilding it. |
measureExpectation(of:shots:) |
(PauliString, Int) -> Double |
Shot-based estimate of one Pauli term's expectation value, built on measure(shots:basis:) + SimulationResult.parityExpectation(qubits:). Traps if pauli.qubits != qubits. An all-I term returns pauli.coefficient exactly, with no sampling. |
measureExpectation(of:shots:) |
(Hamiltonian, Int) -> Double |
Groups hamiltonian.terms via Hamiltonian.commutingGroups() and spends shots once per group rather than once per term: every term in a qubit-wise-commuting group shares one combined basis, so one measure(shots:basis:) call yields every term's parityExpectation at once (samples hamiltonian.commutingGroups().count · shots times total — e.g. the H₂ Hamiltonian's six terms group into three settings). An all-I term still needs no sampling. Traps if hamiltonian.qubits != qubits. |
runDensityMatrix(noise:) |
(NoiseModel? = nil) -> DensityMatrix |
Replays every recorded operation on a DensityMatrix starting from |0…0⟩⟨0…0|, applying noise's single-/multi-qubit KrausChannel (whichever matches the gate's qubit count) to every qubit that gate touched, right after it. noise: nil gives the exact result — mathematically identical to DensityMatrix(run()). |
runTrajectories(noise:shots:) |
(NoiseModel, Int) -> SimulationResult |
The Monte-Carlo "quantum trajectories" unraveling of runDensityMatrix(noise:): runs shots independent pure-state simulations, each stochastically applying one Kraus operator (chosen with probability ‖Kᵢ|ψ⟩‖²) after every noisy gate, then measures once per shot. Converges to runDensityMatrix(noise:)'s probabilities as shots grows. Traps if shots <= 0. |
Each recorded operation also carries a short gate label ("H", "CX", "RX(1.571)", …) and
its own small local tensor (2^k×2^k for the k qubits it touches, un-embedded), consumed by
TensorNetwork below rather than by run()/the execution methods above.
Sources/SwiftQiskit/Circuit/TensorNetwork.swift — a tensor-network view of a
QuantumCircuit's currently recorded operations: one node per |0⟩ input, one per recorded
gate, one per open output leg, wired together by one edge per wire segment.
public struct TensorNetwork {
public enum Kind: Equatable {
case input
case gate(String) // the gate's QuantumCircuit label, e.g. "CX", "RX(1.571)"
case output
}
public struct Node {
public let id: Int
public let kind: Kind
public let qubits: [Int] // leg order; empty only for a leg-less global-phase node
public let column: Int // ASAP-scheduled layout column
public let tensor: Matrix // 2^qubits.count × 2^qubits.count (1×1 if qubits is empty)
}
public struct Edge {
public let id: Int
public let qubit: Int
public let from: Int // node id
public let to: Int // node id
}
public let qubits: Int
public let nodes: [Node]
public let edges: [Edge]
public let columns: Int
public init(_ circuit: QuantumCircuit)
public func contract() -> StateVector
}init(_:) places one .input node per qubit at column 0, one .gate node per recorded
operation (ASAP-scheduled: a gate's column is one more than the latest column already reached
on any qubit it touches, so gates on disjoint qubits can share a column), and one .output
node per qubit at the final column — each gate wired to the most recent node on every qubit it
touches. contract() evaluates the network from |0…0⟩ through every .gate node in
recorded order, applying only each node's own small tensor to the legs it names — never
the full embedded matrices run() uses — so agreement between contract() and run() is an
independent check of both.
Sources/SwiftQiskit/Quantum/SimulationResult.swift — shot counts from
QuantumCircuit.measure(shots:).
public struct SimulationResult {
public let shots: Int
public let counts: [String: Int] // zero-padded binary state -> count
public var sortedCounts: [(state: String, count: Int)] { get } // ascending by state string
}Keys in counts are zero-padded to qubits characters via String.leftPadding (below), and
qubit 0 is the leftmost character, matching the rest of the library's indexing convention.
| Member | Signature | Notes |
|---|---|---|
marginalCounts(over:) |
([Int]) -> [String: Int] |
Groups counts by a subset of qubits, summing out the rest. Keys are the bits of qubits, in the order given; only observed keys appear (unlike StateVector.marginalProbabilities, which always returns every key). Traps if qubits is empty, has a duplicate, or has an out-of-range index; also traps if the counts keys don't all share one length. |
parityExpectation(qubits:) |
([Int]) -> Double |
The ±1 parity-product average over counts — Σ count · (−1)^(number of 1 bits among qubits) / shots — the shot-based estimator for a Pauli-Z-string expectation value such as ⟨Z⊗Z⟩ across the listed qubits. |
Sources/SwiftQiskit/Quantum/PauliBasis.swift:
public enum PauliBasis: Character, CaseIterable {
case x = "X"
case y = "Y"
case z = "Z"
}A single-qubit Pauli measurement basis. The Character raw value interoperates with the
plain Pauli-string labels pauliRotation(_:theta:) already accepts (.x.rawValue == "X",
etc.). Used by QuantumCircuit.rotateToZ/measure(shots:basis:) above.
Sources/SwiftQiskit/Quantum/PauliString.swift — a single weighted Pauli tensor-product
term: coefficient · P₀ ⊗ P₁ ⊗ … ⊗ Pₙ₋₁, one label per qubit (nil = I).
public struct PauliString: Equatable, Hashable {
public let labels: [PauliBasis?] // nil = I; qubit 0 first (MSB)
public let coefficient: Double
public init(labels: [PauliBasis?], coefficient: Double = 1)
public init(_ label: String, coefficient: Double = 1) // e.g. "XIZ"; I/X/Y/Z only
public var qubits: Int { get }
public var activeQubits: [Int] { get } // indices with a non-nil label
public var label: String { get } // round-trips through init(_:coefficient:)
public var matrix: Matrix { get } // coefficient · (P₀ ⊗ P₁ ⊗ …)
public func expectation(_ state: StateVector) -> Double // coefficient · ⟨ψ|P₀⊗P₁⊗…|ψ⟩
public func isQubitWiseCommuting(with other: PauliString) -> Bool
}init(_:coefficient:) traps on an empty string or a character outside IXYZ.
expectation(_:) traps if state.dimension != 1 << qubits. Shares its label type with
QuantumCircuit.rotateToZ/pauliRotation(_:theta:) — label is exactly the string
pauliRotation accepts.
isQubitWiseCommuting(with:) — true iff, on every qubit, the two strings' labels agree or
at least one is I. Stricter than literal Pauli commutativity (which only needs an even
number of disagreeing qubits) but the condition that actually matters for
Hamiltonian.commutingGroups(): two QWC-compatible terms share one well-defined per-qubit
basis. Traps if qubits != other.qubits.
Sources/SwiftQiskit/Quantum/Hamiltonian.swift — a qubit Hamiltonian as a plain sum of
PauliString terms (no merging of duplicate labels; commutingGroups() below groups terms
without merging them).
public struct Hamiltonian: Equatable {
public let terms: [PauliString]
public init(_ terms: [PauliString]) // traps if empty or the terms disagree on qubit count
public var qubits: Int { get }
public var matrix: Matrix { get } // Σ term.matrix
public func expectation(_ state: StateVector) -> Double // Σ term.expectation(state)
public func trotterCircuit(time: Double, steps: Int, order: Int = 1) -> QuantumCircuit
public func commutingGroups() -> [[PauliString]]
}Replaces the entrywise "build a matrix, then add coefficient·term to it index by index"
idiom page 18VQE (and the app's VQE/Trotter chapters) hand-roll for an H₂-style
Hamiltonian. trotterCircuit(time:steps:order:) is QuantumCircuit(qubits:) +
QuantumCircuit.evolve(_:time:steps:order:) (see above) — a fresh circuit implementing
Trotterized time evolution exp(-i·self·time).
commutingGroups() partitions terms into groups where every pair is qubit-wise commuting
(PauliString.isQubitWiseCommuting(with:)), via a greedy heuristic (Qiskit's default
group_commuting strategy: each term joins the first existing group every member of which
it's compatible with, else starts a new group) — deterministic given terms' order, but not
guaranteed to minimize the group count. measureExpectation(of: Hamiltonian, shots:) (above)
uses it automatically; evolve/trotterCircuit don't (see there) — a caller opts in with
Hamiltonian(hamiltonian.commutingGroups().flatMap { $0 }). Example: the six-term H₂
Hamiltonian (II, ZI, IZ, ZZ, YY, XX) groups into three: {II, ZI, IZ, ZZ}, {YY}, {XX}.
Sources/SwiftQiskit/Quantum/ParameterShift.swift — the parameter-shift gradient rule,
generalized to any number of parameters and any real-valued cost closure, plus a minimal
gradient-descent optimizer built on it.
public enum ParameterShift {
static func gradient(at parameters: [Double], shift: Double = .pi / 2,
_ cost: ([Double]) -> Double) -> [Double]
static func gradient(of hamiltonian: Hamiltonian, at parameters: [Double],
shift: Double = .pi / 2,
ansatz: ([Double]) -> QuantumCircuit) -> [Double]
}
public enum GradientDescent {
public struct Result {
public let parameters: [Double]
public let value: Double // cost(parameters), the last entry of history
public let history: [Double] // cost at every visited parameter vector
public let parameterHistory: [[Double]] // the parameter vector at every history entry
public let iterations: Int
public let converged: Bool // true iff the gradient norm dropped below tolerance
}
static func minimize(initial: [Double], learningRate: Double = 0.1,
maxIterations: Int = 200, tolerance: Double = 1e-10,
cost: @escaping ([Double]) -> Double,
gradient: (([Double]) -> [Double])? = nil) -> Result
}ParameterShift.gradient computes, for each parameter k, [cost(θ + shift·eₖ) − cost(θ − shift·eₖ)] / (2·sin(shift)). This is exact, not a finite-difference
approximation, whenever every parameter enters cost as the angle of a single
exp(-iθP/2) rotation — the shape of every parameterized QuantumCircuit gate
(rx/ry/rz/rzz/rxx/ryy/pauliRotation). The default shift = π/2 makes the
divisor 1, matching the textbook rule [cost(θ+π/2) − cost(θ−π/2)] / 2 (page 18VQE's
own one-parameter derivation). cost may be exact (Hamiltonian.expectation) or shot-based
(QuantumCircuit.measureExpectation) — the rule doesn't care which. Traps if parameters
is empty or shift is a multiple of π (zero divisor). The of hamiltonian:/ansatz:
overload is a convenience for the common VQE shape, using
hamiltonian.expectation(ansatz(θ).run()) as the cost.
GradientDescent.minimize takes fixed-size steps of learningRate · gradient until the
gradient's Euclidean norm drops below tolerance or maxIterations is reached.
gradient defaults to ParameterShift.gradient(at:_:) applied to cost itself. No line
search, momentum, or COBYLA/Nelder-Mead — sufficient for the small, smooth landscapes a
VQE-style ansatz produces. Traps if initial is empty, learningRate <= 0, or
maxIterations <= 0. parameterHistory[i] is the parameter vector history[i] was
evaluated at, so zipping the two gives the full optimization trajectory (e.g. for a
landscape/descent-path chart, as page 18VQE's live view does).
Sources/SwiftQiskit/Quantum/StateTomography.swift — a caseless namespace turning
measure(shots:basis:) results into a reconstructed single-qubit Bloch vector.
public enum StateTomography {
static func estimate(qubit: Int, result: SimulationResult) -> Double
static func estimateBlochVector(of circuit: QuantumCircuit, qubit: Int, shots: Int)
-> (x: Double, y: Double, z: Double)
static func reconstructSingleQubit(x: Double, y: Double, z: Double, tolerance: Double = 1e-9)
-> (vector: (x: Double, y: Double, z: Double), isPhysical: Bool)
static func clampToPhysical(_ v: (x: Double, y: Double, z: Double))
-> (x: Double, y: Double, z: Double)
}| Member | Notes |
|---|---|
estimate(qubit:result:) |
(N₀ − N₁)/N for qubit, from a result already measured in the desired basis — a single-qubit wrapper over SimulationResult.parityExpectation(qubits:). |
estimateBlochVector(of:qubit:shots:) |
Runs three settings — every qubit measured in X, then Y, then Z, via measure(shots:basis:) — and returns qubit's estimated (x, y, z). Spends shots per setting (3·shots total). Never mutates circuit. Traps if qubit is out of range. |
reconstructSingleQubit(x:y:z:tolerance:) |
Packages a three-axis estimate as a vector plus isPhysical (|r| ≤ 1 + tolerance). A pure state's per-axis shot estimate lands outside the ball about half the time at any shot count — isPhysical == false there is expected, not a bug. |
clampToPhysical(_:) |
Rescales an out-of-ball vector onto the unit sphere (r → r/|r|); leaves an already-physical vector unchanged. A named rescale, not a maximum-likelihood or linear-inversion estimator. |
Returns plain (x:, y:, z:) tuples rather than a package-level BlochVector type, since
every playground page auto-imports its own BlochVector from
Playgrounds.playground/Sources/, which a same-named Core type would collide with.
Sources/SwiftQiskit/Quantum/DensityMatrix.swift — a mixed-state density matrix ρ, the
open-systems generalization of a pure StateVector. Every public initializer validates
that the wrapped matrix is square, 2ⁿ-sized, (approximately) Hermitian, and has trace ≈ 1
— the three defining properties of a physical density matrix.
public struct DensityMatrix: Equatable {
public let matrix: Matrix
public init(_ state: StateVector) // ρ = |ψ⟩⟨ψ|
public init(mixture: [(probability: Double, state: StateVector)]) // Σᵢ pᵢ|ψᵢ⟩⟨ψᵢ|
public init(matrix: Matrix) // validates Hermitian, trace 1
public var qubits: Int { get } // matrix.rows == 2^qubits
}| Member | Signature | Notes |
|---|---|---|
purity |
Double |
Tr(ρ²) — 1 for a pure state, 1/2ⁿ for the maximally mixed state on n qubits. |
probabilities |
[Double] |
The diagonal of ρ (real parts): the probability of each basis state. |
expectation(_:) |
(Matrix) -> Double |
Tr(ρA), the density-matrix analogue of StateVector.expectation(_:). |
fidelity(to:) |
(StateVector) -> Double |
⟨ψ|ρ|ψ⟩. Traps on a dimension mismatch. |
apply(_:) |
(Matrix) -> DensityMatrix |
UρU† — the density-matrix analogue of StateVector.apply(_:). Traps on a dimension mismatch. |
partialTrace(keeping:) |
([Int]) -> DensityMatrix |
Traces out every qubit not listed, returning the reduced density matrix on the kept qubits. As with StateVector.marginalProbabilities(over:), the kept qubits appear in the order given in the result's basis ordering, not necessarily ascending index. Traps if qubits is empty, has a duplicate, or has an out-of-range index. |
blochVector |
(x: Double, y: Double, z: Double)? |
(Tr(ρX), Tr(ρY), Tr(ρZ)), nil unless this is a single-qubit density matrix. Unlike a pure state's Bloch vector, |r| need not be 1 — it shrinks toward the origin as the state becomes more mixed. |
eigenvalues |
[Double] |
The (real, non-negative) eigenvalues of ρ, ascending. Computed via a real-symmetric embedding of the Hermitian matrix plus a small internal Jacobi eigenvalue solver — adequate for qubit-count sizes, not a general-purpose or performance-tuned routine. |
vonNeumannEntropy |
Double |
S(ρ) = −Σᵢ λᵢ log₂λᵢ, in bits. Uses the closed form from the Bloch vector's magnitude for a single qubit; falls back to eigenvalues otherwise. |
Sources/SwiftQiskit/Quantum/KrausChannel.swift — a quantum channel ρ' = Σᵢ KᵢρKᵢ†,
given by its Kraus operators.
public struct KrausChannel {
public let operators: [Matrix]
public init(operators: [Matrix]) // traps unless all square and the same size
func isTracePreserving(tolerance: Double = 1e-10) -> Bool
func apply(to rho: DensityMatrix) -> DensityMatrix
func apply(to rho: DensityMatrix, qubit: Int) -> DensityMatrix
}| Member | Notes |
|---|---|
isTracePreserving(tolerance:) |
true iff Σᵢ Kᵢ†Kᵢ ≈ I entrywise within tolerance — mirrors Matrix.isUnitary(tolerance:)'s style. |
apply(to:) |
ρ' = Σᵢ KᵢρKᵢ† on the whole register. The channel's operator size must match rho's dimension exactly. |
apply(to:qubit:) |
Embeds a single-qubit (2×2) channel onto one qubit of an n-qubit rho, the same idiom CNOTGate.matrix(qubits:control:target:) uses for a 2×2 gate. Traps unless operators[0].rows == 2. |
| Factory | Notes |
|---|---|
bitFlip(_ p:) |
Applies X with probability p. |
phaseFlip(_ p:) |
Applies Z with probability p. |
depolarizing(_ p:) |
Applies X, Y, or Z with equal probability p/4 each, leaving the qubit untouched with probability 1 - 3p/4. |
amplitudeDamping(_ gamma:) |
T1-style energy relaxation toward |0⟩: shrinks x/y by √(1-γ) and pulls z toward +1. |
phaseDamping(_ lambda:) |
T2-style pure dephasing: shrinks x/y by √(1-λ) with no energy loss (z is left exactly alone). |
All five take a probability/rate in [0, 1] and trap outside that range.
Sources/SwiftQiskit/Quantum/NoiseModel.swift — maps gate applications in a
QuantumCircuit to per-qubit KrausChannels, for runDensityMatrix(noise:)/
runTrajectories(noise:shots:) above.
public struct NoiseModel {
public let singleQubitGate: KrausChannel?
public let multiQubitGate: KrausChannel?
public init(singleQubitGate: KrausChannel? = nil, multiQubitGate: KrausChannel? = nil)
public static func uniform(_ channel: KrausChannel) -> NoiseModel
}singleQubitGate is applied to every qubit touched by a single-qubit gate; multiQubitGate
is applied to every qubit touched by a multi-qubit gate (cx, ccx, mcx, …) — each qubit
independently, not as one genuinely correlated multi-qubit channel. Either may be nil to
skip noise for that gate class. Both channels must be single-qubit (2×2) and
trace-preserving — init traps otherwise. uniform(_:) applies the same channel after
both gate classes.
Sources/SwiftQiskit/Utils/String+Padding.swift:
public extension String {
func leftPadding(toLength: Int, withPad character: Character) -> String
}Left-pads a string with character up to toLength (no-ops if already that long or
longer). This is really an internal implementation detail of measurement-result formatting
that happens to be public; don't build new API around it.
Sources/SwiftQiskitViews/ — a small SwiftUI library product, import SwiftQiskitViews,
depending only on SwiftQiskit. It holds the presentational types shared by the playground
and by the sibling SwiftQiskitApp repo, so Core itself stays UI-free. Deliberately not
folded into SwiftQiskit/namespaced there: a Core type named BlochVector would collide
with call sites that want the bare name (see StateTomography above, which returns a plain
tuple for the same reason).
public struct BlochVector: Equatable, Sendable {
public let x: Double
public let y: Double
public let z: Double
public init(_ state: StateVector) // 1-qubit pure state
public init(_ state: StateVector, qubit: Int) // reduced (partial-trace) qubit
public init?(_ rho: DensityMatrix) // 1-qubit density matrix; nil otherwise
public init(_ rho: DensityMatrix, qubit: Int) // reduced qubit via partialTrace(keeping:)
public init(x: Double, y: Double, z: Double) // raw coordinates, e.g. |r| < 1
public var magnitude: Double // 1 for a pure state, < 1 for mixed/entangled
public var theta: Double // polar angle from +Z, radians
public var phi: Double // azimuthal angle in the XY plane, radians
}CHSHChartView — a minimal, stateless 2D line/scatter View (Canvas-based), used by
pages 15CHSH/18VQE/21Trotter/22Walk for correlation curves, energy landscapes, and
distribution comparisons. See PLAYGROUNDSUPPORT.md/90LIVEVIEWHELP.md for usage snippets
and the live-view recipe.
TensorNetworkView — a stateless, circuit-aligned drawing of a TensorNetwork (above):
qubit wires run left to right, |0⟩ caps on the left, open output legs on the right, and
every gate sits in its own time column as a labelled box on the wires it touches (a
multi-qubit gate's box spans every row it touches, with a dashed overlay on any wire that
merely crosses the box without being one of its legs). Used by page 23TensorNetwork.
public struct TensorNetworkView: View {
public init(_ network: TensorNetwork, title: String = "Tensor Network", size: CGSize? = nil)
}A few capabilities that later playground pages need are still implemented inside
individual playground pages rather than in Sources/SwiftQiskit (see each page's plan doc
under PlaygroundDocs/):
- Mid-circuit or partial measurement — page
13Teleportationroutes around this with deferred-measurement-principle gates instead. - A real maximum-likelihood or linear-inversion state-tomography reconstruction
(
StateTomographyabove is a rescale, not this).
Proposed Core extensions for these and other areas, with rationale, are tracked in
STATUSandTODO.md under "Proposed Core extensions — ...". (Hamiltonian.trotterCircuit,
the increment/decrement register builders, open-systems support
(DensityMatrix/KrausChannel/NoiseModel/runDensityMatrix/runTrajectories), and the
shared SwiftQiskitViews module (BlochVector/CHSHChartView), previously listed here, are
now implemented — see QuantumCircuit.evolve/Hamiltonian.trotterCircuit,
QuantumCircuit.increment/decrement, DensityMatrix/KrausChannel/NoiseModel above,
and SwiftQiskitViews above.)