[Draft] 1855 experimental exact simulators quax - #1866
Closed
erichulburd wants to merge 5 commits into
Closed
erichulburd wants to merge 5 commits into
erichulburd wants to merge 5 commits into
Conversation
#1855) Add the pyquil.simulation core (_resolver pipeline + vectorized gate constructor) and the grad-able PureStateVectorSimulator and DensityMatrixSimulator, with qutrit support and noise-model integration. Restore the density-matrix ResetChannel tests deferred from #1854. Part of splitting PR #1848 into a reviewable stack. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Promote the channel base classes to public names, since consumers must branch on
them: `_ChannelBase` -> `ChannelBase`, `_ResetChannelBase` -> `ResetChannelBase`.
Both docstrings now state that they are the type to test in an `isinstance` check
and explain what goes wrong otherwise.
Fix two silent noise drops in the resolver. Gate and reset channels each come in
a Lindbladian-backed and a raw-superoperator flavor, but the dispatch branched on
the concrete Lindbladian classes:
if isinstance(channel, Channel): # missed SuperopChannel
if isinstance(channel, ResetChannel): # missed SuperopResetChannel
Both fell through to the *ideal* operation, so the noise vanished with no error.
This was the common case rather than an exotic one: every derived channel
operation -- composition, `pauli_twirl`, `to_coherent_channel`,
`to_stochastic_channel` -- returns a `SuperopChannel`. Verified at the resolver:
a `SuperopChannel` on `X 0` previously emitted the ideal X superoperator, and a
`SuperopResetChannel` at fidelity 0.90 gave a post-reset state fidelity of
exactly 1.0.
Also make a measurement inside a cycle honor `measurement="superop"`. The cycle
path emitted the constituent's `process` verbatim, leaking a `QuantumInstrument`
into the grad-able pipeline that a standalone MEASURE correctly collapses to its
total channel.
Test and doc updates for the merged API:
- The five deferred `ResetChannel` end-to-end tests restored by the rebase now
target `SuperopResetChannel.from_reset_fidelity`.
`test_noisy_reset_reduces_fidelity` needed the resolver fix as well as the
rename -- its `0.85 < fidelity < 1.0` band only holds once the channel is
actually applied (it lands at exactly 0.90).
- `DefGate` dimension assertions match the current message, which says "power of
a prime qudit dimension" rather than "perfect power".
- `test_cycle_channel_applies_process_faithfully` builds a `SuperopChannel` with
`ideal_unitary=` instead of the removed `Channel(..., target_unitary=...)`.
- New `TestChannelFlavorDispatch` and `TestCycleMeasurementMode` cover both
flavors of gate and reset channel, the derived-channel path, and both
measurement modes. All four new assertions fail against the old dispatch.
- Use each constituent's `qubits` property in the cycle path rather than
reaching through to `inst.get_qubit_indices()`, whose return type varies by
family (a list for gates, a set-or-None for resets) and did not type-check.
- Note in the architecture docs that `from_isa` now takes a QCS
`InstructionSetArchitecture`, and point at `from_compiler_isa` for rpcq.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QB5EGDshZbFtBvyfPPQqU
…/grad tests Three silent wrong-answer bugs, each now covered by a regression test: - The compressor advertised a *sorted* subsystem for every group, but `compress` emits an unmerged operation verbatim, still in its own operand order. The density-matrix path dispatches purely on the advertised subsystem, so any non-ascending multi-qubit gate was applied to permuted qudits: `X 1; X 2; CCNOT 2 1 0` gave |011> instead of |111>. A 3+ qubit gate can never merge under the default max_subsystem_size=2, so it is always a singleton and every reversed CCNOT/CSWAP in a noisy simulation was wrong. Singleton groups now report the instruction's own operand order, which is also what the state-vector path already derived independently (hence its immunity). Verified across all operand permutations of CCNOT/CSWAP/CNOT/CZ/SWAP for max_subsystem_size 1-4. - `DAGGER` was dropped: `DAGGER RX(0.7) 0` applied RX(+0.7). The root cause is in `get_instruction_unitary`, so channel fidelities were also measured against the wrong ideal unitary. DAGGER is now applied (parity of the modifier count) and matches pyQuil's reference simulator exactly. CONTROLLED and FORKED add a qudit to the instruction and are rejected explicitly rather than silently ignored. - A global `RESET` never consulted the noise model, so reset channels were honoured for `RESET <q>` but silently dropped for `RESET`. It now resolves per qubit through the same path as a targeted reset. Errors and validation, replacing raw XLA/KeyError failures: - Parameter-count mismatches (too few, too many, or omitted) now report the expected count instead of an out-of-range gather, a silent truncation, and a bare KeyError respectively. - Duplicate entries in `qubits=` are rejected; a qubit outside the register names itself and points at `qubits=`. - Expression-valued parameters (`RX(theta[0]/2)`) and feed-forward parameters (`RX(ro[0])` after a MEASURE) are rejected with an explanation. Full expression support would have to become part of the vmap batching key, so it is left as follow-up rather than half-implemented. - A DEFCIRCUIT body may name a literal qubit; arity mismatches between an invocation and its DEFCIRCUIT are reported as such. - `PureStateVectorSimulator` validates the *expanded* operations, so a MEASURE or RESET reached through a DEFCIRCUIT is rejected by name instead of failing later with a padding shape error. Docstrings: - The big-endian convention is now stated in the module docstring, with the contrast against the rest of pyQuil made explicit, and repeated on each simulator's `qubits` parameter. It was previously pinned only by a test. - All three simulator `__init__`s are documented, including `max_subsystem_size` (a pure performance knob) and the fact that MEASURE is applied as dephasing so no classical outcome is recorded. - `:param:`/`:return:` added to the public pipeline methods and the resolver entry points, matching house style. - The docs no longer present `TrajectorySimulator` as available; its import would raise ImportError. Tests (+191): - `test_density_matrix.py` is a superset of the state-vector tests: every gate-only program is checked to reproduce |psi><psi|, to be invariant under max_subsystem_size, and to be physical, then cross-checked against pyQuil's in-repo (little-endian) reference simulators. Noise coverage spans 1Q and 2Q depolarizing with analytic Bloch-vector shrink factors, exact Pauli channels, coherent over-rotation and random coherent error, T1/T2 decoherence including its state dependence, readout noise, ideal/noisy/global resets, and qutrits with explicit leakage population. Two Lindblad results are frozen as end-to-end goldens; they were produced with qutip's `mesolve`, but since quax's `evolve` is already tested against qutip upstream they pin the pyQuil-side chain (T1/T2 conversion, gate-Hamiltonian scaling, subsystem embedding, application) rather than corroborating the integrator. qutip is deliberately not a dependency, so the values are hard-coded with the snippet that produced them. - `test_simulator_jit_grad.py` pins the jit/grad claim for both simulators against central finite differences and closed forms (<Z> = cos(theta) for RY), covering grad-under-jit, second derivatives, vmap over parameter batches, gradients through entangling programs, through a noise channel, and through measurement dephasing. Noise on runtime-parametric gates is intentionally unsupported and now documented as such: channels are keyed by instruction, and a symbolic `RX(theta[0]) 0` matches no concrete gate. This is a design decision rather than a gap -- the only genuinely continuous hardware gate is `RZ`, which is virtual and so has no pulse and no noise, while every other rotation is calibrated at fixed angles and is correctly described by a channel on that concrete instruction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QB5EGDshZbFtBvyfPPQqU
…, drop barriers
Withdrawn features (both added earlier in this branch, now reversed by request):
- **Gate modifiers are no longer supported at all.** DAGGER support is removed;
`_validate_and_count_dagger_modifiers` becomes `_reject_gate_modifiers`, which
raises for any modifier. Rejecting is the half that matters: the original defect
was DAGGER being *silently dropped*, so `DAGGER RX(0.7)` simulated `RX(+0.7)`.
- **Literal qubits in a DEFCIRCUIT body are rejected.** Quil permits them, but such
a qubit is invisible to `Program.get_qubit_indices`, so it escapes the register
the simulator sizes itself for. The arity checks are kept.
Extend gate batching to the density-matrix simulator:
`_build_vectorized_unitary_constructor` becomes
`_build_vectorized_operator_constructor(..., as_superop)`, with `_embed_op_to_group`
and `_make_group_fold` parameterized by stack width. Embedding and lifting commute
for a unitary, so the lift happens last and the existing embedding machinery is
reused unchanged; constant non-unitary operations (channel SuperOps, instrument
total channels) join the eager path, since `qx.embed` handles them and `to_superop`
is idempotent. `DensityMatrixSimulator` drops `_stack_superops` and, with it,
`_const_op_stack` -- the cache existed only to dodge the slow trace that batching
removes, so deleting it also removes mutable state from a jit-friendly method.
Measured, parametric programs:
6q x 10L DM compile 17.67s -> 0.32s (55x; DM/SV ratio 51x -> 1.0x)
4q x 5L DM compile 3.64s -> 0.27s
The batching itself was reviewed for possible simplification and kept: removing it
costs 518x in compile time (180s vs 0.35s at 12q x 20L, with XLA printing "Very slow
compile?"). That justification is now recorded in the function docstring and the
architecture doc rather than left as an assertion.
Remove barrier nodes:
`barrier_nodes` was derived from QuantumInstrument ops, and `measurement="superop"`
produces none -- so it was always empty, i.e. untested dead code rather than a live
constraint on the differentiable simulators. Operation order is protected by the
quotient-graph convexity check and the lexicographic topological emission, both
independent of barriers. The trajectory simulator will introduce instrument merging
along with the code that needs it.
Bake in the measurement mode:
`measurement` is removed from `ProgramSimulator.__init__`, where it was accepted,
documented, and then silently ignored (`resolve_for_gradable` hard-coded "superop",
so `ProgramSimulator(measurement="instrument").resolve()` returned a SuperOp). The
parameter remains on `expand_program`/`resolve_program`, which are the mechanism and
which #1856 needs for the instrument path. The mode is now unrepresentable at the
level where it was being ignored.
Rename gradable -> differentiable:
`_GradableSimulator` -> `_DifferentiableSimulator`, `resolve_for_gradable` ->
`resolve_for_differentiable`, and "grad-able" -> "differentiable" in prose.
Resolver simplifications:
- Remove `_ExpansionContext` and `_build_expansion_context`. Measured at 1-4 ms,
under 6% of a resolve, paid once per construction -- not worth a cache. The four
derivations are recomputed in `expand_program` and the `context` parameter is gone.
- Remove `_freeze_resolver`. `Resolution` now has a `resolve` method that iterates
its own fields, and the `callable(item)` test plus `cast` becomes
`isinstance(item, ParametricGate)`, which is what was meant.
Architecture documentation:
- Qudit support is promoted to a first-class goal; it was only a parenthetical.
- The dimension-inference double expansion is acknowledged as the least elegant part
of the pipeline, with the circularity that forces it and the two rejected
alternatives spelled out.
- `compute`'s parameter vector is documented as optional, including the
one-slot-per-gate-occurrence layout.
- The two new restrictions are stated, as is why rejecting beats ignoring.
`ProgramSimulator` keeps its name and its inheritance; a note records why it is a
shared base rather than a composed "simulation plan" (only two consumers), so the
question does not resurface in review of #1856/#1857.
Verification: 1838 unit tests pass (suite runtime 212s -> 130s from the compile win),
11 doctests, ruff format/check and mypy clean on the pinned toolchain. Correctness
held fixed across the density-matrix rewrite: all 120 operand-permutation cases still
agree between the two simulators, and the notebook's section-4 optimum-shift figures
are bit-identical (global depolarizing shift exactly 0.000000, E/E_ideal exactly
0.9500).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QB5EGDshZbFtBvyfPPQqU
erichulburd
marked this pull request as draft
September 8, 2026 23:48
Contributor
Author
|
See #1867 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Insert your PR description here. Thanks for contributing to pyQuil! 🙂
Checklist
masterbranch