From edaf984ad89d1ec91a68880c701e9882ce01fb85 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Tue, 4 Aug 2026 09:35:48 -0600 Subject: [PATCH] Rename NoiseModel to NoiseParameters, export from pecos, add a fluent interface --- docs/user-guide/dem-from-guppy.md | 13 +- docs/workflows/guppy-dem-decoding.md | 8 +- examples/surface/decoder_comparison.py | 8 +- .../surface/dem_decomposition_diagnostics.py | 4 +- examples/surface/dem_method_ler_comparison.py | 4 +- examples/surface/generate_data.py | 8 +- .../graphlike_dem_projection_benchmark.py | 4 +- examples/surface/ml_lookup_decoder.py | 4 +- .../surface/native_dem_threshold_sweep.py | 4 +- .../surface/szz_circuit_quality_report.py | 4 +- examples/surface_code_experiments.ipynb | 6 +- examples/surface_code_noisy_decoding.ipynb | 8 +- examples/surface_code_threshold.ipynb | 6 +- examples/surface_code_thresholds.ipynb | 8 +- python/quantum-pecos/src/pecos/__init__.py | 2 + python/quantum-pecos/src/pecos/qec/dem.py | 34 +-- .../src/pecos/qec/surface/__init__.py | 19 +- .../src/pecos/qec/surface/circuit_gen.py | 4 +- .../src/pecos/qec/surface/decode.py | 234 +++++++++++++++--- .../tests/qec/surface/test_check_plan.py | 8 +- .../qec/surface/test_clifford_deformation.py | 6 +- .../qec/surface/test_idle_noise_families.py | 22 +- .../qec/surface/test_noise_parameters.py | 181 ++++++++++++++ .../qec/surface/test_pauli_mask_harvest.py | 12 +- .../qec/surface/test_pauli_twirl_handoff.py | 34 +-- .../tests/qec/surface/test_surface_decoder.py | 76 +++--- .../qec/surface/test_szz_interaction_basis.py | 38 +-- .../qec/test_decomposed_dem_invariants.py | 8 +- .../tests/qec/test_from_guppy_dem.py | 20 +- .../tests/qec/test_guppy_dem_builder.py | 14 +- .../tests/qec/test_qec_ux_entrypoints.py | 12 +- 31 files changed, 585 insertions(+), 228 deletions(-) create mode 100644 python/quantum-pecos/tests/qec/surface/test_noise_parameters.py diff --git a/docs/user-guide/dem-from-guppy.md b/docs/user-guide/dem-from-guppy.md index ed9f84184..e571aaf0c 100644 --- a/docs/user-guide/dem-from-guppy.md +++ b/docs/user-guide/dem-from-guppy.md @@ -198,10 +198,13 @@ different DEM. ## Grouping Noise Parameters Both Guppy DEM entry points accept either the existing flat noise keywords or -one `NoiseModel` containing the complete noise configuration. The forms below -are equivalent. Do not mix them in one call: even an explicitly passed flat +one `NoiseParameters` instance containing the complete noise configuration. +`NoiseParameters` is available from the `pecos` top level, and supports both +its original dataclass constructor and immutable `with_` chaining. +The grouped and flat forms below are equivalent. Do not mix them in one call: +even an explicitly passed flat default conflicts with `noise`. When `noise` is present, its defaults fully -replace the entry point's defaults; for example, `NoiseModel().p1` is `0.0`, +replace the entry point's defaults; for example, `NoiseParameters().p1` is `0.0`, not the flat `p1=0.001` default. @@ -210,8 +213,8 @@ from guppylang import guppy from guppylang.std.builtins import result from guppylang.std.quantum import cx, measure, qubit +from pecos import NoiseParameters from pecos.qec import DetectorErrorModel -from pecos.qec.surface import NoiseModel @guppy @@ -228,7 +231,7 @@ common = { "observables_json": '[{"id": "L0", "result_tags": ["m1"]}]', "seed": 0, } -noise = NoiseModel(p1=0.002, p2=0.004, p_meas=0.006, p_prep=0.008) +noise = NoiseParameters().with_p1(0.002).with_p2(0.004).with_p_meas(0.006).with_p_prep(0.008) grouped = DetectorErrorModel.from_guppy(noisy_pair, noise=noise, **common) flat = DetectorErrorModel.from_guppy( diff --git a/docs/workflows/guppy-dem-decoding.md b/docs/workflows/guppy-dem-decoding.md index 5795ae193..17239f4c8 100644 --- a/docs/workflows/guppy-dem-decoding.md +++ b/docs/workflows/guppy-dem-decoding.md @@ -123,8 +123,8 @@ model semantics. `DetectorErrorModel.builder()` configures the run through chained setters and returns the DEM together with the audit trail and the result-column evaluator -used in stage 4b. A `NoiseModel` carries the entire noise configuration as one -argument. +used in stage 4b. A `NoiseParameters` instance carries the entire noise +configuration as one argument. The one-call forms `DetectorErrorModel.from_guppy(...)` and `build_dem_from_guppy(...)` remain available and run this same pipeline; they @@ -132,10 +132,10 @@ take the noise settings as individual keyword arguments instead. ```python +from pecos import NoiseParameters from pecos.qec import DetectorErrorModel -from pecos.qec.surface import NoiseModel -noise = NoiseModel( +noise = NoiseParameters( p1=0.002, p2=0.02, p_meas=0.02, diff --git a/examples/surface/decoder_comparison.py b/examples/surface/decoder_comparison.py index 926b5de7b..4d6d77483 100644 --- a/examples/surface/decoder_comparison.py +++ b/examples/surface/decoder_comparison.py @@ -31,7 +31,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from pecos.qec.surface import NoiseModel + from pecos.qec.surface import NoiseParameters @dataclass @@ -69,7 +69,7 @@ class ComparisonPoint: def _build_sampler( distance: int, num_rounds: int, - noise: NoiseModel, + noise: NoiseParameters, basis: str, circuit_source: str, ) -> tuple: @@ -172,7 +172,7 @@ def run_comparison( p_prep_scale: float, ) -> list[ComparisonPoint]: """Run the full comparison and return results.""" - from pecos.qec.surface import NoiseModel + from pecos.qec.surface import NoiseParameters points: list[ComparisonPoint] = [] total_configs = len(distances) * len(error_rates) @@ -182,7 +182,7 @@ def run_comparison( num_rounds = 2 * distance for p in error_rates: config_idx += 1 - noise = NoiseModel( + noise = NoiseParameters( p1=p * p1_scale, p2=p, p_meas=p * p_meas_scale, diff --git a/examples/surface/dem_decomposition_diagnostics.py b/examples/surface/dem_decomposition_diagnostics.py index 799da3ac2..d7938f8ef 100644 --- a/examples/surface/dem_decomposition_diagnostics.py +++ b/examples/surface/dem_decomposition_diagnostics.py @@ -532,7 +532,7 @@ def run_case( pair_analysis_max_effects: int, ) -> CaseResult: from pecos._traced_circuit import normalize_traced_tick_circuit - from pecos.qec.surface import NoiseModel, SurfacePatch, build_native_sampler + from pecos.qec.surface import NoiseParameters, SurfacePatch, build_native_sampler from pecos.qec.surface.circuit_builder import ( generate_dem_from_tick_circuit_via_stim, ) @@ -542,7 +542,7 @@ def run_case( ) patch = SurfacePatch.create(distance=distance) - noise = NoiseModel(p1=p / 30.0, p2=p, p_meas=p / 3.0, p_prep=p / 3.0) + noise = NoiseParameters(p1=p / 30.0, p2=p, p_meas=p / 3.0, p_prep=p / 3.0) noise_args = { "p1": noise.p1, "p1_gate_rates": SZZ_Z_FRAME_P1_GATE_RATES if interaction_basis == "szz" else None, diff --git a/examples/surface/dem_method_ler_comparison.py b/examples/surface/dem_method_ler_comparison.py index 84dd7ef28..ba692e934 100644 --- a/examples/surface/dem_method_ler_comparison.py +++ b/examples/surface/dem_method_ler_comparison.py @@ -131,12 +131,12 @@ def generate_dems( Returns list of (method_name, raw_dem, decomposed_dem_or_None). decomposed_dem is None when the method cannot produce a graphlike DEM. """ - from pecos.qec.surface import NoiseModel + from pecos.qec.surface import NoiseParameters from pecos.qec.surface.decode import generate_circuit_level_dem_from_builder results = [] - noise = NoiseModel( + noise = NoiseParameters( p1=noise_params.get("p1", 0.0), p2=noise_params.get("p2", 0.0), p_meas=noise_params.get("p_meas", 0.0), diff --git a/examples/surface/generate_data.py b/examples/surface/generate_data.py index 1eb99df7b..9a510ded6 100644 --- a/examples/surface/generate_data.py +++ b/examples/surface/generate_data.py @@ -28,7 +28,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from pecos.qec.surface import NoiseModel + from pecos.qec.surface import NoiseParameters # -- Data model --------------------------------------------------------------- @@ -97,7 +97,7 @@ def _decoder_base_name(name: str) -> str: def _build_sampler( distance: int, num_rounds: int, - noise: NoiseModel, + noise: NoiseParameters, basis: str, circuit_source: str, ) -> tuple: @@ -159,7 +159,7 @@ def generate( duration_multipliers: list[float], ) -> DataShard: """Run the full data generation and return a shard.""" - from pecos.qec.surface import NoiseModel + from pecos.qec.surface import NoiseParameters config = { "distances": distances, @@ -200,7 +200,7 @@ def generate( for d in distances: for p in error_rates: - noise = NoiseModel( + noise = NoiseParameters( p1=p * p1_scale, p2=p, p_meas=p * p_meas_scale, diff --git a/examples/surface/graphlike_dem_projection_benchmark.py b/examples/surface/graphlike_dem_projection_benchmark.py index 98745d728..e6878c097 100644 --- a/examples/surface/graphlike_dem_projection_benchmark.py +++ b/examples/surface/graphlike_dem_projection_benchmark.py @@ -103,7 +103,7 @@ def build_case( variants: list[str], ) -> BenchmarkResult: from pecos._traced_circuit import normalize_traced_tick_circuit - from pecos.qec.surface import NoiseModel, SurfacePatch, build_native_sampler + from pecos.qec.surface import NoiseParameters, SurfacePatch, build_native_sampler from pecos.qec.surface.circuit_builder import ( generate_dem_from_tick_circuit_via_stim, ) @@ -118,7 +118,7 @@ def build_case( ) setup_timings: list[TimedValue] = [] patch = SurfacePatch.create(distance=distance) - noise = NoiseModel(p1=p / 30.0, p2=p, p_meas=p / 3.0, p_prep=p / 3.0) + noise = NoiseParameters(p1=p / 30.0, p2=p, p_meas=p / 3.0, p_prep=p / 3.0) noise_args = { "p1": noise.p1, "p1_gate_rates": SZZ_Z_FRAME_P1_GATE_RATES if interaction_basis == "szz" else None, diff --git a/examples/surface/ml_lookup_decoder.py b/examples/surface/ml_lookup_decoder.py index 6fefa9ca2..9d9ac1a19 100644 --- a/examples/surface/ml_lookup_decoder.py +++ b/examples/surface/ml_lookup_decoder.py @@ -205,10 +205,10 @@ def main(): ler_lookup = errors_lookup / n # Compare with pymatching - from pecos.qec.surface import NoiseModel + from pecos.qec.surface import NoiseParameters from pecos.qec.surface.decode import generate_circuit_level_dem_from_builder - noise_obj = NoiseModel( + noise_obj = NoiseParameters( p1=noise_params["p1"], p2=noise_params["p2"], p_meas=noise_params["p_meas"], diff --git a/examples/surface/native_dem_threshold_sweep.py b/examples/surface/native_dem_threshold_sweep.py index 9ce2b9124..c5b5b8183 100755 --- a/examples/surface/native_dem_threshold_sweep.py +++ b/examples/surface/native_dem_threshold_sweep.py @@ -669,11 +669,11 @@ def _decoder_runtime( p_prep_scale: float = 0.5, ) -> _DecoderRuntime: """Build and cache the expensive native decoder-side objects once.""" - from pecos.qec.surface import NoiseModel, SurfaceDecoder + from pecos.qec.surface import NoiseParameters, SurfaceDecoder basis = basis.upper() patch = _surface_patch(distance) - noise = NoiseModel( + noise = NoiseParameters( p1=physical_error_rate * p1_scale, p2=physical_error_rate, p_meas=physical_error_rate * p_meas_scale, diff --git a/examples/surface/szz_circuit_quality_report.py b/examples/surface/szz_circuit_quality_report.py index a050d4f09..04e6d0678 100644 --- a/examples/surface/szz_circuit_quality_report.py +++ b/examples/surface/szz_circuit_quality_report.py @@ -24,7 +24,7 @@ from dem_decomposition_diagnostics import compare_raw_dems, dem_stats from pecos._traced_circuit import normalize_traced_tick_circuit -from pecos.qec.surface import NoiseModel, OpType, SurfacePatch, build_surface_code_circuit +from pecos.qec.surface import NoiseParameters, OpType, SurfacePatch, build_surface_code_circuit from pecos.qec.surface.circuit_builder import ( _analyze_szz_forward_flow, generate_dem_from_tick_circuit_via_stim, @@ -315,7 +315,7 @@ def _dem_report( p: float, p1_ratio: float, ) -> DemReport: - noise = NoiseModel(p1=p / p1_ratio, p2=p, p_prep=p / 3.0, p_meas=p / 3.0) + noise = NoiseParameters(p1=p / p1_ratio, p2=p, p_prep=p / 3.0, p_meas=p / 3.0) noise_args = { "p1": noise.p1, "p1_gate_rates": SZZ_Z_FRAME_P1_GATE_RATES if interaction_basis == "szz" else None, diff --git a/examples/surface_code_experiments.ipynb b/examples/surface_code_experiments.ipynb index 40d1ff49f..8f05fcd51 100644 --- a/examples/surface_code_experiments.ipynb +++ b/examples/surface_code_experiments.ipynb @@ -45,7 +45,7 @@ "import numpy as np\n", "from pecos.compilation_pipeline import compile_guppy_to_hugr\n", "from pecos.guppy.surface import get_num_qubits, make_surface_code\n", - "from pecos.qec.surface import NoiseModel, SurfaceDecoder, SurfacePatch, plot_surface_code\n", + "from pecos.qec.surface import NoiseParameters, SurfaceDecoder, SurfacePatch, plot_surface_code\n", "from selene_sim import DepolarizingErrorModel, IdealErrorModel, SimpleRuntime, Stim, build" ] }, @@ -542,7 +542,7 @@ "\n", " for p in ERROR_RATES:\n", " error_model = DepolarizingErrorModel(p_1q=p, p_2q=p, p_meas=p, p_init=p)\n", - " noise = NoiseModel(p1=p, p2=p, p_meas=p, p_prep=p)\n", + " noise = NoiseParameters(p1=p, p2=p, p_meas=p, p_prep=p)\n", "\n", " # Simulate once, decode with all decoders\n", " shots = run_shots(instance, nq, NUM_SHOTS, error_model)\n", @@ -744,7 +744,7 @@ "\n", " for p in ERROR_RATES:\n", " error_model = DepolarizingErrorModel(p_1q=p, p_2q=p, p_meas=p, p_init=p)\n", - " noise = NoiseModel(p1=p, p2=p, p_meas=p, p_prep=p)\n", + " noise = NoiseParameters(p1=p, p2=p, p_meas=p, p_prep=p)\n", "\n", " shots = run_shots(instance, nq, NUM_SHOTS, error_model)\n", "\n", diff --git a/examples/surface_code_noisy_decoding.ipynb b/examples/surface_code_noisy_decoding.ipynb index 330d661d7..80ad70990 100644 --- a/examples/surface_code_noisy_decoding.ipynb +++ b/examples/surface_code_noisy_decoding.ipynb @@ -45,7 +45,7 @@ "from pecos.compilation_pipeline import compile_guppy_to_hugr\n", "from pecos.guppy.surface import get_num_qubits, make_surface_code\n", "from pecos.qec.surface import (\n", - " NoiseModel,\n", + " NoiseParameters,\n", " SurfaceDecoder,\n", " SurfacePatch,\n", " plot_surface_code,\n", @@ -101,7 +101,7 @@ } }, "outputs": [], - "source": "from typing import Any\n\n\ndef get_logical_qubits(distance: int, basis: str) -> tuple:\n \"\"\"Get qubits in the logical operator.\"\"\"\n patch = SurfacePatch.create(distance=distance)\n if basis == \"Z\":\n return patch.geometry.logical_z.data_qubits\n return patch.geometry.logical_x.data_qubits\n\n\ndef run_memory_experiment(\n distance: int,\n num_rounds: int,\n num_shots: int,\n basis: str,\n error_model: Any,\n *,\n decode: bool = False,\n decoder_type: str = \"pymatching\",\n) -> dict:\n \"\"\"Run memory experiment and compute logical error rate.\n\n For Z-basis: prepare |0_L>, measure in Z basis, check logical Z parity.\n For X-basis: prepare |+_L>, measure in X basis, check logical X parity.\n\n Args:\n distance: Code distance\n num_rounds: Number of syndrome extraction rounds\n num_shots: Number of shots to run\n basis: 'Z' or 'X' basis\n error_model: Selene error model (IdealErrorModel or DepolarizingErrorModel)\n decode: If True, use decoding to correct errors\n decoder_type: Decoder backend ('pymatching', 'fusion_blossom', 'bp_osd', 'bp_lsd', 'union_find', 'tesseract')\n\n Returns:\n Dictionary with experiment results\n \"\"\"\n patch = SurfacePatch.create(distance=distance)\n logical_qubits = get_logical_qubits(distance, basis)\n\n # Create decoder if needed\n decoder = None\n if decode:\n # Extract noise parameters from error model\n noise = NoiseModel(\n p1=getattr(error_model, \"p_1q\", 0.01),\n p2=getattr(error_model, \"p_2q\", 0.01),\n p_meas=getattr(error_model, \"p_meas\", 0.01),\n p_prep=getattr(error_model, \"p_init\", 0.01),\n )\n decoder = SurfaceDecoder(patch, num_rounds=num_rounds, noise=noise, decoder_type=decoder_type)\n\n # Build circuit\n num_qubits = get_num_qubits(distance)\n prog = make_surface_code(distance=distance, num_rounds=num_rounds, basis=basis)\n hugr_bytes = compile_guppy_to_hugr(prog)\n instance = build(hugr_bytes, name=f\"surface_d{distance}\")\n\n # Run\n num_logical_errors = 0\n num_raw_errors = 0\n\n for shot_results in instance.run_shots(\n simulator=Stim(),\n n_qubits=num_qubits,\n n_shots=num_shots,\n error_model=error_model,\n runtime=SimpleRuntime(),\n n_processes=1,\n ):\n # Collect all syndromes properly (multiple entries per key)\n synx_list = []\n synz_list = []\n final = None\n\n for name, values in shot_results:\n vals = list(values)\n if name == \"synx\":\n synx_list.append(np.array(vals, dtype=np.uint8))\n elif name == \"synz\":\n synz_list.append(np.array(vals, dtype=np.uint8))\n elif name == \"final\":\n final = vals\n\n if final is None:\n continue\n\n # Raw parity check (no decoding)\n raw_parity = sum(final[q] for q in logical_qubits) % 2\n if raw_parity != 0:\n num_raw_errors += 1\n\n if decode and decoder is not None:\n final_arr = np.array(final, dtype=np.uint8)\n\n # Decode based on basis\n if basis == \"Z\":\n is_error, _ = decoder.decode_memory_z(synx_list, synz_list, final_arr)\n else:\n is_error, _ = decoder.decode_memory_x(synx_list, synz_list, final_arr)\n\n if is_error:\n num_logical_errors += 1\n else:\n # No decoding - use raw parity\n if raw_parity != 0:\n num_logical_errors += 1\n\n return {\n \"distance\": distance,\n \"num_shots\": num_shots,\n \"num_logical_errors\": num_logical_errors,\n \"num_raw_errors\": num_raw_errors,\n \"logical_error_rate\": num_logical_errors / num_shots,\n \"raw_error_rate\": num_raw_errors / num_shots,\n \"decoded\": decode,\n \"decoder_type\": decoder_type if decode else None,\n }" + "source": "from typing import Any\n\n\ndef get_logical_qubits(distance: int, basis: str) -> tuple:\n \"\"\"Get qubits in the logical operator.\"\"\"\n patch = SurfacePatch.create(distance=distance)\n if basis == \"Z\":\n return patch.geometry.logical_z.data_qubits\n return patch.geometry.logical_x.data_qubits\n\n\ndef run_memory_experiment(\n distance: int,\n num_rounds: int,\n num_shots: int,\n basis: str,\n error_model: Any,\n *,\n decode: bool = False,\n decoder_type: str = \"pymatching\",\n) -> dict:\n \"\"\"Run memory experiment and compute logical error rate.\n\n For Z-basis: prepare |0_L>, measure in Z basis, check logical Z parity.\n For X-basis: prepare |+_L>, measure in X basis, check logical X parity.\n\n Args:\n distance: Code distance\n num_rounds: Number of syndrome extraction rounds\n num_shots: Number of shots to run\n basis: 'Z' or 'X' basis\n error_model: Selene error model (IdealErrorModel or DepolarizingErrorModel)\n decode: If True, use decoding to correct errors\n decoder_type: Decoder backend ('pymatching', 'fusion_blossom', 'bp_osd', 'bp_lsd', 'union_find', 'tesseract')\n\n Returns:\n Dictionary with experiment results\n \"\"\"\n patch = SurfacePatch.create(distance=distance)\n logical_qubits = get_logical_qubits(distance, basis)\n\n # Create decoder if needed\n decoder = None\n if decode:\n # Extract noise parameters from error model\n noise = NoiseParameters(\n p1=getattr(error_model, \"p_1q\", 0.01),\n p2=getattr(error_model, \"p_2q\", 0.01),\n p_meas=getattr(error_model, \"p_meas\", 0.01),\n p_prep=getattr(error_model, \"p_init\", 0.01),\n )\n decoder = SurfaceDecoder(patch, num_rounds=num_rounds, noise=noise, decoder_type=decoder_type)\n\n # Build circuit\n num_qubits = get_num_qubits(distance)\n prog = make_surface_code(distance=distance, num_rounds=num_rounds, basis=basis)\n hugr_bytes = compile_guppy_to_hugr(prog)\n instance = build(hugr_bytes, name=f\"surface_d{distance}\")\n\n # Run\n num_logical_errors = 0\n num_raw_errors = 0\n\n for shot_results in instance.run_shots(\n simulator=Stim(),\n n_qubits=num_qubits,\n n_shots=num_shots,\n error_model=error_model,\n runtime=SimpleRuntime(),\n n_processes=1,\n ):\n # Collect all syndromes properly (multiple entries per key)\n synx_list = []\n synz_list = []\n final = None\n\n for name, values in shot_results:\n vals = list(values)\n if name == \"synx\":\n synx_list.append(np.array(vals, dtype=np.uint8))\n elif name == \"synz\":\n synz_list.append(np.array(vals, dtype=np.uint8))\n elif name == \"final\":\n final = vals\n\n if final is None:\n continue\n\n # Raw parity check (no decoding)\n raw_parity = sum(final[q] for q in logical_qubits) % 2\n if raw_parity != 0:\n num_raw_errors += 1\n\n if decode and decoder is not None:\n final_arr = np.array(final, dtype=np.uint8)\n\n # Decode based on basis\n if basis == \"Z\":\n is_error, _ = decoder.decode_memory_z(synx_list, synz_list, final_arr)\n else:\n is_error, _ = decoder.decode_memory_x(synx_list, synz_list, final_arr)\n\n if is_error:\n num_logical_errors += 1\n else:\n # No decoding - use raw parity\n if raw_parity != 0:\n num_logical_errors += 1\n\n return {\n \"distance\": distance,\n \"num_shots\": num_shots,\n \"num_logical_errors\": num_logical_errors,\n \"num_raw_errors\": num_raw_errors,\n \"logical_error_rate\": num_logical_errors / num_shots,\n \"raw_error_rate\": num_raw_errors / num_shots,\n \"decoded\": decode,\n \"decoder_type\": decoder_type if decode else None,\n }" }, { "cell_type": "markdown", @@ -1094,7 +1094,7 @@ "\n", "# Create a decoder configuration\n", "patch = SurfacePatch.create(distance=3)\n", - "noise = NoiseModel(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001)\n", + "noise = NoiseParameters(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001)\n", "\n", "# Generate DEM using PECOS native pipeline\n", "tc = generate_tick_circuit_from_patch(patch, num_rounds=3, basis=\"Z\")\n", @@ -1646,7 +1646,7 @@ "\n", "**Noise model**:\n", "```python\n", - "noise = NoiseModel(\n", + "noise = NoiseParameters(\n", " p1=0.001, # Single-qubit gate error rate\n", " p2=0.01, # Two-qubit gate error rate\n", " p_meas=0.01, # Measurement error rate\n", diff --git a/examples/surface_code_threshold.ipynb b/examples/surface_code_threshold.ipynb index 90e15a997..99c4a54ce 100644 --- a/examples/surface_code_threshold.ipynb +++ b/examples/surface_code_threshold.ipynb @@ -46,7 +46,7 @@ "from pecos.guppy.surface import get_num_qubits, make_surface_code\n", "from pecos.misc.threshold_curve import func, func6, threshold_fit\n", "from pecos.qec import DagFaultAnalyzer, DemBuilder\n", - "from pecos.qec.surface import NoiseModel, SurfaceDecoder, SurfacePatch\n", + "from pecos.qec.surface import NoiseParameters, SurfaceDecoder, SurfacePatch\n", "from pecos.qec.surface.circuit_builder import _extract_measurement_order, generate_tick_circuit_from_patch\n", "from pecos.qec.surface.decode import build_stim_circuit_from_patch\n", "from selene_sim import DepolarizingErrorModel, SimpleRuntime, Stim, build" @@ -438,7 +438,7 @@ "\n", " for p in ERROR_RATES:\n", " error_model = DepolarizingErrorModel(p_1q=p, p_2q=p, p_meas=p, p_init=p)\n", - " noise = NoiseModel(p1=p, p2=p, p_meas=p, p_prep=p)\n", + " noise = NoiseParameters(p1=p, p2=p, p_meas=p, p_prep=p)\n", "\n", " t0 = time.time()\n", " shots = run_shots(instance, nq, NUM_SHOTS, error_model)\n", @@ -487,7 +487,7 @@ "\n", " for p in ERROR_RATES:\n", " error_model = DepolarizingErrorModel(p_1q=p, p_2q=p, p_meas=p, p_init=p)\n", - " noise = NoiseModel(p1=p, p2=p, p_meas=p, p_prep=p)\n", + " noise = NoiseParameters(p1=p, p2=p, p_meas=p, p_prep=p)\n", "\n", " t0 = time.time()\n", " shots = run_shots(instance, nq, NUM_SHOTS, error_model)\n", diff --git a/examples/surface_code_thresholds.ipynb b/examples/surface_code_thresholds.ipynb index 2f5050c2e..7a4cd36e4 100644 --- a/examples/surface_code_thresholds.ipynb +++ b/examples/surface_code_thresholds.ipynb @@ -60,7 +60,7 @@ "# For Stim-based sampling (fast)\n", "import stim\n", "from pecos.qec.surface import (\n", - " NoiseModel,\n", + " NoiseParameters,\n", " SurfacePatch,\n", " generate_surface_code_dem,\n", ")\n", @@ -339,7 +339,7 @@ "def generate_code_capacity_dem(patch: SurfacePatch, num_rounds: int, p: float) -> str:\n", " \"\"\"Generate a code-capacity DEM (data errors only, perfect measurements).\"\"\"\n", " # Use phenomenological DEM with p_meas=0\n", - " noise = NoiseModel(p1=0, p2=p, p_meas=0, p_prep=0)\n", + " noise = NoiseParameters(p1=0, p2=p, p_meas=0, p_prep=0)\n", " return generate_surface_code_dem(patch, num_rounds=1, noise=noise, stab_type=\"Z\")\n", "\n", "# Test DEM generation\n", @@ -424,7 +424,7 @@ "source": [ "def generate_phenomenological_dem(patch: SurfacePatch, num_rounds: int, p: float) -> str:\n", " \"\"\"Generate a phenomenological DEM (data + measurement errors).\"\"\"\n", - " noise = NoiseModel(p1=0, p2=p, p_meas=p, p_prep=0)\n", + " noise = NoiseParameters(p1=0, p2=p, p_meas=p, p_prep=0)\n", " return generate_surface_code_dem(patch, num_rounds=num_rounds, noise=noise, stab_type=\"Z\")\n", "\n", "# Test DEM generation\n", @@ -510,7 +510,7 @@ "def generate_circuit_level_dem(patch: SurfacePatch, num_rounds: int, p: float) -> str:\n", " \"\"\"Generate a circuit-level DEM using PECOS native fault propagation.\"\"\"\n", " # Use same error rate for all noise sources (standard depolarizing)\n", - " noise = NoiseModel(p1=p, p2=p, p_meas=p, p_prep=p)\n", + " noise = NoiseParameters(p1=p, p2=p, p_meas=p, p_prep=p)\n", " return generate_circuit_level_dem_from_builder(patch, num_rounds=num_rounds, noise=noise, basis=\"Z\")\n", "\n", "# Test DEM generation\n", diff --git a/python/quantum-pecos/src/pecos/__init__.py b/python/quantum-pecos/src/pecos/__init__.py index 2a6854a93..16e2bd99e 100644 --- a/python/quantum-pecos/src/pecos/__init__.py +++ b/python/quantum-pecos/src/pecos/__init__.py @@ -281,6 +281,7 @@ def __getattr__(name: str): # Import program wrappers from programs submodule for convenience # These can also be accessed via pecos.programs.Qasm, etc. from pecos.programs import Guppy, Hugr, PhirJson, ProgramWrapper, Qasm, Qis, Wasm, Wat +from pecos.qec.surface.decode import NoiseParameters from pecos.tracing import ( capture_qis_operation_trace, qis_operation_trace_to_tick_circuit, @@ -333,6 +334,7 @@ def __getattr__(name: str): "Inexact", "Integer", "Nanoseconds", + "NoiseParameters", "Numeric", "Pauli", "PauliString", diff --git a/python/quantum-pecos/src/pecos/qec/dem.py b/python/quantum-pecos/src/pecos/qec/dem.py index 30cb899cc..502917e15 100644 --- a/python/quantum-pecos/src/pecos/qec/dem.py +++ b/python/quantum-pecos/src/pecos/qec/dem.py @@ -60,7 +60,7 @@ from typing_extensions import Self from pecos.qec.dem_spec import Detector, Observable - from pecos.qec.surface.decode import NoiseModel + from pecos.qec.surface.decode import NoiseParameters P1Weights = Mapping[str, float] P2Weights = Mapping[str, float] @@ -115,7 +115,7 @@ def __repr__(self) -> str: _NOISE_DEFAULT_P2 = _NoiseKeywordDefault(0.01) -def _resolve_guppy_noise(noise: NoiseModel | None, call_arguments: Mapping[str, Any]) -> dict[str, Any]: +def _resolve_guppy_noise(noise: NoiseParameters | None, call_arguments: Mapping[str, Any]) -> dict[str, Any]: """Resolve one grouped or flat Guppy DEM noise configuration.""" explicitly_flat = [ name for name in _GUPPY_NOISE_KEYWORDS if not isinstance(call_arguments[name], _NoiseKeywordDefault) @@ -137,10 +137,10 @@ def _resolve_guppy_noise(noise: NoiseModel | None, call_arguments: Mapping[str, # Import locally so dem.py remains below surface.decode in the package's # initialization graph instead of introducing a module-level back edge. - from pecos.qec.surface.decode import NoiseModel + from pecos.qec.surface.decode import NoiseParameters - if not isinstance(noise, NoiseModel): - msg = f"noise must be a NoiseModel or None, got {type(noise).__name__}" + if not isinstance(noise, NoiseParameters): + msg = f"noise must be a NoiseParameters instance or None, got {type(noise).__name__}" raise TypeError(msg) unsupported = { @@ -150,7 +150,7 @@ def _resolve_guppy_noise(noise: NoiseModel | None, call_arguments: Mapping[str, } for field, guidance in unsupported.items(): if getattr(noise, field) is not None: - msg = f"NoiseModel.{field} is not supported by the Guppy DEM entry points; {guidance}" + msg = f"NoiseParameters.{field} is not supported by the Guppy DEM entry points; {guidance}" raise ValueError(msg) expanded = {name: getattr(noise, name) for name in _GUPPY_NOISE_KEYWORDS} @@ -331,7 +331,7 @@ def from_guppy( detectors_json: str, observables_json: str = "[]", num_measurements: int | None = None, - noise: NoiseModel | None = None, + noise: NoiseParameters | None = None, p1: float = _NOISE_DEFAULT_P1, p1_weights: P1Weights | None = _NOISE_DEFAULT_NONE, p2: float = _NOISE_DEFAULT_P2, @@ -442,7 +442,7 @@ def from_guppy( circuit; if given, it must match the traced count. noise: Complete grouped noise configuration. When supplied, its values replace all flat noise keywords, including this entry - point's defaults. In particular, ``NoiseModel`` defaults such + point's defaults. In particular, ``NoiseParameters`` defaults such as ``p1=0.0`` apply instead of this function's ``p1=0.001``. Mixing ``noise`` with any flat noise keyword is rejected. p1: Single-qubit gate Pauli error rate. @@ -605,7 +605,7 @@ def from_guppy( if num_measurements is not None: builder.num_measurements(num_measurements) # Same-module private seam: the flat keyword surface stays on this - # function while noise() remains strictly a NoiseModel setter. + # function while noise() remains strictly a NoiseParameters-instance setter. return builder._legacy_noise(noise, noise_keywords).build().dem # noqa: SLF001 @@ -1012,21 +1012,21 @@ def num_measurements(self, count: int) -> Self: self._set_once("_num_measurements", count, "num_measurements") return self - def noise(self, noise_model: NoiseModel) -> Self: + def noise(self, noise_model: NoiseParameters) -> Self: """Set the complete grouped noise configuration.""" - from pecos.qec.surface.decode import NoiseModel + from pecos.qec.surface.decode import NoiseParameters - if not isinstance(noise_model, NoiseModel): - msg = f"noise() requires a NoiseModel, got {type(noise_model).__name__}" + if not isinstance(noise_model, NoiseParameters): + msg = f"noise() requires a NoiseParameters instance, got {type(noise_model).__name__}" raise TypeError(msg) self._set_once("_noise", noise_model, "noise") return self - def _legacy_noise(self, noise_model: NoiseModel | None, flat_keywords: Mapping[str, Any]) -> Self: + def _legacy_noise(self, noise_model: NoiseParameters | None, flat_keywords: Mapping[str, Any]) -> Self: """Carry the legacy entry points' flat noise keywords through the builder. Private: the flat keyword surface stays on ``from_guppy`` and - ``build_dem_from_guppy``; ``noise()`` accepts only a ``NoiseModel``. + ``build_dem_from_guppy``; ``noise()`` accepts only a ``NoiseParameters``. """ self._set_once("_noise", (_LEGACY_NOISE, noise_model, dict(flat_keywords)), "noise") return self @@ -1291,7 +1291,7 @@ def build_dem_from_guppy( num_qubits: int, detectors: Sequence[Detector], observables: Sequence[Observable] = (), - noise: NoiseModel | None = None, + noise: NoiseParameters | None = None, p1: float = _NOISE_DEFAULT_P1, p1_weights: P1Weights | None = _NOISE_DEFAULT_NONE, p2: float = _NOISE_DEFAULT_P2, @@ -1354,7 +1354,7 @@ def build_dem_from_guppy( measurement-reference forms as ``detectors``. noise: Complete grouped noise configuration. When supplied, its values replace all flat noise keywords, including this entry point's - defaults. In particular, ``NoiseModel`` defaults such as + defaults. In particular, ``NoiseParameters`` defaults such as ``p1=0.0`` apply instead of this function's ``p1=0.001``. Mixing ``noise`` with any flat noise keyword is rejected. p1: Single-qubit gate Pauli error rate. diff --git a/python/quantum-pecos/src/pecos/qec/surface/__init__.py b/python/quantum-pecos/src/pecos/qec/surface/__init__.py index f2c0df689..3789be7a7 100644 --- a/python/quantum-pecos/src/pecos/qec/surface/__init__.py +++ b/python/quantum-pecos/src/pecos/qec/surface/__init__.py @@ -17,6 +17,8 @@ parity_matrix_z: Generate Z parity check matrix """ +import warnings + # Circuit generation from geometry (unified abstraction) from pecos.qec.surface._clifford_deformation import ( LocalCliffordFrame, @@ -62,7 +64,7 @@ DecoderType, DecodingResult, NativeSampler, - NoiseModel, + NoiseParameters, SimulationResult, SurfaceDecoder, build_memory_circuit, @@ -124,6 +126,20 @@ get_stab_schedule, ) + +def __getattr__(name: str) -> type[NoiseParameters]: + """Resolve deprecated surface-code attributes lazily.""" + if name == "NoiseModel": + warnings.warn( + "NoiseModel is deprecated; use NoiseParameters instead (from pecos import NoiseParameters).", + DeprecationWarning, + stacklevel=2, + ) + return NoiseParameters + msg = f"module {__name__!r} has no attribute {name!r}" + raise AttributeError(msg) + + __all__ = [ # Twirling config (Pauli-frame randomization) "GuppyRngMaskConfig", @@ -170,6 +186,7 @@ "DecodingResult", "NativeSampler", "NoiseModel", + "NoiseParameters", "RUNTIME_IDLE_TIME_UNITS_PER_SECOND", "SimulationResult", "SurfaceDecoder", diff --git a/python/quantum-pecos/src/pecos/qec/surface/circuit_gen.py b/python/quantum-pecos/src/pecos/qec/surface/circuit_gen.py index 0886f965e..742f997bf 100644 --- a/python/quantum-pecos/src/pecos/qec/surface/circuit_gen.py +++ b/python/quantum-pecos/src/pecos/qec/surface/circuit_gen.py @@ -468,13 +468,13 @@ def compare_dems( Returns: Dictionary with comparison results """ - from pecos.qec.surface.decode import NoiseModel, generate_surface_code_dem + from pecos.qec.surface.decode import NoiseParameters, generate_surface_code_dem # Generate circuit-level DEM via Stim stim_dem = generate_circuit_level_dem(patch, num_rounds, basis, p=p) # Generate phenomenological DEM - noise = NoiseModel(p1=p, p2=p, p_meas=p, p_prep=p) + noise = NoiseParameters(p1=p, p2=p, p_meas=p, p_prep=p) stab_type = "X" if basis.upper() == "X" else "Z" phenom_dem = generate_surface_code_dem(patch, num_rounds, noise, stab_type) diff --git a/python/quantum-pecos/src/pecos/qec/surface/decode.py b/python/quantum-pecos/src/pecos/qec/surface/decode.py index e9322106d..9eac2e134 100644 --- a/python/quantum-pecos/src/pecos/qec/surface/decode.py +++ b/python/quantum-pecos/src/pecos/qec/surface/decode.py @@ -44,6 +44,7 @@ from __future__ import annotations import math +import warnings from collections.abc import Mapping, Sequence from dataclasses import dataclass, replace from enum import Enum @@ -139,8 +140,8 @@ class DecoderType(str, Enum): @dataclass -class NoiseModel: - """Circuit-level noise parameters for QEC simulation. +class NoiseParameters: + """Noise parameters consumed during detector error model construction. Matches the Rust ``NoiseConfig`` type. All parameters are optional beyond the four base rates. @@ -290,6 +291,146 @@ def __post_init__(self) -> None: self.p_idle_coherent = None self.p_idle_coherent_model = None + def with_p1(self, p1: float) -> NoiseParameters: + """Return a copy with ``p1`` set to the given value.""" + return replace(self, p1=p1) + + def with_p1_weights(self, p1_weights: P1Weights | None) -> NoiseParameters: + """Return a copy with ``p1_weights`` set to the given value.""" + return replace(self, p1_weights=p1_weights) + + def with_p2(self, p2: float) -> NoiseParameters: + """Return a copy with ``p2`` set to the given value.""" + return replace(self, p2=p2) + + def with_p2_szz(self, p2_szz: float | None) -> NoiseParameters: + """Return a copy with ``p2_szz`` set to the given value.""" + return replace(self, p2_szz=p2_szz) + + def with_p2_szzdg(self, p2_szzdg: float | None) -> NoiseParameters: + """Return a copy with ``p2_szzdg`` set to the given value.""" + return replace(self, p2_szzdg=p2_szzdg) + + def with_p2_weights(self, p2_weights: P2Weights | None) -> NoiseParameters: + """Return a copy with ``p2_weights`` set to the given value.""" + return replace(self, p2_weights=p2_weights) + + def with_p2_replacement_approximation( + self, + p2_replacement_approximation: str | None, + ) -> NoiseParameters: + """Return a copy with ``p2_replacement_approximation`` set to the given value.""" + return replace(self, p2_replacement_approximation=p2_replacement_approximation) + + def with_p_meas(self, p_meas: float) -> NoiseParameters: + """Return a copy with ``p_meas`` set to the given value.""" + return replace(self, p_meas=p_meas) + + def with_p_prep(self, p_prep: float) -> NoiseParameters: + """Return a copy with ``p_prep`` set to the given value.""" + return replace(self, p_prep=p_prep) + + def with_p_idle(self, p_idle: float | None) -> NoiseParameters: + """Return a copy with ``p_idle`` set to the given value.""" + return replace(self, p_idle=p_idle) + + def with_t1(self, t1: float | None) -> NoiseParameters: + """Return a copy with ``t1`` set to the given value.""" + return replace(self, t1=t1) + + def with_t2(self, t2: float | None) -> NoiseParameters: + """Return a copy with ``t2`` set to the given value.""" + return replace(self, t2=t2) + + def with_p_idle_linear_rate(self, p_idle_linear_rate: float | None) -> NoiseParameters: + """Return a copy with ``p_idle_linear_rate`` set to the given value.""" + return replace(self, p_idle_linear_rate=p_idle_linear_rate) + + def with_p_idle_quadratic_rate(self, p_idle_quadratic_rate: float | None) -> NoiseParameters: + """Return a copy with ``p_idle_quadratic_rate`` set to the given value.""" + return replace(self, p_idle_quadratic_rate=p_idle_quadratic_rate) + + def with_p_idle_x_linear_rate(self, p_idle_x_linear_rate: float | None) -> NoiseParameters: + """Return a copy with ``p_idle_x_linear_rate`` set to the given value.""" + return replace(self, p_idle_x_linear_rate=p_idle_x_linear_rate) + + def with_p_idle_y_linear_rate(self, p_idle_y_linear_rate: float | None) -> NoiseParameters: + """Return a copy with ``p_idle_y_linear_rate`` set to the given value.""" + return replace(self, p_idle_y_linear_rate=p_idle_y_linear_rate) + + def with_p_idle_z_linear_rate(self, p_idle_z_linear_rate: float | None) -> NoiseParameters: + """Return a copy with ``p_idle_z_linear_rate`` set to the given value.""" + return replace(self, p_idle_z_linear_rate=p_idle_z_linear_rate) + + def with_p_idle_x_quadratic_rate(self, p_idle_x_quadratic_rate: float | None) -> NoiseParameters: + """Return a copy with ``p_idle_x_quadratic_rate`` set to the given value.""" + return replace(self, p_idle_x_quadratic_rate=p_idle_x_quadratic_rate) + + def with_p_idle_y_quadratic_rate(self, p_idle_y_quadratic_rate: float | None) -> NoiseParameters: + """Return a copy with ``p_idle_y_quadratic_rate`` set to the given value.""" + return replace(self, p_idle_y_quadratic_rate=p_idle_y_quadratic_rate) + + def with_p_idle_z_quadratic_rate(self, p_idle_z_quadratic_rate: float | None) -> NoiseParameters: + """Return a copy with ``p_idle_z_quadratic_rate`` set to the given value.""" + return replace(self, p_idle_z_quadratic_rate=p_idle_z_quadratic_rate) + + def with_p_idle_quadratic_sine_rate( + self, + p_idle_quadratic_sine_rate: float | None, + ) -> NoiseParameters: + """Return a copy with ``p_idle_quadratic_sine_rate`` set to the given value.""" + return replace(self, p_idle_quadratic_sine_rate=p_idle_quadratic_sine_rate) + + def with_p_idle_x_quadratic_sine_rate( + self, + p_idle_x_quadratic_sine_rate: float | None, + ) -> NoiseParameters: + """Return a copy with ``p_idle_x_quadratic_sine_rate`` set to the given value.""" + return replace(self, p_idle_x_quadratic_sine_rate=p_idle_x_quadratic_sine_rate) + + def with_p_idle_y_quadratic_sine_rate( + self, + p_idle_y_quadratic_sine_rate: float | None, + ) -> NoiseParameters: + """Return a copy with ``p_idle_y_quadratic_sine_rate`` set to the given value.""" + return replace(self, p_idle_y_quadratic_sine_rate=p_idle_y_quadratic_sine_rate) + + def with_p_idle_z_quadratic_sine_rate( + self, + p_idle_z_quadratic_sine_rate: float | None, + ) -> NoiseParameters: + """Return a copy with ``p_idle_z_quadratic_sine_rate`` set to the given value.""" + return replace(self, p_idle_z_quadratic_sine_rate=p_idle_z_quadratic_sine_rate) + + # The idle families take their rate and model together: a model without a + # rate is inert and rejected, and __post_init__ translates a family into the + # canonical per-axis fields and then clears it -- so setting the two halves + # in separate calls would make the second call collide with the per-axis + # values the first one produced. + def with_p_idle_linear( + self, + p_idle_linear: float | None, + model: Mapping[str, float] | None = None, + ) -> NoiseParameters: + """Return a copy with the linear idle family set to the given rate and model.""" + return replace(self, p_idle_linear=p_idle_linear, p_idle_linear_model=model) + + def with_p_idle_sin_squared( + self, + p_idle_sin_squared: float | None, + model: Mapping[str, float] | None = None, + ) -> NoiseParameters: + """Return a copy with the sine-law idle family set to the given rate and model.""" + return replace(self, p_idle_sin_squared=p_idle_sin_squared, p_idle_sin_squared_model=model) + + def with_p_idle_coherent( + self, + p_idle_coherent: float | None, + model: Mapping[str, float] | None = None, + ) -> NoiseParameters: + """Return a copy with the coherent idle family set to the given rate and model.""" + return replace(self, p_idle_coherent=p_idle_coherent, p_idle_coherent_model=model) + @property def effective_p_idle_z_linear_rate(self) -> float | None: """Z-axis linear idle rate, accepting the legacy alias.""" @@ -331,7 +472,7 @@ def for_runtime_idle_time_units( self, *, time_units_per_second: float = RUNTIME_IDLE_TIME_UNITS_PER_SECOND, - ) -> NoiseModel: + ) -> NoiseParameters: """Return a copy whose idle noise is expressed in runtime replay units. Selene-compatible runtimes emit idle durations in seconds, but the @@ -370,10 +511,10 @@ def for_runtime_idle_time_units( ) @staticmethod - def uniform(physical_error_rate: float) -> NoiseModel: + def uniform(physical_error_rate: float) -> NoiseParameters: """Create a uniform circuit-level noise model from one physical error rate.""" p = _validate_probability("physical_error_rate", physical_error_rate) - return NoiseModel(p1=p, p2=p, p_meas=p, p_prep=p) + return NoiseParameters(p1=p, p2=p, p_meas=p, p_prep=p) @property def is_noiseless(self) -> bool: @@ -399,6 +540,19 @@ def physical_error_rate(self) -> float: return max(rates) +def __getattr__(name: str) -> Any: + """Resolve deprecated module attributes lazily.""" + if name == "NoiseModel": + warnings.warn( + "NoiseModel is deprecated; use NoiseParameters instead (from pecos import NoiseParameters).", + DeprecationWarning, + stacklevel=2, + ) + return NoiseParameters + msg = f"module {__name__!r} has no attribute {name!r}" + raise AttributeError(msg) + + def _normalize_pauli_weights(weights: P1Weights | P2Weights | None) -> tuple[tuple[str, float], ...] | None: if weights is None: return None @@ -424,7 +578,7 @@ def _p2_weights_dict(p2_weights: P2Weights | None) -> dict[str, float] | None: return None if normalized is None else dict(normalized) -def _p2_gate_rates_dict(noise: NoiseModel) -> dict[str, float] | None: +def _p2_gate_rates_dict(noise: NoiseParameters) -> dict[str, float] | None: rates: dict[str, float] = {} if noise.p2_szz is not None: rates["SZZ"] = noise.p2_szz @@ -600,7 +754,7 @@ def det_id(round_: int, check: int) -> int: def generate_surface_code_dem( patch: SurfacePatch, num_rounds: int, - noise: NoiseModel, + noise: NoiseParameters, stab_type: str = "Z", ) -> str: """Generate a phenomenological DEM for surface code decoding. @@ -1359,7 +1513,7 @@ def _uses_dedicated_idle_noise( ) -def _noise_uses_dedicated_idle_noise(noise: NoiseModel) -> bool: +def _noise_uses_dedicated_idle_noise(noise: NoiseParameters) -> bool: """Return True when this noise model requires explicit idle locations.""" return _uses_dedicated_idle_noise( p_idle=noise.p_idle, @@ -1381,7 +1535,7 @@ def _noise_uses_dedicated_idle_noise(noise: NoiseModel) -> bool: def _reject_szz_unlowered_physical_noise( - noise: NoiseModel, + noise: NoiseParameters, interaction_basis: str, circuit_source: Literal["abstract", "traced_qis"], ) -> None: @@ -1403,7 +1557,7 @@ def _reject_szz_unlowered_physical_noise( def _use_szz_physical_prefixes( - noise: NoiseModel, + noise: NoiseParameters, interaction_basis: str, circuit_source: Literal["abstract", "traced_qis"], ) -> bool: @@ -1430,7 +1584,7 @@ def _szz_z_frame_p1_gate_rates(topology: _CachedNativeSurfaceTopology) -> dict[s def _with_noise_compat( builder: Any, - noise: NoiseModel, + noise: NoiseParameters, *, p1_gate_rates: Mapping[str, float] | None = None, ) -> Any: @@ -1645,7 +1799,7 @@ def _cached_surface_native_topology( def _dem_string_from_cached_surface_topology( topology: _CachedNativeSurfaceTopology, - noise: NoiseModel, + noise: NoiseParameters, *, decompose_errors: bool, dem_decomposition: NativeDemDecomposition = "source_graphlike", @@ -1772,7 +1926,7 @@ def _cached_surface_native_dem_string( ) return _dem_string_from_cached_surface_topology( topology, - NoiseModel( + NoiseParameters( p1=p1, p1_weights=p1_weights, p2=p2, @@ -1813,7 +1967,7 @@ def _cached_parsed_dem(dem_str: str) -> Any: def _build_native_sampler_from_cached_surface_topology( topology: _CachedNativeSurfaceTopology, - noise: NoiseModel, + noise: NoiseParameters, *, sampling_model: Literal[ "dem", @@ -1871,7 +2025,7 @@ def _build_native_sampler_from_cached_surface_topology( def generate_circuit_level_dem_from_builder( patch: SurfacePatch, num_rounds: int, - noise: NoiseModel, + noise: NoiseParameters, basis: str = "Z", *, decompose_errors: bool = False, @@ -1956,10 +2110,10 @@ def generate_circuit_level_dem_from_builder( DEM string in standard format Example: - >>> from pecos.qec.surface import SurfacePatch, NoiseModel + >>> from pecos.qec.surface import SurfacePatch, NoiseParameters >>> from pecos.qec.surface.decode import generate_circuit_level_dem_from_builder >>> patch = SurfacePatch.create(distance=3) - >>> noise = NoiseModel(p1=0.001, p2=0.01, p_meas=0.01) + >>> noise = NoiseParameters(p1=0.001, p2=0.01, p_meas=0.01) >>> dem = generate_circuit_level_dem_from_builder(patch, num_rounds=3, noise=noise) """ ancilla_budget = _canonical_ancilla_budget(patch, ancilla_budget) @@ -2047,7 +2201,7 @@ def generate_circuit_level_dem_from_builder( def generate_circuit_level_dem( distance: int, num_rounds: int, - noise: NoiseModel, + noise: NoiseParameters, basis: str = "Z", ) -> str: """Generate a circuit-level DEM using Stim's surface code generator. @@ -2071,8 +2225,8 @@ def generate_circuit_level_dem( DEM string in Stim format Example: - >>> from pecos.qec.surface import generate_circuit_level_dem, NoiseModel - >>> noise = NoiseModel(p1=0.001, p2=0.01, p_meas=0.01) + >>> from pecos.qec.surface import generate_circuit_level_dem, NoiseParameters + >>> noise = NoiseParameters(p1=0.001, p2=0.01, p_meas=0.01) >>> dem = generate_circuit_level_dem(distance=3, num_rounds=3, noise=noise, basis="Z") """ import stim @@ -2103,7 +2257,7 @@ def generate_circuit_level_dem( def build_stim_circuit_from_patch( patch: SurfacePatch, num_rounds: int, - noise: NoiseModel | None = None, + noise: NoiseParameters | None = None, basis: str = "Z", ) -> stim.Circuit: """Build a Stim circuit from our patch geometry and CNOT schedule. @@ -2134,11 +2288,11 @@ def build_stim_circuit_from_patch( Example: >>> from pecos.qec.surface import ( ... SurfacePatch, - ... NoiseModel, + ... NoiseParameters, ... build_stim_circuit_from_patch, ... ) >>> patch = SurfacePatch.create(distance=3) - >>> noise = NoiseModel(p2=0.01, p_meas=0.01) + >>> noise = NoiseParameters(p2=0.01, p_meas=0.01) >>> circuit = build_stim_circuit_from_patch(patch, num_rounds=3, noise=noise) >>> dem = circuit.detector_error_model() """ @@ -2345,7 +2499,7 @@ def stab_coords(stab: Stabilizer) -> tuple[float, float]: def generate_dem_from_patch( patch: SurfacePatch, num_rounds: int, - noise: NoiseModel, + noise: NoiseParameters, basis: str = "Z", *, decompose_errors: bool = True, @@ -2371,11 +2525,11 @@ def generate_dem_from_patch( Example: >>> from pecos.qec.surface import ( ... SurfacePatch, - ... NoiseModel, + ... NoiseParameters, ... generate_dem_from_patch, ... ) >>> patch = SurfacePatch.create(distance=3) - >>> noise = NoiseModel(p2=0.01, p_meas=0.01) + >>> noise = NoiseParameters(p2=0.01, p_meas=0.01) >>> dem = generate_dem_from_patch(patch, num_rounds=3, noise=noise) """ circuit = build_stim_circuit_from_patch(patch, num_rounds, noise, basis) @@ -2393,7 +2547,7 @@ class SurfaceDecoder: >>> from pecos.qec.surface import SurfacePatch, SurfaceDecoder >>> patch = SurfacePatch.create(distance=3) >>> # Default: PyMatching MWPM - >>> decoder = SurfaceDecoder(patch, num_rounds=3, noise=NoiseModel(p2=0.01, p_meas=0.01)) + >>> decoder = SurfaceDecoder(patch, num_rounds=3, noise=NoiseParameters(p2=0.01, p_meas=0.01)) >>> # Alternative: FusionBlossom MWPM >>> decoder = SurfaceDecoder(patch, num_rounds=3, decoder_type="fusion_blossom") >>> # Alternative: BP+OSD (LDPC) @@ -2405,7 +2559,7 @@ def __init__( self, patch: SurfacePatch, num_rounds: int = 1, - noise: NoiseModel | None = None, + noise: NoiseParameters | None = None, decoder_type: Literal[ "pymatching", "pymatching_correlated", @@ -2479,7 +2633,7 @@ def __init__( self.patch = patch self.num_rounds = num_rounds - self.noise = noise or NoiseModel(p2=0.01, p_meas=0.01) + self.noise = noise or NoiseParameters(p2=0.01, p_meas=0.01) self.decoder_type = DecoderType(decoder_type) self.use_circuit_level_dem = use_circuit_level_dem if circuit_level_dem_mode not in { @@ -3420,16 +3574,16 @@ class SimulationResult: def _memory_noise_model( physical_error_rate: float | None, - noise_model: NoiseModel | None, -) -> NoiseModel: - """Resolve the surface-memory noise inputs into an explicit NoiseModel.""" + noise_model: NoiseParameters | None, +) -> NoiseParameters: + """Resolve the surface-memory noise inputs into explicit noise parameters.""" if noise_model is not None: if physical_error_rate is not None: msg = "pass either physical_error_rate or noise_model, not both" raise ValueError(msg) return noise_model p = 0.001 if physical_error_rate is None else physical_error_rate - return NoiseModel.uniform(p) + return NoiseParameters.uniform(p) def _recommended_graphlike_decomposition_for_decoder(decoder_type: str) -> NativeDemDecomposition: @@ -3443,7 +3597,7 @@ def surface_code_memory( *, distance: int = 3, physical_error_rate: float | None = None, - noise_model: NoiseModel | None = None, + noise_model: NoiseParameters | None = None, shots: int = 1000, rounds: int | None = None, basis: str = "Z", @@ -3571,7 +3725,7 @@ def run_noisy_memory_experiment( num_rounds: int, num_shots: int, basis: str, - noise: NoiseModel, + noise: NoiseParameters, *, decode: bool = True, decoder_type: str = "pymatching", @@ -3604,8 +3758,8 @@ def run_noisy_memory_experiment( SimulationResult with error rate statistics Example: - >>> from pecos.qec.surface import run_noisy_memory_experiment, NoiseModel - >>> noise = NoiseModel(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001) + >>> from pecos.qec.surface import run_noisy_memory_experiment, NoiseParameters + >>> noise = NoiseParameters(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001) >>> result = run_noisy_memory_experiment( ... distance=3, ... num_rounds=3, @@ -3831,7 +3985,7 @@ def sample( def build_native_sampler( patch: SurfacePatch, num_rounds: int, - noise: NoiseModel, + noise: NoiseParameters, basis: str = "Z", ancilla_budget: int | None = None, circuit_source: Literal["abstract", "traced_qis"] = "abstract", @@ -3905,9 +4059,9 @@ def build_native_sampler( NativeSampler that can generate samples for threshold estimation Example: - >>> from pecos.qec.surface import SurfacePatch, NoiseModel, build_native_sampler + >>> from pecos.qec.surface import SurfacePatch, NoiseParameters, build_native_sampler >>> patch = SurfacePatch.create(distance=5) - >>> noise = NoiseModel(p1=0.001, p2=0.001, p_meas=0.001) + >>> noise = NoiseParameters(p1=0.001, p2=0.001, p_meas=0.001) >>> sampler = build_native_sampler(patch, num_rounds=5, noise=noise) >>> detection_events, observable_flips = sampler.sample(num_shots=10000) """ diff --git a/python/quantum-pecos/tests/qec/surface/test_check_plan.py b/python/quantum-pecos/tests/qec/surface/test_check_plan.py index fa45b9696..385ee7831 100644 --- a/python/quantum-pecos/tests/qec/surface/test_check_plan.py +++ b/python/quantum-pecos/tests/qec/surface/test_check_plan.py @@ -316,11 +316,11 @@ def test_surface_code_memory_rejects_plan_basis_mismatch() -> None: def test_check_plan_does_not_change_current_szz_dem() -> None: - from pecos.qec.surface import NoiseModel, SurfacePatch + from pecos.qec.surface import NoiseParameters, SurfacePatch from pecos.qec.surface.decode import generate_circuit_level_dem_from_builder patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p2=0.001, p_meas=0.001, p_prep=0.001) + noise = NoiseParameters(p2=0.001, p_meas=0.001, p_prep=0.001) by_basis = generate_circuit_level_dem_from_builder( patch, @@ -689,13 +689,13 @@ def test_direct_surface_renderers_reject_plan_basis_mismatch() -> None: def test_native_sampler_records_resolved_check_plan() -> None: - from pecos.qec.surface import NoiseModel, SurfacePatch, build_native_sampler + from pecos.qec.surface import NoiseParameters, SurfacePatch, build_native_sampler patch = SurfacePatch.create(distance=3) sampler = build_native_sampler( patch, num_rounds=1, - noise=NoiseModel(p2=0.001), + noise=NoiseParameters(p2=0.001), check_plan="szz_current_v1", sampling_model="influence_dem", ) diff --git a/python/quantum-pecos/tests/qec/surface/test_clifford_deformation.py b/python/quantum-pecos/tests/qec/surface/test_clifford_deformation.py index 401d0fbff..4e966df65 100644 --- a/python/quantum-pecos/tests/qec/surface/test_clifford_deformation.py +++ b/python/quantum-pecos/tests/qec/surface/test_clifford_deformation.py @@ -3,7 +3,7 @@ import pytest from pecos.qec.surface import ( LocalCliffordFrame, - NoiseModel, + NoiseParameters, OpType, SignedPauli, SurfacePatch, @@ -206,7 +206,7 @@ def test_global_axis_cycle_f_native_abstract_dem_path_accepts_frame_policy() -> dem = generate_circuit_level_dem_from_builder( patch, num_rounds=1, - noise=NoiseModel(p1=0.0, p2=0.0, p_meas=0.0, p_prep=0.0), + noise=NoiseParameters(p1=0.0, p2=0.0, p_meas=0.0, p_prep=0.0), basis="Z", circuit_source="abstract", interaction_basis="szz", @@ -223,7 +223,7 @@ def test_checkerboard_native_abstract_dem_path_accepts_frame_policy(policy: str) dem = generate_circuit_level_dem_from_builder( patch, num_rounds=1, - noise=NoiseModel(p1=0.0, p2=0.0, p_meas=0.0, p_prep=0.0), + noise=NoiseParameters(p1=0.0, p2=0.0, p_meas=0.0, p_prep=0.0), basis="Z", circuit_source="abstract", interaction_basis="szz", diff --git a/python/quantum-pecos/tests/qec/surface/test_idle_noise_families.py b/python/quantum-pecos/tests/qec/surface/test_idle_noise_families.py index edf74b93e..20f947c33 100644 --- a/python/quantum-pecos/tests/qec/surface/test_idle_noise_families.py +++ b/python/quantum-pecos/tests/qec/surface/test_idle_noise_families.py @@ -1,11 +1,11 @@ from __future__ import annotations import pytest -from pecos.qec.surface import NoiseModel, SurfacePatch, TwirlConfig +from pecos.qec.surface import NoiseParameters, SurfacePatch, TwirlConfig from pecos.qec.surface.decode import generate_circuit_level_dem_from_builder -def _native_surface_dem(noise: NoiseModel) -> bytes: +def _native_surface_dem(noise: NoiseParameters) -> bytes: patch = SurfacePatch.create(distance=3) dem = generate_circuit_level_dem_from_builder( patch, @@ -21,9 +21,9 @@ def _native_surface_dem(noise: NoiseModel) -> bytes: def test_linear_family_matches_per_axis_native_surface_dem() -> None: rate = 0.003 - structured = _native_surface_dem(NoiseModel(p_idle_linear=rate)) + structured = _native_surface_dem(NoiseParameters(p_idle_linear=rate)) primitive = _native_surface_dem( - NoiseModel( + NoiseParameters( p_idle_x_linear_rate=rate / 3.0, p_idle_y_linear_rate=rate / 3.0, p_idle_z_linear_rate=rate / 3.0, @@ -37,18 +37,18 @@ def test_sin_squared_family_matches_per_axis_native_surface_dem() -> None: rate = 0.03 structured = _native_surface_dem( - NoiseModel( + NoiseParameters( p_idle_sin_squared=rate, p_idle_sin_squared_model={"Z": 1.0}, ), ) - primitive = _native_surface_dem(NoiseModel(p_idle_z_quadratic_sine_rate=rate)) + primitive = _native_surface_dem(NoiseParameters(p_idle_z_quadratic_sine_rate=rate)) assert structured == primitive def test_structured_families_survive_runtime_idle_unit_conversion() -> None: - noise = NoiseModel( + noise = NoiseParameters( p_idle_linear=0.3, p_idle_sin_squared=0.2, p_idle_sin_squared_model={"Z": 1.0}, @@ -79,7 +79,7 @@ def test_structured_families_survive_runtime_idle_unit_conversion() -> None: ) def test_structured_family_conflicts_with_corresponding_primitive(kwargs: dict[str, object]) -> None: with pytest.raises(ValueError, match="cannot be combined"): - NoiseModel(**kwargs) + NoiseParameters(**kwargs) @pytest.mark.parametrize( @@ -92,11 +92,11 @@ def test_structured_family_conflicts_with_corresponding_primitive(kwargs: dict[s ) def test_bare_z_only_alias_warns_through_noise_model(field: str) -> None: with pytest.warns(DeprecationWarning, match=field): - NoiseModel(**{field: 0.01}) + NoiseParameters(**{field: 0.01}) def test_idle_memory_rates_include_translated_family_values() -> None: - noise = NoiseModel( + noise = NoiseParameters( p_idle_linear=0.3, p_idle_sin_squared=0.2, p_idle_sin_squared_model={"Z": 1.0}, @@ -109,4 +109,4 @@ def test_idle_memory_rates_include_translated_family_values() -> None: def test_nonzero_coherent_family_is_rejected_by_standard_dem_model() -> None: with pytest.raises(ValueError, match="cannot represent coherent idle noise"): - NoiseModel(p_idle_coherent=0.01) + NoiseParameters(p_idle_coherent=0.01) diff --git a/python/quantum-pecos/tests/qec/surface/test_noise_parameters.py b/python/quantum-pecos/tests/qec/surface/test_noise_parameters.py new file mode 100644 index 000000000..d7a943c5f --- /dev/null +++ b/python/quantum-pecos/tests/qec/surface/test_noise_parameters.py @@ -0,0 +1,181 @@ +# Copyright 2026 The PECOS Developers +# Licensed under the Apache License, Version 2.0 + +"""Contract tests for DEM-construction noise parameters.""" + +from __future__ import annotations + +import warnings +from dataclasses import fields + +import pecos.qec.surface as surface +import pytest +from guppylang import guppy +from guppylang.std.builtins import result +from guppylang.std.quantum import cx, measure, qubit +from pecos import NoiseParameters +from pecos.qec import DetectorErrorModel + + +@guppy +def _two_qubit_program() -> None: + q0 = qubit() + q1 = qubit() + cx(q0, q1) + result("m0", measure(q0)) + result("m1", measure(q1)) + + +def _dem_bytes(noise: NoiseParameters) -> bytes: + build = ( + DetectorErrorModel.builder() + .program(_two_qubit_program) + .qubits(2) + .detectors_json('[{"id":0,"result_tags":["m0"]}]') + .observables_json('[{"id":0,"result_tags":["m1"]}]') + .noise(noise) + .build() + ) + return build.dem.to_string().encode() + + +def test_fluent_chain_matches_constructor_and_dem() -> None: + constructor = NoiseParameters( + p1=0.001, + p1_weights={"X": 0.2, "Y": 0.3, "Z": 0.5}, + p2=0.01, + p2_weights={"IX": 1.0}, + p2_replacement_approximation="ignore_gate_removal", + p_meas=0.002, + p_prep=0.003, + ) + fluent = ( + NoiseParameters() + .with_p1(0.001) + .with_p1_weights({"X": 0.2, "Y": 0.3, "Z": 0.5}) + .with_p2(0.01) + .with_p2_weights({"IX": 1.0}) + .with_p2_replacement_approximation("ignore_gate_removal") + .with_p_meas(0.002) + .with_p_prep(0.003) + ) + + assert fluent == constructor + assert _dem_bytes(fluent) == _dem_bytes(constructor) + + +# The idle-family model fields are the one deliberate exception to the +# mechanical rule: they are set through their family's rate setter, because a +# model without a rate is inert and the two cannot be set in separate calls. +_FAMILY_MODEL_FIELDS = { + "p_idle_linear_model", + "p_idle_sin_squared_model", + "p_idle_coherent_model", +} + + +def test_every_field_has_a_mechanical_fluent_setter() -> None: + field_names = {field.name for field in fields(NoiseParameters)} + + assert len(field_names) == 30 + for field_name in field_names - _FAMILY_MODEL_FIELDS: + assert callable(getattr(NoiseParameters, f"with_{field_name}")), field_name + + +def test_family_models_are_set_through_their_rate_setter() -> None: + import inspect + + for family in ("p_idle_linear", "p_idle_sin_squared", "p_idle_coherent"): + signature = inspect.signature(getattr(NoiseParameters, f"with_{family}")) + assert "model" in signature.parameters, family + + +def test_fluent_setter_returns_a_new_object() -> None: + original = NoiseParameters(p1=0.001) + + updated = original.with_p1(0.002) + + assert updated is not original + assert original.p1 == 0.001 + assert updated.p1 == 0.002 + + +def test_structured_family_survives_fluent_chain_and_runtime_conversion() -> None: + noise = NoiseParameters().with_p_idle_linear(0.3).with_p1(0.001).with_p_meas(0.002) + + converted = noise.for_runtime_idle_time_units(time_units_per_second=10.0) + + assert converted.p_idle_x_linear_rate == pytest.approx(0.01) + assert converted.p_idle_y_linear_rate == pytest.approx(0.01) + assert converted.p_idle_z_linear_rate == pytest.approx(0.01) + assert converted.p_idle_linear is None + assert converted.p_idle_linear_model is None + + +def test_idle_family_rate_and_model_set_together() -> None: + # The family halves must be settable in ONE call: __post_init__ translates a + # family into per-axis fields and clears it, so a separate model-setting call + # would collide with the per-axis values the rate call just produced. + noise = NoiseParameters().with_p_idle_linear(0.01, {"Z": 1.0}).with_p1(0.001) + + assert noise.p_idle_z_linear_rate == pytest.approx(0.01) + assert noise.p_idle_x_linear_rate in (None, 0.0) + assert noise.p_idle_linear is None + assert noise.p1 == pytest.approx(0.001) + + +def test_idle_families_have_no_separate_model_setters() -> None: + # A model without a rate is inert and rejected, so exposing a lone model + # setter would only ever produce an error or a collision. + for name in ( + "with_p_idle_linear_model", + "with_p_idle_sin_squared_model", + "with_p_idle_coherent_model", + ): + assert not hasattr(NoiseParameters, name), name + + +def test_each_idle_family_round_trips_through_runtime_conversion() -> None: + linear = NoiseParameters().with_p_idle_linear(0.3, {"Z": 1.0}) + sine = NoiseParameters().with_p_idle_sin_squared(0.2, {"X": 1.0}) + + assert linear.for_runtime_idle_time_units(time_units_per_second=10.0).p_idle_z_linear_rate == pytest.approx(0.03) + converted_sine = sine.for_runtime_idle_time_units(time_units_per_second=10.0) + assert converted_sine.p_idle_x_quadratic_sine_rate == pytest.approx(0.02) + + +def test_deprecated_alias_warns_and_returns_noise_parameters() -> None: + with pytest.warns( + DeprecationWarning, + match=r"NoiseModel.*NoiseParameters.*from pecos import NoiseParameters", + ): + legacy = surface.NoiseModel(p1=0.001) + + assert type(legacy) is NoiseParameters + assert legacy == NoiseParameters(p1=0.001) + + +def test_public_import_paths_refer_to_the_same_class() -> None: + from pecos import NoiseParameters as TopLevelNoiseParameters + from pecos.qec.surface import NoiseParameters as SurfaceNoiseParameters + + assert TopLevelNoiseParameters is NoiseParameters + assert SurfaceNoiseParameters is NoiseParameters + + +def test_legacy_idle_alias_setter_warns_but_family_setter_does_not() -> None: + with pytest.warns(DeprecationWarning, match="p_idle_linear_rate"): + NoiseParameters().with_p_idle_linear_rate(0.01) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + NoiseParameters().with_p_idle_linear(0.01) + + assert not caught + + +def test_chaining_order_does_not_matter() -> None: + first = NoiseParameters().with_p1(0.001).with_p2(0.01).with_p_meas(0.002).with_p_prep(0.003) + second = NoiseParameters().with_p_prep(0.003).with_p_meas(0.002).with_p2(0.01).with_p1(0.001) + + assert first == second diff --git a/python/quantum-pecos/tests/qec/surface/test_pauli_mask_harvest.py b/python/quantum-pecos/tests/qec/surface/test_pauli_mask_harvest.py index e627c41e2..d4848e4ec 100644 --- a/python/quantum-pecos/tests/qec/surface/test_pauli_mask_harvest.py +++ b/python/quantum-pecos/tests/qec/surface/test_pauli_mask_harvest.py @@ -4,7 +4,7 @@ import pytest from pecos.qec.surface import ( GuppyRngMaskConfig, - NoiseModel, + NoiseParameters, SurfacePatch, TwirlConfig, build_memory_circuit, @@ -482,7 +482,7 @@ def test_runtime_twirled_theta0_demask_null( sampler = build_native_sampler( patch_d3, num_rounds=num_rounds, - noise=NoiseModel(), + noise=NoiseParameters(), basis=basis, twirl=TwirlConfig(), ) @@ -542,7 +542,7 @@ def test_runtime_gate_local_twirled_theta0_demask_null( sampler = build_native_sampler( patch_d3, num_rounds=num_rounds, - noise=NoiseModel(), + noise=NoiseParameters(), basis=basis, twirl=twirl, ) @@ -608,7 +608,7 @@ def _assert_canonical_frame_output_matches_lookup( sampler = build_native_sampler( patch, num_rounds=num_rounds, - noise=NoiseModel(), + noise=NoiseParameters(), basis=basis, twirl=TwirlConfig(), ) @@ -673,7 +673,7 @@ def test_runtime_gate_local_canonical_frame_output_matches_lookup( sampler = build_native_sampler( patch_d3, num_rounds=num_rounds, - noise=NoiseModel(), + noise=NoiseParameters(), basis=basis, twirl=abstract_twirl, ) @@ -763,7 +763,7 @@ def test_harvested_runtime_masks_drive_fixed_dem_sampler_null(patch_d3: SurfaceP sampler = build_native_sampler( patch_d3, num_rounds=num_rounds, - noise=NoiseModel(), + noise=NoiseParameters(), basis="Z", twirl=twirl, ) diff --git a/python/quantum-pecos/tests/qec/surface/test_pauli_twirl_handoff.py b/python/quantum-pecos/tests/qec/surface/test_pauli_twirl_handoff.py index a8d705039..c05923587 100644 --- a/python/quantum-pecos/tests/qec/surface/test_pauli_twirl_handoff.py +++ b/python/quantum-pecos/tests/qec/surface/test_pauli_twirl_handoff.py @@ -4,7 +4,7 @@ import pytest from pecos.qec.surface import ( GuppyRngMaskConfig, - NoiseModel, + NoiseParameters, SurfacePatch, TwirlConfig, build_memory_circuit, @@ -177,7 +177,7 @@ def test_demask_helper_cancels_known_pauli_frame_xor() -> None: sampler = build_native_sampler( patch, num_rounds=2, - noise=NoiseModel(), + noise=NoiseParameters(), basis="Z", twirl=TwirlConfig(), ) @@ -212,7 +212,7 @@ def test_native_sampler_accepts_harvested_uint8_pauli_masks() -> None: sampler = build_native_sampler( patch, num_rounds=2, - noise=NoiseModel(), + noise=NoiseParameters(), basis="Z", twirl=TwirlConfig(), ) @@ -233,21 +233,21 @@ def test_canonical_frame_output_reuses_raw_abstract_sampler_topology() -> None: raw = build_native_sampler( patch, num_rounds=2, - noise=NoiseModel(), + noise=NoiseParameters(), basis="Z", twirl=TwirlConfig(), ) canonical = build_native_sampler( patch, num_rounds=2, - noise=NoiseModel(), + noise=NoiseParameters(), basis="Z", twirl=TwirlConfig(frame_output="canonical"), ) scaled = build_native_sampler( patch, num_rounds=2, - noise=NoiseModel(), + noise=NoiseParameters(), basis="Z", twirl=TwirlConfig(twirl_probability=0.5), ) @@ -298,7 +298,7 @@ def test_abstract_twirl_builders_reject_unsupported_config( build_native_sampler( patch, num_rounds=2, - noise=NoiseModel(), + noise=NoiseParameters(), basis="Z", twirl=twirl, ) @@ -307,7 +307,7 @@ def test_abstract_twirl_builders_reject_unsupported_config( generate_circuit_level_dem_from_builder( patch, num_rounds=2, - noise=NoiseModel(), + noise=NoiseParameters(), basis="Z", twirl=twirl, ) @@ -315,7 +315,7 @@ def test_abstract_twirl_builders_reject_unsupported_config( def test_twirl_sine_law_idle_noise_builds_dem_and_sampler() -> None: patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p_idle_x_quadratic_sine_rate=0.03) + noise = NoiseParameters(p_idle_x_quadratic_sine_rate=0.03) twirl = TwirlConfig() dem = generate_circuit_level_dem_from_builder( @@ -345,17 +345,17 @@ def test_twirl_sine_law_idle_noise_builds_dem_and_sampler() -> None: @pytest.mark.parametrize( ("label", "noise"), [ - ("depolarizing", NoiseModel(p1=0.001, p2=0.01, p_meas=0.001, p_prep=0.001)), - ("uniform_idle", NoiseModel(p_idle=0.002)), - ("t1_t2", NoiseModel(t1=1000.0, t2=800.0)), - ("linear_idle", NoiseModel(p_idle_z_linear_rate=0.001)), - ("quadratic_idle", NoiseModel(p_idle_z_quadratic_rate=0.01)), - ("sine_law_idle", NoiseModel(p_idle_x_quadratic_sine_rate=0.03)), + ("depolarizing", NoiseParameters(p1=0.001, p2=0.01, p_meas=0.001, p_prep=0.001)), + ("uniform_idle", NoiseParameters(p_idle=0.002)), + ("t1_t2", NoiseParameters(t1=1000.0, t2=800.0)), + ("linear_idle", NoiseParameters(p_idle_z_linear_rate=0.001)), + ("quadratic_idle", NoiseParameters(p_idle_z_quadratic_rate=0.01)), + ("sine_law_idle", NoiseParameters(p_idle_x_quadratic_sine_rate=0.03)), ], ) def test_twirling_does_not_change_canonical_dem( label: str, - noise: NoiseModel, + noise: NoiseParameters, ) -> None: del label patch = SurfacePatch.create(distance=3) @@ -381,7 +381,7 @@ def test_twirling_does_not_change_canonical_dem( def test_gate_local_twirling_does_not_change_canonical_dem() -> None: patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p1=0.001, p2=0.01, p_meas=0.001, p_prep=0.001) + noise = NoiseParameters(p1=0.001, p2=0.01, p_meas=0.001, p_prep=0.001) untwirled = generate_circuit_level_dem_from_builder( patch, diff --git a/python/quantum-pecos/tests/qec/surface/test_surface_decoder.py b/python/quantum-pecos/tests/qec/surface/test_surface_decoder.py index 1c3af712a..94d8190fc 100644 --- a/python/quantum-pecos/tests/qec/surface/test_surface_decoder.py +++ b/python/quantum-pecos/tests/qec/surface/test_surface_decoder.py @@ -13,7 +13,7 @@ import numpy as np import pytest from pecos.qec.surface import ( - NoiseModel, + NoiseParameters, SurfaceDecoder, SurfacePatch, generate_dem_from_tick_circuit, @@ -51,11 +51,11 @@ def _count_singleton_error_parts(dem: str) -> int: class TestNoiseModel: - """Tests for NoiseModel dataclass.""" + """Tests for NoiseParameters dataclass.""" def test_default_values(self) -> None: """Default noise model should have zero error rates.""" - noise = NoiseModel() + noise = NoiseParameters() assert noise.p1 == 0.0 assert noise.p2 == 0.0 assert noise.p_meas == 0.0 @@ -63,15 +63,15 @@ def test_default_values(self) -> None: def test_is_noiseless(self) -> None: """Test is_noiseless property.""" - assert NoiseModel().is_noiseless - assert not NoiseModel(p1=0.01).is_noiseless - assert not NoiseModel(p2=0.01).is_noiseless - assert not NoiseModel(p_meas=0.01).is_noiseless - assert not NoiseModel(p_prep=0.01).is_noiseless + assert NoiseParameters().is_noiseless + assert not NoiseParameters(p1=0.01).is_noiseless + assert not NoiseParameters(p2=0.01).is_noiseless + assert not NoiseParameters(p_meas=0.01).is_noiseless + assert not NoiseParameters(p_prep=0.01).is_noiseless def test_physical_error_rate(self) -> None: """Test physical_error_rate property.""" - noise = NoiseModel(p1=0.001, p2=0.01, p_meas=0.005, p_prep=0.002) + noise = NoiseParameters(p1=0.001, p2=0.01, p_meas=0.005, p_prep=0.002) assert noise.physical_error_rate == 0.01 # max of all rates @@ -154,7 +154,7 @@ class TestSurfaceDecoder: def test_create_decoder_d3(self) -> None: """Create decoder for distance-3 patch.""" patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p2=0.01, p_meas=0.01) + noise = NoiseParameters(p2=0.01, p_meas=0.01) decoder = SurfaceDecoder(patch, num_rounds=1, noise=noise) assert decoder.patch == patch @@ -164,7 +164,7 @@ def test_create_decoder_d3(self) -> None: def test_create_decoder_d5(self) -> None: """Create decoder for distance-5 patch.""" patch = SurfacePatch.create(distance=5) - noise = NoiseModel(p2=0.01, p_meas=0.01) + noise = NoiseParameters(p2=0.01, p_meas=0.01) decoder = SurfaceDecoder(patch, num_rounds=3, noise=noise) assert decoder.patch == patch @@ -173,7 +173,7 @@ def test_create_decoder_d5(self) -> None: def test_decoder_types(self) -> None: """Test different decoder type options.""" patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p2=0.01, p_meas=0.01) + noise = NoiseParameters(p2=0.01, p_meas=0.01) # PyMatching (default) d1 = SurfaceDecoder(patch, decoder_type="pymatching", noise=noise) @@ -200,7 +200,7 @@ def test_circuit_level_pymatching_uses_correlations_by_default(self, monkeypatch import pecos.qec.surface.decode as decode_module patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p2=0.01, p_meas=0.01) + noise = NoiseParameters(p2=0.01, p_meas=0.01) seen: dict[str, object] = {} def wrapped_generate(*_args: object, **_kwargs: object) -> str: @@ -242,7 +242,7 @@ def test_circuit_level_uncorrelated_pymatching_uses_plain_dem(self, monkeypatch: import pecos.qec.surface.decode as decode_module patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p2=0.01, p_meas=0.01) + noise = NoiseParameters(p2=0.01, p_meas=0.01) seen: dict[str, object] = {} def wrapped_generate(*_args: object, **_kwargs: object) -> str: @@ -279,7 +279,7 @@ def from_dem(cls, dem: str) -> object: def test_correlated_pymatching_requires_circuit_level_dem(self) -> None: """The correlated option needs DEM metadata and should fail without it.""" patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p2=0.01, p_meas=0.01) + noise = NoiseParameters(p2=0.01, p_meas=0.01) decoder = SurfaceDecoder( patch, decoder_type="pymatching_correlated", @@ -293,7 +293,7 @@ def test_correlated_pymatching_requires_circuit_level_dem(self) -> None: def test_correlated_pymatching_requires_decomposed_dem_mode(self) -> None: """The explicit correlated option needs decomposed DEM metadata.""" patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p2=0.01, p_meas=0.01) + noise = NoiseParameters(p2=0.01, p_meas=0.01) decoder = SurfaceDecoder( patch, decoder_type="pymatching_correlated", @@ -315,7 +315,7 @@ def test_recommended_memory_workflow_uses_terminal_graphlike_for_pymatching(self def test_get_dem(self) -> None: """Test DEM generation via decoder.""" patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p2=0.01, p_meas=0.01) + noise = NoiseParameters(p2=0.01, p_meas=0.01) decoder = SurfaceDecoder(patch, num_rounds=3, noise=noise) # Test circuit-level DEM (default) @@ -337,7 +337,7 @@ def test_get_dem_caches_circuit_level_dem(self, monkeypatch: pytest.MonkeyPatch) import pecos.qec.surface.decode as decode_module patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001) + noise = NoiseParameters(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001) decoder = SurfaceDecoder( patch, num_rounds=3, @@ -366,7 +366,7 @@ def test_get_dem_passes_interaction_basis_to_native_builder(self, monkeypatch: p import pecos.qec.surface.decode as decode_module patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p2=0.01, p_meas=0.01) + noise = NoiseParameters(p2=0.01, p_meas=0.01) seen: dict[str, object] = {} def wrapped_generate(*_args: object, **kwargs: object) -> str: @@ -395,7 +395,7 @@ def test_get_dem_passes_terminal_graphlike_mode_to_native_builder( import pecos.qec.surface.decode as decode_module patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p2=0.01, p_meas=0.01) + noise = NoiseParameters(p2=0.01, p_meas=0.01) seen: dict[str, object] = {} def wrapped_generate(*_args: object, **kwargs: object) -> str: @@ -421,7 +421,7 @@ def wrapped_generate(*_args: object, **kwargs: object) -> str: def test_decode_trivial_syndrome_z(self) -> None: """Decode trivial Z syndrome (no errors).""" patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p2=0.01, p_meas=0.01) + noise = NoiseParameters(p2=0.01, p_meas=0.01) decoder = SurfaceDecoder(patch, num_rounds=1, noise=noise) # All-zero syndrome @@ -446,7 +446,7 @@ def test_decode_trivial_syndrome_z(self) -> None: def test_decode_trivial_syndrome_x(self) -> None: """Decode trivial X syndrome (no errors).""" patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p2=0.01, p_meas=0.01) + noise = NoiseParameters(p2=0.01, p_meas=0.01) decoder = SurfaceDecoder(patch, num_rounds=1, noise=noise) num_x_stab = len(patch.geometry.x_stabilizers) @@ -536,7 +536,7 @@ class TestDemGeneration: def test_generate_surface_code_dem_z(self) -> None: """Generate Z-stabilizer DEM.""" patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p2=0.01, p_meas=0.01) + noise = NoiseParameters(p2=0.01, p_meas=0.01) dem = generate_surface_code_dem(patch, num_rounds=3, noise=noise, stab_type="Z") @@ -548,7 +548,7 @@ def test_generate_surface_code_dem_z(self) -> None: def test_generate_surface_code_dem_x(self) -> None: """Generate X-stabilizer DEM.""" patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p2=0.01, p_meas=0.01) + noise = NoiseParameters(p2=0.01, p_meas=0.01) dem = generate_surface_code_dem(patch, num_rounds=3, noise=noise, stab_type="X") @@ -560,7 +560,7 @@ def test_generate_dem_from_patch_can_skip_stim_decomposition(self) -> None: from pecos.qec.surface.decode import generate_dem_from_patch patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001) + noise = NoiseParameters(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001) full_dem = generate_dem_from_patch(patch, num_rounds=4, noise=noise, basis="X", decompose_errors=False) decomposed_dem = generate_dem_from_patch(patch, num_rounds=4, noise=noise, basis="X", decompose_errors=True) @@ -586,7 +586,7 @@ def test_native_circuit_level_dem_threads_ancilla_budget(self) -> None: from pecos.qec.surface.decode import generate_circuit_level_dem_from_builder patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001) + noise = NoiseParameters(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001) params = {"p1": noise.p1, "p2": noise.p2, "p_meas": noise.p_meas, "p_prep": noise.p_prep} full_tc = generate_tick_circuit_from_patch(patch, num_rounds=2, basis="X") @@ -636,7 +636,7 @@ def test_constrained_budget_uses_cache_and_matches_fresh_build(self) -> None: ) patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001) + noise = NoiseParameters(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001) params = {"p1": noise.p1, "p2": noise.p2, "p_meas": noise.p_meas, "p_prep": noise.p_prep} # abstract source @@ -682,7 +682,7 @@ def test_unconstrained_budget_spellings_collapse_to_one_dem(self) -> None: patch = SurfacePatch.create(distance=3) total = len(patch.geometry.x_stabilizers) + len(patch.geometry.z_stabilizers) - noise = NoiseModel(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001) + noise = NoiseParameters(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001) # Canonicalization: every unconstrained spelling -> None; a real # constraint passes through unchanged. @@ -716,7 +716,7 @@ def test_constrained_budget_sampler_builds_for_all_models(self) -> None: from pecos.qec.surface.decode import _build_surface_tick_circuit_for_native_model patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001) + noise = NoiseParameters(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001) abstract_tc = _build_surface_tick_circuit_for_native_model( patch, 2, @@ -864,7 +864,7 @@ def test_native_circuit_level_dem_cache_respects_patch_geometry(self) -> None: from pecos.qec.surface.decode import generate_circuit_level_dem_from_builder patch = SurfacePatch.create(dx=3, dz=5) - noise = NoiseModel(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001) + noise = NoiseParameters(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001) params = {"p1": noise.p1, "p2": noise.p2, "p_meas": noise.p_meas, "p_prep": noise.p_prep} tc = generate_tick_circuit_from_patch(patch, num_rounds=2, basis="X") @@ -884,7 +884,7 @@ def test_native_circuit_level_dem_cache_inserts_idle_gates_only_for_idle_noise(s from pecos.qec.surface.decode import generate_circuit_level_dem_from_builder patch = SurfacePatch.create(distance=3) - base_noise = NoiseModel(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001) + base_noise = NoiseParameters(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001) base_params = { "p1": base_noise.p1, "p2": base_noise.p2, @@ -902,7 +902,7 @@ def test_native_circuit_level_dem_cache_inserts_idle_gates_only_for_idle_noise(s basis="X", ) - idle_noise = NoiseModel(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001, p_idle=0.002) + idle_noise = NoiseParameters(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001, p_idle=0.002) idle_tc = generate_tick_circuit_from_patch(patch, num_rounds=2, basis="X") idle_tc.fill_idle_gates() expected_idle_dem = generate_dem_from_tick_circuit( @@ -929,7 +929,7 @@ def test_traced_qis_native_dem_and_sampler_build(self) -> None: _require_selene_runtime() patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p1=0.001, p2=0.001, p_meas=0.001, p_prep=0.001) + noise = NoiseParameters(p1=0.001, p2=0.001, p_meas=0.001, p_prep=0.001) dem = generate_circuit_level_dem_from_builder( patch, @@ -1014,7 +1014,7 @@ def extract_errors(dem_str: str) -> dict[str, float]: return errors patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p1=0.003, p2=0.003, p_meas=0.003, p_prep=0.003) + noise = NoiseParameters(p1=0.003, p2=0.003, p_meas=0.003, p_prep=0.003) for basis in ("X", "Z"): tc = _build_surface_tick_circuit_for_native_model( @@ -1069,8 +1069,8 @@ def test_traced_qis_native_topology_cache_is_shared_across_public_apis(self) -> _require_selene_runtime() patch = SurfacePatch.create(distance=3) - noise_a = NoiseModel(p1=0.001, p2=0.001, p_meas=0.001, p_prep=0.001) - noise_b = NoiseModel(p1=0.002, p2=0.002, p_meas=0.002, p_prep=0.002) + noise_a = NoiseParameters(p1=0.001, p2=0.001, p_meas=0.001, p_prep=0.001) + noise_b = NoiseParameters(p1=0.002, p2=0.002, p_meas=0.002, p_prep=0.002) _cached_surface_native_topology.cache_clear() _cached_surface_native_dem_string.cache_clear() @@ -1136,7 +1136,7 @@ def test_generate_dem_from_tick_circuit_maximal_decomposition_prefers_singletons def test_dem_detector_count(self) -> None: """DEM should have correct number of detectors.""" patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p2=0.01, p_meas=0.01) + noise = NoiseParameters(p2=0.01, p_meas=0.01) num_rounds = 3 dem = generate_surface_code_dem( @@ -1156,7 +1156,7 @@ def test_dem_detector_count(self) -> None: def test_dem_single_round(self) -> None: """DEM with single round should have boundary measurement errors.""" patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p2=0.01, p_meas=0.01) + noise = NoiseParameters(p2=0.01, p_meas=0.01) dem = generate_surface_code_dem(patch, num_rounds=1, noise=noise, stab_type="Z") diff --git a/python/quantum-pecos/tests/qec/surface/test_szz_interaction_basis.py b/python/quantum-pecos/tests/qec/surface/test_szz_interaction_basis.py index 83a9e7799..f6580fce7 100644 --- a/python/quantum-pecos/tests/qec/surface/test_szz_interaction_basis.py +++ b/python/quantum-pecos/tests/qec/surface/test_szz_interaction_basis.py @@ -11,7 +11,7 @@ import pytest import stim from pecos._traced_circuit import normalize_traced_tick_circuit -from pecos.qec.surface import NoiseModel, SurfacePatch, TwirlConfig +from pecos.qec.surface import NoiseParameters, SurfacePatch, TwirlConfig from pecos.qec.surface.circuit_builder import ( OpType, SurfaceCircuitStep, @@ -492,7 +492,7 @@ def test_szz_runtime_barriers_allow_strict_traced_hosted_order() -> None: dem = generate_circuit_level_dem_from_builder( patch, num_rounds=1, - noise=NoiseModel(p1=0.0, p2=0.001, p_meas=0.0, p_prep=0.0), + noise=NoiseParameters(p1=0.0, p2=0.001, p_meas=0.0, p_prep=0.0), circuit_source="traced_qis", interaction_basis="szz", szz_runtime_barriers="data-prefix", @@ -581,7 +581,7 @@ def test_round_order_szz_noiseless_detector_record_equivalence( def test_szz_native_dem_path_uses_interaction_basis() -> None: patch = SurfacePatch.create(distance=3) - noise = NoiseModel(p1=0.0, p2=0.01, p2_weights={"ZI": 1.0}, p_meas=0.001, p_prep=0.001) + noise = NoiseParameters(p1=0.0, p2=0.01, p2_weights={"ZI": 1.0}, p_meas=0.001, p_prep=0.001) cx_dem = generate_circuit_level_dem_from_builder( patch, @@ -606,25 +606,25 @@ def test_szz_native_dem_respects_gate_specific_p2_overrides() -> None: inherited_dem = generate_circuit_level_dem_from_builder( patch, num_rounds=2, - noise=NoiseModel(p1=0.0, p2=0.01, p2_weights={"ZI": 1.0}), + noise=NoiseParameters(p1=0.0, p2=0.01, p2_weights={"ZI": 1.0}), interaction_basis="szz", ) no_szz_dem = generate_circuit_level_dem_from_builder( patch, num_rounds=2, - noise=NoiseModel(p1=0.0, p2=0.01, p2_szz=0.0, p2_weights={"ZI": 1.0}), + noise=NoiseParameters(p1=0.0, p2=0.01, p2_szz=0.0, p2_weights={"ZI": 1.0}), interaction_basis="szz", ) no_szzdg_dem = generate_circuit_level_dem_from_builder( patch, num_rounds=2, - noise=NoiseModel(p1=0.0, p2=0.01, p2_szzdg=0.0, p2_weights={"ZI": 1.0}), + noise=NoiseParameters(p1=0.0, p2=0.01, p2_szzdg=0.0, p2_weights={"ZI": 1.0}), interaction_basis="szz", ) override_only_dem = generate_circuit_level_dem_from_builder( patch, num_rounds=2, - noise=NoiseModel( + noise=NoiseParameters( p1=0.0, p2=0.0, p2_szz=0.01, @@ -645,14 +645,14 @@ def test_szz_native_influence_sampler_respects_override_only_p2() -> None: zero_sampler = build_native_sampler( patch, num_rounds=2, - noise=NoiseModel(p1=0.0, p2=0.0, p2_szz=0.0, p2_szzdg=0.0, p2_weights={"ZI": 1.0}), + noise=NoiseParameters(p1=0.0, p2=0.0, p2_szz=0.0, p2_szzdg=0.0, p2_weights={"ZI": 1.0}), interaction_basis="szz", sampling_model="influence_dem", ) active_sampler = build_native_sampler( patch, num_rounds=2, - noise=NoiseModel(p1=0.0, p2=0.0, p2_szz=0.01, p2_szzdg=0.01, p2_weights={"ZI": 1.0}), + noise=NoiseParameters(p1=0.0, p2=0.0, p2_szz=0.01, p2_szzdg=0.01, p2_weights={"ZI": 1.0}), interaction_basis="szz", sampling_model="influence_dem", ) @@ -665,7 +665,7 @@ def test_szz_native_influence_sampler_respects_override_only_p2() -> None: def test_szz_prefix_lowering_preserves_p2_influence_dem(basis: str) -> None: patch = SurfacePatch.create(distance=3) patch_key = _surface_patch_cache_key(patch) - noise = NoiseModel(p1=0.0, p2=0.01, p_meas=0.0, p_prep=0.0) + noise = NoiseParameters(p1=0.0, p2=0.01, p_meas=0.0, p_prep=0.0) plain = _surface_native_topology( patch_key, @@ -831,7 +831,7 @@ def test_szz_native_sampler_accepts_p1_with_physical_prefix_lowering(sampling_mo sampler = build_native_sampler( patch, num_rounds=1, - noise=NoiseModel(p1=0.001), + noise=NoiseParameters(p1=0.001), interaction_basis="szz", sampling_model=sampling_model, ) @@ -853,7 +853,7 @@ def test_szz_native_dem_rejects_traced_qis_idle_noise() -> None: generate_circuit_level_dem_from_builder( patch, num_rounds=1, - noise=NoiseModel(p_idle=0.001), + noise=NoiseParameters(p_idle=0.001), interaction_basis="szz", circuit_source="traced_qis", ) @@ -973,7 +973,7 @@ def test_szz_public_native_dem_accepts_traced_qis_p1() -> None: dem = generate_circuit_level_dem_from_builder( patch, num_rounds=1, - noise=NoiseModel(p1=0.001), + noise=NoiseParameters(p1=0.001), interaction_basis="szz", circuit_source="traced_qis", ) @@ -995,7 +995,7 @@ def test_szz_public_traced_qis_dem_matches_stim_with_z_frame_p1_free(basis: str) interaction_basis="szz", ) normalize_traced_tick_circuit(tick_circuit, context="SZZ public traced-QIS p1 test") - noise = NoiseModel(p1=0.001) + noise = NoiseParameters(p1=0.001) native_errors = _raw_dem_errors( generate_circuit_level_dem_from_builder( @@ -1052,7 +1052,7 @@ def test_szz_native_dem_accepts_p1_with_physical_prefix_lowering() -> None: dem = generate_circuit_level_dem_from_builder( patch, num_rounds=1, - noise=NoiseModel(p1=0.001), + noise=NoiseParameters(p1=0.001), interaction_basis="szz", ) @@ -1067,7 +1067,7 @@ def test_szz_native_sampler_accepts_idle_with_physical_prefix_lowering(sampling_ sampler = build_native_sampler( patch, num_rounds=1, - noise=NoiseModel(p_idle=0.001), + noise=NoiseParameters(p_idle=0.001), interaction_basis="szz", sampling_model=sampling_model, ) @@ -1084,7 +1084,7 @@ def test_szz_native_dem_accepts_idle_with_physical_prefix_lowering() -> None: dem = generate_circuit_level_dem_from_builder( patch, num_rounds=1, - noise=NoiseModel(p_idle=0.001), + noise=NoiseParameters(p_idle=0.001), interaction_basis="szz", ) @@ -1096,7 +1096,7 @@ def test_szz_native_dem_accepts_idle_with_physical_prefix_lowering() -> None: def test_szz_idle_dem_uses_lowered_prefix_topology(basis: str) -> None: patch = SurfacePatch.create(distance=3) patch_key = _surface_patch_cache_key(patch) - noise = NoiseModel(p_idle_z_linear_rate=0.01) + noise = NoiseParameters(p_idle_z_linear_rate=0.01) actual = generate_circuit_level_dem_from_builder( patch, @@ -1178,7 +1178,7 @@ def test_szz_virtual_prefix_ticks_do_not_contribute_idle_dem() -> None: patch, num_rounds=1, basis="Z", - noise=NoiseModel(p_idle_z_linear_rate=0.01), + noise=NoiseParameters(p_idle_z_linear_rate=0.01), interaction_basis="szz", decompose_errors=False, ) diff --git a/python/quantum-pecos/tests/qec/test_decomposed_dem_invariants.py b/python/quantum-pecos/tests/qec/test_decomposed_dem_invariants.py index 45db9a669..ec3d99dd9 100644 --- a/python/quantum-pecos/tests/qec/test_decomposed_dem_invariants.py +++ b/python/quantum-pecos/tests/qec/test_decomposed_dem_invariants.py @@ -147,7 +147,7 @@ def build_source_tracked_dem(distance: int, basis: str, rounds: int = 20) -> obj """Build and cache a source-tracked native DEM for one surface-code shape.""" from pecos.qec import DagFaultAnalyzer, DemBuilder from pecos.qec.surface import ( - NoiseModel, + NoiseParameters, SurfacePatch, generate_tick_circuit_from_patch, get_measurement_order_from_tick_circuit, @@ -158,7 +158,7 @@ def build_source_tracked_dem(distance: int, basis: str, rounds: int = 20) -> obj dag = tc.to_dag_circuit() analyzer = DagFaultAnalyzer(dag) influence_map = analyzer.build_influence_map() - noise = NoiseModel(p1=0.01, p2=0.01, p_meas=0.01, p_prep=0.01) + noise = NoiseParameters(p1=0.01, p2=0.01, p_meas=0.01, p_prep=0.01) builder = DemBuilder(influence_map) builder.with_noise(noise.p1, noise.p2, noise.p_meas, noise.p_prep) @@ -175,7 +175,7 @@ def test_dem_builder_accepts_public_surface_descriptor_json() -> None: """Public surface descriptor JSON should reproduce the legacy builder output.""" from pecos.qec import DagFaultAnalyzer, DemBuilder from pecos.qec.surface import ( - NoiseModel, + NoiseParameters, SurfacePatch, generate_tick_circuit_from_patch, get_detector_descriptors_from_tick_circuit, @@ -187,7 +187,7 @@ def test_dem_builder_accepts_public_surface_descriptor_json() -> None: tc = generate_tick_circuit_from_patch(patch, num_rounds=4, basis="X") dag = tc.to_dag_circuit() influence_map = DagFaultAnalyzer(dag).build_influence_map() - noise = NoiseModel(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001) + noise = NoiseParameters(p1=0.001, p2=0.01, p_meas=0.01, p_prep=0.001) def _build(detectors_json: str, observables_json: str | None) -> object: """Build one source-tracked DEM from serialized detector metadata.""" diff --git a/python/quantum-pecos/tests/qec/test_from_guppy_dem.py b/python/quantum-pecos/tests/qec/test_from_guppy_dem.py index 674d05242..c8d2a824d 100644 --- a/python/quantum-pecos/tests/qec/test_from_guppy_dem.py +++ b/python/quantum-pecos/tests/qec/test_from_guppy_dem.py @@ -23,7 +23,7 @@ ) from pecos.guppy import get_num_qubits, make_surface_code from pecos.qec import Detector, DetectorErrorModel, Observable, build_dem_from_guppy, rec -from pecos.qec.surface import RUNTIME_IDLE_TIME_UNITS_PER_SECOND, NoiseModel, SurfacePatch +from pecos.qec.surface import RUNTIME_IDLE_TIME_UNITS_PER_SECOND, NoiseParameters, SurfacePatch from pecos.qec.surface.circuit_builder import ( generate_tick_circuit_from_patch, ) @@ -259,7 +259,7 @@ def _noise_model_entrypoint_dem(entrypoint: str, **kwargs): def test_noise_model_matches_flat_gate_noise(entrypoint: str) -> None: rates = {"p1": 0.003, "p2": 0.007, "p_meas": 0.011, "p_prep": 0.013} - grouped = _noise_model_entrypoint_dem(entrypoint, noise=NoiseModel(**rates)) + grouped = _noise_model_entrypoint_dem(entrypoint, noise=NoiseParameters(**rates)) flat = _noise_model_entrypoint_dem(entrypoint, **rates) assert grouped.to_string() == flat.to_string() @@ -276,7 +276,7 @@ def test_noise_model_matches_flat_pauli_weights(entrypoint: str) -> None: "p_prep": 0.013, } - grouped = _noise_model_entrypoint_dem(entrypoint, noise=NoiseModel(**noise_kwargs)) + grouped = _noise_model_entrypoint_dem(entrypoint, noise=NoiseParameters(**noise_kwargs)) flat = _noise_model_entrypoint_dem(entrypoint, **noise_kwargs) assert grouped.to_string() == flat.to_string() @@ -289,7 +289,7 @@ def test_noise_model_structured_idle_family_matches_flat_axis_rates(entrypoint: grouped = _noise_model_entrypoint_dem( entrypoint, - noise=NoiseModel(p_idle_linear=rate, p_idle_linear_model=model), + noise=NoiseParameters(p_idle_linear=rate, p_idle_linear_model=model), idle_after_2q_duration=2.0, ) flat = _noise_model_entrypoint_dem( @@ -310,21 +310,21 @@ def test_noise_model_structured_idle_family_matches_flat_axis_rates(entrypoint: @pytest.mark.parametrize("keyword", ["p1", "p2", "p_meas", "p_idle_linear"]) def test_noise_model_rejects_flat_noise_keyword(entrypoint: str, keyword: str) -> None: with pytest.raises(ValueError, match=keyword): - _noise_model_entrypoint_dem(entrypoint, noise=NoiseModel(), **{keyword: 0.01}) + _noise_model_entrypoint_dem(entrypoint, noise=NoiseParameters(), **{keyword: 0.01}) @pytest.mark.parametrize("entrypoint", ["from_guppy", "build_dem_from_guppy"]) @pytest.mark.parametrize("field", ["p_idle", "p2_szz", "p2_szzdg"]) def test_noise_model_rejects_fields_not_supported_by_guppy_dem(entrypoint: str, field: str) -> None: with pytest.raises(ValueError, match=field): - _noise_model_entrypoint_dem(entrypoint, noise=NoiseModel(**{field: 0.01})) + _noise_model_entrypoint_dem(entrypoint, noise=NoiseParameters(**{field: 0.01})) @pytest.mark.parametrize("entrypoint", ["from_guppy", "build_dem_from_guppy"]) def test_noise_model_combines_with_non_noise_keywords(entrypoint: str) -> None: dem = _noise_model_entrypoint_dem( entrypoint, - noise=NoiseModel(p_idle_linear=0.01), + noise=NoiseParameters(p_idle_linear=0.01), idle_after_2q_duration=1.0, strip_traced_idles=True, seed=17, @@ -992,7 +992,7 @@ def test_lowered_replay_converts_runtime_idle_seconds_to_nanosecond_time_units() def test_noise_model_converts_runtime_idle_rates_from_seconds_to_dem_time_units() -> None: - noise = NoiseModel( + noise = NoiseParameters( p1=0.001, p2=0.002, p_meas=0.003, @@ -1021,7 +1021,7 @@ def test_noise_model_converts_runtime_idle_rates_from_seconds_to_dem_time_units( def test_noise_model_rejects_invalid_runtime_idle_time_unit_scale() -> None: with pytest.raises(ValueError, match="time_units_per_second"): - NoiseModel(p_idle_z_linear_rate=1.0).for_runtime_idle_time_units(time_units_per_second=0.0) + NoiseParameters(p_idle_z_linear_rate=1.0).for_runtime_idle_time_units(time_units_per_second=0.0) def test_lowered_replay_preserves_gate_metadata() -> None: @@ -1682,7 +1682,7 @@ def test_native_abstract_surface_dem_uses_record_metadata_only_for_r0(basis: str assert json.loads(native_tc.get_meta("detectors") or "[]") assert json.loads(native_tc.get_meta("observables") or "[]") - noise = NoiseModel(p1=0.0, p2=0.001, p_meas=0.0, p_prep=0.0) + noise = NoiseParameters(p1=0.0, p2=0.001, p_meas=0.0, p_prep=0.0) for decompose_errors in (False, True): dem_text = generate_circuit_level_dem_from_builder( patch, diff --git a/python/quantum-pecos/tests/qec/test_guppy_dem_builder.py b/python/quantum-pecos/tests/qec/test_guppy_dem_builder.py index 5893a8848..6143af3f1 100644 --- a/python/quantum-pecos/tests/qec/test_guppy_dem_builder.py +++ b/python/quantum-pecos/tests/qec/test_guppy_dem_builder.py @@ -21,7 +21,7 @@ build_dem_from_guppy, rec, ) -from pecos.qec.surface import NoiseModel +from pecos.qec.surface import NoiseParameters from pecos_rslib.quantum import TickCircuit if TYPE_CHECKING: @@ -42,7 +42,7 @@ def _tagged_two_qubit_program() -> None: def test_builder_matches_both_wrappers_with_noise_and_inserted_idles() -> None: - noise = NoiseModel(p1=0.0, p2=0.01, p_meas=0.02, p_prep=0.0, p_idle_z_linear_rate=0.03) + noise = NoiseParameters(p1=0.0, p2=0.01, p_meas=0.02, p_prep=0.0, p_idle_z_linear_rate=0.03) via_json_builder = ( DetectorErrorModel.builder() .program(_tagged_two_qubit_program) @@ -91,7 +91,7 @@ def test_builder_matches_both_wrappers_with_noise_and_inserted_idles() -> None: def test_builder_matches_both_wrappers_with_result_tags() -> None: detectors_json = '[{"id":0,"result_tags":["m0"]}]' observables_json = '[{"id":0,"result_tags":["m1"]}]' - noise = NoiseModel(p1=0.0, p2=0.0, p_meas=0.1, p_prep=0.0) + noise = NoiseParameters(p1=0.0, p2=0.0, p_meas=0.1, p_prep=0.0) via_json_builder = ( DetectorErrorModel.builder() .program(_tagged_two_qubit_program) @@ -157,7 +157,7 @@ def test_builder_reports_missing_required_setters( ("detectors_json", _DETECTORS_JSON), ("observables_json", _OBSERVABLES_JSON), ("num_measurements", 2), - ("noise", NoiseModel()), + ("noise", NoiseParameters()), ("idle_after_2q", 1.0), ("strip_traced_idles", True), ("runtime", None), @@ -220,7 +220,7 @@ def test_builder_result_evaluates_simulation_result_columns() -> None: .qubits(2) .detectors([Detector("m0")]) .observables([Observable("m1")]) - .noise(NoiseModel(p_meas=0.1)) + .noise(NoiseParameters(p_meas=0.1)) .build() ) columns = ( @@ -244,7 +244,7 @@ def test_json_builder_audit_accepts_legacy_id_aliases() -> None: .qubits(2) .detectors_json('[{"detector_id":"D0","records":[-2]}]') .observables_json('[{"observable_id":"L0","records":[-1]}]') - .noise(NoiseModel(p_meas=0.1)) + .noise(NoiseParameters(p_meas=0.1)) .build() ) @@ -252,7 +252,7 @@ def test_json_builder_audit_accepts_legacy_id_aliases() -> None: def test_builder_setter_order_does_not_change_the_dem() -> None: - noise = NoiseModel(p1=0.01, p2=0.02, p_meas=0.03, p_prep=0.04) + noise = NoiseParameters(p1=0.01, p2=0.02, p_meas=0.03, p_prep=0.04) first = ( DetectorErrorModel.builder() .program(_tagged_two_qubit_program) diff --git a/python/quantum-pecos/tests/qec/test_qec_ux_entrypoints.py b/python/quantum-pecos/tests/qec/test_qec_ux_entrypoints.py index 44f228224..da58f8899 100644 --- a/python/quantum-pecos/tests/qec/test_qec_ux_entrypoints.py +++ b/python/quantum-pecos/tests/qec/test_qec_ux_entrypoints.py @@ -99,12 +99,12 @@ def test_surface_code_memory_accepts_traced_qis_runtime() -> None: def test_surface_decoder_accepts_traced_qis_runtime() -> None: - from pecos.qec.surface import NoiseModel, SurfaceDecoder, SurfacePatch + from pecos.qec.surface import NoiseParameters, SurfaceDecoder, SurfacePatch decoder = SurfaceDecoder( SurfacePatch.create(distance=3), num_rounds=1, - noise=NoiseModel(p1=0.0, p2=0.0, p_meas=0.0, p_prep=0.0), + noise=NoiseParameters(p1=0.0, p2=0.0, p_meas=0.0, p_prep=0.0), decoder_type="pymatching_uncorrelated", circuit_level_dem_source="traced_qis", runtime=_NON_DEFAULT_RUNTIME, @@ -114,12 +114,12 @@ def test_surface_decoder_accepts_traced_qis_runtime() -> None: def test_build_native_sampler_accepts_traced_qis_runtime() -> None: - from pecos.qec.surface import NoiseModel, SurfacePatch, build_native_sampler + from pecos.qec.surface import NoiseParameters, SurfacePatch, build_native_sampler sampler = build_native_sampler( SurfacePatch.create(distance=3), num_rounds=1, - noise=NoiseModel(p1=0.0, p2=0.0, p_meas=0.0, p_prep=0.0), + noise=NoiseParameters(p1=0.0, p2=0.0, p_meas=0.0, p_prep=0.0), circuit_source="traced_qis", runtime=_NON_DEFAULT_RUNTIME, ) @@ -128,12 +128,12 @@ def test_build_native_sampler_accepts_traced_qis_runtime() -> None: def test_surface_code_memory_rejects_ambiguous_noise_inputs() -> None: - from pecos.qec.surface import NoiseModel, surface_code_memory + from pecos.qec.surface import NoiseParameters, surface_code_memory with pytest.raises(ValueError, match="either physical_error_rate or noise_model"): surface_code_memory( physical_error_rate=0.0, - noise_model=NoiseModel.uniform(0.001), + noise_model=NoiseParameters.uniform(0.001), shots=0, rounds=1, )