Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/research/spec-quantum-predictive-coder.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The predictive-coding loop is then a **hybrid** q-orca machine: the unitary piec
- Parametric gates with `angle_context` references — **shipped** (see `openspec/changes/archive/2026-04-18-context-angle-references/`)
- Mid-circuit measurement (`measure(qs[N]) -> bits[M]`) and classical-feedforward conditionals — **shipped** (archive `2026-04-17-mid-circuit-measurement`)
- Runtime state-category assertions (e.g., `[assert:separable]`, `[assert:entangled]`) — **proposed** in `openspec/changes/add-runtime-state-assertions/`; would let the verifier confirm the ancilla is disentangled after measurement
- Parameter-update actions that mutate `list<float>` context fields — **not yet shipped**; this is the one new primitive the QPC needs
- Parameter-update actions that mutate `list<float>` context fields — **grammar, AST, verifier, and compiler annotations landed** in OpenSpec change `add-classical-context-updates`. Shot-to-shot runtime execution of the mutation is parked for a follow-up change.

Nothing in this architecture requires rewriting the q-orca execution model. What it requires is a single new action kind: a **classical context update** that reads a classical bit from the machine's bit-register and writes a new float into the angle register. Concretely: `if bits[0] == 1: θ[0] -= η · δ; else: θ[0] += η · δ` as an effect, where `δ` and `η` are further context fields. This is a strictly classical operation on the context record and does not touch the quantum state; it only runs between simulator shots.

Expand Down
82 changes: 38 additions & 44 deletions openspec/changes/add-classical-context-updates/tasks.md
Original file line number Diff line number Diff line change
@@ -1,105 +1,99 @@
## 1. AST

- [ ] 1.1 Add `QContextMutation` dataclass in `q_orca/ast.py` with
- [x] 1.1 Add `QContextMutation` dataclass in `q_orca/ast.py` with
fields `target_field: str`, `target_idx: Optional[int]`,
`op: str` (= | += | -=), `rhs_literal: Optional[float]`,
`rhs_field: Optional[str]`.
- [ ] 1.2 Add `QEffectContextUpdate` dataclass with fields
- [x] 1.2 Add `QEffectContextUpdate` dataclass with fields
`bit_idx: Optional[int]`, `bit_value: Optional[int]`,
`then_mutations: list[QContextMutation]`,
`else_mutations: list[QContextMutation]`.
- [ ] 1.3 Add `context_update: Optional[QEffectContextUpdate] = None`
- [x] 1.3 Add `context_update: Optional[QEffectContextUpdate] = None`
to `QActionSignature`.

## 2. Parser

- [ ] 2.1 Add `_parse_context_update_from_effect(effect_str, errors,
- [x] 2.1 Add `_parse_context_update_from_effect(effect_str, errors,
action_name)` in `q_orca/parser/markdown_parser.py`. Handle the
unconditional `<lhs> <op> <rhs>` form first, then the
`if bits[i] == v: ... else: ...` form. Return `None` if the
effect doesn't match the grammar (so other parsers still get a
chance).
- [ ] 2.2 Wire the new parser into `_parse_actions_table` alongside
- [x] 2.2 Wire the new parser into `_parse_actions_table` alongside
the existing measurement/conditional parsers. If a row produces
both a `context_update` and any of `gate` / `measurement` /
`conditional_gate`, emit a structured parse error and drop the
row's `context_update`.
- [ ] 2.3 Enforce LHS constraints at parse time: the LHS identifier
- [x] 2.3 Enforce LHS constraints at parse time: the LHS identifier
must be a simple ident (no dot paths); indexed LHS requires a
non-negative integer literal index.
- [ ] 2.4 Unit tests in `tests/test_parser.py` covering: scalar
- [x] 2.4 Unit tests in `tests/test_context_updates.py` covering: scalar
increment, list-element increment with literal RHS, list-element
update with field-ref RHS, conditional form with then+else,
conditional with only then-branch, malformed forms (unknown op,
nested conditions, non-bit condition).

## 3. Verifier — classical context stage

- [ ] 3.1 Create `q_orca/verifier/classical_context.py` with
- [x] 3.1 Create `q_orca/verifier/classical_context.py` with
`check_classical_context(machine: QMachineDef) ->
QVerificationResult`. Iterate all actions with `context_update`.
- [ ] 3.2 Implement the typing check (Requirement:
- [x] 3.2 Implement the typing check (Requirement:
"Classical Context Update — Static Typing") — emit
`UNDECLARED_CONTEXT_FIELD`, `CONTEXT_FIELD_TYPE_MISMATCH`,
`CONTEXT_INDEX_OUT_OF_RANGE` per the spec.
- [ ] 3.3 Implement the feedforward-completeness check (Requirement:
- [x] 3.3 Implement the feedforward-completeness check (Requirement:
"Classical Context Update — Feedforward Completeness") using
`analyze_machine` for reachability. Walk paths from initial
state; for each context-update with a `bit_idx`, confirm every
path to that transition contains a prior `mid_circuit_measure`
or `measurement` writing that bit. Emit
acyclic-path enumeration from the initial state. For each
context-update with a `bit_idx`, confirm every path to that
transition contains a prior `mid_circuit_measure` or
`measurement` writing that bit. Emit
`BIT_READ_BEFORE_WRITE` on violation.
- [ ] 3.4 Wire the stage into `q_orca/verifier/__init__.py::verify()`
between completeness and quantum-static. Respect
`VerifyOptions.skip_classical_context` (add that flag to
`VerifyOptions`).
- [ ] 3.5 Unit tests in `tests/test_verifier.py` covering each
- [x] 3.4 Wire the stage into `q_orca/verifier/__init__.py::verify()`
between completeness/determinism and quantum-static. Respect
`VerifyOptions.skip_classical_context` (added to `VerifyOptions`).
- [x] 3.5 Unit tests in `tests/test_context_updates.py` covering each
error code (missing field, wrong type, out-of-range index,
bit-read-before-write) and the happy path where a measurement
transition writes the bit before the update.

## 4. Compiler — annotation emission

- [ ] 4.1 In `q_orca/compiler/qasm.py`, detect
- [x] 4.1 In `q_orca/compiler/qasm.py`, detect
`QEffectContextUpdate` actions and emit a `// context_update: ...`
comment at the action's site. Track presence-of-any to decide
whether to emit the file-level banner.
- [ ] 4.2 Same treatment in `q_orca/compiler/qiskit.py` with Python
- [x] 4.2 Same treatment in `q_orca/compiler/qiskit.py` with Python
`#` comments.
- [ ] 4.3 In `q_orca/compiler/mermaid.py`, confirm transition-arrow
labels render unchanged — likely no code change, just a test.
- [ ] 4.4 Serialize the original effect string on
`QEffectContextUpdate` at parse time (add `raw: str` field) so
- [x] 4.3 In `q_orca/compiler/mermaid.py`, confirm transition-arrow
labels render unchanged — no code change needed; covered by test.
- [x] 4.4 Serialize the original effect string on
`QEffectContextUpdate` at parse time (`raw: str` field) so
compilers can emit the round-trippable text without re-stringifying
the AST.
- [ ] 4.5 Unit tests in `tests/test_compiler.py` covering QASM and
Qiskit emission (presence of comment, presence of banner) and
- [x] 4.5 Unit tests in `tests/test_context_updates.py` covering QASM
and Qiskit emission (presence of comment, presence of banner) and
the no-context-update negative case (banner absent).

## 5. Spec + docs sync

- [ ] 5.1 Run `openspec validate add-classical-context-updates
--strict` — already required at the end of the OpenSpec propose
flow; re-run after code lands.
- [ ] 5.2 Add a one-line pointer in
`docs/research/spec-quantum-predictive-coder.md` referencing this
change ID as the blocker the research proposal flagged (research
doc's §Proposed Architecture item 5, "Parameter-update
actions...not yet shipped").
- [x] 5.1 Run `openspec validate add-classical-context-updates
--strict` — performed after code lands.
- [x] 5.2 Update `docs/research/spec-quantum-predictive-coder.md`
to reference this change as the source of the grammar/AST/verifier/
compiler landing (§Proposed Architecture item 5).

## 6. End-to-end verification

- [ ] 6.1 Write a minimal fixture machine using the new grammar
(extends `examples/predictive-coder-minimal.q.orca.md` with a
`gradient_step` action and a loop-back transition). Confirm it
parses, verifies, and compiles to QASM/Qiskit with annotations
present.
- [ ] 6.2 Run the full suite
- [x] 6.1 Inline fixture machine in `tests/test_context_updates.py`
exercises the new grammar through parser, verifier, and both
compilers (QASM + Qiskit) end-to-end.
- [x] 6.2 Run the full suite
`.venv/bin/python -m pytest tests/ -q
--ignore=tests/test_cuquantum_backend.py
--ignore=tests/test_cudaq_backend.py` and confirm green.
- [ ] 6.3 Run `.venv/bin/q-orca verify` on all examples in
`examples/` and confirm none regress.
--ignore=tests/test_cudaq_backend.py` — 468 passed, 5 skipped.
- [x] 6.3 Run `.venv/bin/q-orca verify` on all examples in
`examples/` — all 10 remain VALID.

## 7. Follow-up parked (NOT this change)

Expand Down
21 changes: 21 additions & 0 deletions q_orca/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,26 @@ class QEffectConditional:
gate: QuantumGate


@dataclass
class QContextMutation:
"""A single classical-context mutation: <lhs> <op> <rhs>."""
target_field: str
target_idx: Optional[int] = None # None for scalar, int for list element
op: str = "=" # "=", "+=", "-="
rhs_literal: Optional[float] = None
rhs_field: Optional[str] = None # mutually exclusive with rhs_literal


@dataclass
class QEffectContextUpdate:
"""Optionally-bit-gated mutation of classical context fields."""
then_mutations: list["QContextMutation"] = field(default_factory=list)
else_mutations: list["QContextMutation"] = field(default_factory=list)
bit_idx: Optional[int] = None
bit_value: Optional[int] = None # 0 or 1; None iff bit_idx is None
raw: Optional[str] = None # original effect string for round-trip emission


@dataclass
class QActionSignature:
name: str
Expand All @@ -221,6 +241,7 @@ class QActionSignature:
measurement: Optional[Measurement] = None
mid_circuit_measure: Optional[QEffectMeasure] = None
conditional_gate: Optional[QEffectConditional] = None
context_update: Optional[QEffectContextUpdate] = None


@dataclass
Expand Down
9 changes: 9 additions & 0 deletions q_orca/compiler/qasm.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ def compile_to_qasm(machine: QMachineDef) -> str:

lines.append("// Generated by Q-Orca compiler")
lines.append(f"// Machine: {machine.name}")
has_context_update = any(a.context_update is not None for a in machine.actions)
if has_context_update:
lines.append(
"// NOTE: context-update actions are annotations only; "
"shot-to-shot execution not yet implemented."
)
lines.append('OPENQASM 3.0;')
lines.append('include "stdgates.inc";')
lines.append("")
Expand Down Expand Up @@ -55,6 +61,9 @@ def compile_to_qasm(machine: QMachineDef) -> str:
# OpenQASM 3.0 per-bit conditional: bare bit for 1, negated for 0
cond = f"c[{cg.bit_idx}]" if cg.value else f"!c[{cg.bit_idx}]"
lines.append(f"if ({cond}) {{ {gate_str}; }}")
elif action and action.context_update is not None:
raw = action.context_update.raw or action.effect or ""
lines.append(f"// context_update: {raw}")
else:
for gate in gates:
lines.append(_gate_to_qasm(gate, qubit_count))
Expand Down
8 changes: 8 additions & 0 deletions q_orca/compiler/qiskit.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,11 @@ def compile_to_qiskit(machine: QMachineDef, options: QSimulationOptions) -> str:

lines.append("# Generated by Q-Orca compiler")
lines.append(f"# Machine: {machine.name}")
if any(a.context_update is not None for a in machine.actions):
lines.append(
"# NOTE: context-update actions are annotations only; "
"shot-to-shot execution not yet implemented."
)
lines.append("")

lines.append("import json")
Expand Down Expand Up @@ -338,6 +343,9 @@ def compile_to_qiskit(machine: QMachineDef, options: QSimulationOptions) -> str:
cg = action.conditional_gate
lines.append(f"with qc.if_test((qc.clbits[{cg.bit_idx}], {cg.value})):")
lines.append(f" {_gate_to_qiskit(cg.gate)}")
elif action and action.context_update is not None:
raw = action.context_update.raw or action.effect or ""
lines.append(f"# context_update: {raw}")
elif gates:
for gate in gates:
lines.append(_gate_to_qiskit(gate))
Expand Down
Loading
Loading