PML-361: phase errors on Unitary-based circuit using a decomposition - #288
Conversation
…and imprecision with tests
…rcuit-noise-from-unitary
|
@claude review |
|
Claude finished @CassNot's task in 6m 54s —— View job Code review — PML-361 (phase errors on
|
… non-silent fallback for the Optimizer and warning on fit error
…rcuit-noise-from-unitary
|
@claude review |
|
Claude finished @CassNot's task in 5m 47s —— View job Code review — PML-361 (phase errors on
|
| Location | Stale claim |
|---|---|
locirc_to_tensor.py:141-143 (_decompose_unitaries docstring) |
"reaches every physical phase … including the theta and phi parameters of every BS in the mesh" |
locirc_to_tensor.py:718-721 (comment) |
"a constant BS must also stay dynamic" |
docs/source/quantum_expert_area/noisy_simulations.rst:112 |
"so the configured phase noise applies to every physical phase" |
tests/pcvl_pytorch/test_unitary_decomposition_noise.py:253 |
"phase_imprecision=0.1 quantizes every BS and PS phase in the mesh" |
The rst line matters most — it's the user-facing promise, and it's the PR's stated goal. If the MZI template used by optimize_rectangle keeps its two tunable phases as PS (which the phL* output-layer naming suggests), then "PS only" is complete in practice and the docs just need rewording to say so explicitly. If any tunable phase lives in a BS parameter, then it now receives no noise and the goal is only partly met. Which is the case is worth stating in the docs either way, since it's the difference between "all mesh phases are noisy" and "only the output layer is noisy".
4. Neither new warning has a test, and neither new failure branch does. AGENTS.md is explicit: "If new warning behavior is added, add or update tests that assert the warning is emitted." grep over both test files finds no pytest.warns for:
- the
m >= 16slow-decomposition warning (line 187); - the fit-error-vs-noise-scale warning (line 289).
Also untested: the overlap-quality RuntimeError (line 272), the phL count-mismatch RuntimeError (line 314), and the new component.m == 1 branch (line 170) — test_polarized_unitary_raises_value_error builds a 1-mode circuit but raises on polarization before reaching it, so the bare-phase path has zero coverage.
Medium
5. The fit-error warning is mis-scaled by ~3 orders of magnitude and will essentially never fire — locirc_to_tensor.py:281-299
fit_error = 1.0 - abs(overlap)
if fit_error > 0.1 * noise_scale:1 - |overlap| is second order in the phase discrepancy, while noise_scale is first order. For per-entry error σ, |overlap| ≈ 1 - σ²/2. Your own docs state entries agree to ~1e-3, i.e. fit_error ≈ 5e-7. The condition then needs noise_scale < 5e-6 rad to trigger — but the artifact it's meant to catch (σ ≈ 1e-3) already dominates a phase_error of 1e-4 rad, silently. Compare like with like:
entry_error = float(np.sqrt(2.0 * max(0.0, 1.0 - abs(overlap)))) # ≈ per-phase discrepancy, rad
if entry_error > 0.1 * noise_scale:Same dimensional issue in _FIT_TOLERANCE = 1e-3: as a bound on 1 - |overlap| it admits σ ≈ 0.045 rad, which is a much looser gate than the docstring's "overlap should be > 0.999" implies (with fidelity error ~1e-6 you should see > 0.999999). The gate is still a real improvement over no check — it's the warning that's ineffective.
6. _DECOMPOSITION_CACHE is an unbounded process-global with no way to clear it — locirc_to_tensor.py:47
The memoization is the right fix for the RNG/determinism problem, but as written:
- No eviction. Every distinct matrix permanently retains a full mesh
Circuit(≈m(m−1)components; ~380 form=20). AReservoirClassifiersweep or ensemble — random reservoir unitary per trial, which is the headline use case — leaks one entry per trial for the process lifetime. A bounded LRU (collections.OrderedDict+maxsize, orfunctools.lru_cacheover a small keyed helper) plus a documentedclear_decomposition_cache()would fix both this and test isolation. - The determinism claim overreaches. The docstring says "(b) rebuilding or reloading a model always produces the same mesh phases". That holds within a process; a save + reload in a fresh interpreter re-runs
optimize_rectanglefrom a fresh global NumPy RNG and gets different phases — and sincephase_imprecisionquantization is discontinuous in those phases, a different noisy unitary. Please scope the claim to a single process. Truly reproducible results need either an explicit seed or the exact algebraic Clements decomposition (pcvl.Circuit.decomposition(..., shape=InterferometerShape.RECTANGLE)), which would also remove the ~1e-3 artifact behind move tests to root, add license header in all files #5. - No test. The cache is the mechanism behind the determinism guarantee and has no direct coverage. It also has no isolation fixture, so
test_non_convergence_error_names_component(which monkeypatchesoptimize_rectangleto raise, and doesn't seed NumPy) depends on its unseeded matrix never being a cache hit. Anautousefixture clearing the dict would make that robust. Note thattest_circuit_converter_with_phase_noise_on_gpu's finalatol=1e-12CPU-vs-GPU comparison silently relies on the cache — worth a comment, since it looks like a device-parity assertion but is really a cache-determinism assertion.
7. The m >= 16 warning fires before the cache lookup — locirc_to_tensor.py:185-193
It sits above the cache_key in _DECOMPOSITION_CACHE check, so a cached 16-mode unitary still warns "this may take several seconds … The result is cached after the first build" on every subsequent build, when the build is in fact instant. Move it below the cache check. (The fit-error warning has the mirror-image asymmetry — it only fires on a cache miss, so a genuine accuracy problem is reported once and then goes quiet for the rest of the process. Worth a deliberate decision on which of the two you want.)
Low / nits
ruff formatwas still not run ontests/models/test_reservoir_classifier.py— 41 lines with trailing whitespace (grep -c '[ \t]$'). CI only lintsmerlin/, so it won't fail the build, but the "Code formatted" checkbox isn't accurate for that file.- Cosmetic churn in
_compute_tensor(BS). Aftercfee3fatheraw = ...; param_values.append(raw)indirection (lines 1013-1026) is a no-op leftover, and the reformattedunitary_tensor = (unitary_tensor .unsqueeze(0) ...)chain at lines 1060-1065 is a leading-dot styleruff formatwould collapse. Reverting both keeps the diff to the actual change (AGENTS.md: "Do not rewrite unrelated code"). assert feature_diff < 1.0/assert feature_diff > 0.0(test_reservoir_classifier.py) still don't encode a property; the second is trivially true for any float comparison.not torch.allclose(a, b, rtol=0.1)— this passes largely because standardized features contain near-zero entries, where0.1 * |b|collapses toatol. It reads as a strong check but is really an incidental one.(a - b).abs().max() > 1e-6would state the intent (fresh noise sample per call) directly and won't drift.- Tolerance mismatch persists:
test_unitary_decomposition_noise.pydocuments "entries agreeing to ~1e-3" but sets_ELEMENTWISE_ATOL = 1e-2. phLprefix coupling (line 306) is still tied to perceval's internal template naming with onlyperceval-quandela>=1.2.1pinned. The count-mismatchRuntimeErrorguards it, which is the right shape — just make sure the pin reflects the version that guarantees the naming._decompose_unitariesdropscircuit.nameon the rebuilt circuit (display only).- Release note still worth adding: existing users with only
phase_imprecisionset and aUnitaryin their circuit get different numerical results and a slower first build.
What's good
- The
cache=Falserework of the reservoir tests is exactly right — they now exercise the real stochastic path instead of the feature cache. - The overlap-quality gate makes a bad fit loud instead of producing a meaningless
alpha, per the failure policy. - Global-phase folding for sub-mode blocks is correct and has a dedicated test;
PERMexclusion is tested by object identity. - Fast path preserved and regression-tested from both ends (
test_reservoir_unitary_layer_neutral_noise_matches_no_noise,test_no_noise_converter_keeps_unitary_fast_path). - Memoization is the right call and the cache docstring reasons carefully about thread safety and RNG perturbation.
- Gradient flow through input parameters verified end-to-end on the decomposed circuit, plus unitarity checks.
• BranchPML-361-circuit-noise-from-unitary
…fix_values() required, phase-aligned fit residual, cached meshes revalidated on reuse
…align message, fail fix_value, fixed cache bypass
|
I agreed with all Claude's comments (High) and addressed them. I also added more thorough GPU tests to ensure compatibility and they all run. |
|
Claude finished @CassNot's task in 6m 50s —— View job Code review — PML-361 (phase errors on
|
| Location | Claim | Reality |
|---|---|---|
locirc_to_tensor.py:142-143 |
"the fixed mesh parameters themselves are not tunable and receive no phase noise" | wrong |
docs/source/quantum_expert_area/noisy_simulations.rst:113-115 |
"This makes the unitary's structure explicit for analysis. Phase noise applies only to explicitly defined tunable pcvl.PS components in the circuit" |
wrong |
tests/pcvl_pytorch/test_unitary_decomposition_noise.py:413 |
"Decomposed mesh parameters are fixed constants and receive no phase noise" | wrong |
_compute_tensor(PS) reaches _apply_phase_noise on both branches — the is_variable one at line 1168 and the fixed-value _value one at line 1181-1184 — quantization and perturbation are applied unconditionally at line 1197. The PR's own tests confirm it: line 141-160 counts 16 noise-carrying PS in a 4-mode mesh, and line 178-188 asserts the quantized mesh output deviates from the target. The class docstring at locirc_to_tensor.py:459-466 says the correct thing ("Phase noise is then applied to the PS components of the mesh") and directly contradicts the function docstring 320 lines above it.
The rst line is the one that actually matters: it is the user-facing promise, and as written it tells a reader that this PR's feature does not do what the PR title says. Since #1 confirms all mesh phases are PS, the honest wording is "phase noise reaches every tunable phase of the mesh, which are all pcvl.PS; pcvl.BS parameters carry no noise, and the mesh's beam splitters are fixed 50:50 couplers with no tunable phase".
Medium
3. The cache write is not guarded by optimizer_is_default, so a patched optimizer poisons a process-global. The read is guarded (line 251), the write at line 366 is not. Concretely: test_fit_error_vs_noise_scale_warning warns rather than raises, so its MockMesh is deep-copied into _DECOMPOSITION_CACHE under the seed-4 matrix key — the same key used by test_phase_error_sampling_varies_and_is_reproducible, which also does np.random.seed(4) + _single_unitary_circuit(4). File order saves it today (line 163 runs before line 275); any reordering (-k selection, split runs, random ordering) serves a mock mesh to a real test, and the mock's get_parameters returns 4 phL entries so the reuse validation would pass it. Seeds 5 and 6 collide the same way but those tests raise before the write. Guard the write with optimizer_is_default, and add an autouse fixture clearing the cache — the same fixture also removes the current unseeded dependence in test_non_convergence_error_names_component.
4. SUPPORTED_COMPONENTS lost its docstring — locirc_to_tensor.py:46-83. The new _DECOMPOSITION_CACHE (48-71) and _DEFAULT_OPTIMIZE_RECTANGLE (73-74) assignments-plus-docstrings were inserted between SUPPORTED_COMPONENTS = (...) and its docstring, so the "Tuple of quantum components supported by CircuitConverter" block at lines 75-83 is now a stray literal trailing _DEFAULT_OPTIMIZE_RECTANGLE's own docstring — documenting nothing, and invisible to autodoc for either symbol. Move the new globals below line 83.
5. The m >= 16 warning still fires above the cache lookup — lines 230-238 precede the cache_key in _DECOMPOSITION_CACHE check at 251. Masked today by #1, but once the cache works a cached 16-mode unitary will warn "may take several seconds … The result is cached after the first build" on every rebuild, when the build is instant. The residual warning has the mirror asymmetry (miss-only, so a genuine accuracy problem is reported once per process then goes quiet) — worth a deliberate decision on which side each belongs. Separately, test_slow_decomposition_warning_for_large_unitary runs a real 16-mode optimization, which by the warning's own text is "several seconds" of suite time; monkeypatching the optimizer keeps the assertion and drops the cost.
6. _FIT_TOLERANCE is still a second-order gate behind a now-first-order warning — lines 85-94, 304. The hard RuntimeError compares 1 - |overlap| against 1e-3; since |overlap| ≈ 1 - σ²/2, that admits a per-entry discrepancy of σ ≈ 0.045 rad. The warning at line 336 now correctly uses the first-order phase-aligned residual. Net effect: a band of genuinely bad fits warns but never errors. Also _FIT_TOLERANCE's docstring says "the overlap should be > 0.999" where the real figure is > 0.999999 — the stated bound is 3 orders looser than the behavior it describes.
7. _DECOMPOSITION_CACHE is still unbounded with no clear API (carried over). Once #1 is fixed this becomes load-bearing: one full mesh Circuit (≈ m(m−1) components) retained per unique matrix for the process lifetime. A ReservoirClassifier sweep or ensemble draws a fresh random unitary per trial — one leaked entry per trial. A bounded OrderedDict LRU plus a documented clear_decomposition_cache() covers both this and test isolation (#3). Also please scope the "rebuilding or reloading a model always produces the same mesh phases" claim (line 159-160) to a single process — a save/reload in a fresh interpreter re-fits from a fresh RNG.
Low / nits
ruff formatstill not run on the touched test files.tests/models/test_reservoir_classifier.pyhas 41 trailing-whitespace lines, and the newtests/pcvl_pytorch/test_unitary_decomposition_noise.pyisn't formatted either (lines 153-155 collapse to one line, the 267-268circuit.add(...)split, over-lengthmonkeypatch.setattrat 311/343/373, and 375). CI only lintsmerlin/(.github/workflows/ci.yml:56-57), so the build won't fail — but the "Code formatted (ruff format)" checkbox isn't accurate.test_one_mode_unitary_creates_single_ps:249assignsphase_value = np.pi / 4and never uses it (ruffF841). More to the point, the test asserts only that aPSis emitted, never that its phase equalsangle(target[0, 0])— which is the part of line 224 worth protecting._apply_phase_noisedocstring first line: "Apply configured phase noise to a phase tensor in-place (returns new tensor)" — self-contradictory; nothing is in-place.assert feature_diff > 0.0(combined reservoir test) is still trivially true for two independently-sampled noisy runs.not torch.allclose(a, b, rtol=0.1)is used in five places as the "differs" check. On standardized features0.1 · |b|collapses toatolnear zero entries, so it reads much stronger than it is.(a - b).abs().max() > 1e-6states "fresh noise sample per call" directly.- Tolerance mismatch persists: the new module docstring says "entries agreeing to ~1e-3" while
_ELEMENTWISE_ATOL = 1e-2. pytest.ini:5-6setsfilterwarnings = error, so both newUserWarnings are exceptions under test. The reservoir tests run atm = 3(reservoir_classifier.py:197,encoded_input_features + 1withPCA(n_components=2)), residual ≈ 1.7e-3 against thresholds of 5e-3–1e-2, so there's headroom — but a future test at realistic mrad-scalephase_errorwould hard-fail rather than warn. Worth knowing this is the chosen behavior.- The
alphafold at line 357-361 can push aphLvalue negative and relies on perceval'sParameterbeing periodic sofix_valuewraps rather than range-failing. Worth one inline sentence, since it's an implicit dependency on the template's parameter construction — same category as thephLprefix coupling (guarded by the count check, good) with onlyperceval-quandela>=1.2.1pinned. _decompose_unitariesstill dropscircuit.nameon the rebuilt circuit (display only).- Release note still worth adding: existing users with only
phase_imprecisionset and aUnitaryin their circuit get different numerical results and a slower build.
What's good
_apply_phase_noiseis now genuinely shared by thePSoverload — the dead code and the duplicated quantization/perturbation logic are both gone.- Dropping the
BSclause fromis_phase_error_sensitiverestores the adjacent-numeric-tensor merge for the whole decomposed mesh; that was the biggest hidden cost. - The phase-aligned Frobenius residual is dimensionally correct and detects entry-wise error the trace overlap can cancel.
- Every new warning and failure branch is tested:
m >= 16, fit residual, overlap gate,phLcount mismatch,m == 1, polarized, non-convergence. That's the full set AGENTS.md asks for. test_decomposed_mesh_keeps_all_phase_shifters_noise_sensitivewith the exact2·m(m−1)/2 + mcount is precisely the right test, and it also answers the coverage question definitively.- Read-side cache bypass under a replaced optimizer, and cached-mesh unitarity revalidation, are both careful defensive additions — the reuse validation is the right shape, it just checks the wrong parameter set (Update issue templates #1).
- Global-phase folding for sub-mode blocks remains correct and dedicated-tested;
PERMexclusion tested by object identity; fast path regression-guarded from both ends.
• BranchPML-361-circuit-noise-from-unitary
LF-Vigneux
left a comment
There was a problem hiding this comment.
Everything seems good
Summary
Currently, if the circuit we give the
QuantumLayeris Unitary-based and we have aNoiseModel, the phase error cannot be applied.This is problematic because:
ReservoirClassifier)Here, I am adding automatic decomposition of black-box
Unitarycomponents into Clements MZI meshes when circuit phase noise is configured. This enables applying phase noise (phase_imprecisionandphase_error) to every physical phase in the circuit, not just parameterized phase shifters.We should now be able to model realistic hardware imperfections affecting all circuit phases, including those inside black-box unitary blocks (like ReservoirClassifier)
Related Issue
PML-361
Type of change
Proposed changes
New helper function
_decompose_unitaries()that:PERMUnitarycomponentsCircuitOptimizerHow to test / How to run
Performance considerations (optional)
Documentation
Checklist
SPHINXOPTS="-W --keep-going -n" make -C docs clean htmlthe docs are built without any warning or errors.