From 3179b9379542a5d8ee0a063a64cd71db585ebf3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiberiu=20Sab=C4=83u?= Date: Thu, 5 Feb 2026 17:42:55 +0100 Subject: [PATCH 1/9] Make tests use the BranchSelector feature --- tests/test_noisy_density_matrix.py | 196 ++++++++++++++++++----------- 1 file changed, 125 insertions(+), 71 deletions(-) diff --git a/tests/test_noisy_density_matrix.py b/tests/test_noisy_density_matrix.py index 20c5cf91e..87e41828d 100644 --- a/tests/test_noisy_density_matrix.py +++ b/tests/test_noisy_density_matrix.py @@ -6,6 +6,7 @@ import numpy.typing as npt import pytest +from graphix.branch_selector import ConstBranchSelector, FixedBranchSelector from graphix.fundamentals import angle_to_rad from graphix.noise_models import DepolarisingNoiseModel from graphix.noise_models.noise_model import NoiselessNoiseModel @@ -69,14 +70,22 @@ def test_noisy_measure_confuse_hadamard(self, fx_rng: Generator) -> None: assert isinstance(res, DensityMatrix) assert np.allclose(res.rho, np.array([[0.0, 0.0], [0.0, 1.0]])) - # arbitrary probability + @pytest.mark.parametrize("outcome", [0, 1]) + def test_noisy_measure_confuse_hadamard_arbitrary(self, fx_rng: Generator, outcome: int) -> None: + # arbitrary probability with fixed branch + hadamardpattern = hpat() measure_error_pr = fx_rng.random() - print(f"measure_error_pr = {measure_error_pr}") + print(f"measure_error_pr = {measure_error_pr}, outcome = {outcome}") res = hadamardpattern.simulate_pattern( - backend="densitymatrix", noise_model=DepolarisingNoiseModel(measure_error_prob=measure_error_pr), rng=fx_rng + backend="densitymatrix", + noise_model=DepolarisingNoiseModel(measure_error_prob=measure_error_pr), + branch_selector=ConstBranchSelector(outcome), + rng=fx_rng, ) - # result should be |1> assert isinstance(res, DensityMatrix) + # With measure_error_prob, the outcome might be flipped, resulting in different X corrections + # However, we cannot predict the exact result without knowing if the error occurred + # So we check both possibilities assert np.allclose(res.rho, np.array([[1.0, 0.0], [0.0, 0.0]])) or np.allclose( res.rho, np.array([[0.0, 0.0], [0.0, 1.0]]), @@ -100,21 +109,21 @@ def test_noisy_measure_channel_hadamard(self, fx_rng: Generator) -> None: ) # test Pauli X error - def test_noisy_x_hadamard(self, fx_rng: Generator) -> None: + @pytest.mark.parametrize("outcome", [0, 1]) + def test_noisy_x_hadamard(self, fx_rng: Generator, outcome: int) -> None: hadamardpattern = hpat() # x error only x_error_pr = fx_rng.random() - print(f"x_error_pr = {x_error_pr}") + print(f"x_error_pr = {x_error_pr}, outcome = {outcome}") res = hadamardpattern.simulate_pattern( backend="densitymatrix", noise_model=DepolarisingNoiseModel(x_error_prob=x_error_pr), + branch_selector=ConstBranchSelector(outcome), rng=fx_rng, ) - # analytical result since deterministic pattern output is |0>. - # if no X applied, no noise. If X applied X noise on |0><0| - + # Both outcomes lead to |0> after correction. ApplyNoise is unconditional, so noise is always applied. assert isinstance(res, DensityMatrix) - assert np.allclose(res.rho, np.array([[1.0, 0.0], [0.0, 0.0]])) or np.allclose( + assert np.allclose( res.rho, np.array([[1 - 2 * x_error_pr / 3.0, 0.0], [0.0, 2 * x_error_pr / 3.0]]), ) @@ -310,26 +319,36 @@ def test_noisy_measure_channel_rz(self, fx_rng: Generator) -> None: ), ) - def test_noisy_x_rz(self, fx_rng: Generator) -> None: + @pytest.mark.parametrize("outcome_z,outcome_x", [(0, 0), (0, 1), (1, 0), (1, 1)]) + def test_noisy_x_rz(self, fx_rng: Generator, outcome_z: int, outcome_x: int) -> None: alpha = fx_rng.random() rzpattern = rzpat(alpha) # x error only x_error_pr = fx_rng.random() - print(f"x_error_pr = {x_error_pr}") + print(f"x_error_pr = {x_error_pr}, outcome_z = {outcome_z}, outcome_x = {outcome_x}") + + # M(0) determines Z, M(1) determines X + results = {} + cmd_count = 0 + for cmd in rzpattern: + if cmd.kind.name == "M": + if cmd_count == 0: + results[cmd.node] = outcome_z + elif cmd_count == 1: + results[cmd.node] = outcome_x + cmd_count += 1 + res = rzpattern.simulate_pattern( backend="densitymatrix", noise_model=DepolarisingNoiseModel(x_error_prob=x_error_pr), + branch_selector=FixedBranchSelector(results), rng=fx_rng, ) - # only two cases: if no X correction, Z or no Z correction but exact result. - # If X correction the noise result is the same with or without the PERFECT Z correction. + # All outcomes lead to the same state after corrections. ApplyNoise is unconditional, so noise is always applied. assert isinstance(res, DensityMatrix) rad = angle_to_rad(alpha) assert np.allclose( - res.rho, - 0.5 * np.array([[1.0, np.exp(-1j * rad)], [np.exp(1j * rad), 1.0]]), - ) or np.allclose( res.rho, 0.5 * np.array( @@ -340,26 +359,36 @@ def test_noisy_x_rz(self, fx_rng: Generator) -> None: ), ) - def test_noisy_z_rz(self, fx_rng: Generator) -> None: + @pytest.mark.parametrize("outcome_z,outcome_x", [(0, 0), (0, 1), (1, 0), (1, 1)]) + def test_noisy_z_rz(self, fx_rng: Generator, outcome_z: int, outcome_x: int) -> None: alpha = fx_rng.random() rzpattern = rzpat(alpha) # z error only z_error_pr = fx_rng.random() - print(f"z_error_pr = {z_error_pr}") + print(f"z_error_pr = {z_error_pr}, outcome_z = {outcome_z}, outcome_x = {outcome_x}") + + # M(0) determines Z, M(1) determines X + results = {} + cmd_count = 0 + for cmd in rzpattern: + if cmd.kind.name == "M": + if cmd_count == 0: + results[cmd.node] = outcome_z + elif cmd_count == 1: + results[cmd.node] = outcome_x + cmd_count += 1 + res = rzpattern.simulate_pattern( backend="densitymatrix", noise_model=DepolarisingNoiseModel(z_error_prob=z_error_pr), + branch_selector=FixedBranchSelector(results), rng=fx_rng, ) - # only two cases: if no Z correction, X or no X correction but exact result. - # If Z correction the noise result is the same with or without the PERFECT X correction. + # All outcomes lead to the same state after corrections. ApplyNoise is unconditional, so noise is always applied. assert isinstance(res, DensityMatrix) rad = angle_to_rad(alpha) assert np.allclose( - res.rho, - 0.5 * np.array([[1.0, np.exp(-1j * rad)], [np.exp(1j * rad), 1.0]]), - ) or np.allclose( res.rho, 0.5 * np.array( @@ -370,7 +399,8 @@ def test_noisy_z_rz(self, fx_rng: Generator) -> None: ), ) - def test_noisy_xz_rz(self, fx_rng: Generator) -> None: + @pytest.mark.parametrize("z_outcome,x_outcome", [(0, 0), (0, 1), (1, 0), (1, 1)]) + def test_noisy_xz_rz(self, fx_rng: Generator, z_outcome: int, x_outcome: int) -> None: alpha = fx_rng.random() rzpattern = rzpat(alpha) # x and z errors @@ -378,78 +408,102 @@ def test_noisy_xz_rz(self, fx_rng: Generator) -> None: print(f"x_error_pr = {x_error_pr}") z_error_pr = fx_rng.random() print(f"z_error_pr = {z_error_pr}") + print(f"z_outcome = {z_outcome}, x_outcome = {x_outcome}") + + # M(0) determines Z correction, M(1) determines X correction + results = {} + cmd_count = 0 + for cmd in rzpattern: + if cmd.kind.name == "M": + if cmd_count == 0: + results[cmd.node] = z_outcome + elif cmd_count == 1: + results[cmd.node] = x_outcome + cmd_count += 1 + res = rzpattern.simulate_pattern( backend="densitymatrix", noise_model=DepolarisingNoiseModel(x_error_prob=x_error_pr, z_error_prob=z_error_pr), + branch_selector=FixedBranchSelector(results), rng=fx_rng, ) - # 4 cases : no corr, noisy X, noisy Z, noisy XZ. + # All outcomes lead to same state after corrections. Both X and Z noise applied unconditionally. assert isinstance(res, DensityMatrix) rad = angle_to_rad(alpha) - assert ( - np.allclose(res.rho, 0.5 * np.array([[1.0, np.exp(-1j * rad)], [np.exp(1j * rad), 1.0]])) - or np.allclose( - res.rho, - 0.5 - * np.array( - [ - [1.0, np.exp(-1j * rad) * (3 - 4 * x_error_pr) * (3 - 4 * z_error_pr) / 9], - [np.exp(1j * rad) * (3 - 4 * x_error_pr) * (3 - 4 * z_error_pr) / 9, 1.0], - ], - ), - ) - or np.allclose( - res.rho, - 0.5 - * np.array( - [ - [1.0, np.exp(-1j * rad) * (3 - 4 * z_error_pr) / 3], - [np.exp(1j * rad) * (3 - 4 * z_error_pr) / 3, 1.0], - ], - ), - ) - or np.allclose( - res.rho, - 0.5 - * np.array( - [ - [1.0, np.exp(-1j * rad) * (3 - 4 * x_error_pr) / 3], - [np.exp(1j * rad) * (3 - 4 * x_error_pr) / 3, 1.0], - ], - ), - ) + assert np.allclose( + res.rho, + 0.5 + * np.array( + [ + [1.0, np.exp(-1j * rad) * (3 - 4 * x_error_pr) * (3 - 4 * z_error_pr) / 9], + [np.exp(1j * rad) * (3 - 4 * x_error_pr) * (3 - 4 * z_error_pr) / 9, 1.0], + ], + ), ) # test measurement confuse outcome - def test_noisy_measure_confuse_rz(self, fx_rng: Generator) -> None: + @pytest.mark.parametrize("z_outcome,x_outcome", [(0, 0), (0, 1), (1, 0), (1, 1)]) + def test_noisy_measure_confuse_rz(self, fx_rng: Generator, z_outcome: int, x_outcome: int) -> None: alpha = fx_rng.random() rzpattern = rzpat(alpha) - # probability 1 to shift both outcome + + # M(0) determines Z, M(1) determines X + results = {} + cmd_count = 0 + for cmd in rzpattern: + if cmd.kind.name == "M": + if cmd_count == 0: + results[cmd.node] = z_outcome + elif cmd_count == 1: + results[cmd.node] = x_outcome + cmd_count += 1 + + # Test with probability 1 to flip both outcomes res = rzpattern.simulate_pattern( - backend="densitymatrix", noise_model=DepolarisingNoiseModel(measure_error_prob=1.0), rng=fx_rng + backend="densitymatrix", + noise_model=DepolarisingNoiseModel(measure_error_prob=1.0), + branch_selector=FixedBranchSelector(results), + rng=fx_rng, ) - # result X, XZ or Z exact = rz_exact_res(alpha) - assert isinstance(res, DensityMatrix) - assert ( - np.allclose(res.rho, Ops.X @ exact @ Ops.X) - or np.allclose(res.rho, Ops.Z @ exact @ Ops.Z) - or np.allclose(res.rho, Ops.Z @ Ops.X @ exact @ Ops.X @ Ops.Z) - ) + # All outcomes lead to same result: both corrections applied due to flipping + assert np.allclose(res.rho, Ops.Z @ Ops.X @ exact @ Ops.X @ Ops.Z) - # arbitrary probability + @pytest.mark.parametrize("z_outcome,x_outcome", [(0, 0), (0, 1), (1, 0), (1, 1)]) + def test_noisy_measure_confuse_rz_arbitrary(self, fx_rng: Generator, z_outcome: int, x_outcome: int) -> None: + alpha = fx_rng.random() + rzpattern = rzpat(alpha) + + # M(0) determines Z, M(1) determines X + results = {} + cmd_count = 0 + for cmd in rzpattern: + if cmd.kind.name == "M": + if cmd_count == 0: + results[cmd.node] = z_outcome + elif cmd_count == 1: + results[cmd.node] = x_outcome + cmd_count += 1 + + # Test with arbitrary probability measure_error_pr = fx_rng.random() - print(f"measure_error_pr = {measure_error_pr}") + print(f"measure_error_pr = {measure_error_pr}, z_outcome = {z_outcome}, x_outcome = {x_outcome}") res = rzpattern.simulate_pattern( backend="densitymatrix", noise_model=DepolarisingNoiseModel(measure_error_prob=measure_error_pr), + branch_selector=FixedBranchSelector(results), rng=fx_rng, ) - # just add the case without readout errors + + exact = rz_exact_res(alpha) assert isinstance(res, DensityMatrix) + + # With arbitrary measure_error_pr, outcomes may or may not be flipped + # The physical result depends on whether the error occurs + # We check all possible cases assert ( np.allclose(res.rho, exact) or np.allclose(res.rho, Ops.X @ exact @ Ops.X) From 977e290412808ec7313c651215b8131c51dbec44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiberiu=20Sab=C4=83u?= <96194994+tibisabau@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:21:19 +0100 Subject: [PATCH 2/9] Update tests/test_noisy_density_matrix.py Co-authored-by: thierry-martinez --- tests/test_noisy_density_matrix.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_noisy_density_matrix.py b/tests/test_noisy_density_matrix.py index 87e41828d..aacb51ea9 100644 --- a/tests/test_noisy_density_matrix.py +++ b/tests/test_noisy_density_matrix.py @@ -319,7 +319,8 @@ def test_noisy_measure_channel_rz(self, fx_rng: Generator) -> None: ), ) - @pytest.mark.parametrize("outcome_z,outcome_x", [(0, 0), (0, 1), (1, 0), (1, 1)]) + @pytest.mark.parametrize("z_outcome", [0, 1]) + @pytest.mark.parametrize("x_outcome", [0, 1]) def test_noisy_x_rz(self, fx_rng: Generator, outcome_z: int, outcome_x: int) -> None: alpha = fx_rng.random() rzpattern = rzpat(alpha) From 9dc02aca33630379e2a2c3d1c099bb59b12cc171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiberiu=20Sab=C4=83u?= <96194994+tibisabau@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:21:42 +0100 Subject: [PATCH 3/9] Update tests/test_noisy_density_matrix.py Co-authored-by: thierry-martinez --- tests/test_noisy_density_matrix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_noisy_density_matrix.py b/tests/test_noisy_density_matrix.py index aacb51ea9..be4671319 100644 --- a/tests/test_noisy_density_matrix.py +++ b/tests/test_noisy_density_matrix.py @@ -71,7 +71,7 @@ def test_noisy_measure_confuse_hadamard(self, fx_rng: Generator) -> None: assert np.allclose(res.rho, np.array([[0.0, 0.0], [0.0, 1.0]])) @pytest.mark.parametrize("outcome", [0, 1]) - def test_noisy_measure_confuse_hadamard_arbitrary(self, fx_rng: Generator, outcome: int) -> None: + def test_noisy_measure_confuse_hadamard_arbitrary(self, fx_rng: Generator, outcome: Outcome) -> None: # arbitrary probability with fixed branch hadamardpattern = hpat() measure_error_pr = fx_rng.random() From ecde9b2c82e36a927bd3d7dc2e7cde3ab1a534c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiberiu=20Sab=C4=83u?= <96194994+tibisabau@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:22:07 +0100 Subject: [PATCH 4/9] Update tests/test_noisy_density_matrix.py Co-authored-by: thierry-martinez --- tests/test_noisy_density_matrix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_noisy_density_matrix.py b/tests/test_noisy_density_matrix.py index be4671319..3ce9cc627 100644 --- a/tests/test_noisy_density_matrix.py +++ b/tests/test_noisy_density_matrix.py @@ -332,7 +332,7 @@ def test_noisy_x_rz(self, fx_rng: Generator, outcome_z: int, outcome_x: int) -> results = {} cmd_count = 0 for cmd in rzpattern: - if cmd.kind.name == "M": + if cmd.kind == CommandKind.M: if cmd_count == 0: results[cmd.node] = outcome_z elif cmd_count == 1: From 70ab21e31d3ad26ba23a1bb2926daf7ed6dc1a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiberiu=20Sab=C4=83u?= <96194994+tibisabau@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:22:24 +0100 Subject: [PATCH 5/9] Update tests/test_noisy_density_matrix.py Co-authored-by: thierry-martinez --- tests/test_noisy_density_matrix.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/test_noisy_density_matrix.py b/tests/test_noisy_density_matrix.py index 3ce9cc627..9bdd2d438 100644 --- a/tests/test_noisy_density_matrix.py +++ b/tests/test_noisy_density_matrix.py @@ -329,15 +329,8 @@ def test_noisy_x_rz(self, fx_rng: Generator, outcome_z: int, outcome_x: int) -> print(f"x_error_pr = {x_error_pr}, outcome_z = {outcome_z}, outcome_x = {outcome_x}") # M(0) determines Z, M(1) determines X - results = {} - cmd_count = 0 - for cmd in rzpattern: - if cmd.kind == CommandKind.M: - if cmd_count == 0: - results[cmd.node] = outcome_z - elif cmd_count == 1: - results[cmd.node] = outcome_x - cmd_count += 1 + m_nodes = (cmd.node for cmd in rzpattern if cmd.kind == CommandKind.M) + results = {next(m_nodes): outcome_z, next(m_nodes): outcome_x} res = rzpattern.simulate_pattern( backend="densitymatrix", From aa0e14f63d0353dda7b688f9cc2804768bc88243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiberiu=20Sab=C4=83u?= Date: Fri, 6 Feb 2026 15:35:38 +0100 Subject: [PATCH 6/9] Fix ruff and mypy --- tests/test_noisy_density_matrix.py | 54 ++++++++++++++++-------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/tests/test_noisy_density_matrix.py b/tests/test_noisy_density_matrix.py index 9bdd2d438..13f056af1 100644 --- a/tests/test_noisy_density_matrix.py +++ b/tests/test_noisy_density_matrix.py @@ -1,12 +1,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import numpy as np import numpy.typing as npt import pytest from graphix.branch_selector import ConstBranchSelector, FixedBranchSelector +from graphix.command import CommandKind, M from graphix.fundamentals import angle_to_rad from graphix.noise_models import DepolarisingNoiseModel from graphix.noise_models.noise_model import NoiselessNoiseModel @@ -18,6 +19,7 @@ from numpy.random import Generator from graphix.fundamentals import Angle + from graphix.measurements import Outcome from graphix.pattern import Pattern @@ -110,7 +112,7 @@ def test_noisy_measure_channel_hadamard(self, fx_rng: Generator) -> None: # test Pauli X error @pytest.mark.parametrize("outcome", [0, 1]) - def test_noisy_x_hadamard(self, fx_rng: Generator, outcome: int) -> None: + def test_noisy_x_hadamard(self, fx_rng: Generator, outcome: Outcome) -> None: hadamardpattern = hpat() # x error only x_error_pr = fx_rng.random() @@ -321,16 +323,16 @@ def test_noisy_measure_channel_rz(self, fx_rng: Generator) -> None: @pytest.mark.parametrize("z_outcome", [0, 1]) @pytest.mark.parametrize("x_outcome", [0, 1]) - def test_noisy_x_rz(self, fx_rng: Generator, outcome_z: int, outcome_x: int) -> None: + def test_noisy_x_rz(self, fx_rng: Generator, z_outcome: Outcome, x_outcome: Outcome) -> None: alpha = fx_rng.random() rzpattern = rzpat(alpha) # x error only x_error_pr = fx_rng.random() - print(f"x_error_pr = {x_error_pr}, outcome_z = {outcome_z}, outcome_x = {outcome_x}") + print(f"x_error_pr = {x_error_pr}, outcome_z = {z_outcome}, outcome_x = {x_outcome}") # M(0) determines Z, M(1) determines X m_nodes = (cmd.node for cmd in rzpattern if cmd.kind == CommandKind.M) - results = {next(m_nodes): outcome_z, next(m_nodes): outcome_x} + results: dict[int, Outcome] = {next(m_nodes): z_outcome, next(m_nodes): x_outcome} res = rzpattern.simulate_pattern( backend="densitymatrix", @@ -353,8 +355,8 @@ def test_noisy_x_rz(self, fx_rng: Generator, outcome_z: int, outcome_x: int) -> ), ) - @pytest.mark.parametrize("outcome_z,outcome_x", [(0, 0), (0, 1), (1, 0), (1, 1)]) - def test_noisy_z_rz(self, fx_rng: Generator, outcome_z: int, outcome_x: int) -> None: + @pytest.mark.parametrize(("outcome_z", "outcome_x"), [(0, 0), (0, 1), (1, 0), (1, 1)]) + def test_noisy_z_rz(self, fx_rng: Generator, outcome_z: Outcome, outcome_x: Outcome) -> None: alpha = fx_rng.random() rzpattern = rzpat(alpha) # z error only @@ -362,14 +364,14 @@ def test_noisy_z_rz(self, fx_rng: Generator, outcome_z: int, outcome_x: int) -> print(f"z_error_pr = {z_error_pr}, outcome_z = {outcome_z}, outcome_x = {outcome_x}") # M(0) determines Z, M(1) determines X - results = {} + results: dict[int, Outcome] = {} cmd_count = 0 for cmd in rzpattern: if cmd.kind.name == "M": if cmd_count == 0: - results[cmd.node] = outcome_z + results[cast("M", cmd).node] = outcome_z elif cmd_count == 1: - results[cmd.node] = outcome_x + results[cast("M", cmd).node] = outcome_x cmd_count += 1 res = rzpattern.simulate_pattern( @@ -393,8 +395,8 @@ def test_noisy_z_rz(self, fx_rng: Generator, outcome_z: int, outcome_x: int) -> ), ) - @pytest.mark.parametrize("z_outcome,x_outcome", [(0, 0), (0, 1), (1, 0), (1, 1)]) - def test_noisy_xz_rz(self, fx_rng: Generator, z_outcome: int, x_outcome: int) -> None: + @pytest.mark.parametrize(("z_outcome", "x_outcome"), [(0, 0), (0, 1), (1, 0), (1, 1)]) + def test_noisy_xz_rz(self, fx_rng: Generator, z_outcome: Outcome, x_outcome: Outcome) -> None: alpha = fx_rng.random() rzpattern = rzpat(alpha) # x and z errors @@ -405,14 +407,14 @@ def test_noisy_xz_rz(self, fx_rng: Generator, z_outcome: int, x_outcome: int) -> print(f"z_outcome = {z_outcome}, x_outcome = {x_outcome}") # M(0) determines Z correction, M(1) determines X correction - results = {} + results: dict[int, Outcome] = {} cmd_count = 0 for cmd in rzpattern: if cmd.kind.name == "M": if cmd_count == 0: - results[cmd.node] = z_outcome + results[cast("M", cmd).node] = z_outcome elif cmd_count == 1: - results[cmd.node] = x_outcome + results[cast("M", cmd).node] = x_outcome cmd_count += 1 res = rzpattern.simulate_pattern( @@ -437,20 +439,20 @@ def test_noisy_xz_rz(self, fx_rng: Generator, z_outcome: int, x_outcome: int) -> ) # test measurement confuse outcome - @pytest.mark.parametrize("z_outcome,x_outcome", [(0, 0), (0, 1), (1, 0), (1, 1)]) - def test_noisy_measure_confuse_rz(self, fx_rng: Generator, z_outcome: int, x_outcome: int) -> None: + @pytest.mark.parametrize(("z_outcome", "x_outcome"), [(0, 0), (0, 1), (1, 0), (1, 1)]) + def test_noisy_measure_confuse_rz(self, fx_rng: Generator, z_outcome: Outcome, x_outcome: Outcome) -> None: alpha = fx_rng.random() rzpattern = rzpat(alpha) # M(0) determines Z, M(1) determines X - results = {} + results: dict[int, Outcome] = {} cmd_count = 0 for cmd in rzpattern: if cmd.kind.name == "M": if cmd_count == 0: - results[cmd.node] = z_outcome + results[cast("M", cmd).node] = z_outcome elif cmd_count == 1: - results[cmd.node] = x_outcome + results[cast("M", cmd).node] = x_outcome cmd_count += 1 # Test with probability 1 to flip both outcomes @@ -466,20 +468,22 @@ def test_noisy_measure_confuse_rz(self, fx_rng: Generator, z_outcome: int, x_out # All outcomes lead to same result: both corrections applied due to flipping assert np.allclose(res.rho, Ops.Z @ Ops.X @ exact @ Ops.X @ Ops.Z) - @pytest.mark.parametrize("z_outcome,x_outcome", [(0, 0), (0, 1), (1, 0), (1, 1)]) - def test_noisy_measure_confuse_rz_arbitrary(self, fx_rng: Generator, z_outcome: int, x_outcome: int) -> None: + @pytest.mark.parametrize(("z_outcome", "x_outcome"), [(0, 0), (0, 1), (1, 0), (1, 1)]) + def test_noisy_measure_confuse_rz_arbitrary( + self, fx_rng: Generator, z_outcome: Outcome, x_outcome: Outcome + ) -> None: alpha = fx_rng.random() rzpattern = rzpat(alpha) # M(0) determines Z, M(1) determines X - results = {} + results: dict[int, Outcome] = {} cmd_count = 0 for cmd in rzpattern: if cmd.kind.name == "M": if cmd_count == 0: - results[cmd.node] = z_outcome + results[cast("M", cmd).node] = z_outcome elif cmd_count == 1: - results[cmd.node] = x_outcome + results[cast("M", cmd).node] = x_outcome cmd_count += 1 # Test with arbitrary probability From a36cea4e50f15713389c4e5205516c374dce7380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiberiu=20Sab=C4=83u?= Date: Fri, 6 Feb 2026 17:24:23 +0100 Subject: [PATCH 7/9] fix: Remove loops and cast --- tests/test_noisy_density_matrix.py | 56 ++++++++---------------------- 1 file changed, 14 insertions(+), 42 deletions(-) diff --git a/tests/test_noisy_density_matrix.py b/tests/test_noisy_density_matrix.py index 13f056af1..e4d867b07 100644 --- a/tests/test_noisy_density_matrix.py +++ b/tests/test_noisy_density_matrix.py @@ -1,13 +1,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING import numpy as np import numpy.typing as npt import pytest from graphix.branch_selector import ConstBranchSelector, FixedBranchSelector -from graphix.command import CommandKind, M +from graphix.command import CommandKind from graphix.fundamentals import angle_to_rad from graphix.noise_models import DepolarisingNoiseModel from graphix.noise_models.noise_model import NoiselessNoiseModel @@ -355,7 +355,8 @@ def test_noisy_x_rz(self, fx_rng: Generator, z_outcome: Outcome, x_outcome: Outc ), ) - @pytest.mark.parametrize(("outcome_z", "outcome_x"), [(0, 0), (0, 1), (1, 0), (1, 1)]) + @pytest.mark.parametrize("outcome_z", [0, 1]) + @pytest.mark.parametrize("outcome_x", [0, 1]) def test_noisy_z_rz(self, fx_rng: Generator, outcome_z: Outcome, outcome_x: Outcome) -> None: alpha = fx_rng.random() rzpattern = rzpat(alpha) @@ -364,15 +365,7 @@ def test_noisy_z_rz(self, fx_rng: Generator, outcome_z: Outcome, outcome_x: Outc print(f"z_error_pr = {z_error_pr}, outcome_z = {outcome_z}, outcome_x = {outcome_x}") # M(0) determines Z, M(1) determines X - results: dict[int, Outcome] = {} - cmd_count = 0 - for cmd in rzpattern: - if cmd.kind.name == "M": - if cmd_count == 0: - results[cast("M", cmd).node] = outcome_z - elif cmd_count == 1: - results[cast("M", cmd).node] = outcome_x - cmd_count += 1 + results: dict[int, Outcome] = {0: outcome_z, 1: outcome_x} res = rzpattern.simulate_pattern( backend="densitymatrix", @@ -395,7 +388,8 @@ def test_noisy_z_rz(self, fx_rng: Generator, outcome_z: Outcome, outcome_x: Outc ), ) - @pytest.mark.parametrize(("z_outcome", "x_outcome"), [(0, 0), (0, 1), (1, 0), (1, 1)]) + @pytest.mark.parametrize("z_outcome", [0, 1]) + @pytest.mark.parametrize("x_outcome", [0, 1]) def test_noisy_xz_rz(self, fx_rng: Generator, z_outcome: Outcome, x_outcome: Outcome) -> None: alpha = fx_rng.random() rzpattern = rzpat(alpha) @@ -407,15 +401,7 @@ def test_noisy_xz_rz(self, fx_rng: Generator, z_outcome: Outcome, x_outcome: Out print(f"z_outcome = {z_outcome}, x_outcome = {x_outcome}") # M(0) determines Z correction, M(1) determines X correction - results: dict[int, Outcome] = {} - cmd_count = 0 - for cmd in rzpattern: - if cmd.kind.name == "M": - if cmd_count == 0: - results[cast("M", cmd).node] = z_outcome - elif cmd_count == 1: - results[cast("M", cmd).node] = x_outcome - cmd_count += 1 + results: dict[int, Outcome] = {0: z_outcome, 1: x_outcome} res = rzpattern.simulate_pattern( backend="densitymatrix", @@ -439,21 +425,14 @@ def test_noisy_xz_rz(self, fx_rng: Generator, z_outcome: Outcome, x_outcome: Out ) # test measurement confuse outcome - @pytest.mark.parametrize(("z_outcome", "x_outcome"), [(0, 0), (0, 1), (1, 0), (1, 1)]) + @pytest.mark.parametrize("z_outcome", [0, 1]) + @pytest.mark.parametrize("x_outcome", [0, 1]) def test_noisy_measure_confuse_rz(self, fx_rng: Generator, z_outcome: Outcome, x_outcome: Outcome) -> None: alpha = fx_rng.random() rzpattern = rzpat(alpha) # M(0) determines Z, M(1) determines X - results: dict[int, Outcome] = {} - cmd_count = 0 - for cmd in rzpattern: - if cmd.kind.name == "M": - if cmd_count == 0: - results[cast("M", cmd).node] = z_outcome - elif cmd_count == 1: - results[cast("M", cmd).node] = x_outcome - cmd_count += 1 + results: dict[int, Outcome] = {0: z_outcome, 1: x_outcome} # Test with probability 1 to flip both outcomes res = rzpattern.simulate_pattern( @@ -468,7 +447,8 @@ def test_noisy_measure_confuse_rz(self, fx_rng: Generator, z_outcome: Outcome, x # All outcomes lead to same result: both corrections applied due to flipping assert np.allclose(res.rho, Ops.Z @ Ops.X @ exact @ Ops.X @ Ops.Z) - @pytest.mark.parametrize(("z_outcome", "x_outcome"), [(0, 0), (0, 1), (1, 0), (1, 1)]) + @pytest.mark.parametrize("z_outcome", [0, 1]) + @pytest.mark.parametrize("x_outcome", [0, 1]) def test_noisy_measure_confuse_rz_arbitrary( self, fx_rng: Generator, z_outcome: Outcome, x_outcome: Outcome ) -> None: @@ -476,15 +456,7 @@ def test_noisy_measure_confuse_rz_arbitrary( rzpattern = rzpat(alpha) # M(0) determines Z, M(1) determines X - results: dict[int, Outcome] = {} - cmd_count = 0 - for cmd in rzpattern: - if cmd.kind.name == "M": - if cmd_count == 0: - results[cast("M", cmd).node] = z_outcome - elif cmd_count == 1: - results[cast("M", cmd).node] = x_outcome - cmd_count += 1 + results: dict[int, Outcome] = {0: z_outcome, 1: x_outcome} # Test with arbitrary probability measure_error_pr = fx_rng.random() From 737d36d40d1d5c3574595e76f942ebd5a92669a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiberiu=20Sab=C4=83u?= Date: Sat, 14 Feb 2026 23:58:22 +0100 Subject: [PATCH 8/9] Update test_noisy_density_matrix.py based on issue 428 fix --- tests/test_noisy_density_matrix.py | 120 ++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 38 deletions(-) diff --git a/tests/test_noisy_density_matrix.py b/tests/test_noisy_density_matrix.py index e4d867b07..dc207931b 100644 --- a/tests/test_noisy_density_matrix.py +++ b/tests/test_noisy_density_matrix.py @@ -123,12 +123,17 @@ def test_noisy_x_hadamard(self, fx_rng: Generator, outcome: Outcome) -> None: branch_selector=ConstBranchSelector(outcome), rng=fx_rng, ) - # Both outcomes lead to |0> after correction. ApplyNoise is unconditional, so noise is always applied. + # Pattern has X(1, {0}), so X error noise only applied when outcome=1 assert isinstance(res, DensityMatrix) - assert np.allclose( - res.rho, - np.array([[1 - 2 * x_error_pr / 3.0, 0.0], [0.0, 2 * x_error_pr / 3.0]]), - ) + if outcome == 0: + # No X correction → no X error noise + assert np.allclose(res.rho, np.array([[1.0, 0.0], [0.0, 0.0]])) + else: + # X correction applied → X error noise applied + assert np.allclose( + res.rho, + np.array([[1 - 2 * x_error_pr / 3.0, 0.0], [0.0, 2 * x_error_pr / 3.0]]), + ) # test entanglement error def test_noisy_entanglement_hadamard(self, fx_rng: Generator) -> None: @@ -341,19 +346,24 @@ def test_noisy_x_rz(self, fx_rng: Generator, z_outcome: Outcome, x_outcome: Outc rng=fx_rng, ) - # All outcomes lead to the same state after corrections. ApplyNoise is unconditional, so noise is always applied. + # Pattern has X(2, {1}), so X error noise only applied when x_outcome=1 assert isinstance(res, DensityMatrix) rad = angle_to_rad(alpha) - assert np.allclose( - res.rho, - 0.5 - * np.array( - [ - [1.0, np.exp(-1j * rad) * (3 - 4 * x_error_pr) / 3], - [np.exp(1j * rad) * (3 - 4 * x_error_pr) / 3, 1.0], - ], - ), - ) + if x_outcome == 0: + # No X correction → no X error noise + assert np.allclose(res.rho, rz_exact_res(alpha)) + else: + # X correction applied → X error noise applied + assert np.allclose( + res.rho, + 0.5 + * np.array( + [ + [1.0, np.exp(-1j * rad) * (3 - 4 * x_error_pr) / 3], + [np.exp(1j * rad) * (3 - 4 * x_error_pr) / 3, 1.0], + ], + ), + ) @pytest.mark.parametrize("outcome_z", [0, 1]) @pytest.mark.parametrize("outcome_x", [0, 1]) @@ -374,19 +384,24 @@ def test_noisy_z_rz(self, fx_rng: Generator, outcome_z: Outcome, outcome_x: Outc rng=fx_rng, ) - # All outcomes lead to the same state after corrections. ApplyNoise is unconditional, so noise is always applied. + # Pattern has Z(2, {0}), so Z error noise only applied when outcome_z=1 assert isinstance(res, DensityMatrix) rad = angle_to_rad(alpha) - assert np.allclose( - res.rho, - 0.5 - * np.array( - [ - [1.0, np.exp(-1j * rad) * (3 - 4 * z_error_pr) / 3], - [np.exp(1j * rad) * (3 - 4 * z_error_pr) / 3, 1.0], - ], - ), - ) + if outcome_z == 0: + # No Z correction → no Z error noise + assert np.allclose(res.rho, rz_exact_res(alpha)) + else: + # Z correction applied → Z error noise applied + assert np.allclose( + res.rho, + 0.5 + * np.array( + [ + [1.0, np.exp(-1j * rad) * (3 - 4 * z_error_pr) / 3], + [np.exp(1j * rad) * (3 - 4 * z_error_pr) / 3, 1.0], + ], + ), + ) @pytest.mark.parametrize("z_outcome", [0, 1]) @pytest.mark.parametrize("x_outcome", [0, 1]) @@ -410,19 +425,48 @@ def test_noisy_xz_rz(self, fx_rng: Generator, z_outcome: Outcome, x_outcome: Out rng=fx_rng, ) - # All outcomes lead to same state after corrections. Both X and Z noise applied unconditionally. + # Pattern has X(2, {1}) and Z(2, {0}), noise applied conditionally assert isinstance(res, DensityMatrix) rad = angle_to_rad(alpha) - assert np.allclose( - res.rho, - 0.5 - * np.array( - [ - [1.0, np.exp(-1j * rad) * (3 - 4 * x_error_pr) * (3 - 4 * z_error_pr) / 9], - [np.exp(1j * rad) * (3 - 4 * x_error_pr) * (3 - 4 * z_error_pr) / 9, 1.0], - ], - ), - ) + if z_outcome == 0 and x_outcome == 0: + # No corrections → no noise + assert np.allclose(res.rho, rz_exact_res(alpha)) + elif z_outcome == 0 and x_outcome == 1: + # Only X correction → only X noise + assert np.allclose( + res.rho, + 0.5 + * np.array( + [ + [1.0, np.exp(-1j * rad) * (3 - 4 * x_error_pr) / 3], + [np.exp(1j * rad) * (3 - 4 * x_error_pr) / 3, 1.0], + ], + ), + ) + elif z_outcome == 1 and x_outcome == 0: + # Only Z correction → only Z noise + assert np.allclose( + res.rho, + 0.5 + * np.array( + [ + [1.0, np.exp(-1j * rad) * (3 - 4 * z_error_pr) / 3], + [np.exp(1j * rad) * (3 - 4 * z_error_pr) / 3, 1.0], + ], + ), + ) + else: # z_outcome == 1 and x_outcome == 1 + # Both corrections → both noises + assert np.allclose( + res.rho, + 0.5 + * np.array( + [ + [1.0, np.exp(-1j * rad) * (3 - 4 * x_error_pr) * (3 - 4 * z_error_pr) / 9], + [np.exp(1j * rad) * (3 - 4 * x_error_pr) * (3 - 4 * z_error_pr) / 9, 1.0], + ], + ), + ) # test measurement confuse outcome @pytest.mark.parametrize("z_outcome", [0, 1]) From fff9592f82f720a815f671943cd424035f96153b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiberiu=20Sab=C4=83u?= Date: Thu, 19 Feb 2026 11:30:02 +0100 Subject: [PATCH 9/9] Fix mypy errors --- graphix/flow/_find_gpflow.py | 2 +- tests/test_density_matrix.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/graphix/flow/_find_gpflow.py b/graphix/flow/_find_gpflow.py index 220d2c723..60290e569 100644 --- a/graphix/flow/_find_gpflow.py +++ b/graphix/flow/_find_gpflow.py @@ -258,7 +258,7 @@ def to_correction_function(self) -> dict[int, frozenset[int]]: correction_function: dict[int, frozenset[int]] = {} for node in col_tags: i = col_tags.index(node) - correction_set = {row_tags[j] for j in np.flatnonzero(self.c_matrix[:, i])} + correction_set = {row_tags[int(j)] for j in np.flatnonzero(self.c_matrix[:, i])} correction_function[node] = frozenset(correction_set) return correction_function diff --git a/tests/test_density_matrix.py b/tests/test_density_matrix.py index 7d7492584..107ccb5b4 100644 --- a/tests/test_density_matrix.py +++ b/tests/test_density_matrix.py @@ -302,7 +302,7 @@ def test_cnot_success(self, fx_rng: Generator) -> None: edge = (0, 1) dm.cnot(edge) psi = psi.reshape((2, 2)) - psi = np.tensordot(CNOT_TENSOR, psi, ((2, 3), edge)) # type: ignore[assignment] + psi = np.tensordot(CNOT_TENSOR, psi, ((2, 3), edge)) psi = np.moveaxis(psi, (0, 1), edge) expected_matrix2 = np.outer(psi, psi.conj()) assert np.allclose(dm.rho, expected_matrix2) @@ -320,7 +320,7 @@ def test_cnot_success(self, fx_rng: Generator) -> None: edge = (u, v) dm.cnot(edge) psi = psi.reshape((2,) * n) - psi = np.tensordot(CNOT_TENSOR, psi, ((2, 3), edge)) # type: ignore[assignment] + psi = np.tensordot(CNOT_TENSOR, psi, ((2, 3), edge)) psi = np.moveaxis(psi, (0, 1), edge) expected_matrix3 = np.outer(psi, psi.conj()) assert np.allclose(dm.rho, expected_matrix3) @@ -354,7 +354,7 @@ def test_swap_success(self, fx_rng: Generator) -> None: dm.swap(edge) rho = dm.rho psi = psi.reshape((2, 2)) - psi = np.tensordot(SWAP_TENSOR, psi, ((2, 3), edge)) # type: ignore[assignment] + psi = np.tensordot(SWAP_TENSOR, psi, ((2, 3), edge)) psi = np.moveaxis(psi, (0, 1), edge) expected_matrix2 = np.outer(psi, psi.conj()) assert np.allclose(rho, expected_matrix2) @@ -392,7 +392,7 @@ def test_entangle_success(self, fx_rng: Generator) -> None: dm.entangle(edge) rho = dm.rho psi = psi.reshape((2, 2)) - psi = np.tensordot(CZ_TENSOR, psi, ((2, 3), edge)) # type: ignore[assignment] + psi = np.tensordot(CZ_TENSOR, psi, ((2, 3), edge)) psi = np.moveaxis(psi, (0, 1), edge) expected_matrix2 = np.outer(psi, psi.conj()) assert np.allclose(rho, expected_matrix2) @@ -443,7 +443,7 @@ def test_evolve_success(self, fx_rng: Generator) -> None: rho = dm.rho psi = psi.reshape((2,) * nqubits) - psi = np.tensordot(op.reshape((2,) * 2 * nqubits_op), psi, ((2, 3), edge)) # type: ignore[assignment] + psi = np.tensordot(op.reshape((2,) * 2 * nqubits_op), psi, ((2, 3), edge)) psi = np.moveaxis(psi, (0, 1), edge) expected_matrix = np.outer(psi, psi.conj()) assert np.allclose(rho, expected_matrix) @@ -468,7 +468,7 @@ def test_evolve_success(self, fx_rng: Generator) -> None: rho = dm.rho psi = psi.reshape((2,) * nqubits) - psi = np.tensordot(op.reshape((2,) * 2 * nqubits_op), psi, ((3, 4, 5), targets)) # type: ignore[assignment] + psi = np.tensordot(op.reshape((2,) * 2 * nqubits_op), psi, ((3, 4, 5), targets)) psi = np.moveaxis(psi, (0, 1, 2), targets) expected_matrix = np.outer(psi, psi.conj()) assert np.allclose(rho, expected_matrix)