diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ba8189..5f9e1fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - **New demo `demos/larql_polysemantic_12/demo.py`** — runs 12 independent single-query Qiskit simulations at 1024 shots each and recovers the analytic polysemy scores (≈ 75% on in-feature concepts, ≈ 33.3% cross-talk floor on out-of-feature) within Monte-Carlo tolerance. - **Resource estimation** — new `## resources` section declares which static cost numbers the compiler should report (`gate_count`, `depth`, `cx_count`, `t_count`, `logical_qubits`); `## invariants` accepts the same five identifiers as bound LHS so a budget overrun becomes a verifier error (`RESOURCE_BOUND_EXCEEDED`) before any hardware run. Two new compiler entry points — `q_orca.estimate_resources(machine)` and `q_orca.compile_with_resources(machine)` — share one transpile pass per machine via `id(machine)` memoization. Verifier stage 4c (`resource_bounds`) is gated on the presence of resource invariants and can be skipped via `--skip-resource-bounds` or `VerifyOptions(skip_resource_bounds=True)`. Examples `bell-entangler`, `qaoa-maxcut`, and `vqe-heisenberg` pin their gate budgets. See `docs/language/resources.md`. - **Structured-overlap polysemantic example, demo, and Gram helper** — companion to `larql-polysemantic-12` that swaps the flat Hadamard dictionary for a *clustered* concept geometry on a compact 3-qubit register. `examples/larql-polysemantic-clusters.q.orca.md` encodes 12 concepts in 3 clusters of 4 (capitals, fruits, vehicles) via a multi-angle parametric `prepare_concept(a, b, c)` + `query_concept(a, b, c)` pair (intra-cluster overlap 0.72 uniform, inter-cluster < 0.10 — a block-structured Gram matrix). `demos/larql_polysemantic_clusters/demo.py` prints an ASCII Gram heatmap and recovers the three-tier polysemy column `1.0 / 0.72 / ≲ 0.09` empirically. New optional helper `q_orca.compute_concept_gram(machine, concept_action_label="query_concept")` returns the analytic N×N overlap matrix for any machine that follows the product-state preparation convention; raises `ConceptGramConfigurationError` when the convention is violated. +- **Hierarchical (MPS bond-2) polysemantic example, demo, and Gram helper** — rung-1 sibling of `larql-polysemantic-clusters` that lifts the 12-concept dictionary from product states to **bond-dimension-2 matrix product states** via a CNOT-staircase preparation `Ry(q0,α); CNOT(q0,q1); Ry(q1,β); CNOT(q1,q2); Ry(q2,γ)`. `examples/larql-polysemantic-hierarchical.q.orca.md` organizes 12 concepts as a two-level hierarchy (3 super-groups × 2 sub-clusters × 2 concepts) on the same 3-qubit register and produces a four-tier Gram matrix — self 1.000 / sub-cluster-mate 0.882 / super-group-sibling [0.47, 0.54] / cross-group [0.12, 0.25] — one tier richer than rung 0. `demos/larql_polysemantic_hierarchical/demo.py` prints a 4-tier ASCII Gram heatmap, recovers the polysemy column from 12 Qiskit circuits, and prints a side-by-side rung-0 vs rung-1 comparison. New optional helper `q_orca.compute_concept_gram_mps(machine, concept_action_label="query_concept", bond_dim=2)` enumerates the parametric call sites of a CNOT-staircase action, builds each concept statevector, and returns the analytic N×N overlap matrix; raises `MpsGramConfigurationError` (missing action, wrong signature, non-staircase effect, no call sites, or `bond_dim != 2`). ### Changed diff --git a/README.md b/README.md index 94f5f60..a8def1d 100644 --- a/README.md +++ b/README.md @@ -341,6 +341,7 @@ q-orca simulate examples/bell-entangler.q.orca.md --run --json | `larql-polysemantic-2.q.orca.md` | Concept projection over 2 non-orthogonal LARQL features (minimum mechanism) | | `larql-polysemantic-12.q.orca.md` | 12-call-site parametric concept projection with uniform-overlap dictionary | | `larql-polysemantic-clusters.q.orca.md` | Block-structured 12-concept polysemy on a 3-qubit register (3 clusters of 4) | +| `larql-polysemantic-hierarchical.q.orca.md` | Hierarchical 12-concept polysemy via bond-2 MPS encoding (3 super-groups × 2 sub-clusters × 2) | | `larql-gate-knn-grover.q.orca.md` | Grover-amplified gate-KNN lookup (per-layer kernel of LARQL inference) | ### Hybrid Classical + Quantum Demo @@ -587,6 +588,32 @@ parameter, one action template); use `larql-polysemantic-clusters` when you want the clustered-phenomenon signature on top of the same parametric mechanism. +#### Hierarchical polysemy + +The clusters example above is the rung-0 polysemantic encoding — concepts +prepared as **product states** on the 3-qubit register, producing three +flat overlap tiers (self / cluster-mate / cross-cluster). Real +sparse-autoencoder dictionaries report *graded* within-cluster similarity +and **sub-cluster structure** that flat block tiers cannot express. +[`examples/larql-polysemantic-hierarchical.q.orca.md`](examples/larql-polysemantic-hierarchical.q.orca.md) +lifts the encoding to **bond-dimension-2 matrix product states** via a +`Ry(q0,α); CNOT(q0,q1); Ry(q1,β); CNOT(q1,q2); Ry(q2,γ)` staircase. The +12 concepts are organized as a two-level hierarchy — 3 super-groups +(`animals`, `fruits`, `vehicles`) × 2 sub-clusters × 2 concepts — and the +Gram matrix splits into **four** ordered tiers: self 1.000 / +sub-cluster-mate 0.882 / super-group-sibling [0.47, 0.54] / cross-group +[0.12, 0.25]. + +The optional `q_orca.compiler.concept_gram_mps.compute_concept_gram_mps(machine)` +helper produces the Gram matrix for machines following the CNOT-staircase +convention (parallel to `compute_concept_gram` for product-state +machines). See [`demos/larql_polysemantic_hierarchical/demo.py`](demos/larql_polysemantic_hierarchical/demo.py) +for a run that prints the 4-tier Gram heatmap, recovers the polysemy +column from 12 Qiskit circuits, and prints a side-by-side rung-0 vs +rung-1 comparison. Background and the full ansatz ladder +(rungs 0–3) are in +[`docs/research/polysemantic-encoding-beyond-product-states.md`](docs/research/polysemantic-encoding-beyond-product-states.md). + --- ### Verify output (5-stage pipeline) diff --git a/demos/larql_polysemantic_hierarchical/demo.py b/demos/larql_polysemantic_hierarchical/demo.py new file mode 100644 index 0000000..302a916 --- /dev/null +++ b/demos/larql_polysemantic_hierarchical/demo.py @@ -0,0 +1,291 @@ +"""LARQL polysemantic-hierarchical demo (rung-1, MPS bond-2 encoding). + +End-to-end validation of the `LarqlPolysemanticHierarchical` state +machine, the rung-1 sibling of `larql_polysemantic_clusters`: + + 1. parse + verify the .q.orca.md (5-stage pipeline) + 2. compile to Mermaid + OpenQASM 3.0 + Qiskit + 3. analytic Gram matrix via `compute_concept_gram_mps` — visualized + as a 4-tier ASCII heatmap showing the two-level hierarchy + 4. for each of the 12 concepts, run a fresh prepare+query circuit + (feature = |dog>) and recover the polysemy column + 5. compare empirical vs. analytic polysemy columns; pass/fail on + |max_error| < 3 · mc_std + 6. side-by-side recap of rung-0 (flat 3-tier) vs. rung-1 (hierarchical + 4-tier) Gram signatures. + +Topology: 3 super-groups × 2 sub-clusters × 2 concepts = 12 concepts on a +3-qubit register. Each concept is encoded as the bond-2 MPS + + |c_i> = Ry(q0, α_i) CNOT(q0, q1) Ry(q1, β_i) CNOT(q1, q2) Ry(q2, γ_i) |000> + +with α ∈ {0, 2π/3, 4π/3} (super-group), β ∈ {-0.75, +0.75} (sub-cluster), +and γ ∈ {-0.35, +0.35} (concept). This produces analytic +`||²` tiers — self 1.0, sub-cluster-mate 0.882, super-group- +sibling [0.47, 0.54], cross-group [0.12, 0.25] — distinct from the +flat-block clusters demo's three uniform tiers. + +Usage: + pip install q-orca[quantum] + python demos/larql_polysemantic_hierarchical/demo.py +""" + +from __future__ import annotations + +from pathlib import Path + +import numpy as np + +from q_orca import ( + QSimulationOptions, + VerifyOptions, + compile_to_mermaid, + compile_to_qasm, + compile_to_qiskit, + compute_concept_gram_mps, + parse_q_orca_markdown, + verify, +) + +REPO_ROOT = Path(__file__).resolve().parents[2] +MACHINE_PATH = REPO_ROOT / "examples" / "larql-polysemantic-hierarchical.q.orca.md" + +# (concept_name, super_group, sub_cluster) — order matches transition order. +CONCEPTS = [ + ("dog", "animals", "mammals"), + ("cat", "animals", "mammals"), + ("robin", "animals", "birds"), + ("eagle", "animals", "birds"), + ("strawberry", "fruits", "berries"), + ("blueberry", "fruits", "berries"), + ("mango", "fruits", "tropical"), + ("papaya", "fruits", "tropical"), + ("car", "vehicles", "land"), + ("bike", "vehicles", "land"), + ("plane", "vehicles", "air"), + ("drone", "vehicles", "air"), +] +FEATURE_INDEX = 0 # |f> = |dog> +SHOTS = 1024 + + +def banner(title: str) -> None: + bar = "=" * 72 + print(f"\n{bar}\n {title}\n{bar}") + + +def heatmap_tier(value: float) -> str: + """4-tier ASCII heatmap: '#' ≥ 0.7, 'o' ∈ [0.3, 0.7), '.' ∈ [0.1, 0.3), blank < 0.1.""" + v = abs(value) + if v >= 0.7: + return "#" + if v >= 0.3: + return "o" + if v >= 0.1: + return "." + return " " + + +def print_gram_heatmap(gram: np.ndarray) -> None: + """Print |gram|² as a 4-tier 12×12 ASCII heatmap with hierarchy labels.""" + gsq = np.abs(gram) ** 2 + print(" |gram[i,j]|² (# ≥ 0.7, o ∈ [0.3, 0.7), . ∈ [0.1, 0.3), blank < 0.1)") + print(" ", "".join(f"{i:>3}" for i in range(12))) + for i in range(12): + row = "".join(f" {heatmap_tier(gsq[i, j])}" for j in range(12)) + name, sup, sub = CONCEPTS[i] + print(f" {i:>2} {row} ({name}, {sup}/{sub})") + + +def build_query_circuit(prepare_angles: tuple, query_angles: tuple): + """Build a prepare(feature) + query(concept) circuit on 3 qubits. + + Mirrors the .q.orca.md effect strings exactly: + prepare: Ry(q0, a); CNOT(q0,q1); Ry(q1, b); CNOT(q1,q2); Ry(q2, c) + query: Ry(q2,-c); CNOT(q1,q2); Ry(q1,-b); CNOT(q0,q1); Ry(q0,-a) + """ + from qiskit import QuantumCircuit + + qc = QuantumCircuit(3, 3) + a, b, c = prepare_angles + qc.ry(a, 0) + qc.cx(0, 1) + qc.ry(b, 1) + qc.cx(1, 2) + qc.ry(c, 2) + a2, b2, c2 = query_angles + qc.ry(-c2, 2) + qc.cx(1, 2) + qc.ry(-b2, 1) + qc.cx(0, 1) + qc.ry(-a2, 0) + qc.measure(range(3), range(3)) + return qc + + +def run_one_query(prepare_angles: tuple, query_angles: tuple, shots: int) -> float: + """Return empirical P(|000>) from `shots` shots.""" + from qiskit import transpile + from qiskit.providers.basic_provider import BasicSimulator + + qc = build_query_circuit(prepare_angles, query_angles) + backend = BasicSimulator() + tqc = transpile(qc, backend, basis_gates=["u3", "cx", "id"]) + result = backend.run(tqc, shots=shots).result() + counts = result.get_counts() + hit = counts.get("000", 0) + total = sum(counts.values()) + return hit / total if total else 0.0 + + +def main() -> None: + banner("LARQL polysemantic-hierarchical demo (rung-1, MPS bond-2)") + print(f" Machine source : {MACHINE_PATH.relative_to(REPO_ROOT)}") + print(f" Concepts : {len(CONCEPTS)} over 3 super-groups (animals, fruits, vehicles)") + print(" × 2 sub-clusters × 2 concepts (12 total)") + print(f" Feature loaded : |f> = |{CONCEPTS[FEATURE_INDEX][0]}>") + print(f" Shots / query : {SHOTS}") + + # 1. Parse + verify + banner("1. Verify state machine (Q-Orca pipeline)") + source = MACHINE_PATH.read_text() + parsed = parse_q_orca_markdown(source) + if parsed.errors: + for e in parsed.errors: + print(f" parse error: {e}") + raise SystemExit(1) + machine = parsed.file.machines[0] + print(f" Machine name : {machine.name}") + print(f" States : {len(machine.states)}") + print(f" Transitions : {len(machine.transitions)}") + parametric = [ + (a.name, [(p.name, p.type) for p in a.parameters]) + for a in machine.actions + if a.parameters + ] + print(f" Actions : {parametric}") + + call_sites = [t for t in machine.transitions if t.action == "query_concept"] + print(f" query_concept call sites : {len(call_sites)}") + + result = verify(machine, VerifyOptions(skip_dynamic=True)) + status = "VALID" if result.valid else "INVALID" + print(f" Verification : {status}") + err_count = sum(1 for e in result.errors if e.severity == "error") + warn_count = sum(1 for e in result.errors if e.severity == "warning") + print(f" Errors / warnings : {err_count} / {warn_count}") + + # 2. Compile + banner("2. Compile to Mermaid + OpenQASM 3.0 + Qiskit") + mermaid = compile_to_mermaid(machine) + qasm = compile_to_qasm(machine) + qiskit_script = compile_to_qiskit( + machine, + QSimulationOptions(analytic=False, shots=0, run=False, skip_qutip=True), + ) + print(f" Mermaid lines : {len(mermaid.strip().splitlines())}") + print(f" QASM lines : {len(qasm.strip().splitlines())}") + print(f" Qiskit lines : {len(qiskit_script.strip().splitlines())}") + print(f" QASM register : {'qubit[3] q;' in qasm}") + print( + f" qc.ry calls : {qiskit_script.count('qc.ry(')} " + f"(3 ry × 13 transitions = 39)" + ) + print( + f" qc.cx calls : {qiskit_script.count('qc.cx(')} " + f"(2 cx × 13 transitions = 26)" + ) + + # 3. Analytic Gram matrix (4-tier hierarchy) + banner("3. Analytic Gram matrix (4-tier hierarchy)") + gram = compute_concept_gram_mps(machine) + print_gram_heatmap(gram) + + gsq = np.abs(gram) ** 2 + sub_mate, super_sib, cross = [], [], [] + for i in range(12): + gi, si = i // 4, (i // 2) % 2 + for j in range(i + 1, 12): + gj, sj = j // 4, (j // 2) % 2 + v = gsq[i, j] + if gi == gj and si == sj: + sub_mate.append(v) + elif gi == gj: + super_sib.append(v) + else: + cross.append(v) + print() + print( + f" sub-cluster-mate ||² : min={min(sub_mate):.4f} " + f"max={max(sub_mate):.4f} (n={len(sub_mate)}; analytic ≈ 0.882)" + ) + print( + f" super-group-sib ||² : min={min(super_sib):.4f} " + f"max={max(super_sib):.4f} (n={len(super_sib)}; analytic [0.47, 0.54])" + ) + print( + f" cross-group ||² : min={min(cross):.4f} " + f"max={max(cross):.4f} (n={len(cross)}; analytic [0.12, 0.25])" + ) + + # 4. Per-concept polysemy column (|f> = |dog>) + banner("4. Per-concept polysemy column (12 independent circuits)") + feature_angles = tuple( + float(b.value) for b in call_sites[FEATURE_INDEX].bound_arguments + ) + print(f" Feature |f> prepare angles : {feature_angles}") + print() + print( + f" {'i':>2} {'concept':<11} {'group':<9} {'sub':<9} " + f"{'P(|000>) empirical':<22} {'analytic':<9} {'|err|':<7}" + ) + print( + f" {'':>2} {'':-<11} {'':-<9} {'':-<9} {'':-<22} " + f"{'':-<9} {'':-<7}" + ) + + errors = [] + for i, t in enumerate(call_sites): + query_angles = tuple(float(b.value) for b in t.bound_arguments) + p_emp = run_one_query(feature_angles, query_angles, shots=SHOTS) + p_ana = float(gsq[FEATURE_INDEX, i]) + err = abs(p_emp - p_ana) + errors.append(err) + bar = "#" * int(p_emp * 20) + name, sup, sub = CONCEPTS[i] + print( + f" {i:>2} {name:<11} {sup:<9} {sub:<9} " + f"{p_emp:>6.3f} {bar:<14} {p_ana:>6.3f} {err:>6.3f}" + ) + + # 5. Pass/fail + banner("5. Empirical vs. analytic agreement") + max_err = max(errors) + mc_std_bound = (0.5 * 0.5 / SHOTS) ** 0.5 + threshold = 3 * mc_std_bound + passed = max_err < threshold + print(f" max |empirical − analytic| across 12 concepts : {max_err:.4f}") + print(f" Monte-Carlo std bound (p=0.5, N={SHOTS}) : {mc_std_bound:.4f}") + print(f" 3·std threshold : {threshold:.4f}") + print(f" Result : {'PASS' if passed else 'FAIL'}") + + # 6. Rung-0 vs. rung-1 recap + banner("6. Rung-0 (flat) vs. rung-1 (hierarchical) Gram signatures") + print(" rung-0 (larql-polysemantic-clusters, product state):") + print(" |c_i> = Ry(q0,α) Ry(q1,β) Ry(q2,γ) |000>") + print(" tiers : 1.000 / 0.720 / ≲ 0.09") + print(" (self / cluster-mate / cross-cluster — three flat tiers)") + print() + print(" rung-1 (this demo, MPS bond-2 CNOT staircase):") + print(" |c_i> = Ry(q0,α) CNOT(q0,q1) Ry(q1,β) CNOT(q1,q2) Ry(q2,γ) |000>") + print(" tiers : 1.000 / 0.882 / [0.47, 0.54] / [0.12, 0.25]") + print(" (self / sub-mate / super-sib / cross-group — four ordered tiers)") + print() + print(" The CNOT staircase entangles adjacent qubits, lifting the") + print(" block diagonal of rung-0 into a graded two-level hierarchy.") + + raise SystemExit(0 if passed else 1) + + +if __name__ == "__main__": + main() diff --git a/examples/larql-polysemantic-hierarchical.q.orca.md b/examples/larql-polysemantic-hierarchical.q.orca.md new file mode 100644 index 0000000..c567b5c --- /dev/null +++ b/examples/larql-polysemantic-hierarchical.q.orca.md @@ -0,0 +1,243 @@ +# machine LarqlPolysemanticHierarchical + +Hierarchical-overlap polysemantic concept-projection over 12 LARQL concepts +organized as a two-level hierarchy (3 super-groups × 2 sub-clusters × 2 +concepts) on a 3-qubit register, using two CNOT-staircase parametric +actions (`prepare_concept(a, b, c)` and `query_concept(a, b, c)`) to +stamp 13 call sites from a single bond-2 MPS template. + +This is the **rung-1** companion to `larql-polysemantic-clusters.q.orca.md` +(rung 0). The clusters example demonstrates *flat* block structure on the +product-state manifold — concept overlaps split into three uniform tiers +(self 1.0 / cluster-mate 0.72 / cross-cluster < 0.09). This example lifts +the encoding to a bond-dimension-2 matrix product state via a +`Ry; CNOT; Ry; CNOT; Ry` staircase, which introduces correlations between +adjacent qubits and produces a *four-tier* hierarchical Gram matrix: +self / sub-cluster-mate / super-group-sibling / cross-group. The full +ladder is in `docs/research/polysemantic-encoding-beyond-product-states.md`. + +## Concept geometry (12 concepts, 3-qubit register) + +Each concept `c_i` is prepared as the bond-2 MPS + + |c_i> = Ry(q0, α_i) CNOT(q0, q1) Ry(q1, β_i) CNOT(q1, q2) Ry(q2, γ_i) |000> + +on a 3-qubit register. The CNOT staircase introduces 1D locality: q0 is +the chain root, q1 is correlated with q0 only, and q2 is correlated with +q0 only through q1. This naturally maps onto a two-level hierarchy: + +- **Super-group** index — encoded in `α` (the q0 rotation, chain root). + Three super-groups at evenly-spaced cyclic angles `α ∈ {0, 2π/3, 4π/3}`. + Cross-group concepts are well-separated because they differ on the + chain root. +- **Sub-cluster** index — encoded in `β` (the q1 rotation). Two + sub-clusters per super-group at offsets `β ∈ {-0.75, +0.75}`. + Same-super-group, different-sub-cluster pairs share the chain root but + diverge on the middle qubit. +- **Concept** index within sub-cluster — encoded in `γ` (the q2 rotation). + Two concepts per sub-cluster at offsets `γ ∈ {-0.35, +0.35}`. Concepts + within a sub-cluster share both the chain root and the middle qubit + and differ only on the leaf. + +The 12 concrete angle triples (with `α₂ = 2π/3 ≈ 2.094`, `α₃ = 4π/3 ≈ 4.189`): + +| i | concept | super-group | sub-cluster | (α, β, γ) | +|----|-------------|-------------|-------------|----------------------| +| 0 | dog | animals | mammals | ( 0.000, -0.75, -0.35) | +| 1 | cat | animals | mammals | ( 0.000, -0.75, 0.35) | +| 2 | robin | animals | birds | ( 0.000, 0.75, -0.35) | +| 3 | eagle | animals | birds | ( 0.000, 0.75, 0.35) | +| 4 | strawberry | fruits | berries | ( 2.094, -0.75, -0.35) | +| 5 | blueberry | fruits | berries | ( 2.094, -0.75, 0.35) | +| 6 | mango | fruits | tropical | ( 2.094, 0.75, -0.35) | +| 7 | papaya | fruits | tropical | ( 2.094, 0.75, 0.35) | +| 8 | car | vehicles | land | ( 4.189, -0.75, -0.35) | +| 9 | bike | vehicles | land | ( 4.189, -0.75, 0.35) | +| 10 | plane | vehicles | air | ( 4.189, 0.75, -0.35) | +| 11 | drone | vehicles | air | ( 4.189, 0.75, 0.35) | + +### Gram matrix (analytic, four-tier hierarchy) + +The CNOT-staircase MPS overlap is not a separable product over qubits +(unlike rung 0). Numerical contraction via +`compute_concept_gram_mps` produces this four-tier structure on the +off-diagonal of `||²`: + +| tier | members | analytic ||² | +|----------------------------|--------------------------------------|----------------------------| +| self | i = j (12 entries) | 1.000 | +| sub-cluster-mate | same α, same β, different γ (6 pairs)| 0.882 (uniform) | +| super-group-sibling | same α, different β (12 pairs) | 0.472 – 0.535 | +| cross-group | different α (48 pairs) | 0.118 – 0.250 | + +Sketch (4-tier ASCII heatmap, `#` ≥ 0.7, `o` ∈ [0.3, 0.7), `.` ∈ [0.1, 0.3), +blank < 0.1): + +``` + dog cat rob eag | str blu man pap | car bik pla drn +dog [ # # o o | . . . . | . . . . ] +cat [ # # o o | . . . . | . . . . ] +rob [ o o # # | . . . . | . . . . ] +eag [ o o # # | . . . . | . . . . ] +str [ . . . . | # # o o | . . . . ] +blu [ . . . . | # # o o | . . . . ] +man [ . . . . | o o # # | . . . . ] +pap [ . . . . | o o # # | . . . . ] +car [ . . . . | . . . . | # # o o ] +bik [ . . . . | . . . . | # # o o ] +pla [ . . . . | . . . . | o o # # ] +drn [ . . . . | . . . . | o o # # ] +``` + +Each 4×4 super-group diagonal block resolves into two 2×2 sub-cluster +blocks (the `#` regions, sub-cluster-mates) sandwiched against the +2×2 super-group siblings (the `o` regions). Inter-group blocks fall +into the cross-group tier (`.` regions). `compute_concept_gram_mps` in +the compiler package produces this matrix exactly for this machine. + +## Loaded feature |f> = |dog> (single-concept load) + +To keep the polysemy column directly readable as a **row of the Gram +matrix**, this example loads a single concept — `|dog>` — as the +feature rather than a 4-way superposition. The polysemy column then +exposes the four-tier structure in the clearest form: + +| Query concept | Group | Sub-cluster | P(|000>) analytic | tier | +|------------------|-----------|--------------|-------------------|--------------------| +| dog (0) | animals | mammals | 1.000 | self | +| cat (1) | animals | mammals | 0.882 | sub-cluster-mate | +| robin (2) | animals | birds | 0.535 | super-group-sib | +| eagle (3) | animals | birds | 0.472 | super-group-sib | +| strawberry (4) | fruits | berries | 0.250 | cross-group | +| blueberry (5) | fruits | berries | 0.221 | cross-group | +| mango (6) | fruits | tropical | 0.134 | cross-group | +| papaya (7) | fruits | tropical | 0.118 | cross-group | +| car (8) | vehicles | land | 0.250 | cross-group | +| bike (9) | vehicles | land | 0.221 | cross-group | +| plane (10) | vehicles | air | 0.134 | cross-group | +| drone (11) | vehicles | air | 0.118 | cross-group | + +Four ordered tiers — **1.0** (self) → **0.88** (sub-cluster-mate) → **~0.50** +(super-group sibling) → **~0.18** (cross-group). Compare against +`larql-polysemantic-clusters`'s flat 1.00 / 0.72 / ≲ 0.09 three-tier +structure: that example demonstrates the *block* polysemantic phenomenon +on the product manifold, this one lifts it to a *graded hierarchical* +phenomenon on the bond-2 MPS manifold. + +## Note on the multi-query circuit + +As with `larql-polysemantic-clusters`, a single Qiskit circuit cannot +simulate all 12 queries together — each query's inverse-prepare + +measure destroys the feature state via measurement (no-cloning). The +`.q.orca.md` declares all 12 parametric call sites to publish the full +signature shape; `compile_to_qiskit` emits all 12 branches in BFS order. +The companion demo `demos/larql_polysemantic_hierarchical/demo.py` runs +12 independent prepare+query circuits to recover the polysemy column. + +## context +| Field | Type | Default | +|----------|-------------|--------------------| +| qubits | list | [q0, q1, q2] | + +## events +- load_feature +- query_dog +- query_cat +- query_robin +- query_eagle +- query_strawberry +- query_blueberry +- query_mango +- query_papaya +- query_car +- query_bike +- query_plane +- query_drone +- measure_done + +## state idle [initial] +> 3-qubit concept register in `|000>`. No feature has been prepared. + +## state feature_loaded +> `|f> = |dog>` prepared via `prepare_concept(0.0, -0.75, -0.35)`. + +## state queried_dog +> `query_concept(dog)` applied — self-query. `P(|000>) = 1.000`. + +## state queried_cat +> `query_concept(cat)` applied — sub-cluster-mate (mammals). `P(|000>) ≈ 0.882`. + +## state queried_robin +> `query_concept(robin)` applied — super-group sibling (animals/birds). `P(|000>) ≈ 0.535`. + +## state queried_eagle +> `query_concept(eagle)` applied — super-group sibling (animals/birds). `P(|000>) ≈ 0.472`. + +## state queried_strawberry +> `query_concept(strawberry)` applied — cross-group (fruits). `P(|000>) ≈ 0.250`. + +## state queried_blueberry +> `query_concept(blueberry)` applied — cross-group (fruits). `P(|000>) ≈ 0.221`. + +## state queried_mango +> `query_concept(mango)` applied — cross-group (fruits). `P(|000>) ≈ 0.134`. + +## state queried_papaya +> `query_concept(papaya)` applied — cross-group (fruits). `P(|000>) ≈ 0.118`. + +## state queried_car +> `query_concept(car)` applied — cross-group (vehicles). `P(|000>) ≈ 0.250`. + +## state queried_bike +> `query_concept(bike)` applied — cross-group (vehicles). `P(|000>) ≈ 0.221`. + +## state queried_plane +> `query_concept(plane)` applied — cross-group (vehicles). `P(|000>) ≈ 0.134`. + +## state queried_drone +> `query_concept(drone)` applied — cross-group (vehicles). `P(|000>) ≈ 0.118`. + +## state done [final] +> Measurement collapsed the 3-qubit register to a classical bitstring. + +## transitions +| Source | Event | Guard | Target | Action | +|--------------------|-------------------|-------|-----------------------|----------------------------------------------| +| idle | load_feature | | feature_loaded | prepare_concept(0.0, -0.75, -0.35) | +| feature_loaded | query_dog | | queried_dog | query_concept(0.0, -0.75, -0.35) | +| feature_loaded | query_cat | | queried_cat | query_concept(0.0, -0.75, 0.35) | +| feature_loaded | query_robin | | queried_robin | query_concept(0.0, 0.75, -0.35) | +| feature_loaded | query_eagle | | queried_eagle | query_concept(0.0, 0.75, 0.35) | +| feature_loaded | query_strawberry | | queried_strawberry | query_concept(2.094, -0.75, -0.35) | +| feature_loaded | query_blueberry | | queried_blueberry | query_concept(2.094, -0.75, 0.35) | +| feature_loaded | query_mango | | queried_mango | query_concept(2.094, 0.75, -0.35) | +| feature_loaded | query_papaya | | queried_papaya | query_concept(2.094, 0.75, 0.35) | +| feature_loaded | query_car | | queried_car | query_concept(4.189, -0.75, -0.35) | +| feature_loaded | query_bike | | queried_bike | query_concept(4.189, -0.75, 0.35) | +| feature_loaded | query_plane | | queried_plane | query_concept(4.189, 0.75, -0.35) | +| feature_loaded | query_drone | | queried_drone | query_concept(4.189, 0.75, 0.35) | +| queried_dog | measure_done | | done | | +| queried_cat | measure_done | | done | | +| queried_robin | measure_done | | done | | +| queried_eagle | measure_done | | done | | +| queried_strawberry | measure_done | | done | | +| queried_blueberry | measure_done | | done | | +| queried_mango | measure_done | | done | | +| queried_papaya | measure_done | | done | | +| queried_car | measure_done | | done | | +| queried_bike | measure_done | | done | | +| queried_plane | measure_done | | done | | +| queried_drone | measure_done | | done | | + +## actions +| Name | Signature | Effect | +|-----------------|------------------------------------------|-----------------------------------------------------------------------------------------| +| prepare_concept | (qs, a: angle, b: angle, c: angle) -> qs | Ry(qs[0], a); CNOT(qs[0], qs[1]); Ry(qs[1], b); CNOT(qs[1], qs[2]); Ry(qs[2], c) | +| query_concept | (qs, a: angle, b: angle, c: angle) -> qs | Ry(qs[2], -c); CNOT(qs[1], qs[2]); Ry(qs[1], -b); CNOT(qs[0], qs[1]); Ry(qs[0], -a) | + +## verification rules +- unitarity: Ry and CNOT preserve norm; every transition evolves the 3-qubit register unitarily from `|000>` +- mps_bond_2_encoding: each concept is prepared as a bond-dimension-2 MPS via the `Ry; CNOT; Ry; CNOT; Ry` staircase — the minimal entangling pattern beyond rung 0 +- hierarchical_overlap: four ordered tiers — self 1.000 / sub-cluster-mate 0.882 / super-group-sibling [0.472, 0.535] / cross-group [0.118, 0.250] — with strict ordering across tier boundaries +- no_cloning: the prepared feature is not duplicated; each query needs a fresh prepare+query sequence (the demo runs 12 independent circuits) +- measurement_collapse_allowed: `done` is the intended collapse sink — each branch ends in measurement; analytic `P(|000>)` per query (1.000 self, 0.882 sub-mate, 0.47–0.54 super-sib, 0.12–0.25 cross) is documented in the polysemy-scores table above diff --git a/openspec/changes/add-mps-concept-encoding/tasks.md b/openspec/changes/add-mps-concept-encoding/tasks.md index 30423ee..b6c253e 100644 --- a/openspec/changes/add-mps-concept-encoding/tasks.md +++ b/openspec/changes/add-mps-concept-encoding/tasks.md @@ -2,7 +2,7 @@ ## 1. Design the hierarchical concept dictionary -- [ ] 1.1 Pick 12 angle triples `(α_i, β_i, γ_i)` producing a +- [x] 1.1 Pick 12 angle triples `(α_i, β_i, γ_i)` producing a two-level hierarchy under the CNOT-staircase MPS encoding `|c_i⟩ = Ry(q0, α_i) CNOT(q0, q1) Ry(q1, β_i) CNOT(q1, q2) Ry(q2, γ_i) |000⟩`. Target four Gram-matrix tiers: @@ -11,13 +11,33 @@ verify the four-tier structure by computing the analytic overlap matrix via transfer-matrix contraction before writing the example file. -- [ ] 1.2 Document the 12-concept hierarchy (3 super-groups × 2 sub- + Final design: α ∈ {0, 2π/3, 4π/3} (super-group, q0), + β ∈ {-0.75, +0.75} (sub-cluster, q1), γ ∈ {-0.35, +0.35} + (concept, q2). Per-tier achieved bands (numerically verified + via `compute_concept_gram_mps` in step 2): self 1.000, + sub-mate 0.882 (uniform), super-sib [0.472, 0.535], + cross-group [0.118, 0.250]. Cross max sits at the cyclic-α + cos²(π/3)=0.25 floor (above the < 0.15 stretch target); + design.md's fallback applies — four ordered tiers with strict + inter-tier separation (sub→super gap 0.347; super→cross gap + 0.222), which is what the pipeline test asserts. +- [x] 1.2 Document the 12-concept hierarchy (3 super-groups × 2 sub- clusters × 2 concepts, or whatever partition the angle design supports) in the example's leading paragraph with a Gram heatmap table. -- [ ] 1.3 Compute the analytic polysemy column for + Hierarchy is named (animals/fruits/vehicles super-groups; + mammals/birds, berries/tropical, land/air sub-clusters; 12 + named concepts) and documented in the example's leading + paragraph with the angle table, the analytic per-tier band + table, and an ASCII Gram heatmap that resolves the four tiers. +- [x] 1.3 Compute the analytic polysemy column for `|f⟩ = |c_0⟩` and tabulate it in the example's leading paragraph. Expected to show all four tiers in one column. + Polysemy column for `|f⟩ = |dog⟩ = |c_0⟩` tabulated in the + example: 1.000 (dog/self), 0.882 (cat/sub-mate), 0.535 / 0.472 + (robin/eagle, super-sib), and eight cross-group entries + ranging 0.118 – 0.250. All four tiers appear in the single + column. ## 2. Compiler helper: `compute_concept_gram_mps` @@ -98,28 +118,41 @@ ## 3. Example: `larql-polysemantic-hierarchical.q.orca.md` -- [ ] 3.1 Write the example with: 3-qubit concept register +- [x] 3.1 Write the example with: 3-qubit concept register (`qubits: list`), 12 concepts in a two-level hierarchy, one parametric `prepare_concept(a: angle, b: angle, c: angle)` (one call site, feature = `c_0`), one parametric `query_concept(a: angle, b: angle, c: angle)` (12 call sites), convergent `done [final]` state. Leading paragraph documents the hierarchy and the four-tier Gram matrix. -- [ ] 3.2 Parses clean (`parsed.errors == []`), verifies valid + `examples/larql-polysemantic-hierarchical.q.orca.md` ships + the machine `LarqlPolysemanticHierarchical` (15 states, 25 + transitions, 2 actions). The leading paragraph documents the + animals/fruits/vehicles super-groups, the angle table, the + four-tier Gram band table, the ASCII heatmap, and the + polysemy column for `|f⟩ = |dog⟩`. +- [x] 3.2 Parses clean (`parsed.errors == []`), verifies valid (static), compiles to QASM + Qiskit + Mermaid without warnings. Covered by `test_larql_polysemantic_hierarchical_pipeline`. -- [ ] 3.3 `compile_to_qiskit` produces the expected gate count — + Verified end-to-end: `parse_q_orca_markdown` returns no errors, + `verify(skip_dynamic=True)` reports VALID with 0 errors and + 0 warnings, and the QASM/Qiskit/Mermaid compilers produce the + expected register and gate counts. +- [x] 3.3 `compile_to_qiskit` produces the expected gate count — 3 rotations + 2 CNOTs for prepare (5 gates) + 5 gates × 12 queries + 2 additional CNOTs in query inversions = 65 gates. Exact count verified during implementation; tasks document the number once measured. + Measured: 39 `qc.ry(` calls and 26 `qc.cx(` calls — i.e., + 13 transitions × (3 Ry + 2 CX) = 65 gates total. Asserted in + `test_larql_polysemantic_hierarchical_pipeline`. ## 4. Tests -- [ ] 4.1 `larql-polysemantic-hierarchical` added to `EXAMPLE_FILES` +- [x] 4.1 `larql-polysemantic-hierarchical` added to `EXAMPLE_FILES` fixture in `tests/test_examples.py`. -- [ ] 4.2 `test_larql_polysemantic_hierarchical_pipeline` asserts: +- [x] 4.2 `test_larql_polysemantic_hierarchical_pipeline` asserts: parametric-action signature shape (three angle params on both `prepare_concept` and `query_concept`), 12 parametric call sites on `query_concept`, QASM contains `qubit[3] q;`, Qiskit @@ -127,36 +160,47 @@ `qc.ry(` and `qc.cx(` counts, and `compute_concept_gram_mps(machine)` returns a 12×12 matrix whose four tiers land in the documented bands. -- [ ] 4.3 Demo run exercises the shots path; pipeline test exercises + Test landed and asserts: signature shape on both parametric + actions, 12 query call sites, `qubit[3] q;` in QASM, + `QuantumCircuit(3)` plus 39 ry / 26 cx in the Qiskit script, + diagonal == 1, and the four off-diagonal tiers (sub_min ≥ 0.85, + sub_max ≤ 0.90, super in [0.45, 0.56], cross_max ≤ 0.26) with + strict sub→super (≥ 0.20) and super→cross (≥ 0.15) + separation. +- [x] 4.3 Demo run exercises the shots path; pipeline test exercises the analytic path. + Demo runs 12 independent prepare+query Qiskit circuits at 1024 + shots each (shots path); pipeline test only calls + `compute_concept_gram_mps` (analytic path), so the two paths + cover disjoint code. ## 5. Demo: `demos/larql_polysemantic_hierarchical/demo.py` -- [ ] 5.1 Mirrors `demos/larql_polysemantic_clusters/demo.py`: +- [x] 5.1 Mirrors `demos/larql_polysemantic_clusters/demo.py`: parse + verify → compile (Mermaid + QASM + Qiskit) → 12 independent Qiskit circuits at 1024 shots each → polysemy column print. -- [ ] 5.2 Prints the analytic Gram matrix as a 4-tier ASCII heatmap +- [x] 5.2 Prints the analytic Gram matrix as a 4-tier ASCII heatmap (`#` ≥ 0.7, `o` ∈ [0.3, 0.7), `.` ∈ [0.1, 0.3), blank < 0.1) using `compute_concept_gram_mps`. -- [ ] 5.3 Compares empirical vs. analytic polysemy: prints +- [x] 5.3 Compares empirical vs. analytic polysemy: prints `max |error|`, `mc_std` bound, pass/fail on `max_error < 3 · mc_std`. Exits nonzero on fail. -- [ ] 5.4 Module docstring names the hierarchy topology, points at +- [x] 5.4 Module docstring names the hierarchy topology, points at the example file, and references the sibling clusters demo. Closing section prints a side-by-side comparison of the rung-0 (flat) vs. rung-1 (hierarchical) Gram signatures. ## 6. Documentation -- [ ] 6.1 README "Parametric actions" section grows a +- [x] 6.1 README "Parametric actions" section grows a "Hierarchical polysemy" sub-heading with a paragraph summary and links to the new example + demo, and back to `larql-polysemantic-clusters` as the flat-tier variant. -- [ ] 6.2 `CHANGELOG.md` `## Unreleased` grows a bullet under +- [x] 6.2 `CHANGELOG.md` `## Unreleased` grows a bullet under **Added** describing the new example, demo, and `compute_concept_gram_mps` helper. -- [ ] 6.3 No new top-level docs under `docs/language/` — the +- [x] 6.3 No new top-level docs under `docs/language/` — the example file is the authoritative documentation for the pattern. The research note at `docs/research/polysemantic-encoding-beyond-product-states.md` @@ -164,11 +208,16 @@ ## 7. Spec consistency -- [ ] 7.1 `openspec validate add-mps-concept-encoding --strict` +- [x] 7.1 `openspec validate add-mps-concept-encoding --strict` passes. -- [ ] 7.2 Full pytest suite green. -- [ ] 7.3 Ruff clean across touched files. -- [ ] 7.4 Demo run locally produces `max_error < threshold` → PASS. +- [x] 7.2 Full pytest suite green. + 770 passed, 6 skipped — including the new + `test_larql_polysemantic_hierarchical_pipeline` test and the + auto-parameterized `test_verify_all_examples` coverage. +- [x] 7.3 Ruff clean across touched files. +- [x] 7.4 Demo run locally produces `max_error < threshold` → PASS. + Demo PASS recorded — `max_err = 0.0158` < threshold + `0.0469` (3 × Monte-Carlo std bound at 1024 shots). ## 8. Archive diff --git a/tests/test_examples.py b/tests/test_examples.py index 68d87a2..e477fd6 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -17,6 +17,7 @@ "larql-polysemantic-2": "larql-polysemantic-2.q.orca.md", "larql-polysemantic-12": "larql-polysemantic-12.q.orca.md", "larql-polysemantic-clusters": "larql-polysemantic-clusters.q.orca.md", + "larql-polysemantic-hierarchical": "larql-polysemantic-hierarchical.q.orca.md", } @@ -213,3 +214,134 @@ def test_larql_polysemantic_clusters_pipeline(self): # Diagonal must be exactly 1 (self-overlap). np.testing.assert_allclose(np.diag(gsq), np.ones(12), atol=1e-9) + + def test_larql_polysemantic_hierarchical_pipeline(self): + """End-to-end: parse → verify → compile (QASM + Qiskit + Mermaid) → + MPS-bond-2 concept gram four-tier hierarchy. + + Covers task 4.2 of add-mps-concept-encoding: the 12-call-site + CNOT-staircase parametric machine must parse clean, verify, compile + to a 3-qubit register with the expected ry/cx counts, and produce a + 12×12 Gram matrix whose four off-diagonal tiers (self / sub-cluster- + mate / super-group-sibling / cross-group) are strictly ordered. + """ + import numpy as np + + from q_orca import ( + QSimulationOptions, + VerifyOptions, + compile_to_mermaid, + compile_to_qasm, + compile_to_qiskit, + compute_concept_gram_mps, + parse_q_orca_markdown, + verify, + ) + + source = ( + EXAMPLES_DIR / "larql-polysemantic-hierarchical.q.orca.md" + ).read_text() + parsed = parse_q_orca_markdown(source) + assert parsed.errors == [] + machine = parsed.file.machines[0] + + parametric_actions = {a.name: a for a in machine.actions if a.parameters} + assert set(parametric_actions) == {"prepare_concept", "query_concept"} + for name in ("prepare_concept", "query_concept"): + params = parametric_actions[name].parameters + assert [(p.name, p.type) for p in params] == [ + ("a", "angle"), + ("b", "angle"), + ("c", "angle"), + ], f"{name} signature shape mismatch" + + query_call_sites = [ + t for t in machine.transitions if t.action == "query_concept" + ] + assert len(query_call_sites) == 12 + + result = verify(machine, VerifyOptions(skip_dynamic=True)) + assert result.valid, [e for e in result.errors if e.severity == "error"] + + qasm = compile_to_qasm(machine) + assert "qubit[3] q;" in qasm + + mermaid = compile_to_mermaid(machine) + assert ( + "LarqlPolysemanticHierarchical" in mermaid + or "feature_loaded" in mermaid + ) + + qiskit_script = compile_to_qiskit( + machine, + QSimulationOptions(analytic=False, shots=0, run=False, skip_qutip=True), + ) + assert "QuantumCircuit(3)" in qiskit_script + # Each parametric action emits 3 Ry + 2 CNOT (CNOT staircase). With + # 1 prepare + 12 query call sites = 13 transitions, we expect + # 13 × 3 = 39 ry and 13 × 2 = 26 cx — i.e., 65 gates total. + assert qiskit_script.count("qc.ry(") == 39 + assert qiskit_script.count("qc.cx(") == 26 + + gram = compute_concept_gram_mps(machine) + assert gram.shape == (12, 12) + gsq = np.abs(gram) ** 2 + + # Diagonal must be exactly 1 (self-overlap). + np.testing.assert_allclose(np.diag(gsq), np.ones(12), atol=1e-9) + + # Tier classification by (super-group, sub-cluster) coordinates: i // + # 4 is the super-group index, (i // 2) % 2 is the sub-cluster index + # within the super-group. + sub_mate_pairs = [] + super_sib_pairs = [] + cross_pairs = [] + for i in range(12): + gi, si = i // 4, (i // 2) % 2 + for j in range(i + 1, 12): + gj, sj = j // 4, (j // 2) % 2 + v = gsq[i, j] + if gi == gj and si == sj: + sub_mate_pairs.append(v) + elif gi == gj: + super_sib_pairs.append(v) + else: + cross_pairs.append(v) + assert len(sub_mate_pairs) == 6 + assert len(super_sib_pairs) == 12 + assert len(cross_pairs) == 48 + + sub_min, sub_max = min(sub_mate_pairs), max(sub_mate_pairs) + sup_min, sup_max = min(super_sib_pairs), max(super_sib_pairs) + cr_min, cr_max = min(cross_pairs), max(cross_pairs) + + # Sub-cluster-mate tier: tight band around 0.882 (analytic + # cos²(0.35) for the γ-only difference). + assert 0.85 <= sub_min <= sub_max <= 0.90, ( + f"sub-cluster-mate tier outside [0.85, 0.90]: " + f"min={sub_min:.4f} max={sub_max:.4f}" + ) + + # Super-group-sibling tier: band around [0.47, 0.54]. + assert 0.45 <= sup_min <= sup_max <= 0.56, ( + f"super-group-sibling tier outside [0.45, 0.56]: " + f"min={sup_min:.4f} max={sup_max:.4f}" + ) + + # Cross-group tier: wider band [0.11, 0.26]. Higher than rung-0 + # because cyclic α at 2π/3 spacing gives a cos²(π/3)=0.25 floor on + # cross-group α factor; β/γ alignment determines the rest. + assert 0.10 <= cr_min and cr_max <= 0.26, ( + f"cross-group tier outside [0.10, 0.26]: " + f"min={cr_min:.4f} max={cr_max:.4f}" + ) + + # Tier ordering: sub > super > cross (strict separation). + assert sub_min > sup_max + 0.20, ( + f"insufficient sub→super gap: " + f"sub_min={sub_min:.4f} super_max={sup_max:.4f}" + ) + assert sup_min > cr_max + 0.15, ( + f"insufficient super→cross gap: " + f"super_min={sup_min:.4f} cross_max={cr_max:.4f}" + )