From 64f2ad95e89f10c887dff8b9b4f1e2eda5306731 Mon Sep 17 00:00:00 2001 From: Brian Granger Date: Mon, 25 May 2026 14:31:48 -0700 Subject: [PATCH] Add single-qubit quantum system (kata qat6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A quantum-information-framed single (non-composite) qubit: the standard single-qubit gate set acting on the symbolic computational basis |b>, where the bit label may be a concrete bit, a symbol, or a boolean expression. - Rust: `Qubit` (Namespace impl, dual-cased gate accessors) and `qubit_system` / `qubit_system_for` builders in `crates/uhlenbeck/src/qubit.rs`; gates X/Y/Z/H/S/T, Sdag/Tdag, Rx/Ry/Rz, P; action rules over a wildcard bit fire on concrete, symbolic, and boolean labels; Pauli commutators, Hermitian/dagger rules, =δ inner products, completeness Σ|b>{0,1} coercion (the issue's main design question) needs no special machinery: boolean true/false are integers 1/0, so concrete bits fold inside pow/exp while symbolic/boolean labels stay symbolic. Follow-ups filed as kata #8agy (Python/evaluate path for inner-product rules) and #nyxn (triggering API for commutator_rules). Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/uhlenbeck/src/lib.rs | 4 + crates/uhlenbeck/src/qubit.rs | 448 +++++++++++++++++++++++ crates/uhlenbeck/tests/qubit.rs | 449 +++++++++++++++++++++++ notebooks/systems/qubit.ipynb | 516 +++++++++++++++++++++++++++ python/uhlenbeck/systems/__init__.py | 14 + python/uhlenbeck/systems/qubit.py | 353 ++++++++++++++++++ tests/test_qubit.py | 307 ++++++++++++++++ 7 files changed, 2091 insertions(+) create mode 100644 crates/uhlenbeck/src/qubit.rs create mode 100644 crates/uhlenbeck/tests/qubit.rs create mode 100644 notebooks/systems/qubit.ipynb create mode 100644 python/uhlenbeck/systems/__init__.py create mode 100644 python/uhlenbeck/systems/qubit.py create mode 100644 tests/test_qubit.py diff --git a/crates/uhlenbeck/src/lib.rs b/crates/uhlenbeck/src/lib.rs index c7e2cba..1d770d1 100644 --- a/crates/uhlenbeck/src/lib.rs +++ b/crates/uhlenbeck/src/lib.rs @@ -122,6 +122,7 @@ pub mod pow; pub mod product; pub mod qapply; pub mod quantum_system; +pub mod qubit; pub mod query; pub mod relation; pub mod replace; @@ -320,6 +321,9 @@ pub use quantum_system::{ // namespace.rs pub use namespace::{Namespace, SimpleNamespace}; +// qubit.rs +pub use qubit::{qubit_system, qubit_system_for, Qubit, QUBIT_NAMESPACE}; + // normal_order.rs pub use normal_order::normal_order; diff --git a/crates/uhlenbeck/src/qubit.rs b/crates/uhlenbeck/src/qubit.rs new file mode 100644 index 0000000..7f32464 --- /dev/null +++ b/crates/uhlenbeck/src/qubit.rs @@ -0,0 +1,448 @@ +//! Qubit — a quantum-information-framed **single** qubit (non-composite). +//! +//! Provides the standard single-qubit gate set (Pauli `X`/`Y`/`Z`, +//! Hadamard `H`, phase `S`/`T` and their adjoints `Sdag`/`Tdag`, +//! rotations `Rx`/`Ry`/`Rz`, and the phase gate `P`) acting on the +//! symbolic computational basis `|b>`, where the bit label `b` may be a +//! concrete bit (`0`/`1`), a symbol, or a boolean expression +//! (`xor`/`not`/`and`/`or` — see `spec/accepted/boolean.md`). +//! +//! Multi-qubit registers are *composite* and out of scope here; see the +//! parameterized N-qubit notebook for that. +//! +//! # Boolean ↔ {0,1} coercion +//! +//! The bit-flip gates rewrite the label with boolean `Not` +//! (`X·|b> → |¬b>`), while the phase gates need the label as the +//! integer `0`/`1` in expressions like `(−1)^b`, `i^b`, and +//! `exp(iπb/4)`. These two requirements are reconciled for free: +//! per `accepted/boolean.md`, boolean `true`/`false` **are** the +//! integers `1`/`0`, so a concrete-bit label collapses automatically +//! inside `pow` / `exp` (`not(0) = 1`, `(−1)^0 = 1`, `(−1)^1 = −1`, +//! `i^1 = i`, `exp(iπ·0/4) = 1`). A symbolic or boolean-expression +//! label leaves the phase symbolic (`(−1)^a`, `(−1)^(a⊕b)`), which is +//! the correct, consistent behavior. No special coercion machinery is +//! required. +//! +//! See kata issue `qat6` and `spec/accepted/quantum_systems.md`. + +use std::collections::HashMap; + +use crate::add::{add, sub}; +use crate::boolean::not_expr; +use crate::commutator::commutator; +use crate::context::{dummy, i, integer, neg_one, one, pi, rational, symbol, zero}; +use crate::dagger::dagger; +use crate::expr_ref::ExprRef; +use crate::functions::deltas::kronecker_delta; +use crate::functions::exp::exp; +use crate::functions::trig::{cos, sin}; +use crate::hilbert_space::HilbertSpace; +use crate::limits::definite; +use crate::mul::{div, mul, neg}; +use crate::namespace::Namespace; +use crate::ncmul::ncmul; +use crate::operator::operator_namespaced; +use crate::pattern::Rule; +use crate::pow::pow; +use crate::quantum_system::{ + ArgChannel, ArgLayout, DisplaySpec, HeadDisplay, NameOverride, OrderingPolicy, QuantumSystem, +}; +use crate::state::{bra_namespaced, inner_product, ket_namespaced, outer_product}; +use crate::sum::sum; + +/// The default namespace symbol name for the single-qubit system. +pub const QUBIT_NAMESPACE: &str = "qubit"; + +/// The state head name for computational-basis kets `|b>`. Suppressed +/// in display so `qubit(0)` renders as `|0>`, not `|b(0)>`. +const STATE_HEAD: &str = "b"; + +/// A handle for constructing single-qubit operators and computational +/// basis states in a given namespace. +/// +/// Implements [`Namespace`], so the generic `operator` / `ket` / `bra` +/// constructors are available alongside the gate-specific accessors +/// below. Each gate accessor comes in two casings that return the +/// **same** interned atom: a lowercase `snake_case` form (the +/// evcxr-kernel-safe default) and an UPPERCASE physics alias. +/// +/// # Example +/// +/// ```ignore +/// use std::sync::Arc; +/// use uhlenbeck::{register_quantum_system, qapply, Qubit}; +/// +/// let qb = Qubit::new(); +/// register_quantum_system(Arc::new(qb.system())).unwrap(); +/// // H|0> = (|0> + |1>)/√2 +/// let state = qapply(uhlenbeck::ncmul([qb.h(), qb.zero()]), false); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Qubit { + ns: ExprRef, +} + +impl Default for Qubit { + fn default() -> Self { + Self::new() + } +} + +impl Namespace for Qubit { + fn namespace(&self) -> ExprRef { + self.ns + } +} + +impl Qubit { + /// Construct a handle on the default `"qubit"` namespace. + pub fn new() -> Self { + Self { + ns: symbol(QUBIT_NAMESPACE), + } + } + + /// Construct a handle on an explicit namespace symbol. Useful for + /// registering more than one independent qubit in the same context + /// (each needs a distinct namespace). + pub fn with_namespace(ns: ExprRef) -> Self { + Self { ns } + } + + // ── Computational-basis states ─────────────────────────────────── + + /// The computational-basis ket `|b>`. `b` may be a concrete bit + /// (`0`/`1`), a symbol, or a boolean expression. + pub fn qubit(&self, b: ExprRef) -> ExprRef { + ket_namespaced(self.ns, STATE_HEAD, &[b]) + } + + /// The computational-basis bra ` ExprRef { + bra_namespaced(self.ns, STATE_HEAD, &[b]) + } + + /// `|0>`. + pub fn zero(&self) -> ExprRef { + self.qubit(zero()) + } + + /// `|1>`. + pub fn one(&self) -> ExprRef { + self.qubit(one()) + } + + // ── Gate accessors — lowercase (kernel-safe default) ───────────── + + /// Pauli X (bit-flip / NOT). + pub fn x(&self) -> ExprRef { + operator_namespaced(self.ns, "X", &[]) + } + /// Pauli Y. + pub fn y(&self) -> ExprRef { + operator_namespaced(self.ns, "Y", &[]) + } + /// Pauli Z (phase-flip). + pub fn z(&self) -> ExprRef { + operator_namespaced(self.ns, "Z", &[]) + } + /// Hadamard. + pub fn h(&self) -> ExprRef { + operator_namespaced(self.ns, "H", &[]) + } + /// Phase gate S (= √Z). + pub fn s(&self) -> ExprRef { + operator_namespaced(self.ns, "S", &[]) + } + /// T gate (= √S). + pub fn t(&self) -> ExprRef { + operator_namespaced(self.ns, "T", &[]) + } + /// S† — adjoint of the S gate (its own atom). + pub fn sdag(&self) -> ExprRef { + operator_namespaced(self.ns, "Sdag", &[]) + } + /// T† — adjoint of the T gate (its own atom). + pub fn tdag(&self) -> ExprRef { + operator_namespaced(self.ns, "Tdag", &[]) + } + /// X-rotation `Rx(θ)`. + pub fn rx(&self, theta: ExprRef) -> ExprRef { + operator_namespaced(self.ns, "Rx", &[theta]) + } + /// Y-rotation `Ry(θ)`. + pub fn ry(&self, theta: ExprRef) -> ExprRef { + operator_namespaced(self.ns, "Ry", &[theta]) + } + /// Z-rotation `Rz(θ)`. + pub fn rz(&self, theta: ExprRef) -> ExprRef { + operator_namespaced(self.ns, "Rz", &[theta]) + } + /// Phase gate `P(φ)` = diag(1, e^{iφ}). + pub fn p(&self, phi: ExprRef) -> ExprRef { + operator_namespaced(self.ns, "P", &[phi]) + } + + // ── Gate accessors — UPPERCASE physics aliases ─────────────────── + // + // Same interned atoms as the lowercase forms; for compiled / + // non-interactive use where the physics-canonical casing reads + // better. `#[allow(non_snake_case)]` per the dual-casing convention. + + #[allow(non_snake_case)] + pub fn X(&self) -> ExprRef { + self.x() + } + #[allow(non_snake_case)] + pub fn Y(&self) -> ExprRef { + self.y() + } + #[allow(non_snake_case)] + pub fn Z(&self) -> ExprRef { + self.z() + } + #[allow(non_snake_case)] + pub fn H(&self) -> ExprRef { + self.h() + } + #[allow(non_snake_case)] + pub fn S(&self) -> ExprRef { + self.s() + } + #[allow(non_snake_case)] + pub fn T(&self) -> ExprRef { + self.t() + } + #[allow(non_snake_case)] + pub fn Sdag(&self) -> ExprRef { + self.sdag() + } + #[allow(non_snake_case)] + pub fn Tdag(&self) -> ExprRef { + self.tdag() + } + #[allow(non_snake_case)] + pub fn Rx(&self, theta: ExprRef) -> ExprRef { + self.rx(theta) + } + #[allow(non_snake_case)] + pub fn Ry(&self, theta: ExprRef) -> ExprRef { + self.ry(theta) + } + #[allow(non_snake_case)] + pub fn Rz(&self, theta: ExprRef) -> ExprRef { + self.rz(theta) + } + #[allow(non_snake_case)] + pub fn P(&self, phi: ExprRef) -> ExprRef { + self.p(phi) + } + + /// Build the [`QuantumSystem`] data for this qubit's namespace. + /// Register the returned value via + /// [`Context::register_quantum_system`](crate::Context::register_quantum_system). + pub fn system(&self) -> QuantumSystem { + qubit_system_for(self.ns) + } +} + +/// Build the single-qubit [`QuantumSystem`] on the default `"qubit"` +/// namespace. +pub fn qubit_system() -> QuantumSystem { + qubit_system_for(symbol(QUBIT_NAMESPACE)) +} + +/// Build the single-qubit [`QuantumSystem`] on an explicit namespace. +/// +/// **Call before registering `ns`.** The dagger rule patterns are built +/// with the structural [`dagger`] constructor, which only stays +/// unevaluated (`Dagger(X)`) while `ns` has no registered system; once +/// registered, `dagger(X)` would fire the Hermitian self-rule and +/// corrupt the pattern. +pub fn qubit_system_for(ns: ExprRef) -> QuantumSystem { + let op = |name: &str, args: &[ExprRef]| operator_namespaced(ns, name, args); + let ket = |b: ExprRef| ket_namespaced(ns, STATE_HEAD, &[b]); + let bra = |b: ExprRef| bra_namespaced(ns, STATE_HEAD, &[b]); + + // Gate atoms. + let x = op("X", &[]); + let y = op("Y", &[]); + let z = op("Z", &[]); + let h = op("H", &[]); + let s = op("S", &[]); + let t = op("T", &[]); + let sdag = op("Sdag", &[]); + let tdag = op("Tdag", &[]); + + // Wildcards: `b_` for the bit label, `theta_` for a rotation angle, + // `m_`/`n_` for inner-product bit labels. + let b_ = symbol("b_"); + let th_ = symbol("theta_"); + let m_ = symbol("m_"); + let n_ = symbol("n_"); + + // Constants. + let im = i(); + let two = integer(2); + let four = integer(4); + let inv_sqrt2 = pow(two, rational(-1, 2)); + let m1 = neg_one(); + + // ── Action rules: written over a wildcard bit `b_` so they fire on + // concrete, symbolic, and boolean labels alike. ─────────────── + let action_rules = vec![ + // X|b> = |¬b> + Rule::new(ncmul([x, ket(b_)]), ket(not_expr(b_))), + // Y|b> = i·(−1)^b·|¬b> + Rule::new( + ncmul([y, ket(b_)]), + mul([im, pow(m1, b_), ket(not_expr(b_))]), + ), + // Z|b> = (−1)^b·|b> + Rule::new(ncmul([z, ket(b_)]), mul([pow(m1, b_), ket(b_)])), + // H|b> = (|0> + (−1)^b·|1>)/√2 + Rule::new( + ncmul([h, ket(b_)]), + mul([ + inv_sqrt2, + add([ket(zero()), mul([pow(m1, b_), ket(one())])]), + ]), + ), + // S|b> = i^b·|b> + Rule::new(ncmul([s, ket(b_)]), mul([pow(im, b_), ket(b_)])), + // T|b> = exp(iπb/4)·|b> + Rule::new( + ncmul([t, ket(b_)]), + mul([exp(div(mul([im, pi(), b_]), four)), ket(b_)]), + ), + // S†|b> = (−i)^b·|b> (conjugate phase of S) + Rule::new(ncmul([sdag, ket(b_)]), mul([pow(neg(im), b_), ket(b_)])), + // T†|b> = exp(−iπb/4)·|b> (conjugate phase of T) + Rule::new( + ncmul([tdag, ket(b_)]), + mul([exp(neg(div(mul([im, pi(), b_]), four))), ket(b_)]), + ), + // Rz(θ)|b> = exp(−iθ/2)·exp(iθb)·|b> + Rule::new( + ncmul([op("Rz", &[th_]), ket(b_)]), + mul([ + exp(neg(div(mul([im, th_]), two))), + exp(mul([im, th_, b_])), + ket(b_), + ]), + ), + // Rx(θ)|b> = cos(θ/2)|b> − i·sin(θ/2)|¬b> + Rule::new( + ncmul([op("Rx", &[th_]), ket(b_)]), + sub( + mul([cos(div(th_, two)), ket(b_)]), + mul([im, sin(div(th_, two)), ket(not_expr(b_))]), + ), + ), + // Ry(θ)|b> = cos(θ/2)|b> + (−1)^b·sin(θ/2)|¬b> + Rule::new( + ncmul([op("Ry", &[th_]), ket(b_)]), + add([ + mul([cos(div(th_, two)), ket(b_)]), + mul([pow(m1, b_), sin(div(th_, two)), ket(not_expr(b_))]), + ]), + ), + // P(φ)|b> = exp(iφb)·|b> + Rule::new( + ncmul([op("P", &[th_]), ket(b_)]), + mul([exp(mul([im, th_, b_])), ket(b_)]), + ), + ]; + + // ── Inner products: = δ_{b',b}. For concrete bits the + // Kronecker delta folds to 0/1; for symbolic/boolean labels it + // stays symbolic (equality of bits). ─────────────────────────── + let inner_product_rules = vec![Rule::new( + inner_product(bra(m_), ket(n_)), + kronecker_delta(m_, n_), + )]; + + // ── Dagger: Pauli X/Y/Z and H are Hermitian (self-adjoint); S/T map + // to their dedicated dagger atoms; rotations and P invert their + // angle. ──────────────────────────────────────────────────────── + let dagger_rules = vec![ + Rule::new(dagger(x), x), + Rule::new(dagger(y), y), + Rule::new(dagger(z), z), + Rule::new(dagger(h), h), + Rule::new(dagger(s), sdag), + Rule::new(dagger(sdag), s), + Rule::new(dagger(t), tdag), + Rule::new(dagger(tdag), t), + Rule::new(dagger(op("Rx", &[th_])), op("Rx", &[neg(th_)])), + Rule::new(dagger(op("Ry", &[th_])), op("Ry", &[neg(th_)])), + Rule::new(dagger(op("Rz", &[th_])), op("Rz", &[neg(th_)])), + Rule::new(dagger(op("P", &[th_])), op("P", &[neg(th_)])), + ]; + + // ── Pauli algebra commutators, declared once in canonical operand + // order (X < Y < Z). The antisymmetric direction is handled by + // `commutator(...)` construction-time canonicalization. These are + // declarative data, consumed by `eval_commutator_once`. ───────── + let commutator_rules = vec![ + // [X, Y] = 2iZ + Rule::new(commutator(x, y), mul([two, im, z])), + // [Y, Z] = 2iX + Rule::new(commutator(y, z), mul([two, im, x])), + // [X, Z] = −2iY (equivalently [Z, X] = 2iY) + Rule::new(commutator(x, z), neg(mul([two, im, y]))), + ]; + + // ── Resolution of identity: Σ_{b=0}^{1} |b>, |1>, |b> + // render without a head label. + states.insert( + symbol(STATE_HEAD), + HeadDisplay { + plain: NameOverride::Suppressed, + latex: NameOverride::Suppressed, + args: ArgLayout::All(ArgChannel::Concat), + }, + ); + + QuantumSystem { + namespace: ns, + shorthand: None, + hilbert_space: HilbertSpace::Complex(two), + commutator_rules, + swap_rules: vec![], + action_rules, + inner_product_rules, + dagger_rules, + ordering: OrderingPolicy::None, + completeness, + display: DisplaySpec { operators, states }, + } +} diff --git a/crates/uhlenbeck/tests/qubit.rs b/crates/uhlenbeck/tests/qubit.rs new file mode 100644 index 0000000..3ba930a --- /dev/null +++ b/crates/uhlenbeck/tests/qubit.rs @@ -0,0 +1,449 @@ +//! Integration tests for the single-qubit `QuantumSystem` (`Qubit`). +//! +//! Covers every gate acting on `|0>`, `|1>`, a symbolic `|b>`, and a +//! boolean-expression label; gate composition (`H·H = I`, `X·Z` vs +//! `Z·X`); daggers; inner products; the Pauli-algebra commutators; and +//! `DisplaySpec`-driven rendering. Each test registers on a unique +//! namespace to stay independent of the others. + +use std::sync::Arc; + +use uhlenbeck::expr_ref::ExprRef; +use uhlenbeck::{ + add, commutator, completeness_of, dagger, eval_commutator_once, eval_dagger_once, + eval_inner_product_once, expand, format_expr, i, integer, latex_expr, mul, ncmul, neg, neg_one, + pow, qapply, qubit_system_for, register_quantum_system as register_qs, symbol, + unregister_quantum_system as unregister_qs, xor_expr, zero, Namespace, QuantumSystem, Qubit, +}; + +/// Register a fresh single-qubit system on a unique namespace and +/// return the `(Qubit handle, Arc)`. The caller must +/// `unregister_qs(&sys)` when done. +fn register(ns_name: &str) -> (Qubit, Arc) { + let ns = symbol(ns_name); + let qb = Qubit::with_namespace(ns); + let sys = Arc::new(qubit_system_for(ns)); + register_qs(Arc::clone(&sys)).unwrap(); + (qb, sys) +} + +/// `qapply` with `dagger = false` — the common path for op·ket action. +fn apply(expr: ExprRef) -> ExprRef { + qapply(expr, false) +} + +// ════════════════════════════════════════════════════════════════════ +// Pauli X — bit flip via boolean Not +// ════════════════════════════════════════════════════════════════════ + +#[test] +fn x_flips_concrete_bits() { + let (qb, sys) = register("qb_x_concrete"); + assert_eq!(apply(ncmul([qb.x(), qb.zero()])), qb.one()); + assert_eq!(apply(ncmul([qb.x(), qb.one()])), qb.zero()); + unregister_qs(&sys); +} + +#[test] +fn x_on_symbolic_label_is_not_b() { + let (qb, sys) = register("qb_x_sym"); + let a = symbol("a"); + // X|a> = |¬a> + let expected = qb.qubit(uhlenbeck::not_expr(a)); + assert_eq!(apply(ncmul([qb.x(), qb.qubit(a)])), expected); + unregister_qs(&sys); +} + +#[test] +fn x_on_boolean_label() { + let (qb, sys) = register("qb_x_bool"); + let a = symbol("a"); + let b = symbol("b"); + let label = xor_expr([a, b]); + // X|a⊕b> = |¬(a⊕b)> + let expected = qb.qubit(uhlenbeck::not_expr(label)); + assert_eq!(apply(ncmul([qb.x(), qb.qubit(label)])), expected); + unregister_qs(&sys); +} + +#[test] +fn x_is_an_involution() { + let (qb, sys) = register("qb_x_invol"); + let a = symbol("a"); + // X·X|a> = |a> (¬¬a = a) + assert_eq!(apply(ncmul([qb.x(), qb.x(), qb.qubit(a)])), qb.qubit(a)); + unregister_qs(&sys); +} + +// ════════════════════════════════════════════════════════════════════ +// Pauli Z — phase flip +// ════════════════════════════════════════════════════════════════════ + +#[test] +fn z_phase_on_concrete_bits() { + let (qb, sys) = register("qb_z_concrete"); + // Z|0> = |0>, Z|1> = -|1> + assert_eq!(apply(ncmul([qb.z(), qb.zero()])), qb.zero()); + let neg_one_ket = mul([neg_one(), qb.one()]); + assert_eq!(apply(ncmul([qb.z(), qb.one()])), neg_one_ket); + unregister_qs(&sys); +} + +#[test] +fn z_phase_on_symbolic_label_stays_symbolic() { + let (qb, sys) = register("qb_z_sym"); + let a = symbol("a"); + // Z|a> = (-1)^a · |a> + let expected = mul([pow(neg_one(), a), qb.qubit(a)]); + assert_eq!(apply(ncmul([qb.z(), qb.qubit(a)])), expected); + unregister_qs(&sys); +} + +#[test] +fn z_phase_over_negated_label_stays_symbolic() { + let (qb, sys) = register("qb_z_notb"); + let a = symbol("a"); + // Z·X|a> = Z|¬a> = (-1)^(¬a)·|¬a>. The phase over a boolean (¬a) + // label does not collapse — it stays symbolic, the consistent + // counterpart to the concrete-bit case folding to ±1. + let not_a = uhlenbeck::not_expr(a); + let expected = mul([pow(neg_one(), not_a), qb.qubit(not_a)]); + assert_eq!(apply(ncmul([qb.z(), qb.x(), qb.qubit(a)])), expected); + unregister_qs(&sys); +} + +// ════════════════════════════════════════════════════════════════════ +// Pauli Y +// ════════════════════════════════════════════════════════════════════ + +#[test] +fn y_on_concrete_bits() { + let (qb, sys) = register("qb_y_concrete"); + // Y|0> = i·(-1)^0·|1> = i|1> + let y0 = apply(ncmul([qb.y(), qb.zero()])); + assert_eq!(y0, mul([i(), qb.one()])); + // Y|1> = i·(-1)^1·|0> = -i|0> + let y1 = apply(ncmul([qb.y(), qb.one()])); + assert_eq!(y1, mul([neg(i()), qb.zero()])); + unregister_qs(&sys); +} + +// ════════════════════════════════════════════════════════════════════ +// Hadamard +// ════════════════════════════════════════════════════════════════════ + +#[test] +fn h_on_concrete_bits() { + let (qb, sys) = register("qb_h_concrete"); + let inv_sqrt2 = pow(integer(2), uhlenbeck::rational(-1, 2)); + // H|0> = (|0> + |1>)/√2 + let h0 = apply(ncmul([qb.h(), qb.zero()])); + let expected0 = mul([inv_sqrt2, add([qb.zero(), qb.one()])]); + assert_eq!(h0, expected0); + // H|1> = (|0> - |1>)/√2 + let h1 = apply(ncmul([qb.h(), qb.one()])); + let expected1 = mul([inv_sqrt2, add([qb.zero(), mul([neg_one(), qb.one()])])]); + assert_eq!(h1, expected1); + unregister_qs(&sys); +} + +#[test] +fn h_squared_is_identity_on_basis() { + let (qb, sys) = register("qb_hh"); + // H·H = I, so H·H|0> = |0> and H·H|1> = |1> after expansion. + assert_eq!(expand(apply(ncmul([qb.h(), qb.h(), qb.zero()]))), qb.zero()); + assert_eq!(expand(apply(ncmul([qb.h(), qb.h(), qb.one()]))), qb.one()); + unregister_qs(&sys); +} + +// ════════════════════════════════════════════════════════════════════ +// Phase gates S, T and their daggers +// ════════════════════════════════════════════════════════════════════ + +#[test] +fn s_and_sdag_phases() { + let (qb, sys) = register("qb_s"); + // S|1> = i|1>; S†|1> = -i|1> + assert_eq!(apply(ncmul([qb.s(), qb.one()])), mul([i(), qb.one()])); + assert_eq!( + apply(ncmul([qb.sdag(), qb.one()])), + mul([neg(i()), qb.one()]) + ); + // Both leave |0> unchanged (i^0 = 1). + assert_eq!(apply(ncmul([qb.s(), qb.zero()])), qb.zero()); + assert_eq!(apply(ncmul([qb.sdag(), qb.zero()])), qb.zero()); + unregister_qs(&sys); +} + +#[test] +fn t_leaves_zero_unchanged() { + let (qb, sys) = register("qb_t"); + // T|0> = exp(0)·|0> = |0> + assert_eq!(apply(ncmul([qb.t(), qb.zero()])), qb.zero()); + // T|1> carries a non-trivial phase (just check it is not bare |1>). + assert_ne!(apply(ncmul([qb.t(), qb.one()])), qb.one()); + unregister_qs(&sys); +} + +#[test] +fn phase_gates_on_symbolic_label() { + let (qb, sys) = register("qb_phase_sym"); + let a = symbol("a"); + let im = i(); + // S|a> = i^a·|a> + assert_eq!( + apply(ncmul([qb.s(), qb.qubit(a)])), + mul([pow(im, a), qb.qubit(a)]) + ); + // T|a> = exp(iπa/4)·|a> + assert_eq!( + apply(ncmul([qb.t(), qb.qubit(a)])), + mul([ + uhlenbeck::exp(uhlenbeck::div(mul([im, uhlenbeck::pi(), a]), integer(4))), + qb.qubit(a) + ]) + ); + // S†|a> = (−i)^a·|a> + assert_eq!( + apply(ncmul([qb.sdag(), qb.qubit(a)])), + mul([pow(neg(im), a), qb.qubit(a)]) + ); + unregister_qs(&sys); +} + +// ════════════════════════════════════════════════════════════════════ +// Parameterized gates: Rx, Ry, Rz, P +// ════════════════════════════════════════════════════════════════════ + +#[test] +fn rotations_and_phase_gate_apply() { + let (qb, sys) = register("qb_rot"); + let theta = symbol("theta"); + + // Rz(θ)|0> = exp(-iθ/2)·exp(0)·|0> = exp(-iθ/2)|0>. + let rz0 = apply(ncmul([qb.rz(theta), qb.zero()])); + let im = i(); + let expected_rz0 = mul([ + uhlenbeck::exp(neg(uhlenbeck::div(mul([im, theta]), integer(2)))), + qb.zero(), + ]); + assert_eq!(rz0, expected_rz0); + + // P(φ)|0> = exp(0)·|0> = |0>; P(φ)|1> = exp(iφ)|1>. + assert_eq!(apply(ncmul([qb.p(theta), qb.zero()])), qb.zero()); + let p1 = apply(ncmul([qb.p(theta), qb.one()])); + assert_eq!(p1, mul([uhlenbeck::exp(mul([im, theta])), qb.one()])); + + // Rx(θ)|0> = cos(θ/2)|0> - i·sin(θ/2)|1> (just check it branches). + let rx0 = apply(ncmul([qb.rx(theta), qb.zero()])); + assert!(uhlenbeck::is_add(rx0)); + + // Ry(θ)|0> branches too. + let ry0 = apply(ncmul([qb.ry(theta), qb.zero()])); + assert!(uhlenbeck::is_add(ry0)); + + unregister_qs(&sys); +} + +// ════════════════════════════════════════════════════════════════════ +// Composition / non-commutativity: X·Z vs Z·X +// ════════════════════════════════════════════════════════════════════ + +#[test] +fn xz_and_zx_differ_on_basis() { + let (qb, sys) = register("qb_xz"); + // X·Z|0> = X(|0>) = |1>. + let xz = apply(ncmul([qb.x(), qb.z(), qb.zero()])); + assert_eq!(xz, qb.one()); + // Z·X|0> = Z(|1>) = -|1>. + let zx = apply(ncmul([qb.z(), qb.x(), qb.zero()])); + assert_eq!(zx, mul([neg_one(), qb.one()])); + // They differ — XZ ≠ ZX. + assert_ne!(xz, zx); + unregister_qs(&sys); +} + +// ════════════════════════════════════════════════════════════════════ +// Daggers +// ════════════════════════════════════════════════════════════════════ + +#[test] +fn pauli_and_hadamard_are_hermitian() { + let (qb, sys) = register("qb_dag_herm"); + assert_eq!(dagger(qb.x()), qb.x()); + assert_eq!(dagger(qb.y()), qb.y()); + assert_eq!(dagger(qb.z()), qb.z()); + assert_eq!(dagger(qb.h()), qb.h()); + unregister_qs(&sys); +} + +#[test] +fn s_t_dagger_map_to_dedicated_atoms() { + let (qb, sys) = register("qb_dag_st"); + assert_eq!(dagger(qb.s()), qb.sdag()); + assert_eq!(dagger(qb.sdag()), qb.s()); + assert_eq!(dagger(qb.t()), qb.tdag()); + assert_eq!(dagger(qb.tdag()), qb.t()); + // Involution: dagger(dagger(S)) = S. + assert_eq!(dagger(dagger(qb.s())), qb.s()); + unregister_qs(&sys); +} + +#[test] +fn rotation_daggers_invert_the_angle() { + let (qb, sys) = register("qb_dag_rot"); + let theta = symbol("theta"); + assert_eq!(dagger(qb.rx(theta)), qb.rx(neg(theta))); + assert_eq!(dagger(qb.ry(theta)), qb.ry(neg(theta))); + assert_eq!(dagger(qb.rz(theta)), qb.rz(neg(theta))); + assert_eq!(dagger(qb.p(theta)), qb.p(neg(theta))); + unregister_qs(&sys); +} + +#[test] +fn eval_dagger_once_fires_on_s() { + let (qb, sys) = register("qb_eval_dag"); + assert_eq!(eval_dagger_once(qb.s()), Some(qb.sdag())); + // Hermitian self-rule. + assert_eq!(eval_dagger_once(qb.x()), Some(qb.x())); + unregister_qs(&sys); +} + +// ════════════════════════════════════════════════════════════════════ +// Inner products: = δ_{b',b} +// ════════════════════════════════════════════════════════════════════ + +#[test] +fn inner_products_of_concrete_bits() { + let (qb, sys) = register("qb_ip_concrete"); + // <0|0> = 1, <1|1> = 1, <0|1> = 0, <1|0> = 0. + assert_eq!( + eval_inner_product_once(qb.qubit_bra(zero()), qb.zero()), + Some(integer(1)) + ); + assert_eq!( + eval_inner_product_once(qb.qubit_bra(integer(1)), qb.one()), + Some(integer(1)) + ); + assert_eq!( + eval_inner_product_once(qb.qubit_bra(zero()), qb.one()), + Some(zero()) + ); + assert_eq!( + eval_inner_product_once(qb.qubit_bra(integer(1)), qb.zero()), + Some(zero()) + ); + unregister_qs(&sys); +} + +#[test] +fn inner_product_symbolic_is_kronecker() { + let (qb, sys) = register("qb_ip_sym"); + let a = symbol("a"); + let b = symbol("b"); + let got = eval_inner_product_once(qb.qubit_bra(a), qb.qubit(b)).unwrap(); + assert_eq!(got, uhlenbeck::kronecker_delta(a, b)); + unregister_qs(&sys); +} + +// ════════════════════════════════════════════════════════════════════ +// Pauli-algebra commutators (declarative data, via eval_commutator_once) +// ════════════════════════════════════════════════════════════════════ + +#[test] +fn pauli_commutators_in_canonical_order() { + let (qb, sys) = register("qb_comm"); + let (x, y, z) = (qb.x(), qb.y(), qb.z()); + let two_i = mul([integer(2), i()]); + // [X, Y] = 2iZ + assert_eq!(eval_commutator_once(x, y), Some(mul([two_i, z]))); + // [Y, Z] = 2iX + assert_eq!(eval_commutator_once(y, z), Some(mul([two_i, x]))); + // [Z, X] = 2iY + assert_eq!(eval_commutator_once(z, x), Some(mul([two_i, y]))); + unregister_qs(&sys); +} + +#[test] +fn pauli_commutator_reverse_negates() { + let (qb, sys) = register("qb_comm_rev"); + let (x, y) = (qb.x(), qb.y()); + let fwd = eval_commutator_once(x, y).unwrap(); + let rev = eval_commutator_once(y, x).unwrap(); + // [Y, X] = -[X, Y]. + assert_eq!(add([fwd, rev]), zero()); + unregister_qs(&sys); +} + +#[test] +fn commutator_construction_is_canonical() { + let (qb, sys) = register("qb_comm_construct"); + // The construction node canonicalizes operand order (X < Y). + assert_ne!(commutator(qb.x(), qb.y()), commutator(qb.y(), qb.x())); + unregister_qs(&sys); +} + +// ════════════════════════════════════════════════════════════════════ +// Completeness: Σ_{b=0}^{1} |b>"); + assert_eq!(format_expr(qb.one()), "|1>"); + assert_eq!(format_expr(qb.qubit(symbol("b"))), "|b>"); + unregister_qs(&sys); +} + +#[test] +fn pauli_gates_render_plainly() { + let (qb, sys) = register("qb_disp_op"); + assert_eq!(format_expr(qb.x()), "X"); + assert_eq!(format_expr(qb.h()), "H"); + assert_eq!(latex_expr(qb.z()), "Z"); + unregister_qs(&sys); +} + +#[test] +fn dagger_gate_display_overrides() { + let (qb, sys) = register("qb_disp_dag"); + assert_eq!(format_expr(qb.sdag()), "S†"); + assert_eq!(format_expr(qb.tdag()), "T†"); + assert_eq!(latex_expr(qb.sdag()), "S^{\\dagger}"); + assert_eq!(latex_expr(qb.tdag()), "T^{\\dagger}"); + unregister_qs(&sys); +} + +#[test] +fn rotation_latex_uses_subscripts() { + let (qb, sys) = register("qb_disp_rot"); + let theta = symbol("theta"); + // Plain uses stored "Rx" with function-call args. + assert_eq!(format_expr(qb.rx(theta)), "Rx(theta)"); + // LaTeX overrides to R_x and recognises the Greek arg. + assert_eq!(latex_expr(qb.rx(theta)), "R_x\\left(\\theta\\right)"); + unregister_qs(&sys); +} + +// ════════════════════════════════════════════════════════════════════ +// Default namespace constructor +// ════════════════════════════════════════════════════════════════════ + +#[test] +fn default_qubit_uses_qubit_namespace() { + let qb = Qubit::new(); + assert_eq!(qb.namespace(), symbol("qubit")); +} diff --git a/notebooks/systems/qubit.ipynb b/notebooks/systems/qubit.ipynb new file mode 100644 index 0000000..3fac585 --- /dev/null +++ b/notebooks/systems/qubit.ipynb @@ -0,0 +1,516 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9fcb434a-ab78-4bbb-8bdf-935a840145b2", + "metadata": {}, + "source": [ + "# The Single Qubit\n", + "\n", + "This notebook builds a quantum-information-framed **single qubit** as an\n", + "uhlenbeck `QuantumSystem` and walks through its gates, action rules,\n", + "daggers, commutators, and inner products.\n", + "\n", + "A qubit lives in $\\mathbb{C}^2$. Its computational basis is written\n", + "$|b\\rangle$, where the **bit label** $b$ may be\n", + "\n", + "- a concrete bit `0` or `1`,\n", + "- a symbol (an unknown bit), or\n", + "- a **boolean expression** built from `~`, `^`, `&`, `|`\n", + " (`not_`, `xor`, `and_`, `or_`).\n", + "\n", + "So `|a^b\\rangle` is a perfectly good basis ket whose label is $a\\oplus b$.\n", + "This is what makes the qubit *symbolic*: gate action rules are written\n", + "once over a wildcard bit and fire on every kind of label.\n", + "\n", + "> **Multi-qubit registers are composite and out of scope here.** This is\n", + "> a single, non-composite qubit; see the parameterized N-qubit notebook\n", + "> for registers." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "8c71a15b-e334-461f-a53a-517ec5161267", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Namespace(qubit)" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from uhlenbeck import qapply, expand, dagger, commutator, inner_product, symbols\n", + "from uhlenbeck.systems.qubit import Qubit\n", + "\n", + "a, b, theta = symbols(\"a b theta\")\n", + "\n", + "qb = Qubit() # builds and registers the system on namespace \"qubit\"\n", + "qb" + ] + }, + { + "cell_type": "markdown", + "id": "110d9e81-24d4-44ed-bf4d-aeae7aa6efc8", + "metadata": {}, + "source": [ + "## The computational basis\n", + "\n", + "`qb.qubit(b)` is the ket $|b\\rangle$; `qb.zero()` and `qb.one()` are\n", + "$|0\\rangle$ and $|1\\rangle$. The state head is suppressed in display, so\n", + "a ket renders as just its label between the bars — including symbolic and\n", + "boolean labels." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "6af5f332-9dc9-45ca-b058-5a50294466d1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$\\left(\\left|{0}\\right\\rangle, \\left|{1}\\right\\rangle, \\left|{a}\\right\\rangle, \\left|{a \\oplus b}\\right\\rangle\\right)$" + ], + "text/plain": [ + "(|0>, |1>, |a>, |a ^ b>)" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qb.zero(), qb.one(), qb.qubit(a), qb.qubit(a ^ b)" + ] + }, + { + "cell_type": "markdown", + "id": "11935f9e-77f3-46bf-912a-6ed4237d0551", + "metadata": {}, + "source": [ + "## The gate set\n", + "\n", + "Every gate has a **lowercase** accessor (the default, kernel-safe) and an\n", + "**UPPERCASE** physics alias that returns the *same* interned atom:\n", + "`qb.x()` is `qb.X()`. The set is Pauli `X`/`Y`/`Z`, Hadamard `H`, phase\n", + "gates `S`/`T` with their adjoints `S†`/`T†`, rotations `Rx`/`Ry`/`Rz`,\n", + "and the phase gate `P`." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "798dfed2-c4bd-4c5d-98b9-93aea9de4836", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$$\\left[\\begin{array}{l}X, \\\\ Y, \\\\ Z, \\\\ H, \\\\ S, \\\\ T, \\\\ S^{\\dagger}, \\\\ T^{\\dagger}\\end{array}\\right]$$" + ], + "text/plain": [ + "[X, Y, Z, H, S, T, S†, T†]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[qb.x(), qb.y(), qb.z(), qb.h(), qb.s(), qb.t(), qb.sdag(), qb.tdag()]" + ] + }, + { + "cell_type": "markdown", + "id": "7304000d-6b7c-42d0-8389-5d13191abc04", + "metadata": {}, + "source": [ + "## Gate action — `qapply`\n", + "\n", + "`qapply` applies operators to states from the right. The Pauli and phase\n", + "gates act on the basis as expected: `X` flips the bit, `Z` adds a sign on\n", + "$|1\\rangle$, `Y` does both with a factor of $i$, and `S`/`T` apply\n", + "diagonal phases." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9115c2b3-72e9-489d-911c-1ec2b2264e13", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$$\\left[\\begin{array}{l}\\left|{1}\\right\\rangle, \\\\ \\left|{0}\\right\\rangle, \\\\ -\\left|{1}\\right\\rangle, \\\\ i \\left|{1}\\right\\rangle, \\\\ i \\left|{1}\\right\\rangle, \\\\ \\operatorname{exp}\\left(\\frac{1}{4} i \\pi\\right) \\left|{1}\\right\\rangle\\end{array}\\right]$$" + ], + "text/plain": [ + "[|1>, |0>, -|1>, I*|1>, I*|1>, exp(1/4*I*pi)*|1>]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[qapply(qb.x() @ qb.zero()), # |1>\n", + " qapply(qb.x() @ qb.one()), # |0>\n", + " qapply(qb.z() @ qb.one()), # -|1>\n", + " qapply(qb.y() @ qb.zero()), # i|1>\n", + " qapply(qb.s() @ qb.one()), # i|1>\n", + " qapply(qb.t() @ qb.one())] # exp(iπ/4)|1>" + ] + }, + { + "cell_type": "markdown", + "id": "501f89b3-d822-4e27-a52c-8f649412341c", + "metadata": {}, + "source": [ + "## Symbolic and boolean bit labels\n", + "\n", + "This is the heart of the symbolic qubit. The action rules are written\n", + "once over a wildcard bit, so they fire on symbols and boolean\n", + "expressions too:\n", + "\n", + "- `X` flips with boolean **Not**: $X|b\\rangle = |\\lnot b\\rangle$.\n", + "- `Z` adds a phase $(-1)^b$.\n", + "\n", + "**Boolean ↔ {0,1} coercion.** Boolean `true`/`false` *are* the integers\n", + "`1`/`0`, so a concrete-bit label collapses automatically inside the\n", + "phases ($(-1)^0=1$, $(-1)^1=-1$, $\\lnot 0 = 1$), while a symbolic or\n", + "boolean label leaves the phase symbolic. No special machinery is\n", + "needed — the same rule handles all three label kinds." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "9a950ed3-6d40-43e5-b7ee-de0c6b041384", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$\\left[\\left|{\\neg a}\\right\\rangle, {\\left(-1\\right)}^{a} \\left|{a}\\right\\rangle, \\left|{\\neg \\left(a \\oplus b\\right)}\\right\\rangle, \\left|{a}\\right\\rangle\\right]$" + ], + "text/plain": [ + "[|~a>, (-1)**a*|a>, |~(a ^ b)>, |a>]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[qapply(qb.x() @ qb.qubit(a)), # |~a>\n", + " qapply(qb.z() @ qb.qubit(a)), # (-1)^a |a>\n", + " qapply(qb.x() @ qb.qubit(a ^ b)), # |~(a^b)>\n", + " qapply(qb.x() @ qb.x() @ qb.qubit(a))] # |a> (X is an involution)" + ] + }, + { + "cell_type": "markdown", + "id": "5c3c07f2-b0b1-487d-adeb-52576ef70a85", + "metadata": {}, + "source": [ + "## Superposition: the Hadamard gate\n", + "\n", + "`H` maps a basis state to an equal superposition,\n", + "$H|b\\rangle = (|0\\rangle + (-1)^b|1\\rangle)/\\sqrt 2$. Applying it twice\n", + "returns the identity — `expand` collapses the nested superposition back\n", + "to the original basis state." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "1ab87a5c-38ea-4e7b-b9b7-cfb698beaadf", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$$\\left(\\begin{array}{l}\\frac{1}{\\sqrt{2}} \\left(\\left|{0}\\right\\rangle + \\left|{1}\\right\\rangle\\right), \\\\ \\frac{1}{\\sqrt{2}} \\left(-\\left|{1}\\right\\rangle + \\left|{0}\\right\\rangle\\right)\\end{array}\\right)$$" + ], + "text/plain": [ + "(2**(-1/2)*(|0> + |1>), 2**(-1/2)*(-|1> + |0>))" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qapply(qb.h() @ qb.zero()), qapply(qb.h() @ qb.one())" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "6cbe8871-d292-4444-8884-5395bb453879", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$\\left(\\left|{0}\\right\\rangle, \\left|{1}\\right\\rangle\\right)$" + ], + "text/plain": [ + "(|0>, |1>)" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# H·H = I on the basis\n", + "expand(qapply(qb.h() @ qb.h() @ qb.zero())), expand(qapply(qb.h() @ qb.h() @ qb.one()))" + ] + }, + { + "cell_type": "markdown", + "id": "f2b46fdb-7ac6-4f46-9237-c3b0aa36193f", + "metadata": {}, + "source": [ + "## Composition and non-commutativity\n", + "\n", + "Gates stack as a non-commutative product; `qapply` reduces the whole\n", + "chain. Because $XZ \\neq ZX$, the two orders give different signs on the\n", + "same input." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "8d7403b5-626a-44ff-959e-a5a7b025bdae", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$\\left(\\left|{1}\\right\\rangle, -\\left|{1}\\right\\rangle\\right)$" + ], + "text/plain": [ + "(|1>, -|1>)" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qapply(qb.x() @ qb.z() @ qb.zero()), qapply(qb.z() @ qb.x() @ qb.zero())" + ] + }, + { + "cell_type": "markdown", + "id": "3e16fbc5-d3e5-4eeb-baac-88c175e4bbc1", + "metadata": {}, + "source": [ + "## Rotations and the phase gate\n", + "\n", + "The parameterized gates carry their angle as an argument and act\n", + "symbolically in it: $R_z(\\theta)$, $R_x(\\theta)$, $R_y(\\theta)$, and the\n", + "phase gate $P(\\varphi)$." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "97cfdf16-68ad-4ef3-be22-207dbb05a022", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$$\\left[\\begin{array}{l}-i \\operatorname{sin}\\left(\\frac{1}{2} \\theta\\right) \\left|{1}\\right\\rangle + \\operatorname{cos}\\left(\\frac{1}{2} \\theta\\right) \\left|{0}\\right\\rangle, \\\\ \\operatorname{exp}\\left(-\\frac{1}{2} i \\theta\\right) \\operatorname{exp}\\left(i \\theta\\right) \\left|{1}\\right\\rangle, \\\\ \\operatorname{exp}\\left(i \\theta\\right) \\left|{1}\\right\\rangle\\end{array}\\right]$$" + ], + "text/plain": [ + "[-I*sin(1/2*theta)*|1> + cos(1/2*theta)*|0>,\n", + " exp(-1/2*I*theta)*exp(I*theta)*|1>,\n", + " exp(I*theta)*|1>]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[qapply(qb.rx(theta) @ qb.zero()), # cos(θ/2)|0> − i sin(θ/2)|1>\n", + " qapply(qb.rz(theta) @ qb.one()), # exp(−iθ/2)exp(iθ)|1>\n", + " qapply(qb.p(theta) @ qb.one())] # exp(iφ)|1>" + ] + }, + { + "cell_type": "markdown", + "id": "96c8896b-7608-41f7-96b5-873cfec88e79", + "metadata": {}, + "source": [ + "## Daggers\n", + "\n", + "Pauli `X`/`Y`/`Z` and `H` are Hermitian (self-adjoint). `S`/`T` map to\n", + "their dedicated adjoint atoms `S†`/`T†`, and the rotations and phase gate\n", + "invert their angle: $R_x(\\theta)^\\dagger = R_x(-\\theta)$." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "c8de566c-cb89-4375-9bf7-23ecd29d9a1f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$$\\left[\\begin{array}{l}X, \\\\ S^{\\dagger}, \\\\ T^{\\dagger}, \\\\ R_x\\left(-\\theta\\right), \\\\ P\\left(-\\theta\\right)\\end{array}\\right]$$" + ], + "text/plain": [ + "[X, S†, T†, Rx(-theta), P(-theta)]" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[dagger(qb.x()), dagger(qb.s()), dagger(qb.t()), dagger(qb.rx(theta)), dagger(qb.p(theta))]" + ] + }, + { + "cell_type": "markdown", + "id": "e57b7098-fec2-4359-a505-30fd9694d1ed", + "metadata": {}, + "source": [ + "## The Pauli algebra — commutators\n", + "\n", + "The cyclic Pauli commutators $[X,Y]=2iZ$, $[Y,Z]=2iX$, $[Z,X]=2iY$ are\n", + "recorded on the system as **declarative data**. Constructing a\n", + "`commutator(...)` canonicalizes operand order; the stored rule table is\n", + "consumed by `eval_commutator_once` / `normal_order`." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "5c9d2ac6-bade-4988-b409-274852cdd437", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$$\\left[\\begin{array}{l}\\mathrm{Rule}\\left(\\left[X, Y\\right] \\to 2 i Z\\right), \\\\ \\mathrm{Rule}\\left(\\left[Y, Z\\right] \\to 2 i X\\right), \\\\ \\mathrm{Rule}\\left(\\left[X, Z\\right] \\to -2 i Y\\right)\\end{array}\\right]$$" + ], + "text/plain": [ + "[Rule([X, Y] -> 2*I*Z), Rule([Y, Z] -> 2*I*X), Rule([X, Z] -> -2*I*Y)]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qb.system.commutator_rules" + ] + }, + { + "cell_type": "markdown", + "id": "4acfcc85-5510-4cd5-9e5e-c8feab87ddca", + "metadata": {}, + "source": [ + "## Inner products and completeness\n", + "\n", + "Inner products of basis states are Kronecker deltas,\n", + "$\\langle b'|b\\rangle = \\delta_{b',b}$ (equality of bits), and the\n", + "resolution of identity is $\\sum_{b=0}^{1}|b\\rangle\\langle b|$." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "ee6582e0-523e-41a2-9469-7bc1936838ad", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$\\left(\\left\\langle{a}\\middle|{b}\\right\\rangle, \\sum_{b=0}^{1} \\left|{b}\\right\\rangle\\left\\langle{b}\\right|\\right)$" + ], + "text/plain": [ + "(, Sum(|b>, |0>)" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(expand(qapply(qb.h() @ qb.z() @ qb.h() @ qb.zero())),\n", + " expand(qapply(qb.h() @ qb.z() @ qb.h() @ qb.one())))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/python/uhlenbeck/systems/__init__.py b/python/uhlenbeck/systems/__init__.py new file mode 100644 index 0000000..ae2e72c --- /dev/null +++ b/python/uhlenbeck/systems/__init__.py @@ -0,0 +1,14 @@ +"""Prebuilt quantum systems for uhlenbeck. + +Each module under this package defines a concrete +:class:`~uhlenbeck.Namespace` subclass that builds and registers a +:class:`~uhlenbeck.QuantumSystem`. Import the system you need, e.g.:: + + from uhlenbeck.systems.qubit import Qubit + + qb = Qubit() +""" + +from uhlenbeck.systems.qubit import QUBIT_NAMESPACE, Qubit + +__all__ = ["Qubit", "QUBIT_NAMESPACE"] diff --git a/python/uhlenbeck/systems/qubit.py b/python/uhlenbeck/systems/qubit.py new file mode 100644 index 0000000..ce4d2ea --- /dev/null +++ b/python/uhlenbeck/systems/qubit.py @@ -0,0 +1,353 @@ +"""Single-qubit quantum system (quantum-information conventions). + +A quantum-information-framed **single** qubit (non-composite): the +standard single-qubit gate set acting on the symbolic computational +basis ``|b>``, where the bit label ``b`` may be a concrete bit +(``0``/``1``), a symbol, or a boolean expression (``xor``/``not_``/..., +or Python ``& | ^ ~`` — see ``spec/accepted/boolean.md``). + +Multi-qubit registers are *composite* and out of scope here; the +parameterized N-qubit notebook covers that. + +Boolean ↔ {0,1} coercion +------------------------ + +Bit-flip gates rewrite the label with boolean ``Not`` (``X|b> = |¬b>``), +while phase gates need the label as the integer ``0``/``1`` inside +``(−1)^b``, ``i^b``, ``exp(iπb/4)``. Both requirements are reconciled for +free: boolean ``true``/``false`` *are* the integers ``1``/``0``, so a +concrete-bit label collapses automatically inside ``pow``/``exp``, while +a symbolic or boolean-expression label leaves the phase symbolic. No +special coercion machinery is needed. + +This Python class mirrors the Rust ``Qubit`` / ``qubit_system`` in +``crates/uhlenbeck/src/qubit.rs``. +""" + +from uhlenbeck import ( + ArgChannel, + DisplaySpec, + HeadDisplay, + HilbertSpace, + Namespace, + OrderingPolicy, + QuantumSystem, + commutator, + cos, + dagger, + definite, + dummy, + exp, + inner_product, + integer, + kronecker_delta, + not_, + outer_product, + pi, + quantum_system_of, + rational, + rule, + sin, + sum_, + symbol, + I, +) + +__all__ = ["Qubit", "QUBIT_NAMESPACE"] + +#: Default namespace symbol name for the single-qubit system. +QUBIT_NAMESPACE = "qubit" + +#: State head name for computational-basis kets ``|b>`` (suppressed in +#: display so ``qubit(0)`` renders as ``|0>``). +_STATE_HEAD = "b" + + +class Qubit(Namespace): + """A handle for single-qubit operators and computational-basis states. + + Subclasses :class:`~uhlenbeck.Namespace`, so the generic + ``operator`` / ``ket`` / ``bra`` constructors are available alongside + the gate accessors below. Each gate accessor comes in two casings + that return the **same** interned atom: a lowercase ``snake_case`` + form (the default) and an UPPERCASE physics alias. + + Constructing a ``Qubit`` registers its :class:`QuantumSystem` in the + thread-local context (unless ``register=False``). Re-constructing a + ``Qubit`` on a namespace that already has a registered system reuses + that system rather than rebuilding it. + + Parameters + ---------- + namespace : str | Expr + The namespace identifier. Defaults to ``"qubit"``. + register : bool + Whether to register the system on construction. Default ``True``. + + Examples + -------- + >>> from uhlenbeck import qapply + >>> from uhlenbeck.systems.qubit import Qubit + >>> qb = Qubit() + >>> qapply(qb.x() @ qb.zero()) # X|0> = |1> + |1> + >>> qapply(qb.h() @ qb.zero()) # H|0> = (|0> + |1>)/√2 + 2**(-1/2)*(|0> + |1>) + """ + + def __init__(self, namespace=QUBIT_NAMESPACE, *, register=True): + super().__init__(namespace) + if register: + existing = quantum_system_of(self.operator("X")) + if existing is not None: + # A system is already registered on this namespace; reuse + # it rather than building (and re-registering) a clone. + self._system = existing + return + self._system = self._build_system() + if register: + self._system.register() + + # ── System / registration ─────────────────────────────────────── + + @property + def system(self): + """The underlying :class:`QuantumSystem`.""" + return self._system + + def register(self): + """Register the system (idempotent). Returns ``self``.""" + self._system.register() + return self + + def unregister(self): + """Unregister the system. Returns ``self``.""" + self._system.unregister() + return self + + # ── Computational-basis states ─────────────────────────────────── + + def qubit(self, b): + """The computational-basis ket ``|b>``.""" + return self.ket(_STATE_HEAD, b) + + def qubit_bra(self, b): + """The computational-basis bra ````.""" + return self.qubit(0) + + def one(self): + """``|1>``.""" + return self.qubit(1) + + # ── Gate accessors — lowercase default ─────────────────────────── + + def x(self): + """Pauli X (bit-flip / NOT).""" + return self.operator("X") + + def y(self): + """Pauli Y.""" + return self.operator("Y") + + def z(self): + """Pauli Z (phase-flip).""" + return self.operator("Z") + + def h(self): + """Hadamard.""" + return self.operator("H") + + def s(self): + """Phase gate S (= √Z).""" + return self.operator("S") + + def t(self): + """T gate (= √S).""" + return self.operator("T") + + def sdag(self): + """S† — adjoint of S (its own atom).""" + return self.operator("Sdag") + + def tdag(self): + """T† — adjoint of T (its own atom).""" + return self.operator("Tdag") + + def rx(self, theta): + """X-rotation ``Rx(θ)``.""" + return self.operator("Rx", theta) + + def ry(self, theta): + """Y-rotation ``Ry(θ)``.""" + return self.operator("Ry", theta) + + def rz(self, theta): + """Z-rotation ``Rz(θ)``.""" + return self.operator("Rz", theta) + + def p(self, phi): + """Phase gate ``P(φ)`` = diag(1, e^{iφ}).""" + return self.operator("P", phi) + + # ── Gate accessors — UPPERCASE physics aliases ─────────────────── + # Same interned atoms as the lowercase forms. + + def X(self): + return self.x() + + def Y(self): + return self.y() + + def Z(self): + return self.z() + + def H(self): + return self.h() + + def S(self): + return self.s() + + def T(self): + return self.t() + + def Sdag(self): + return self.sdag() + + def Tdag(self): + return self.tdag() + + def Rx(self, theta): + return self.rx(theta) + + def Ry(self, theta): + return self.ry(theta) + + def Rz(self, theta): + return self.rz(theta) + + def P(self, phi): + return self.p(phi) + + # ── System construction ────────────────────────────────────────── + + def _build_system(self): + """Build (but do not register) the qubit :class:`QuantumSystem`. + + Must run while this namespace has no registered system, so the + ``dagger(...)`` pattern builders stay unevaluated (``Dagger(X)``) + rather than firing the Hermitian self-rules. + """ + + def K(b): + return self.ket(_STATE_HEAD, b) + + def Bra(b): + return self.bra(_STATE_HEAD, b) + + b_ = symbol("b_") + th_ = symbol("theta_") + m_ = symbol("m_") + n_ = symbol("n_") + + X, Y, Z, H, S, T = self.x(), self.y(), self.z(), self.h(), self.s(), self.t() + Sdag, Tdag = self.sdag(), self.tdag() + + m1 = integer(-1) + inv_sqrt2 = integer(2) ** rational(-1, 2) + + # Action rules, written over a wildcard bit `b_` so they fire on + # concrete, symbolic, and boolean labels alike. + action_rules = [ + # X|b> = |¬b> + rule(X @ K(b_), K(not_(b_))), + # Y|b> = i·(−1)^b·|¬b> + rule(Y @ K(b_), I * m1**b_ * K(not_(b_))), + # Z|b> = (−1)^b·|b> + rule(Z @ K(b_), m1**b_ * K(b_)), + # H|b> = (|0> + (−1)^b·|1>)/√2 + rule(H @ K(b_), inv_sqrt2 * (K(0) + m1**b_ * K(1))), + # S|b> = i^b·|b> + rule(S @ K(b_), I**b_ * K(b_)), + # T|b> = exp(iπb/4)·|b> + rule(T @ K(b_), exp(I * pi * b_ / 4) * K(b_)), + # S†|b> = (−i)^b·|b> + rule(Sdag @ K(b_), (-I) ** b_ * K(b_)), + # T†|b> = exp(−iπb/4)·|b> + rule(Tdag @ K(b_), exp(-I * pi * b_ / 4) * K(b_)), + # Rz(θ)|b> = exp(−iθ/2)·exp(iθb)·|b> + rule(self.rz(th_) @ K(b_), exp(-I * th_ / 2) * exp(I * th_ * b_) * K(b_)), + # Rx(θ)|b> = cos(θ/2)|b> − i·sin(θ/2)|¬b> + rule( + self.rx(th_) @ K(b_), + cos(th_ / 2) * K(b_) - I * sin(th_ / 2) * K(not_(b_)), + ), + # Ry(θ)|b> = cos(θ/2)|b> + (−1)^b·sin(θ/2)|¬b> + rule( + self.ry(th_) @ K(b_), + cos(th_ / 2) * K(b_) + m1**b_ * sin(th_ / 2) * K(not_(b_)), + ), + # P(φ)|b> = exp(iφb)·|b> + rule(self.p(th_) @ K(b_), exp(I * th_ * b_) * K(b_)), + ] + + # = δ_{b',b} + inner_product_rules = [ + rule(inner_product(Bra(m_), K(n_)), kronecker_delta(m_, n_)), + ] + + # Pauli X/Y/Z and H are Hermitian; S/T map to their dagger atoms; + # rotations and P invert the angle. + dagger_rules = [ + rule(dagger(X), X), + rule(dagger(Y), Y), + rule(dagger(Z), Z), + rule(dagger(H), H), + rule(dagger(S), Sdag), + rule(dagger(Sdag), S), + rule(dagger(T), Tdag), + rule(dagger(Tdag), T), + rule(dagger(self.rx(th_)), self.rx(-th_)), + rule(dagger(self.ry(th_)), self.ry(-th_)), + rule(dagger(self.rz(th_)), self.rz(-th_)), + rule(dagger(self.p(th_)), self.p(-th_)), + ] + + # Pauli-algebra commutators, declared once in canonical operand + # order (X < Y < Z). Declarative data, consumed by + # eval_commutator_once / normal_order. + commutator_rules = [ + rule(commutator(X, Y), 2 * I * Z), # [X, Y] = 2iZ + rule(commutator(Y, Z), 2 * I * X), # [Y, Z] = 2iX + rule(commutator(X, Z), -2 * I * Y), # [X, Z] = -2iY + ] + + # Resolution of identity: Σ_{b=0}^{1} |b>`, `|1>`, a +symbolic `|b>`, and a boolean-expression label; gate composition +(`H·H = I`, `X·Z` vs `Z·X`); daggers; inner-product / commutator rule +data; and display. +""" + +import pytest + +from uhlenbeck import ( + I, + commutator, + cos, + dagger, + exp, + expand, + inner_product, + integer, + not_, + pi, + qapply, + rational, + sin, + symbols, + xor, +) +from uhlenbeck.systems.qubit import QUBIT_NAMESPACE, Qubit + + +@pytest.fixture(scope="module") +def qb(): + """A registered single-qubit system shared across the module.""" + system = Qubit("qb_pytest") + yield system + system.unregister() + + +# ════════════════════════════════════════════════════════════════════ +# Pauli X — bit flip via boolean Not +# ════════════════════════════════════════════════════════════════════ + + +class TestPauliX: + def test_flips_concrete_bits(self, qb): + assert qapply(qb.x() @ qb.zero()) == qb.one() + assert qapply(qb.x() @ qb.one()) == qb.zero() + + def test_symbolic_label_uses_not(self, qb): + a, = symbols("a") + assert qapply(qb.x() @ qb.qubit(a)) == qb.qubit(not_(a)) + + def test_boolean_label(self, qb): + a, b = symbols("a b") + label = xor(a, b) + assert qapply(qb.x() @ qb.qubit(label)) == qb.qubit(not_(label)) + + def test_involution(self, qb): + a, = symbols("a") + assert qapply(qb.x() @ qb.x() @ qb.qubit(a)) == qb.qubit(a) + + +# ════════════════════════════════════════════════════════════════════ +# Pauli Z / Y — phases +# ════════════════════════════════════════════════════════════════════ + + +class TestPauliZY: + def test_z_concrete(self, qb): + assert qapply(qb.z() @ qb.zero()) == qb.zero() + assert qapply(qb.z() @ qb.one()) == -qb.one() + + def test_z_symbolic_stays_symbolic(self, qb): + a, = symbols("a") + assert qapply(qb.z() @ qb.qubit(a)) == integer(-1) ** a * qb.qubit(a) + + def test_z_over_negated_label_stays_symbolic(self, qb): + a, = symbols("a") + # Z·X|a> = (-1)^(¬a)|¬a> — phase over a boolean label stays symbolic. + expected = integer(-1) ** not_(a) * qb.qubit(not_(a)) + assert qapply(qb.z() @ qb.x() @ qb.qubit(a)) == expected + + def test_y_concrete(self, qb): + # Y|0> = i|1>, Y|1> = -i|0> + assert qapply(qb.y() @ qb.zero()) == I * qb.one() + assert qapply(qb.y() @ qb.one()) == -I * qb.zero() + + +# ════════════════════════════════════════════════════════════════════ +# Hadamard +# ════════════════════════════════════════════════════════════════════ + + +class TestHadamard: + def test_h_on_basis(self, qb): + inv_sqrt2 = integer(2) ** rational(-1, 2) + assert qapply(qb.h() @ qb.zero()) == inv_sqrt2 * (qb.zero() + qb.one()) + assert qapply(qb.h() @ qb.one()) == inv_sqrt2 * (qb.zero() - qb.one()) + + def test_h_squared_is_identity_on_basis(self, qb): + assert expand(qapply(qb.h() @ qb.h() @ qb.zero())) == qb.zero() + assert expand(qapply(qb.h() @ qb.h() @ qb.one())) == qb.one() + + +# ════════════════════════════════════════════════════════════════════ +# Phase gates S, T and daggers +# ════════════════════════════════════════════════════════════════════ + + +class TestPhaseGates: + def test_s_and_sdag(self, qb): + assert qapply(qb.s() @ qb.one()) == I * qb.one() + assert qapply(qb.sdag() @ qb.one()) == -I * qb.one() + # i^0 = 1 leaves |0> alone. + assert qapply(qb.s() @ qb.zero()) == qb.zero() + assert qapply(qb.sdag() @ qb.zero()) == qb.zero() + + def test_t_on_zero_is_identity(self, qb): + assert qapply(qb.t() @ qb.zero()) == qb.zero() + # Non-trivial phase on |1>. + assert qapply(qb.t() @ qb.one()) != qb.one() + + def test_phase_gates_on_symbolic_label(self, qb): + a, = symbols("a") + # S|a> = i^a|a>, T|a> = exp(iπa/4)|a>, S†|a> = (−i)^a|a> + assert qapply(qb.s() @ qb.qubit(a)) == I**a * qb.qubit(a) + assert qapply(qb.t() @ qb.qubit(a)) == exp(I * pi * a / 4) * qb.qubit(a) + assert qapply(qb.sdag() @ qb.qubit(a)) == (-I) ** a * qb.qubit(a) + + +# ════════════════════════════════════════════════════════════════════ +# Parameterized gates: Rx, Ry, Rz, P +# ════════════════════════════════════════════════════════════════════ + + +class TestParameterizedGates: + def test_rz_and_p(self, qb): + theta, = symbols("theta") + # Rz(θ)|0> = exp(-iθ/2)|0> + assert qapply(qb.rz(theta) @ qb.zero()) == exp(-I * theta / 2) * qb.zero() + # P(φ)|0> = |0>; P(φ)|1> = exp(iφ)|1> + assert qapply(qb.p(theta) @ qb.zero()) == qb.zero() + assert qapply(qb.p(theta) @ qb.one()) == exp(I * theta) * qb.one() + + def test_rx_branches(self, qb): + theta, = symbols("theta") + # Rx(θ)|0> = cos(θ/2)|0> - i·sin(θ/2)|1> + expected = cos(theta / 2) * qb.zero() - I * sin(theta / 2) * qb.one() + assert qapply(qb.rx(theta) @ qb.zero()) == expected + + def test_ry_branches(self, qb): + theta, = symbols("theta") + # Ry(θ)|0> = cos(θ/2)|0> + sin(θ/2)|1> + expected = cos(theta / 2) * qb.zero() + sin(theta / 2) * qb.one() + assert qapply(qb.ry(theta) @ qb.zero()) == expected + + +# ════════════════════════════════════════════════════════════════════ +# Composition / non-commutativity +# ════════════════════════════════════════════════════════════════════ + + +class TestComposition: + def test_xz_vs_zx(self, qb): + # X·Z|0> = X|0> = |1>; Z·X|0> = Z|1> = -|1>. + xz = qapply(qb.x() @ qb.z() @ qb.zero()) + zx = qapply(qb.z() @ qb.x() @ qb.zero()) + assert xz == qb.one() + assert zx == -qb.one() + assert xz != zx + + +# ════════════════════════════════════════════════════════════════════ +# Daggers +# ════════════════════════════════════════════════════════════════════ + + +class TestDagger: + def test_pauli_hadamard_hermitian(self, qb): + assert dagger(qb.x()) == qb.x() + assert dagger(qb.y()) == qb.y() + assert dagger(qb.z()) == qb.z() + assert dagger(qb.h()) == qb.h() + + def test_s_t_dagger_atoms(self, qb): + assert dagger(qb.s()) == qb.sdag() + assert dagger(qb.sdag()) == qb.s() + assert dagger(qb.t()) == qb.tdag() + assert dagger(qb.tdag()) == qb.t() + assert dagger(dagger(qb.s())) == qb.s() + + def test_rotation_daggers_invert_angle(self, qb): + theta, = symbols("theta") + assert dagger(qb.rx(theta)) == qb.rx(-theta) + assert dagger(qb.ry(theta)) == qb.ry(-theta) + assert dagger(qb.rz(theta)) == qb.rz(-theta) + assert dagger(qb.p(theta)) == qb.p(-theta) + + +# ════════════════════════════════════════════════════════════════════ +# Inner products & commutators (rule data + construction) +# ════════════════════════════════════════════════════════════════════ + + +class TestInnerProductAndCommutator: + def test_inner_product_constructs(self, qb): + ip = inner_product(qb.qubit_bra(0), qb.one()) + assert ip.is_InnerProduct + + def test_inner_product_rule_present(self, qb): + assert len(qb.system.inner_product_rules) == 1 + + def test_commutator_construction_canonical(self, qb): + # Construction canonicalizes operand order (X < Y). + assert commutator(qb.x(), qb.y()) != commutator(qb.y(), qb.x()) + + def test_pauli_commutator_rules_present(self, qb): + assert len(qb.system.commutator_rules) == 3 + + def test_completeness_present(self, qb): + # Σ_{b=0}^{1} |b>" + assert repr(qb.one()) == "|1>" + assert repr(qb.qubit(b)) == "|b>" + + def test_pauli_plain(self, qb): + assert repr(qb.x()) == "X" + assert repr(qb.h()) == "H" + + def test_dagger_gate_display(self, qb): + assert repr(qb.sdag()) == "S†" + assert repr(qb.tdag()) == "T†" + assert qb.sdag()._repr_latex_() == "$S^{\\dagger}$" + assert qb.tdag()._repr_latex_() == "$T^{\\dagger}$" + + def test_rotation_latex_subscript(self, qb): + theta, = symbols("theta") + assert qb.rz(theta)._repr_latex_() == "$R_z\\left(\\theta\\right)$" + + +# ════════════════════════════════════════════════════════════════════ +# Construction / registration +# ════════════════════════════════════════════════════════════════════ + + +class TestConstruction: + def test_default_namespace(self): + from uhlenbeck import symbol + + q = Qubit(register=False) + assert q.namespace == symbol(QUBIT_NAMESPACE) + + def test_register_unregister_lifecycle(self): + q = Qubit("qb_lifecycle") + assert q.system.is_registered() + q.unregister() + assert not q.system.is_registered() + + def test_idempotent_reuse_on_same_namespace(self): + q1 = Qubit("qb_reuse") + q2 = Qubit("qb_reuse") + try: + # Second construction reuses the registered system. + assert q2.system == q1.system + finally: + q1.unregister() + + def test_subclass_of_namespace(self, qb): + from uhlenbeck import Namespace + + assert isinstance(qb, Namespace)