From e7bea6f7ce849d73549c6efd22232ec7d238995d Mon Sep 17 00:00:00 2001 From: N McDowall Date: Mon, 28 Jul 2025 22:05:28 -0500 Subject: [PATCH 1/2] Add phase-agnostic state comparator and tests (fixes #304) --- .../tests/PhaseAgnosticStateComparator.py | 26 ++++++++++ projectq/tests/helpers.py | 52 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 projectq/tests/PhaseAgnosticStateComparator.py create mode 100644 projectq/tests/helpers.py diff --git a/projectq/tests/PhaseAgnosticStateComparator.py b/projectq/tests/PhaseAgnosticStateComparator.py new file mode 100644 index 00000000..4b8d797b --- /dev/null +++ b/projectq/tests/PhaseAgnosticStateComparator.py @@ -0,0 +1,26 @@ +import numpy as np +from projectq import MainEngine +from projectq.ops import H, CNOT +from projectq.tests.helpers import PhaseAgnosticStateComparator + +def test_hadamard_twice(): + eng = MainEngine() + q = eng.allocate_qureg(1) + H | q[0] + H | q[0] + eng.flush() + _, actual = eng.backend.cheat() + expected = np.array([1, 0], dtype=complex) + comparator = PhaseAgnosticStateComparator() + comparator.compare(actual, expected) + +def test_bell_state(): + eng = MainEngine() + q = eng.allocate_qureg(2) + H | q[0] + CNOT | (q[0], q[1]) + eng.flush() + _, actual = eng.backend.cheat() + expected = np.array([1/np.sqrt(2), 0, 0, 1/np.sqrt(2)], dtype=complex) + comparator = PhaseAgnosticStateComparator() + comparator.compare(actual, expected) \ No newline at end of file diff --git a/projectq/tests/helpers.py b/projectq/tests/helpers.py new file mode 100644 index 00000000..760f45ea --- /dev/null +++ b/projectq/tests/helpers.py @@ -0,0 +1,52 @@ +import numpy as np + +class PhaseAgnosticStateComparator: + def __init__(self, tol=1e-8): + self.tol = tol + + def _validate_state(self, state, label="input"): + if not isinstance(state, np.ndarray): + raise TypeError(f"{label} must be a NumPy array.") + if state.ndim not in (1, 2): + raise ValueError(f"{label} must be a 1D or 2D array, got shape {state.shape}.") + if not np.isfinite(state).all(): + raise ValueError(f"{label} contains NaN or Inf.") + if state.ndim == 1 and np.linalg.norm(state) == 0: + raise ValueError(f"{label} is a zero vector and cannot be normalized.") + if state.ndim == 2: + for i, vec in enumerate(state): + if np.linalg.norm(vec) == 0: + raise ValueError(f"{label}[{i}] is a zero vector.") + + def normalize(self, state, label="input"): + self._validate_state(state, label) + if state.ndim == 1: + return state / np.linalg.norm(state) + else: + return np.array([vec / np.linalg.norm(vec) for vec in state]) + + def align_phase(self, actual, expected): + a = self.normalize(actual, "actual") + b = self.normalize(expected, "expected") + if a.ndim == 1: + phase = np.vdot(b, a) + return actual * phase.conjugate() + else: + aligned = [] + for i in range(a.shape[0]): + phase = np.vdot(b[i], a[i]) + aligned_vec = actual[i] * phase.conjugate() + aligned.append(aligned_vec) + return np.array(aligned) + + def compare(self, actual, expected): + aligned = self.align_phase(actual, expected) + if actual.ndim == 1: + if not np.allclose(aligned, expected, atol=self.tol): + raise AssertionError("States differ beyond tolerance.") + else: + for i in range(actual.shape[0]): + if not np.allclose(aligned[i], expected[i], atol=self.tol): + raise AssertionError( + f"State {i} differs:\nAligned: {aligned[i]}\nExpected: {expected[i]}" + ) \ No newline at end of file From 9315cc35cb52e6965fccb8b99cc7d705014cc13f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 29 Jul 2025 03:33:15 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- projectq/tests/PhaseAgnosticStateComparator.py | 9 ++++++--- projectq/tests/helpers.py | 5 ++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/projectq/tests/PhaseAgnosticStateComparator.py b/projectq/tests/PhaseAgnosticStateComparator.py index 4b8d797b..1fd33fb8 100644 --- a/projectq/tests/PhaseAgnosticStateComparator.py +++ b/projectq/tests/PhaseAgnosticStateComparator.py @@ -1,8 +1,10 @@ import numpy as np + from projectq import MainEngine -from projectq.ops import H, CNOT +from projectq.ops import CNOT, H from projectq.tests.helpers import PhaseAgnosticStateComparator + def test_hadamard_twice(): eng = MainEngine() q = eng.allocate_qureg(1) @@ -14,6 +16,7 @@ def test_hadamard_twice(): comparator = PhaseAgnosticStateComparator() comparator.compare(actual, expected) + def test_bell_state(): eng = MainEngine() q = eng.allocate_qureg(2) @@ -21,6 +24,6 @@ def test_bell_state(): CNOT | (q[0], q[1]) eng.flush() _, actual = eng.backend.cheat() - expected = np.array([1/np.sqrt(2), 0, 0, 1/np.sqrt(2)], dtype=complex) + expected = np.array([1 / np.sqrt(2), 0, 0, 1 / np.sqrt(2)], dtype=complex) comparator = PhaseAgnosticStateComparator() - comparator.compare(actual, expected) \ No newline at end of file + comparator.compare(actual, expected) diff --git a/projectq/tests/helpers.py b/projectq/tests/helpers.py index 760f45ea..671942fb 100644 --- a/projectq/tests/helpers.py +++ b/projectq/tests/helpers.py @@ -1,5 +1,6 @@ import numpy as np + class PhaseAgnosticStateComparator: def __init__(self, tol=1e-8): self.tol = tol @@ -47,6 +48,4 @@ def compare(self, actual, expected): else: for i in range(actual.shape[0]): if not np.allclose(aligned[i], expected[i], atol=self.tol): - raise AssertionError( - f"State {i} differs:\nAligned: {aligned[i]}\nExpected: {expected[i]}" - ) \ No newline at end of file + raise AssertionError(f"State {i} differs:\nAligned: {aligned[i]}\nExpected: {expected[i]}")