Skip to content

Release 0.8.0

Latest

Choose a tag to compare

@github-actions github-actions released this 22 May 19:47

What's Changed

Added

  • Full calc-mode gate parity on Qx.Register
    (plan: api-cleanup-phase-b).
    Eight gates that previously existed
    only in circuit mode are now available on Qx.Register as direct
    state-vector evolutions:

    • Qx.Register.cy/3 — controlled-Y
    • Qx.Register.crx/4, cry/4, crz/4 — controlled rotations
    • Qx.Register.cp/4 — controlled-phase
    • Qx.Register.swap/3, iswap/3 — two-qubit swaps
    • Qx.Register.cswap/4 — Fredkin (controlled-SWAP)
    • Qx.Register.u/5 — general single-qubit unitary

    All five controlled-target gates (cy, crx, cry, crz, cp)
    share an apply_controlled_target/4 internal helper that lifts a
    2×2 gate matrix into the full controlled two-qubit unitary via the
    internal Qx.Gates.controlled_gate factory and applies it to
    register.state.

  • Basis-explicit measurement on Qx.Qubit
    (plan: api-cleanup-phase-b).
    Qx.Qubit.measure_x/1, measure_y/1,
    and measure_z/1 return the probability distribution in the X, Y,
    and Z bases respectively (the Z form is an alias of the existing
    measure_probabilities/1 for symmetry). Maps directly to QAAL Mx,
    My, Mz. Implementation reuses the existing single-qubit gate
    pipeline (H for X-basis, Sdg ; H for Y-basis).

  • Named circuit recipes consolidated under Qx.Patterns
    (plan: api-cleanup-phase-b).
    New helpers
    Qx.Patterns.bell_state_circuit/1, ghz_state_circuit/1, and
    superposition_circuit/1. The top-level Qx.bell_state,
    Qx.ghz_state, and Qx.superposition now delegate to these
    helpers (no break — old call sites continue to work). Two of the
    three got new optional arguments: Qx.ghz_state(num_qubits \\ 3)
    and Qx.superposition(num_qubits \\ 1) — previously hardcoded to
    3 and 1 qubits respectively.

Changed

  • Qx.Behaviours.QuantumState now has callbacks (and an
    implementor).
    Previously a dead behaviour that no module
    implemented — Qx.Register now declares @behaviour Qx.Behaviours.QuantumState and provides every required callback.
    The callback list grew to cover the full Phase B gate surface
    (sdg, u, cy, swap, iswap, cp, crx, cry, crz,
    ccx, cswap); the new callbacks are listed under
    @optional_callbacks so future implementors with smaller surfaces
    (e.g. a 2-qubit-only subset) don't need to provide them. Going
    forward, adding a gate to the multi-qubit calc-mode surface is
    compile-time-enforced to update both the behaviour and Qx.Register.

    Single-qubit Qx.Qubit is not an implementor: its functions
    take (state) rather than (state, qubit_index), so the
    signature is structurally incompatible. Unifying both paradigms
    under one behaviour is a v1.0 redesign and is documented in the
    behaviour's @moduledoc.

  • Qx.Validation.validate_gate_name!/1 removed. It was dead code
    (called only from its own tests) with a stale known-gates list
    (missing CY/CRx/CRy/CRz/CP added in qaal-parity). The
    corresponding test block is removed from validation_test.exs.

  • Qx.Validation removed from mix.exs groups_for_modules.
    After Phase A's @doc false sweep, only valid_qubit?/2 and
    valid_register?/2 remain visible — too thin to warrant a top-level
    group. The module page itself is still reachable; the renamed
    "Utilities" group lists Qx.Math and Qx.StateInit.

  • Internal-only functions hidden from documentation
    (plan: api-cleanup-phase-a).
    A public-API audit found that several
    modules labelled "Low-Level Operations" or "Validation & Utilities"
    in mix.exs were exporting def functions that are used only inside
    the library. The following are now @moduledoc false or
    per-function @doc false:

    • Qx.Gates, Qx.Calc, Qx.CalcFast, Qx.Format, Qx.ResultBuilder
      entire modules tagged @moduledoc false. Removed from mix.exs
      groups_for_modules. Functions remain callable for advanced users;
      they just no longer appear in ExDoc or IDE auto-complete.
    • Qx.QuantumCircuit.add_gate, add_two_qubit_gate,
      add_three_qubit_gate, add_measurement
      @doc false. The
      user-facing API is Qx.h(qc, 0) etc.; these are internal helpers.
    • Qx.Validation validate_*! family (10 functions)@doc false.
      Internal Iron Law #7 contracts. The user-facing predicates
      Qx.Validation.valid_qubit?/2 and valid_register?/2 remain
      public.
    • Qx.Math.complex_to_tensor, tensor_to_complex, complex_matrix
      @doc false. The rest of Qx.Math (complex/2, identity/1,
      unitary?/1, probabilities/1) stays public.

    No call site breaks. Existing tests pass unchanged.

  • Qx.Qubit.draw_bloch/2 converted from a def wrapper to a
    defdelegate. Behaviour unchanged.

  • Qx.Error @moduledoc rewritten to be accurate. The previous
    text described it as a "base exception" Qx users could rescue to
    catch any Qx error. Elixir exceptions do not inherit, so
    rescue Qx.Error catches nothing today — the docstring now says
    so explicitly and lists every typed exception users actually need
    to rescue.

Deprecated

  • Qx.Math.basis_state/2 is deprecated — use
    Qx.StateInit.basis_state/3 instead. The two functions returned
    different types (Math was f32, StateInit is c64), and the StateInit
    form is the canonical one. The deprecated function emits a
    compile-time warning and is hidden from ExDoc; it will be removed
    in v1.0.

Fixed

  • Qx.QuantumCircuit.new now enforces the documented 1..20-qubit cap
    at both bounds (plan: api-cleanup-phase-a, finding D3).
    Previously:
    new(25) silently created an over-cap circuit (upper bound unchecked
    on this path); new(0) raised FunctionClauseError via a guard
    (lower bound untyped). Both paths now raise Qx.QubitCountError
    consistently — the function calls the internal validate_num_qubits!
    validator on every input. Closes Iron Law #7 holes at both bounds,
    surfaced by .claude/plans/public-api-audit/plan.md.

BREAKING

  • Typed errors at public API boundaries (Iron Law #7). Out-of-range
    qubit indices, duplicate qubit indices, classical-bit OOR, invalid
    conditional values, and unsupported gates now raise the matching
    Qx.*Error exception instead of FunctionClauseError, ArgumentError,
    or RuntimeError. Resolves C1/C2/C3 of
    .claude/audit/reports/arch-review.md.

    • Qx.QuantumCircuit.add_gate/4, add_two_qubit_gate/5,
      add_three_qubit_gate/6, add_measurement/3 now raise
      Qx.QubitIndexError for out-of-range or duplicate qubits, and
      Qx.ClassicalBitError for out-of-range classical-bit indices.
    • Qx.Operations.barrier/2 now raises Qx.QubitIndexError.
    • Qx.Operations.c_if/4 now raises Qx.ClassicalBitError
      (out-of-range bit) and Qx.ConditionalError (invalid value,
      non-function gate_fn, nested conditional).
    • Qx.Simulation.run/2 (and the Qx.run/2 delegate) now raises
      Qx.GateError, {:unsupported_gate, gate_name} instead of
      RuntimeError for unsupported gate names at any arity.
    • Qx.QuantumCircuit.set_state/2 now raises Qx.StateShapeError
      instead of ArgumentError on size or rank mismatch.
    • Qx and Qx.Operations docstring ## Raises sections updated
      accordingly.

    Migration: rescue clauses matching FunctionClauseError,
    ArgumentError, or RuntimeError at these public call sites must
    be updated to the matching Qx.*Error exception. The same try
    block can rescue Qx.Error to catch any Qx-raised exception.

    Known deferred (not fixed in 0.8.0): Qx.Validation
    (validate_qubits_different!, validate_state_shape!,
    validate_parameter!) still raises bare ArgumentError; Qx.Qubit
    and Qx.Register still raise ArgumentError from public functions;
    Qx.Operations.u/5 still fires FunctionClauseError for OOR qubit
    (its own bounds guard). These map to arch-review findings H1, M3,
    M4, M5 and are scheduled for a follow-on Iron Law #7 sweep.

Added

  • Qx.StateShapeError — new exception type raised by
    Qx.QuantumCircuit.set_state/2 when the supplied state vector's
    shape doesn't match {2^num_qubits}. Carries :actual and
    :expected size fields.

  • Qx.QubitIndexError {:duplicate, qubits} constructor. New
    exception/1 clause to raise on distinct-indices violations (e.g.
    CNOT with control == target, Toffoli with repeated qubits).
    Message: "Qubit indices must be distinct, got: [...]".

  • Controlled rotations: Qx.cy/3, Qx.crx/4, Qx.cry/4, Qx.crz/4
    (plan: qaal-parity).
    Standard controlled-Pauli-Y and controlled
    rotation gates, mapping directly to QAAL CY/CRx/CRy/CRz and
    OpenQASM 3 cy/crx/cry/crz. Simulation handlers reuse the
    existing two-qubit controlled_gate/4 contraction. The OpenQASM
    importer (Qx.Export.OpenQASM.from_qasm/1) now also recognises these
    gates — previously they were in the unsupported-stdgates set.

  • Basis-explicit measurement: Qx.measure_x/3, Qx.measure_y/3,
    Qx.measure_z/3 (plan: qaal-parity).
    Match QAAL Mx/My/Mz
    classical-outcome semantics: measure_x lowers to H ; Mz,
    measure_y lowers to Sdg ; H ; Mz, measure_z is an alias of
    measure/3 for symmetry. Note: Qx's simulator samples in the
    computational basis at end-of-circuit, so the post-measurement
    quantum state stays Z-basis-aligned (not rotated back into the
    X-/Y-basis eigenstate) — the classical outcome is what tutorials
    care about and matches QAAL.

  • Qx.Patterns sub-register overload (/2 arity)
    (plan: qaal-parity).
    h_all/2, x_all/2, y_all/2, z_all/2,
    measure_all/2, barrier_all/2 accept a list or Range of qubit
    indices in addition to the existing whole-circuit /1 form. Lets
    tutorials operate on a sub-register without re-deriving qubit ranges
    by hand: Qx.h_all(qc, 0..2), Qx.measure_all(qc, [0, 2]). Empty
    list/range is a no-op.

  • Qx.Patterns — composite circuit-building helpers. New module
    providing seven thin wrappers over Qx.Operations for the recurring
    "apply to every qubit" / "CNOT chain" motifs that appear in tutorials
    (Grover diffuser, Bernstein-Vazirani oracle, GHZ preparation):

    • Qx.Patterns.h_all/1, x_all/1, y_all/1, z_all/1 — apply the
      single-qubit gate to every qubit in the circuit.
    • Qx.Patterns.measure_all/1 — measure qubit i into classical bit
      i for all qubits. Raises Qx.ClassicalBitError if
      num_classical_bits < num_qubits (caller owns circuit shape — no
      auto-grow).
    • Qx.Patterns.barrier_all/1 — single barrier across every qubit.
    • Qx.Patterns.cx_chain/2 — linear CNOT cascade
      (cx(q0,q1) → cx(q1,q2) → …) along the supplied qubit list;
      [] and [q] are deliberate no-ops.

    All seven are also exposed at the top level (Qx.h_all/1,
    Qx.measure_all/1, …) via defdelegate. Purely additive — no
    breaking change. Out-of-range qubit indices propagate the existing
    typed Qx.QubitIndexError inherited from
    Qx.QuantumCircuit.add_* / Qx.Validation.

  • Configurable statevector renormalization + dev/test norm-drift
    guard in Qx.Simulation.run/2 / Qx.run/2 (qx-53v).
    New
    :renormalize option (default false — fully backwards compatible,
    zero cost when off): true renormalizes at measurement-time; a
    positive integer N renormalizes every N gates and at
    measurement-time; any other value raises the new typed
    Qx.OptionError. A compile-time-gated assertion
    (Application.compile_env(:qx, :assert_norm, false), on in :test,
    off in :prod/:dev) fails fast if a circuit's total probability
    drifts beyond 1.0e-6. Note: states are :c64 (float32), so the
    practical norm-accuracy floor is ~1e-7; renormalization bounds drift
    rather than eliminating it.

Installation

Add to your mix.exs:

def deps do
  [
    {:qx_sim, "~> 0.8.0"}
  ]
end

Documentation