Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d6d8f96
Make tests use the BranchSelector feature
tibisabau Feb 5, 2026
6d2b0f6
Prepare release v0.3.4 (#424)
thierry-martinez Feb 6, 2026
0115aed
Update tests/test_noisy_density_matrix.py
tibisabau Feb 6, 2026
647adac
Update tests/test_noisy_density_matrix.py
tibisabau Feb 6, 2026
44d1798
Update tests/test_noisy_density_matrix.py
tibisabau Feb 6, 2026
b95dfdc
Update tests/test_noisy_density_matrix.py
tibisabau Feb 6, 2026
f228370
Fix ruff and mypy
tibisabau Feb 6, 2026
e2f54e3
fix: Remove loops and cast
tibisabau Feb 6, 2026
35871a0
Bump ruff from 0.14.14 to 0.15.0 in the python-packages group (#427)
dependabot[bot] Feb 9, 2026
643feb8
Replace ValueError with PatternError (#426)
tibisabau Feb 9, 2026
f05e6cd
Force test reverse dependencies with the current graphix codebase (#431)
thierry-martinez Feb 12, 2026
be598e2
Fix #428 : fix conditionality of `ApplyNoise` commands (#429)
mgarnier59 Feb 12, 2026
2ca6181
Make tests use the BranchSelector feature
tibisabau Feb 5, 2026
8803c11
Update tests/test_noisy_density_matrix.py
tibisabau Feb 6, 2026
efc76d5
Update tests/test_noisy_density_matrix.py
tibisabau Feb 6, 2026
931d3ec
Update tests/test_noisy_density_matrix.py
tibisabau Feb 6, 2026
bfc66a0
Update tests/test_noisy_density_matrix.py
tibisabau Feb 6, 2026
ce5e9a2
Fix ruff and mypy
tibisabau Feb 6, 2026
4acdf57
fix: Remove loops and cast
tibisabau Feb 6, 2026
aece9e6
Merge branch '416-makes-backend-tests-deterministic' of https://githu…
tibisabau Feb 14, 2026
b8c62a7
Update test_noisy_density_matrix.py based on issue 428 fix
tibisabau Feb 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 33 additions & 12 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
## [Unreleased]

### Added

### Fixed

- #429
- Modify `graphix.noise_models.noise_model.ApplyNoise` to handle conditionality based on a `domain` attribute (like `command.X` and `command.Z`).
- Moved the conditional logic to `graphix.simulator` to remove code duplication in the backends.
- Solves [#428](https://github.com/TeamGraphix/graphix/issues/428).

### Changed

## [0.3.4] - 2026-02-05

### Added

Expand All @@ -20,7 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #385
- Introduced `graphix.flow.core.XZCorrections.check_well_formed` which verifies the correctness of an XZ-corrections instance and raises an exception if incorrect.
- Added XZ-correction exceptions to module `graphix.flow.core.exceptions`.

- #378:
- Introduced new method `graphix.flow.core.PauliFlow.check_well_formed`, `graphix.flow.core.GFlow.check_well_formed` and `graphix.flow.core.CausalFlow.check_well_formed` which verify the correctness of flow objects and raise exceptions when the flow is incorrect.
- Introduced new method `graphix.flow.core.PauliFlow.is_well_formed` which verify the correctness of flow objects and returns a boolean when the flow is incorrect.
Expand All @@ -37,13 +50,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- #402: Support for Python 3.14.

- #407: Introduced new method `graphix.optimization.StandardizedPattern.extract_xzcorrections` and its wrapper `graphix.pattern.Pattern.extract_xzcorrections` which extract an `XZCorrections` instance from a pattern.
- #253, #406: Added classes `BaseCommand` and `BaseInstruction`.

- #407: Introduced new method `graphix.optimization.StandardizedPattern.extract_xzcorrections` and its wrapper `graphix.pattern.Pattern.extract_xzcorrections` which extract an `XZCorrections` instance from a pattern.

- #412: Added pretty-print methods (`to_ascii`, `to_latex` and `to_unicode`) for `PauliFlow` and `XZCorrections` classes. Implemented their `__str__` method as a call to `self.to_ascii`.

### Fixed

- #392: `Pattern.remove_input_nodes` is required before the `Pattern.perform_pauli_measurements` method to ensure input nodes are removed and fixed in the |+> state.
-

- #363, #392: `Pattern.remove_input_nodes` is required before the `Pattern.perform_pauli_measurements` method to ensure input nodes are removed and fixed in the |+> state.

- #379: Removed unnecessary `meas_index` from API for rotation instructions `RZ`, `RY` and `RX`.

Expand All @@ -59,10 +76,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
is ensured with normalization passed `incorporate_pauli_results` and
`single_qubit_domains`.

- #409: Axis labels are shown when visualizing a pattern. Legend is placed outside the plot so that the graph remains visible.
- #231, #405: `IXYZ` is now defined as `Literal[I] | Axis`.

- #382, #409: Axis labels are shown when visualizing a pattern. Legend is placed outside the plot so that the graph remains visible.

- #407: Fixed an unreported bug in `OpenGraph.is_equal_structurally` which failed to compare open graphs differing on the output nodes only.

- #157, #417: `Pattern.minimize_space` uses `Pattern.extract_causal_flow()` and preserves runnability

### Changed

- #396: Removed generic `BackendState` from `graphix.sim` modules and methods in `graphix.pattern` and `graphix.simulator` modules.
Expand All @@ -82,7 +103,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Modified the constructor `XZCorrections.from_measured_nodes_mapping` so that it doesn't need to create an `nx.DiGraph` instance. This fixes an unreported bug in the method.
- Removed modules `graphix.gflow` and `graphix.find_pflow`.

- #414: Tests are now type-checked.
- #369, #414: `random_objects.py` and tests are now type-checked.

- #418: `Pattern.extract_measurement_commands` now returns a dictionary. Removed `Pattern.get_meas_plane` and `Pattern.get_angles`.

Expand All @@ -106,7 +127,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
a pattern.

- #358: Refactor of flow tools - Part I
- New module `graphix.flow.core` which introduces classes `PauliFlow`, `GFlow`, `CausalFlow` and `XZCorrections` allowing a finer analysis of MBQC flows. This module subsumes `graphix.generator` which has been removed and part of `graphix.gflow` which will be removed in the future.
- New module `graphix.flow.core` which introduces classes `PauliFlow`, `GFlow`, `CausalFlow` and `XZCorrections` allowing a finer analysis of MBQC flows. This module subsumes `graphix.generator` which has been removed and part of `graphix.gflow` which will be removed in the future.
- New module `graphix.flow._find_cflow` with the existing causal-flow finding algorithm.
- New module `graphix.flow._find_gpflow` with the existing g- and Pauli-flow finding algorithm introduced in #337.
- New abstract types `graphix.fundamentals.AbstractMeasurement` and `graphix.fundamentals.AbstractPlanarMeasurement` which serve as an umbrella of the existing types `graphix.measurements.Measurement`, `graphix.fundamentals.Plane` and `graphix.fundamentals.Axis`.
Expand Down Expand Up @@ -210,12 +231,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- #322: Added a new `optimization` module containing:

* a functional version of `standardize` that returns a standardized
- a functional version of `standardize` that returns a standardized
pattern as a new object;

* a function `incorporate_pauli_results` that returns an equivalent
- a function `incorporate_pauli_results` that returns an equivalent
pattern in which the `results` are incorporated into measurement
and correction domains.
and correction domains.
The resulting pattern is suitable for flow analysis. In
particular, if a pattern has a flow, it is preserved by
`perform_pauli_measurements` after applying `standardize` and
Expand Down Expand Up @@ -266,11 +287,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- #314, #322: The method `Pattern.standardize()` now places C commands
after X and Z commands, making the resulting patterns suitable for
flow analysis.
flow analysis.
The `flow_from_pattern` functions now fail if the input pattern is
not strictly standardized (as checked by
`Pattern.is_standard(strict=True)`, which requires C commands to be
last).
last).
Note: the method `perform_pauli_measurements` still places C
commands before X and Z commands.

Expand Down
4 changes: 2 additions & 2 deletions graphix/noise_models/depolarising.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,9 @@ def command(self, cmd: CommandOrNoise, rng: Generator | None = None) -> list[Com
if cmd.kind == CommandKind.M:
return [ApplyNoise(noise=DepolarisingNoise(self.measure_channel_prob), nodes=[cmd.node]), cmd]
if cmd.kind == CommandKind.X:
return [cmd, ApplyNoise(noise=DepolarisingNoise(self.x_error_prob), nodes=[cmd.node])]
return [cmd, ApplyNoise(noise=DepolarisingNoise(self.x_error_prob), nodes=[cmd.node], domain=cmd.domain)]
if cmd.kind == CommandKind.Z:
return [cmd, ApplyNoise(noise=DepolarisingNoise(self.z_error_prob), nodes=[cmd.node])]
return [cmd, ApplyNoise(noise=DepolarisingNoise(self.z_error_prob), nodes=[cmd.node], domain=cmd.domain)]
# Use of `==` here for mypy
if cmd.kind == CommandKind.C or cmd.kind == CommandKind.T or cmd.kind == CommandKind.ApplyNoise: # noqa: PLR1714
return [cmd]
Expand Down
19 changes: 18 additions & 1 deletion graphix/noise_models/noise_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,28 @@ def to_kraus_channel(self) -> KrausChannel:

@dataclass
class ApplyNoise(_KindChecker):
"""Apply noise command."""
"""Apply noise command.

Parameters
----------
noise : Noise
noise to be applied

nodes : list[Node]
list of node indices on which to apply noise

domain: set[Node] | None = None
Optional domain for conditional noise.
If ``None``, the noise is applied unconditionally.
Otherwise, the noise is applied if there is an odd number of nodes among ``domain`` that have been measured with outcome 1 (as for ``X`` and ``Z`` commands).
Note that the noise is never applied if ``domain`` is the empty set.

"""

kind: ClassVar[Literal[CommandKind.ApplyNoise]] = dataclasses.field(default=CommandKind.ApplyNoise, init=False)
noise: Noise
nodes: list[Node]
domain: set[Node] | None = None


CommandOrNoise = Command | ApplyNoise
Expand Down
26 changes: 15 additions & 11 deletions graphix/pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,22 +211,22 @@ def compose(
nodes_p2 = other.extract_nodes() | other.results.keys()

if not mapping.keys() <= nodes_p2:
raise ValueError("Keys of `mapping` must correspond to the nodes of `other`.")
raise PatternError("Keys of `mapping` must correspond to the nodes of `other`.")

# Cast to set for improved performance in membership test
mapping_values_set = set(mapping.values())
o1_set = set(self.__output_nodes)
i2_set = set(other.input_nodes)

if len(mapping) != len(mapping_values_set):
raise ValueError("Values of `mapping` contain duplicates.")
raise PatternError("Values of `mapping` contain duplicates.")

if mapping_values_set & nodes_p1 - o1_set:
raise ValueError("Values of `mapping` must not contain measured nodes of pattern `self`.")
raise PatternError("Values of `mapping` must not contain measured nodes of pattern `self`.")

for k, v in mapping.items():
if v in o1_set and k not in i2_set:
raise ValueError(
raise PatternError(
f"Mapping {k} -> {v} is not valid. {v} is an output of pattern `self` but {k} is not an input of pattern `other`."
)

Expand Down Expand Up @@ -506,7 +506,7 @@ def shift_signals(self, method: str = "direct") -> dict[int, set[int]]:
self._commute_with_following(target)
target += 1
return signal_dict
raise ValueError("Invalid method")
raise PatternError("Invalid method")

def shift_signals_direct(self) -> dict[int, set[int]]:
"""Perform signal shifting procedure."""
Expand Down Expand Up @@ -1153,7 +1153,7 @@ def extract_opengraph(self) -> OpenGraph[Measurement]:
for cmd in self.__seq:
if cmd.kind == CommandKind.N:
if cmd.state != BasicStates.PLUS:
raise ValueError(
raise PatternError(
f"Open graph extraction requires N commands to represent a |+⟩ state. Error found in {cmd}."
)
nodes.add(cmd.node)
Expand Down Expand Up @@ -1411,7 +1411,7 @@ def perform_pauli_measurements(self, ignore_pauli_with_deps: bool = False) -> No

"""
if self.input_nodes:
raise ValueError("Remove inputs with `self.remove_input_nodes()` before performing Pauli presimulation.")
raise PatternError("Remove inputs with `self.remove_input_nodes()` before performing Pauli presimulation.")
self.__dict__.update(measure_pauli(self, ignore_pauli_with_deps=ignore_pauli_with_deps).__dict__)

def draw_graph(
Expand Down Expand Up @@ -1596,6 +1596,10 @@ def check_measured(cmd: Command, node: int) -> None:
check_active(cmd, cmd.node)


class PatternError(Exception):
"""Exception subclass to handle pattern errors."""


class RunnabilityErrorReason(Enum):
"""Describe the reason for a pattern not being runnable."""

Expand All @@ -1616,7 +1620,7 @@ class RunnabilityErrorReason(Enum):


@dataclass
class RunnabilityError(Exception):
class RunnabilityError(PatternError):
"""Error raised by :method:`Pattern.check_runnability`."""

cmd: Command
Expand Down Expand Up @@ -1778,7 +1782,7 @@ def pauli_nodes(pattern: optimization.StandardizedPattern) -> tuple[list[tuple[c
else:
pauli_node.append((cmd, pm))
else:
raise ValueError("Unknown Pauli measurement basis")
raise PatternError("Unknown Pauli measurement basis")
else:
non_pauli_node.add(cmd.node)
return pauli_node, non_pauli_node
Expand All @@ -1788,12 +1792,12 @@ def assert_permutation(original: list[int], user: list[int]) -> None:
"""Check that the provided `user` node list is a permutation from `original`."""
node_set = set(user)
if node_set != set(original):
raise ValueError(f"{node_set} != {set(original)}")
raise PatternError(f"{node_set} != {set(original)}")
for node in user:
if node in node_set:
node_set.remove(node)
else:
raise ValueError(f"{node} appears twice")
raise PatternError(f"{node} appears twice")


@dataclass
Expand Down
35 changes: 18 additions & 17 deletions graphix/sim/base_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@

from graphix import command
from graphix.measurements import Measurement, Outcome
from graphix.noise_models.noise_model import Noise
from graphix.noise_models.noise_model import ApplyNoise, Noise
from graphix.parameter import ExpressionOrComplex, ExpressionOrFloat
from graphix.sim.data import Data
from graphix.simulator import MeasureMethod


Matrix: TypeAlias = npt.NDArray[np.object_ | np.complex128]
Expand Down Expand Up @@ -619,7 +618,7 @@ def add_nodes(self, nodes: Sequence[int], data: Data = BasicStates.PLUS) -> None
Previously existing nodes remain unchanged.
"""

def apply_noise(self, nodes: Sequence[int], noise: Noise) -> None: # noqa: ARG002,PLR6301
def apply_noise(self, cmd: ApplyNoise) -> None: # noqa: ARG002,PLR6301
"""Apply noise.

The default implementation of this method raises
Expand All @@ -628,6 +627,8 @@ def apply_noise(self, nodes: Sequence[int], noise: Noise) -> None: # noqa: ARG0
`DensityMatrixBackend`) override this method to implement
the effect of noise.

Note: the simulator is responsible for checking that the measurement outcomes match the domain condition before calling this method.

Parameters
----------
nodes : sequence of ints.
Expand All @@ -642,8 +643,11 @@ def apply_clifford(self, node: int, clifford: Clifford) -> None:
"""Apply single-qubit Clifford gate, specified by vop index specified in graphix.clifford.CLIFFORD."""

@abstractmethod
def correct_byproduct(self, cmd: command.X | command.Z, measure_method: MeasureMethod) -> None:
"""Byproduct correction correct for the X or Z byproduct operators, by applying the X or Z gate."""
def correct_byproduct(self, cmd: command.X | command.Z) -> None:
"""Byproduct correction correct for the X or Z byproduct operators, by applying the X or Z gate.

Note: the simulator is responsible for checking that the measurement outcomes match the domain condition before calling this method.
"""

@abstractmethod
def entangle_nodes(self, edge: tuple[int, int]) -> None:
Expand Down Expand Up @@ -782,25 +786,22 @@ def f_expectation0() -> float:
return outcome

@override
def correct_byproduct(self, cmd: command.X | command.Z, measure_method: MeasureMethod) -> None:
def correct_byproduct(self, cmd: command.X | command.Z) -> None:
"""Byproduct correction correct for the X or Z byproduct operators, by applying the X or Z gate."""
if np.mod(sum(measure_method.measurement_outcome(j) for j in cmd.domain), 2) == 1:
op = Ops.X if cmd.kind == CommandKind.X else Ops.Z
self.apply_single(node=cmd.node, op=op)
op = Ops.X if cmd.kind == CommandKind.X else Ops.Z
self.apply_single(node=cmd.node, op=op)

@override
def apply_noise(self, nodes: Sequence[int], noise: Noise) -> None:
"""Apply noise.
def apply_noise(self, cmd: ApplyNoise) -> None:
"""Apply noise for the command `:class: graphix.noise_model.ApplyNoise`.

Parameters
----------
nodes : sequence of ints.
Target qubits
noise : Noise
Noise to apply
cmd : ApplyNoise
command ApplyNoise
"""
indices = [self.node_index.index(i) for i in nodes]
self.state.apply_noise(indices, noise)
indices = [self.node_index.index(i) for i in cmd.nodes]
self.state.apply_noise(indices, cmd.noise)

def apply_single(self, node: int, op: Matrix) -> None:
"""Apply a single gate to the state."""
Expand Down
18 changes: 3 additions & 15 deletions graphix/sim/tensornet.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
from graphix.clifford import Clifford
from graphix.measurements import Measurement, Outcome
from graphix.sim import Data
from graphix.simulator import MeasureMethod

PrepareState: TypeAlias = str | npt.NDArray[np.complex128]

Expand Down Expand Up @@ -768,20 +767,9 @@ def measure(self, node: int, measurement: Measurement, rng: Generator | None = N
return result

@override
def correct_byproduct(self, cmd: command.X | command.Z, measure_method: MeasureMethod) -> None:
"""Perform byproduct correction.

Parameters
----------
cmd : list
Byproduct command
i.e. ['X' or 'Z', node, signal_domain]
measure_method : MeasureMethod
The measure method to use
"""
if sum(measure_method.measurement_outcome(j) for j in cmd.domain) % 2 == 1:
op = Ops.X if isinstance(cmd, command.X) else Ops.Z
self.state.evolve_single(cmd.node, op, str(cmd.kind))
def correct_byproduct(self, cmd: command.X | command.Z) -> None:
op = Ops.X if isinstance(cmd, command.X) else Ops.Z
self.state.evolve_single(cmd.node, op, str(cmd.kind))

@override
def apply_clifford(self, node: int, clifford: Clifford) -> None:
Expand Down
Loading