From 73d6c3f309b8c658d6990572159c59d6aa9893f9 Mon Sep 17 00:00:00 2001 From: matulni Date: Mon, 9 Mar 2026 15:06:23 +0100 Subject: [PATCH 1/7] Fix464 --- graphix/flow/_find_gpflow.py | 3 ++- tests/test_opengraph.py | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/graphix/flow/_find_gpflow.py b/graphix/flow/_find_gpflow.py index faaecdc5..2c3c71dd 100644 --- a/graphix/flow/_find_gpflow.py +++ b/graphix/flow/_find_gpflow.py @@ -504,7 +504,8 @@ def _compute_correction_matrix_general_case( return None # The flow-demand matrix is not invertible, therefore there's no flow. # Steps 5, 6 and 7 - ker_flow_demand_matrix = flow_demand_matrix.null_space().transpose() # F matrix. + ker_flow_demand_matrix = np.ascontiguousarray(flow_demand_matrix.null_space().transpose()) # F matrix. + # `np.ascontiguousarray` guarantees that `ker_flow_demand_matrix` is C_CONTIGUOUS. This is required to fit the signature of jitted functions in `graphix._linalg.py` c_prime_matrix = np.concatenate((correction_matrix_0, ker_flow_demand_matrix), axis=1).view(MatGF2) row_idxs = np.flatnonzero(order_demand_matrix.any(axis=1)) # Row indices of the non-zero rows. diff --git a/tests/test_opengraph.py b/tests/test_opengraph.py index 0cf48c1a..4f6347de 100644 --- a/tests/test_opengraph.py +++ b/tests/test_opengraph.py @@ -557,6 +557,33 @@ def _og_19() -> OpenGraphFlowTestCase: return OpenGraphFlowTestCase(og, has_cflow=False, has_gflow=False, has_pflow=True) +@register_open_graph_flow_test_case +def _og_20() -> OpenGraphFlowTestCase: + r"""Generate open graph. + + Structure: + + 0 + [(1)]-[(2)] + + Notes + ----- + This opengraph triggered issue #464. + https://github.com/TeamGraphix/graphix/issues/464 + """ + graph: nx.Graph[int] = nx.Graph([(1, 2)]) + graph.add_node(0) + og = OpenGraph( + graph=graph, + input_nodes=[], + output_nodes=[1, 2], + measurements={ + 0: Measurement.YZ(angle=0), + }, + ) + return OpenGraphFlowTestCase(og, has_cflow=False, has_gflow=True, has_pflow=True) + + class OpenGraphComposeTestCase(NamedTuple): og1: OpenGraph[AbstractMeasurement] og2: OpenGraph[AbstractMeasurement] From 5cf616c3b45b937ed637e06218d1b6f8b9f264a5 Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 10 Mar 2026 13:35:34 +0100 Subject: [PATCH 2/7] Ensure that all variables passed to jitted functions in linalg are c_contiguous --- graphix/_linalg.py | 24 +++++++++++++++++------- graphix/flow/_find_gpflow.py | 3 +-- tests/test_linalg.py | 31 +++++++++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/graphix/_linalg.py b/graphix/_linalg.py index 3667b597..75f66955 100644 --- a/graphix/_linalg.py +++ b/graphix/_linalg.py @@ -31,7 +31,9 @@ def __new__(cls, data: npt.ArrayLike, copy: bool = True) -> Self: MatGF2 """ arr = np.array(data, dtype=np.uint8, copy=copy) - return super().__new__(cls, shape=arr.shape, dtype=arr.dtype, buffer=arr) + return super().__new__( + cls, shape=arr.shape, dtype=arr.dtype, buffer=arr, strides=arr.strides + ) # `strides=arr.strides` allows to preserve the memory layout of the original data. def mat_mul(self, other: MatGF2 | npt.NDArray[np.uint8]) -> MatGF2: r"""Multiply two matrices. @@ -60,7 +62,7 @@ def mat_mul(self, other: MatGF2 | npt.NDArray[np.uint8]) -> MatGF2: f"Dimension mismatch. Attempted to multiply `self` with shape {self.shape} and `other` with shape {other.shape}" ) - return MatGF2(_mat_mul_jit(self, other), copy=False) + return MatGF2(_mat_mul_jit(np.ascontiguousarray(self), np.ascontiguousarray(other)), copy=False) def compute_rank(self) -> np.intp: """Get the rank of the matrix. @@ -149,7 +151,7 @@ def gauss_elimination(self, ncols: int | None = None, copy: bool = True) -> MatG ncols_value = self.shape[1] if ncols is None else ncols mat_ref = MatGF2(self) if copy else self - return MatGF2(_elimination_jit(mat_ref, ncols=ncols_value, full_reduce=False), copy=False) + return MatGF2(_elimination_jit(np.ascontiguousarray(mat_ref), ncols=ncols_value, full_reduce=False), copy=False) def row_reduction(self, ncols: int | None = None, copy: bool = True) -> MatGF2: """Return row-reduced echelon form (RREF) by performing Gaussian elimination. @@ -170,7 +172,7 @@ def row_reduction(self, ncols: int | None = None, copy: bool = True) -> MatGF2: ncols_value = self.shape[1] if ncols is None else ncols mat_ref = self.copy() if copy else self - return MatGF2(_elimination_jit(mat_ref, ncols=ncols_value, full_reduce=True), copy=False) + return MatGF2(_elimination_jit(np.ascontiguousarray(mat_ref), ncols=ncols_value, full_reduce=True), copy=False) def solve_f2_linear_system(mat: MatGF2, b: MatGF2) -> MatGF2: @@ -192,14 +194,17 @@ def solve_f2_linear_system(mat: MatGF2, b: MatGF2) -> MatGF2: ----- This function is not integrated in `:class: graphix.linalg.MatGF2` because it does not perform any checks on the form of `mat` to ensure that it is in REF or that the system is solvable. """ - return MatGF2(_solve_f2_linear_system_jit(mat, b), copy=False) + return MatGF2(_solve_f2_linear_system_jit(np.ascontiguousarray(mat), np.ascontiguousarray(b)), copy=False) @nb.njit("uint8[::1](uint8[:,::1], uint8[::1])") def _solve_f2_linear_system_jit( mat_data: npt.NDArray[np.uint8], b_data: npt.NDArray[np.uint8] ) -> npt.NDArray[np.uint8]: - """See docstring of `:func:solve_f2_linear_system` for details.""" + """See docstring of `:func:solve_f2_linear_system` for details. + + The signature of the numba decorator requites the input arrays to be C_CONTIGUOUS. + """ m, n = mat_data.shape x = np.zeros(n, dtype=np.uint8) @@ -238,6 +243,8 @@ def _solve_f2_linear_system_jit( def _elimination_jit(mat_data: npt.NDArray[np.uint8], ncols: int, full_reduce: bool) -> npt.NDArray[np.uint8]: r"""Return row echelon form (REF) or row-reduced echelon form (RREF) by performing Gaussian elimination. + The signature of the numba decorator requites the input arrays to be C_CONTIGUOUS. + Parameters ---------- mat_data : npt.NDArray[np.uint8] @@ -302,7 +309,10 @@ def _elimination_jit(mat_data: npt.NDArray[np.uint8], ncols: int, full_reduce: b @nb.njit("uint8[:,::1](uint8[:,::1], uint8[:,::1])", parallel=True) def _mat_mul_jit(m1: npt.NDArray[np.uint8], m2: npt.NDArray[np.uint8]) -> npt.NDArray[np.uint8]: - """See docstring of `:func:MatGF2.__matmul__` for details.""" + """See docstring of `:func:MatGF2.__matmul__` for details. + + The signature of the numba decorator requites the input arrays to be C_CONTIGUOUS. + """ m, l = m1.shape _, n = m2.shape diff --git a/graphix/flow/_find_gpflow.py b/graphix/flow/_find_gpflow.py index c32bb9d0..c32c6a4a 100644 --- a/graphix/flow/_find_gpflow.py +++ b/graphix/flow/_find_gpflow.py @@ -505,8 +505,7 @@ def _compute_correction_matrix_general_case( return None # The flow-demand matrix is not invertible, therefore there's no flow. # Steps 5, 6 and 7 - ker_flow_demand_matrix = np.ascontiguousarray(flow_demand_matrix.null_space().transpose()) # F matrix. - # `np.ascontiguousarray` guarantees that `ker_flow_demand_matrix` is C_CONTIGUOUS. This is required to fit the signature of jitted functions in `graphix._linalg.py` + ker_flow_demand_matrix = flow_demand_matrix.null_space().transpose() # F matrix. c_prime_matrix = np.concatenate((correction_matrix_0, ker_flow_demand_matrix), axis=1).view(MatGF2) row_idxs = np.flatnonzero(order_demand_matrix.any(axis=1)) # Row indices of the non-zero rows. diff --git a/tests/test_linalg.py b/tests/test_linalg.py index 2ae07363..92158f39 100644 --- a/tests/test_linalg.py +++ b/tests/test_linalg.py @@ -8,6 +8,8 @@ from graphix._linalg import MatGF2, solve_f2_linear_system if TYPE_CHECKING: + from typing import Literal + from numpy.random import Generator from pytest_benchmark import BenchmarkFixture @@ -82,6 +84,13 @@ def prepare_test_matrix() -> list[LinalgTestCase]: 0, False, ), + # same as before but F-contiguous matrix + LinalgTestCase( + MatGF2(np.array([[1, 0], [0, 1], [1, 0]], dtype=np.uint8, order="F")), + 2, + 0, + False, + ), ] @@ -110,6 +119,11 @@ def prepare_test_f2_linear_system() -> list[LSF2TestCase]: mat=MatGF2([[1, 0, 1], [0, 1, 0], [0, 0, 1]]), b=MatGF2([1, 1, 1]), ), + # Same as previous one but F-contiguous + LSF2TestCase( + mat=MatGF2(np.array([[1, 0, 1], [0, 1, 0], [0, 0, 1]], dtype=np.uint8, order="F")), + b=MatGF2([1, 1, 1]), + ), ) ) @@ -213,8 +227,21 @@ def test_solve_f2_linear_system(self, benchmark: BenchmarkFixture, test_case: LS def test_row_reduction(self, fx_rng: Generator) -> None: sizes = [(10, 10), (3, 7), (6, 2)] ncols = [4, 5, 2] + orders: list[Literal["K", "C", "F"]] = ["K", "C", "F"] - for size, ncol in zip(sizes, ncols, strict=True): - mat = MatGF2(fx_rng.integers(size=size, low=0, high=2, dtype=np.uint8)) + for size, ncol, order in zip(sizes, ncols, orders, strict=True): + mat = MatGF2(np.asarray(fx_rng.integers(size=size, low=0, high=2, dtype=np.uint8), order=order)) mat_red = mat.row_reduction(ncols=ncol, copy=True) verify_elimination(mat, mat_red, ncol, full_reduce=True) + + def test_initialization(self) -> None: + mat_c = MatGF2(np.array([[1, 0], [0, 1], [1, 0]], dtype=np.uint8)) + mat_f = MatGF2(np.asfortranarray(np.array([[1, 0], [0, 1], [1, 0]], dtype=np.uint8))) + + assert mat_c.flags.c_contiguous + assert not mat_c.flags.f_contiguous + + assert not mat_f.flags.c_contiguous + assert mat_f.flags.f_contiguous + + assert np.all(mat_c == mat_f) From 4c15545393693015203e6989a86bd361cd93ca1c Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 10 Mar 2026 13:41:06 +0100 Subject: [PATCH 3/7] Generalize casting to all linalg routines and add tests --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf672471..69ef5660 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #455: Causal-flow finding algorithm (`graphix.flow._find_cflow.py`) does not raise `RecursionError` now. +- #465: Fix #464. Functions in `graphix._linalg.py` convert arrays to c-contiguous form before passing them to numba-jitted functions. + ### Changed - #181, #423: Structural separation of Pauli measurements From 1e13d4e33dab1c5e1488949d86ee1913b2e75805 Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 10 Mar 2026 13:46:43 +0100 Subject: [PATCH 4/7] Fix typo in docstring --- graphix/_linalg.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphix/_linalg.py b/graphix/_linalg.py index 75f66955..09456069 100644 --- a/graphix/_linalg.py +++ b/graphix/_linalg.py @@ -203,7 +203,7 @@ def _solve_f2_linear_system_jit( ) -> npt.NDArray[np.uint8]: """See docstring of `:func:solve_f2_linear_system` for details. - The signature of the numba decorator requites the input arrays to be C_CONTIGUOUS. + The signature of the numba decorator requires the input arrays to be C_CONTIGUOUS. """ m, n = mat_data.shape x = np.zeros(n, dtype=np.uint8) @@ -243,7 +243,7 @@ def _solve_f2_linear_system_jit( def _elimination_jit(mat_data: npt.NDArray[np.uint8], ncols: int, full_reduce: bool) -> npt.NDArray[np.uint8]: r"""Return row echelon form (REF) or row-reduced echelon form (RREF) by performing Gaussian elimination. - The signature of the numba decorator requites the input arrays to be C_CONTIGUOUS. + The signature of the numba decorator requires the input arrays to be C_CONTIGUOUS. Parameters ---------- @@ -311,7 +311,7 @@ def _elimination_jit(mat_data: npt.NDArray[np.uint8], ncols: int, full_reduce: b def _mat_mul_jit(m1: npt.NDArray[np.uint8], m2: npt.NDArray[np.uint8]) -> npt.NDArray[np.uint8]: """See docstring of `:func:MatGF2.__matmul__` for details. - The signature of the numba decorator requites the input arrays to be C_CONTIGUOUS. + The signature of the numba decorator requires the input arrays to be C_CONTIGUOUS. """ m, l = m1.shape _, n = m2.shape From 2d1c77d127550c314f2afe5a4d706df701592f4b Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 10 Mar 2026 17:51:45 +0100 Subject: [PATCH 5/7] Add test parametrization Co-authored-by: thierry-martinez --- tests/test_linalg.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_linalg.py b/tests/test_linalg.py index 92158f39..ede93691 100644 --- a/tests/test_linalg.py +++ b/tests/test_linalg.py @@ -224,10 +224,12 @@ def test_solve_f2_linear_system(self, benchmark: BenchmarkFixture, test_case: LS assert np.all((mat @ x) % 2 == b) # Test with numpy matrix product. - def test_row_reduction(self, fx_rng: Generator) -> None: - sizes = [(10, 10), (3, 7), (6, 2)] - ncols = [4, 5, 2] - orders: list[Literal["K", "C", "F"]] = ["K", "C", "F"] + @pytest.mark.parametrize("size,ncol,order", [ + ((10, 10), 4, "K"), + ((3, 7), 5, "C"), + ((6, 2), 2, "F"), + ]) + def test_row_reduction(self, fx_rng: Generator, size: tuple[int, int], ncol: int, order: Literal["K", "C", "F"]) -> None: for size, ncol, order in zip(sizes, ncols, orders, strict=True): mat = MatGF2(np.asarray(fx_rng.integers(size=size, low=0, high=2, dtype=np.uint8), order=order)) From eeec5cad3e9315d83fc29ba7d5c75c5cb318fa33 Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 10 Mar 2026 18:01:44 +0100 Subject: [PATCH 6/7] Fix ruff --- tests/test_linalg.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/tests/test_linalg.py b/tests/test_linalg.py index ede93691..8a326232 100644 --- a/tests/test_linalg.py +++ b/tests/test_linalg.py @@ -224,17 +224,21 @@ def test_solve_f2_linear_system(self, benchmark: BenchmarkFixture, test_case: LS assert np.all((mat @ x) % 2 == b) # Test with numpy matrix product. - @pytest.mark.parametrize("size,ncol,order", [ - ((10, 10), 4, "K"), - ((3, 7), 5, "C"), - ((6, 2), 2, "F"), - ]) - def test_row_reduction(self, fx_rng: Generator, size: tuple[int, int], ncol: int, order: Literal["K", "C", "F"]) -> None: - - for size, ncol, order in zip(sizes, ncols, orders, strict=True): - mat = MatGF2(np.asarray(fx_rng.integers(size=size, low=0, high=2, dtype=np.uint8), order=order)) - mat_red = mat.row_reduction(ncols=ncol, copy=True) - verify_elimination(mat, mat_red, ncol, full_reduce=True) + @pytest.mark.parametrize( + "test_case", + [ + ((10, 10), 4, "K"), + ((3, 7), 5, "C"), + ((6, 2), 2, "F"), + ], + ) + def test_row_reduction( + self, fx_rng: Generator, test_case: tuple[tuple[int, int], int, Literal["K", "C", "F"]] + ) -> None: + size, ncol, order = test_case + mat = MatGF2(np.asarray(fx_rng.integers(size=size, low=0, high=2, dtype=np.uint8), order=order)) + mat_red = mat.row_reduction(ncols=ncol, copy=True) + verify_elimination(mat, mat_red, ncol, full_reduce=True) def test_initialization(self) -> None: mat_c = MatGF2(np.array([[1, 0], [0, 1], [1, 0]], dtype=np.uint8)) From 3649af5fb0d3e3a90510678f49c30d3c81d3ef9c Mon Sep 17 00:00:00 2001 From: matulni Date: Wed, 11 Mar 2026 09:20:24 +0100 Subject: [PATCH 7/7] Update tests/test_linalg.py Co-authored-by: thierry-martinez --- tests/test_linalg.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_linalg.py b/tests/test_linalg.py index 8a326232..a1ad1ce2 100644 --- a/tests/test_linalg.py +++ b/tests/test_linalg.py @@ -225,7 +225,7 @@ def test_solve_f2_linear_system(self, benchmark: BenchmarkFixture, test_case: LS assert np.all((mat @ x) % 2 == b) # Test with numpy matrix product. @pytest.mark.parametrize( - "test_case", + ("size", "ncol", "order"), [ ((10, 10), 4, "K"), ((3, 7), 5, "C"), @@ -233,9 +233,8 @@ def test_solve_f2_linear_system(self, benchmark: BenchmarkFixture, test_case: LS ], ) def test_row_reduction( - self, fx_rng: Generator, test_case: tuple[tuple[int, int], int, Literal["K", "C", "F"]] + self, fx_rng: Generator, size: tuple[int, int], ncol: int, order: Literal["K", "C", "F"] ) -> None: - size, ncol, order = test_case mat = MatGF2(np.asarray(fx_rng.integers(size=size, low=0, high=2, dtype=np.uint8), order=order)) mat_red = mat.row_reduction(ncols=ncol, copy=True) verify_elimination(mat, mat_red, ncol, full_reduce=True)