diff --git a/CHANGELOG.md b/CHANGELOG.md index b7008010..74b81175 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- #386, #433: Added `Statevec.fidelity` and `Statevec.isclose` methods for pure-state fidelity computation and equality check up to global phase. + ### Fixed - #429 diff --git a/graphix/sim/statevec.py b/graphix/sim/statevec.py index 259ca3d9..7441c399 100644 --- a/graphix/sim/statevec.py +++ b/graphix/sim/statevec.py @@ -388,6 +388,45 @@ def expectation_value(self, op: Matrix, qargs: Sequence[int]) -> complex: st1.evolve(op, qargs) return complex(np.dot(st2.psi.flatten().conjugate(), st1.psi.flatten())) + def fidelity(self, other: Statevec) -> float: + r"""Calculate the fidelity against another statevector. + + The fidelity is defined as :math:`|\langle\psi_1|\psi_2\rangle|^2`. + + Parameters + ---------- + other : :class:`Statevec` + statevector to compare with + + Returns + ------- + float + Fidelity between the two statevectors. + """ + inner = np.dot(self.flatten().conjugate(), other.flatten()) + return float(np.abs(inner) ** 2) + + def isclose(self, other: Statevec, *, rtol: float = 1e-09, atol: float = 0.0) -> bool: + """Check if two quantum states are equal up to global phase. + + Two states are considered close if their fidelity is close to 1. + + Parameters + ---------- + other : :class:`Statevec` + statevector to compare with + rtol : float + relative tolerance for :func:`math.isclose` + atol : float + absolute tolerance for :func:`math.isclose` + + Returns + ------- + bool + ``True`` if the states are equal up to global phase. + """ + return math.isclose(self.fidelity(other), 1, rel_tol=rtol, abs_tol=atol) + def subs(self, variable: Parameter, substitute: ExpressionOrSupportsFloat) -> Statevec: """Return a copy of the state vector where all occurrences of the given variable in measurement angles are substituted by the given value.""" result = Statevec() diff --git a/tests/test_flow_core.py b/tests/test_flow_core.py index 62cf2947..8513514a 100644 --- a/tests/test_flow_core.py +++ b/tests/test_flow_core.py @@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, NamedTuple import networkx as nx -import numpy as np import pytest from graphix.command import E, M, N, X, Z @@ -412,8 +411,7 @@ def test_corrections_to_pattern(self, test_case: XZCorrectionsTestCase, fx_rng: for _ in range(n_shots): state = pattern.simulate_pattern(input_state=PlanarState(plane, alpha)) - result = np.abs(np.dot(state.flatten().conjugate(), state_ref.flatten())) - assert result == pytest.approx(1) + assert state.isclose(state_ref) class TestFlow: diff --git a/tests/test_graphsim.py b/tests/test_graphsim.py index 6e91f670..bfb4ae4c 100644 --- a/tests/test_graphsim.py +++ b/tests/test_graphsim.py @@ -5,7 +5,6 @@ import networkx as nx import numpy as np import numpy.typing as npt -import pytest from graphix.clifford import Clifford from graphix.fundamentals import ANGLE_PI, Plane, angle_to_rad @@ -87,21 +86,21 @@ def test_fig2(self) -> None: gstate.normalize() gstate.remove_qubit(0) gstate2 = graph_state_to_statevec(g) - assert np.abs(np.dot(gstate.flatten().conjugate(), gstate2.flatten())) == pytest.approx(1) + assert gstate.isclose(gstate2) g.measure_y(1, choice=0) gstate.evolve_single(meas_op(0.5 * ANGLE_PI), 0) # y meas gstate.normalize() gstate.remove_qubit(0) gstate2 = graph_state_to_statevec(g) - assert np.abs(np.dot(gstate.flatten().conjugate(), gstate2.flatten())) == pytest.approx(1) + assert gstate.isclose(gstate2) g.measure_z(3) gstate.evolve_single(meas_op(0.5 * ANGLE_PI, plane=Plane.YZ), 1) # z meas gstate.normalize() gstate.remove_qubit(1) gstate2 = graph_state_to_statevec(g) - assert np.abs(np.dot(gstate.flatten().conjugate(), gstate2.flatten())) == pytest.approx(1) + assert gstate.isclose(gstate2) def test_e2(self) -> None: nqubit = 6 @@ -112,23 +111,23 @@ def test_e2(self) -> None: g.equivalent_graph_e2(3, 4) gstate2 = graph_state_to_statevec(g) - assert np.abs(np.dot(gstate.flatten().conjugate(), gstate2.flatten())) == pytest.approx(1) + assert gstate.isclose(gstate2) g.equivalent_graph_e2(4, 0) gstate3 = graph_state_to_statevec(g) - assert np.abs(np.dot(gstate.flatten().conjugate(), gstate3.flatten())) == pytest.approx(1) + assert gstate.isclose(gstate3) g.equivalent_graph_e2(4, 5) gstate4 = graph_state_to_statevec(g) - assert np.abs(np.dot(gstate.flatten().conjugate(), gstate4.flatten())) == pytest.approx(1) + assert gstate.isclose(gstate4) g.equivalent_graph_e2(0, 3) gstate5 = graph_state_to_statevec(g) - assert np.abs(np.dot(gstate.flatten().conjugate(), gstate5.flatten())) == pytest.approx(1) + assert gstate.isclose(gstate5) g.equivalent_graph_e2(0, 3) gstate6 = graph_state_to_statevec(g) - assert np.abs(np.dot(gstate.flatten().conjugate(), gstate6.flatten())) == pytest.approx(1) + assert gstate.isclose(gstate6) def test_e1(self) -> None: nqubit = 6 @@ -139,15 +138,15 @@ def test_e1(self) -> None: g.equivalent_graph_e1(3) gstate2 = graph_state_to_statevec(g) - assert np.abs(np.dot(gstate.flatten().conjugate(), gstate2.flatten())) == pytest.approx(1) + assert gstate.isclose(gstate2) g.z(4) gstate = graph_state_to_statevec(g) g.equivalent_graph_e1(4) gstate2 = graph_state_to_statevec(g) - assert np.abs(np.dot(gstate.flatten().conjugate(), gstate2.flatten())) == pytest.approx(1) + assert gstate.isclose(gstate2) g.equivalent_graph_e1(4) gstate3 = graph_state_to_statevec(g) - assert np.abs(np.dot(gstate.flatten().conjugate(), gstate3.flatten())) == pytest.approx(1) + assert gstate.isclose(gstate3) def test_local_complement(self) -> None: nqubit = 6 diff --git a/tests/test_opengraph.py b/tests/test_opengraph.py index 51dc365f..b0f319e5 100644 --- a/tests/test_opengraph.py +++ b/tests/test_opengraph.py @@ -10,7 +10,6 @@ from typing import TYPE_CHECKING, NamedTuple import networkx as nx -import numpy as np import pytest from graphix.command import E @@ -800,11 +799,8 @@ def check_determinism(pattern: Pattern, fx_rng: Generator, n_shots: int = 3) -> for _ in range(n_shots): state = pattern.simulate_pattern(input_state=PlanarState(plane, alpha)) - result = np.abs(np.dot(state.flatten().conjugate(), state_ref.flatten())) - - if result == pytest.approx(1): - continue - return False + if not state.isclose(state_ref): + return False return True @@ -872,7 +868,7 @@ def test_double_entanglement(self) -> None: pattern2 = pattern.extract_opengraph().to_pattern() state = pattern.simulate_pattern() state2 = pattern2.simulate_pattern() - assert np.abs(np.dot(state.flatten().conjugate(), state2.flatten())) == pytest.approx(1) + assert state.isclose(state2) def test_from_to_pattern(self, fx_rng: Generator) -> None: n_qubits = 2 @@ -885,7 +881,7 @@ def test_from_to_pattern(self, fx_rng: Generator) -> None: alpha = 2 * ANGLE_PI * fx_rng.random() state_ref = pattern_ref.simulate_pattern(input_state=PlanarState(plane, alpha)) state = pattern.simulate_pattern(input_state=PlanarState(plane, alpha)) - assert np.abs(np.dot(state.flatten().conjugate(), state_ref.flatten())) == pytest.approx(1) + assert state.isclose(state_ref) def test_isclose_measurement(self) -> None: og_1 = OpenGraph( diff --git a/tests/test_optimization.py b/tests/test_optimization.py index 5960a72f..29e66769 100644 --- a/tests/test_optimization.py +++ b/tests/test_optimization.py @@ -1,6 +1,5 @@ from __future__ import annotations -import numpy as np import pytest from numpy.random import PCG64, Generator @@ -50,7 +49,7 @@ def test_standardize_clifford_entanglement(fx_rng: Generator) -> None: state_ref = p_ref.simulate_pattern(input_state=PlanarState(Plane.XY, alpha)) state_p = p.simulate_pattern(input_state=PlanarState(Plane.XY, alpha)) - assert np.abs(np.dot(state_p.flatten().conjugate(), state_ref.flatten())) == pytest.approx(1) + assert state_p.isclose(state_ref) @pytest.mark.parametrize("jumps", range(1, 11)) @@ -67,7 +66,7 @@ def test_incorporate_pauli_results(fx_bg: PCG64, jumps: int) -> None: pattern2 = incorporate_pauli_results(pattern) state = pattern.simulate_pattern(rng=rng) state2 = pattern2.simulate_pattern(rng=rng) - assert np.abs(np.dot(state.flatten().conjugate(), state2.flatten())) == pytest.approx(1) + assert state.isclose(state2) @pytest.mark.parametrize("jumps", range(1, 11)) @@ -101,7 +100,7 @@ def test_remove_useless_domains(fx_bg: PCG64, jumps: int) -> None: pattern2 = remove_useless_domains(pattern) state = pattern.simulate_pattern(rng=rng) state2 = pattern2.simulate_pattern(rng=rng) - assert np.abs(np.dot(state.flatten().conjugate(), state2.flatten())) == pytest.approx(1) + assert state.isclose(state2) def test_to_space_optimal_pattern() -> None: @@ -123,4 +122,4 @@ def test_to_space_optimal_pattern() -> None: pattern2 = StandardizedPattern.from_pattern(pattern).to_space_optimal_pattern() state = pattern.simulate_pattern() state2 = pattern2.simulate_pattern() - assert np.abs(np.dot(state.flatten().conjugate(), state2.flatten())) == pytest.approx(1) + assert state.isclose(state2) diff --git a/tests/test_parameter.py b/tests/test_parameter.py index d01b583b..520adfcf 100644 --- a/tests/test_parameter.py +++ b/tests/test_parameter.py @@ -180,7 +180,7 @@ def test_random_circuit_with_parameters(fx_bg: PCG64, jumps: int, use_xreplace: else: state = circuit.subs(alpha, assignment[alpha]).subs(beta, assignment[beta]).simulate_statevector().statevec state_mbqc = pattern.subs(alpha, assignment[alpha]).subs(beta, assignment[beta]).simulate_pattern() - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state) def test_visualization() -> None: diff --git a/tests/test_pattern.py b/tests/test_pattern.py index a941df14..dc9e9c07 100644 --- a/tests/test_pattern.py +++ b/tests/test_pattern.py @@ -36,7 +36,7 @@ def compare_backend_result_with_statevec(backend_state: Statevec | DensityMatrix, statevec: Statevec) -> float: if isinstance(backend_state, Statevec): - return float(np.abs(np.dot(backend_state.flatten().conjugate(), statevec.flatten()))) + return float(backend_state.fidelity(statevec)) if isinstance(backend_state, DensityMatrix): return float(np.abs(np.dot(backend_state.rho.flatten().conjugate(), DensityMatrix(statevec).rho.flatten()))) raise NotImplementedError(backend_state) @@ -74,7 +74,7 @@ def test_standardize(self, fx_rng: Generator) -> None: assert pattern.is_standard() state = circuit.simulate_statevector().statevec state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state) def test_minimize_space(self, fx_rng: Generator) -> None: nqubits = 5 @@ -85,7 +85,7 @@ def test_minimize_space(self, fx_rng: Generator) -> None: pattern.minimize_space() state = circuit.simulate_statevector().statevec state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state) # https://github.com/TeamGraphix/graphix/issues/157 @pytest.mark.parametrize( @@ -158,7 +158,7 @@ def test_minimize_space_with_gflow(self, fx_bg: PCG64, jumps: int) -> None: pattern.minimize_space() state = circuit.simulate_statevector().statevec state_mbqc = pattern.simulate_pattern(rng=rng) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state) @pytest.mark.filterwarnings("ignore:Simulating using densitymatrix backend with no noise.") @pytest.mark.parametrize("backend_type", ["statevector", "densitymatrix", "tensornetwork"]) @@ -202,7 +202,7 @@ def test_parallelize_pattern(self, fx_rng: Generator) -> None: pattern.parallelize_pattern() state = circuit.simulate_statevector().statevec state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state) @pytest.mark.parametrize("jumps", range(1, 11)) def test_shift_signals(self, fx_bg: PCG64, jumps: int) -> None: @@ -216,7 +216,7 @@ def test_shift_signals(self, fx_bg: PCG64, jumps: int) -> None: assert pattern.is_standard() state = circuit.simulate_statevector().statevec state_mbqc = pattern.simulate_pattern(rng=rng) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state) @pytest.mark.parametrize("jumps", range(1, 11)) @pytest.mark.parametrize("backend", ["statevector", "densitymatrix"]) @@ -266,7 +266,7 @@ def test_pauli_measurement_single(self, pm: PauliMeasurement) -> None: pattern.perform_pauli_measurements() state = pattern.simulate_pattern() state_ref = pattern_ref.simulate_pattern(branch_selector=ConstBranchSelector(0)) - assert np.abs(np.dot(state.flatten().conjugate(), state_ref.flatten())) == pytest.approx(1) + assert state.isclose(state_ref) def test_pauli_measurement(self) -> None: # test pattern is obtained from 3-qubit QFT with pauli measurement @@ -337,7 +337,7 @@ def test_pauli_measured_against_nonmeasured(self, fx_bg: PCG64, jumps: int, igno pattern1.perform_pauli_measurements(ignore_pauli_with_deps=ignore_pauli_with_deps) state = pattern.simulate_pattern(rng=rng) state1 = pattern1.simulate_pattern(rng=rng) - assert np.abs(np.dot(state.flatten().conjugate(), state1.flatten())) == pytest.approx(1) + assert state.isclose(state1) @pytest.mark.parametrize("jumps", range(1, 4)) def test_pauli_repeated_measurement(self, fx_bg: PCG64, jumps: int) -> None: @@ -438,7 +438,7 @@ def test_shift_signals_plane(self, plane: Plane, method: str) -> None: outcomes_p = shift_outcomes(outcomes_ref, signal_dict) branch_selector = FixedBranchSelector(results=outcomes_p) state_p = pattern.simulate_pattern(branch_selector=branch_selector) - assert np.abs(np.dot(state_p.flatten().conjugate(), state_ref.flatten())) == pytest.approx(1) + assert state_p.isclose(state_ref) @pytest.mark.parametrize("jumps", range(1, 11)) def test_standardize_direct(self, fx_bg: PCG64, jumps: int) -> None: @@ -452,7 +452,7 @@ def test_standardize_direct(self, fx_bg: PCG64, jumps: int) -> None: pattern.minimize_space() state_p = pattern.simulate_pattern() state_ref = circuit.simulate_statevector().statevec - assert np.abs(np.dot(state_p.flatten().conjugate(), state_ref.flatten())) == pytest.approx(1) + assert state_p.isclose(state_ref) @pytest.mark.parametrize("jumps", range(1, 11)) def test_shift_signals_direct(self, fx_bg: PCG64, jumps: int) -> None: @@ -466,7 +466,7 @@ def test_shift_signals_direct(self, fx_bg: PCG64, jumps: int) -> None: pattern.minimize_space() state_p = pattern.simulate_pattern() state_ref = circuit.simulate_statevector().statevec - assert np.abs(np.dot(state_p.flatten().conjugate(), state_ref.flatten())) == pytest.approx(1) + assert state_p.isclose(state_ref) @pytest.mark.parametrize("jumps", range(1, 11)) def test_pauli_measurement_then_standardize(self, fx_bg: PCG64, jumps: int) -> None: @@ -494,7 +494,7 @@ def test_standardize_two_cliffords(self, fx_bg: PCG64, jumps: int) -> None: pattern.standardize() state_ref = pattern_ref.simulate_pattern() state_p = pattern.simulate_pattern() - assert np.abs(np.dot(state_p.flatten().conjugate(), state_ref.flatten())) == pytest.approx(1) + assert state_p.isclose(state_ref) @pytest.mark.parametrize("jumps", range(1, 48)) def test_standardize_domains_and_clifford(self, fx_bg: PCG64, jumps: int) -> None: @@ -511,7 +511,7 @@ def test_standardize_domains_and_clifford(self, fx_bg: PCG64, jumps: int) -> Non pattern.standardize() state_ref = pattern_ref.simulate_pattern() state_p = pattern.simulate_pattern() - assert np.abs(np.dot(state_p.flatten().conjugate(), state_ref.flatten())) == pytest.approx(1) + assert state_p.isclose(state_ref) # Simple pattern composition def test_compose_1(self) -> None: @@ -730,7 +730,7 @@ def test_compose_6(self, fx_bg: PCG64, jumps: int) -> None: p_compose.minimize_space() s = p.simulate_pattern() s_compose = p_compose.simulate_pattern() - assert np.abs(np.dot(s.flatten().conjugate(), s_compose.flatten())) == pytest.approx(1) + assert s.isclose(s_compose) # Test warning composition after standardization def test_compose_7(self, fx_rng: Generator) -> None: @@ -1000,7 +1000,7 @@ def test_extract_causal_flow_rnd_circuit(self, fx_bg: PCG64, jumps: int) -> None s_ref = p_ref.simulate_pattern(rng=rng) s_test = p_test.simulate_pattern(rng=rng) - assert np.abs(np.dot(s_ref.flatten().conjugate(), s_test.flatten())) == pytest.approx(1) + assert s_ref.isclose(s_test) # Extract gflow from random circuits @pytest.mark.parametrize("jumps", range(1, 11)) @@ -1019,7 +1019,7 @@ def test_extract_gflow_rnd_circuit(self, fx_bg: PCG64, jumps: int) -> None: s_ref = p_ref.simulate_pattern(rng=rng) s_test = p_test.simulate_pattern(rng=rng) - assert np.abs(np.dot(s_ref.flatten().conjugate(), s_test.flatten())) == pytest.approx(1) + assert s_ref.isclose(s_test) @pytest.mark.parametrize("test_case", PATTERN_FLOW_TEST_CASES) def test_extract_causal_flow(self, fx_rng: Generator, test_case: PatternFlowTestCase) -> None: @@ -1030,7 +1030,7 @@ def test_extract_causal_flow(self, fx_rng: Generator, test_case: PatternFlowTest p_test = test_case.pattern.to_bloch().extract_causal_flow().to_corrections().to_pattern() s_test = p_test.simulate_pattern(input_state=PlanarState(Plane.XZ, alpha), rng=fx_rng) - assert np.abs(np.dot(s_ref.flatten().conjugate(), s_test.flatten())) == pytest.approx(1) + assert s_ref.isclose(s_test) else: with pytest.raises(FlowError): test_case.pattern.extract_causal_flow() @@ -1044,7 +1044,7 @@ def test_extract_gflow(self, fx_rng: Generator, test_case: PatternFlowTestCase) p_test = test_case.pattern.to_bloch().extract_gflow().to_corrections().to_pattern() s_test = p_test.simulate_pattern(input_state=PlanarState(Plane.XZ, alpha), rng=fx_rng) - assert np.abs(np.dot(s_ref.flatten().conjugate(), s_test.flatten())) == pytest.approx(1) + assert s_ref.isclose(s_test) else: with pytest.raises(FlowError): test_case.pattern.extract_gflow() @@ -1070,7 +1070,7 @@ def test_extract_cflow_og(self, fx_rng: Generator) -> None: p_test = p_ref.extract_causal_flow().to_corrections().to_pattern() s_test = p_test.simulate_pattern(input_state=PlanarState(Plane.XZ, alpha)) - assert np.abs(np.dot(s_ref.flatten().conjugate(), s_test.flatten())) == pytest.approx(1) + assert s_ref.isclose(s_test) # From open graph def test_extract_gflow_og(self, fx_rng: Generator) -> None: @@ -1094,7 +1094,7 @@ def test_extract_gflow_og(self, fx_rng: Generator) -> None: p_test = p_ref.extract_gflow().to_corrections().to_pattern() s_test = p_test.simulate_pattern(input_state=PlanarState(Plane.XZ, alpha)) - assert np.abs(np.dot(s_ref.flatten().conjugate(), s_test.flatten())) == pytest.approx(1) + assert s_ref.isclose(s_test) # Extract xz-corrections from random circuits @pytest.mark.parametrize("jumps", range(1, 11)) @@ -1114,7 +1114,7 @@ def test_extract_xzc_rnd_circuit(self, fx_bg: PCG64, jumps: int) -> None: s_ref = p_ref.simulate_pattern(rng=rng) s_test = p_test.simulate_pattern(rng=rng) - assert np.abs(np.dot(s_ref.flatten().conjugate(), s_test.flatten())) == pytest.approx(1) + assert s_ref.isclose(s_test) def test_extract_xzc_empty_domains(self) -> None: p = Pattern(input_nodes=[0], cmds=[N(1), E((0, 1))]) @@ -1243,7 +1243,7 @@ def test_standardize(self, fx_bg: PCG64, jumps: int) -> None: pattern_mc.minimize_space() state_d = pattern.simulate_pattern(rng=rng) state_ref = pattern_mc.simulate_pattern(rng=rng) - assert np.abs(np.dot(state_d.flatten().conjugate(), state_ref.flatten())) == pytest.approx(1) + assert state_d.isclose(state_ref) @pytest.mark.parametrize("jumps", range(1, 11)) def test_shift_signals(self, fx_bg: PCG64, jumps: int) -> None: @@ -1262,7 +1262,7 @@ def test_shift_signals(self, fx_bg: PCG64, jumps: int) -> None: pattern_mc.minimize_space() state_d = pattern.simulate_pattern(rng=rng) state_ref = pattern_mc.simulate_pattern(rng=rng) - assert np.abs(np.dot(state_d.flatten().conjugate(), state_ref.flatten())) == pytest.approx(1) + assert state_d.isclose(state_ref) @pytest.mark.parametrize("jumps", range(1, 11)) def test_standardize_and_shift_signals(self, fx_bg: PCG64, jumps: int) -> None: @@ -1277,7 +1277,7 @@ def test_standardize_and_shift_signals(self, fx_bg: PCG64, jumps: int) -> None: pattern.minimize_space() state_p = pattern.simulate_pattern(rng=rng) state_ref = circuit.simulate_statevector().statevec - assert np.abs(np.dot(state_p.flatten().conjugate(), state_ref.flatten())) == pytest.approx(1) + assert state_p.isclose(state_ref) @pytest.mark.parametrize("jumps", range(1, 4)) def test_mixed_pattern_operations(self, fx_bg: PCG64, jumps: int) -> None: @@ -1306,7 +1306,7 @@ def test_mixed_pattern_operations(self, fx_bg: PCG64, jumps: int) -> None: assert pattern.is_standard() pattern.minimize_space() state_p = pattern.simulate_pattern(rng=rng) - assert np.abs(np.dot(state_p.flatten().conjugate(), state_ref.flatten())) == pytest.approx(1) + assert state_p.isclose(state_ref) def test_pauli_measurement_end_with_measure(self) -> None: # https://github.com/TeamGraphix/graphix/issues/153 diff --git a/tests/test_pyzx.py b/tests/test_pyzx.py index b690d635..19ca3950 100644 --- a/tests/test_pyzx.py +++ b/tests/test_pyzx.py @@ -93,7 +93,7 @@ def test_random_circuit(fx_bg: PCG64, jumps: int) -> None: pattern2.perform_pauli_measurements() pattern2.minimize_space() state2 = pattern2.simulate_pattern() - assert np.abs(np.dot(state.flatten().conjugate(), state2.flatten())) == pytest.approx(1) + assert state.isclose(state2) def test_rz() -> None: @@ -107,7 +107,7 @@ def test_rz() -> None: pattern_zx = og.to_pattern() state = pattern.simulate_pattern() state_zx = pattern_zx.simulate_pattern() - assert np.abs(np.dot(state_zx.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_zx.isclose(state) # Issue #235 @@ -128,4 +128,4 @@ def test_full_reduce_toffoli() -> None: p2 = og2.to_pattern() s = p.simulate_pattern() s2 = p2.simulate_pattern() - print(np.abs(np.dot(s.flatten().conj(), s2.flatten()))) + print(s.fidelity(s2)) diff --git a/tests/test_qasm3_exporter_to_qiskit.py b/tests/test_qasm3_exporter_to_qiskit.py index e9334796..a857daa7 100644 --- a/tests/test_qasm3_exporter_to_qiskit.py +++ b/tests/test_qasm3_exporter_to_qiskit.py @@ -77,7 +77,7 @@ def check_qasm3(pattern: Pattern) -> None: backend.finalize(pattern.output_nodes) state_qiskit = backend.state state_mbqc = pattern.simulate_pattern(branch_selector=branch_selector) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state_qiskit.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state_qiskit) def test_to_qasm3_qubits_preparation() -> None: diff --git a/tests/test_statevec.py b/tests/test_statevec.py index c7685195..d78ced26 100644 --- a/tests/test_statevec.py +++ b/tests/test_statevec.py @@ -145,6 +145,58 @@ def test_copy_fail(self, fx_rng: Generator) -> None: _vec = Statevec(nqubit=length - 1, data=test_vec) +class TestFidelityIsclose: + def test_fidelity_same_state(self) -> None: + state = Statevec(data=BasicStates.PLUS) + assert state.fidelity(state) == pytest.approx(1) + + def test_fidelity_orthogonal(self) -> None: + zero = Statevec(data=BasicStates.ZERO) + one = Statevec(data=BasicStates.ONE) + assert zero.fidelity(one) == pytest.approx(0) + + def test_fidelity_known_value(self) -> None: + # F(|0>, |+>) = 0.5 + zero = Statevec(data=BasicStates.ZERO) + plus = Statevec(data=BasicStates.PLUS) + assert zero.fidelity(plus) == pytest.approx(0.5) + + def test_fidelity_global_phase(self) -> None: + plus = Statevec(data=BasicStates.PLUS) + plus_rotated = Statevec(data=np.array([1, 1]) / np.sqrt(2) * 1j) + assert plus.fidelity(plus_rotated) == pytest.approx(1) + + def test_fidelity_symmetry(self, fx_rng: Generator) -> None: + length = 4 + vec_a = fx_rng.random(length) + 1j * fx_rng.random(length) + vec_a /= np.sqrt(np.sum(np.abs(vec_a) ** 2)) + vec_b = fx_rng.random(length) + 1j * fx_rng.random(length) + vec_b /= np.sqrt(np.sum(np.abs(vec_b) ** 2)) + a = Statevec(data=vec_a) + b = Statevec(data=vec_b) + assert a.fidelity(b) == pytest.approx(b.fidelity(a)) + + def test_isclose_same_state(self) -> None: + state = Statevec(data=BasicStates.PLUS) + assert state.isclose(state) + + def test_isclose_orthogonal(self) -> None: + zero = Statevec(data=BasicStates.ZERO) + one = Statevec(data=BasicStates.ONE) + assert not zero.isclose(one) + + def test_isclose_global_phase(self) -> None: + plus = Statevec(data=BasicStates.PLUS) + rotated = Statevec(data=np.array([1, 1]) / np.sqrt(2) * np.exp(1j * 0.7)) + assert plus.isclose(rotated) + + def test_isclose_tolerance(self) -> None: + zero = Statevec(data=BasicStates.ZERO) + almost = Statevec(data=np.array([np.sqrt(1 - 1e-8), np.sqrt(1e-8)])) + assert not zero.isclose(almost) + assert zero.isclose(almost, atol=1e-6) + + def test_normalize() -> None: statevec = Statevec(nqubit=1, data=BasicStates.PLUS) statevec.remove_qubit(0) diff --git a/tests/test_statevec_backend.py b/tests/test_statevec_backend.py index d46d2369..850d165c 100644 --- a/tests/test_statevec_backend.py +++ b/tests/test_statevec_backend.py @@ -33,7 +33,7 @@ def test_measurement_into_each_xyz_basis(self, state: PlanarState) -> None: sv.remove_qubit(k) sv2 = Statevec(nqubit=n - 1) - assert np.abs(sv.psi.flatten().dot(sv2.psi.flatten().conj())) == pytest.approx(1) + assert sv.isclose(sv2) def test_measurement_into_minus_state(self) -> None: n = 3 diff --git a/tests/test_tnsim.py b/tests/test_tnsim.py index c671cfa8..15a83b97 100644 --- a/tests/test_tnsim.py +++ b/tests/test_tnsim.py @@ -14,6 +14,7 @@ from graphix.fundamentals import ANGLE_PI from graphix.ops import Ops from graphix.random_objects import rand_circuit +from graphix.sim.statevec import Statevec from graphix.sim.tensornet import MBQCTensorNet, gen_str from graphix.states import BasicStates from graphix.transpiler import Circuit @@ -389,8 +390,7 @@ def test_to_statevector(self, fx_bg: PCG64, nqubits: int, jumps: int, fx_rng: Ge tn = pattern.simulate_pattern("tensornetwork", rng=fx_rng) statevec_tn = tn.to_statevector() - inner_product = np.inner(statevec_tn, statevec_ref.flatten().conjugate()) - assert abs(inner_product) == pytest.approx(1) + assert Statevec(data=statevec_tn).isclose(statevec_ref) @pytest.mark.parametrize("jumps", range(1, 11)) def test_evolve(self, fx_bg: PCG64, jumps: int, fx_rng: Generator) -> None: diff --git a/tests/test_transpiler.py b/tests/test_transpiler.py index 6236b348..23ef5ef7 100644 --- a/tests/test_transpiler.py +++ b/tests/test_transpiler.py @@ -49,7 +49,7 @@ def test_cz(self, fx_rng: Generator) -> None: pattern = circuit.transpile().pattern state = circuit.simulate_statevector(rng=fx_rng).statevec state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state) def test_cnot(self, fx_rng: Generator) -> None: circuit = Circuit(2) @@ -57,7 +57,7 @@ def test_cnot(self, fx_rng: Generator) -> None: pattern = circuit.transpile().pattern state = circuit.simulate_statevector(rng=fx_rng).statevec state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state) def test_hadamard(self, fx_rng: Generator) -> None: circuit = Circuit(1) @@ -65,7 +65,7 @@ def test_hadamard(self, fx_rng: Generator) -> None: pattern = circuit.transpile().pattern state = circuit.simulate_statevector(rng=fx_rng).statevec state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state) def test_s(self, fx_rng: Generator) -> None: circuit = Circuit(1) @@ -73,7 +73,7 @@ def test_s(self, fx_rng: Generator) -> None: pattern = circuit.transpile().pattern state = circuit.simulate_statevector(rng=fx_rng).statevec state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state) def test_x(self, fx_rng: Generator) -> None: circuit = Circuit(1) @@ -81,7 +81,7 @@ def test_x(self, fx_rng: Generator) -> None: pattern = circuit.transpile().pattern state = circuit.simulate_statevector(rng=fx_rng).statevec state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state) def test_y(self, fx_rng: Generator) -> None: circuit = Circuit(1) @@ -89,7 +89,7 @@ def test_y(self, fx_rng: Generator) -> None: pattern = circuit.transpile().pattern state = circuit.simulate_statevector(rng=fx_rng).statevec state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state) def test_z(self, fx_rng: Generator) -> None: circuit = Circuit(1) @@ -97,7 +97,7 @@ def test_z(self, fx_rng: Generator) -> None: pattern = circuit.transpile().pattern state = circuit.simulate_statevector(rng=fx_rng).statevec state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state) def test_rx(self, fx_rng: Generator) -> None: theta = fx_rng.uniform() * 2 * ANGLE_PI @@ -106,7 +106,7 @@ def test_rx(self, fx_rng: Generator) -> None: pattern = circuit.transpile().pattern state = circuit.simulate_statevector(rng=fx_rng).statevec state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state) def test_ry(self, fx_rng: Generator) -> None: theta = fx_rng.uniform() * 2 * ANGLE_PI @@ -115,7 +115,7 @@ def test_ry(self, fx_rng: Generator) -> None: pattern = circuit.transpile().pattern state = circuit.simulate_statevector(rng=fx_rng).statevec state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state) def test_rz(self, fx_rng: Generator) -> None: theta = fx_rng.uniform() * 2 * ANGLE_PI @@ -124,7 +124,7 @@ def test_rz(self, fx_rng: Generator) -> None: pattern = circuit.transpile().pattern state = circuit.simulate_statevector(rng=fx_rng).statevec state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state) def test_i(self, fx_rng: Generator) -> None: circuit = Circuit(1) @@ -132,7 +132,7 @@ def test_i(self, fx_rng: Generator) -> None: pattern = circuit.transpile().pattern state = circuit.simulate_statevector(rng=fx_rng).statevec state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state) @pytest.mark.parametrize("jumps", range(1, 11)) def test_ccx(self, fx_bg: PCG64, jumps: int) -> None: @@ -144,7 +144,7 @@ def test_ccx(self, fx_bg: PCG64, jumps: int) -> None: pattern.minimize_space() state = circuit.simulate_statevector(rng=rng).statevec state_mbqc = pattern.simulate_pattern(rng=rng) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state) def test_transpiled(self, fx_rng: Generator) -> None: nqubits = 2 @@ -154,7 +154,7 @@ def test_transpiled(self, fx_rng: Generator) -> None: pattern = circuit.transpile().pattern state = circuit.simulate_statevector(rng=fx_rng).statevec state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state) @pytest.mark.parametrize("jumps", range(1, 11)) @pytest.mark.parametrize("axis", [Axis.X, Axis.Y, Axis.Z]) @@ -168,7 +168,7 @@ def test_measure(self, fx_bg: PCG64, jumps: int, axis: Axis, outcome: Outcome) - state = circuit.simulate_statevector(rng=rng, input_state=input_state, branch_selector=branch_selector).statevec pattern = circuit.transpile().pattern state_mbqc = pattern.simulate_pattern(rng=rng, input_state=input_state, branch_selector=branch_selector) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state) @pytest.mark.parametrize("input_axis", [Axis.X, Axis.Y, Axis.Z]) @pytest.mark.parametrize("input_sign", [Sign.PLUS, Sign.MINUS]) @@ -208,7 +208,7 @@ def test_transpile_measurements_to_z_axis(self, fx_bg: PCG64, jumps: int, axis: state_z = circuit.simulate_statevector( rng=rng, input_state=input_state, branch_selector=branch_selector ).statevec - assert np.abs(np.dot(state_z.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_z.isclose(state) def test_add_extend(self) -> None: circuit = Circuit(3) @@ -246,7 +246,7 @@ def test_instructions(self, fx_bg: PCG64, jumps: int, instruction: InstructionTe input_state = rand_state_vector(3, rng=rng) state = circuit.simulate_statevector(input_state=input_state).statevec state_mbqc = pattern.simulate_pattern(input_state=input_state, rng=rng) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state) def test_simple(self) -> None: rng = np.random.default_rng(420) @@ -256,4 +256,4 @@ def test_simple(self) -> None: input_state = rand_state_vector(3, rng=rng) state = circuit.simulate_statevector(input_state=input_state).statevec state_mbqc = pattern.simulate_pattern(input_state=input_state, rng=rng) - assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) + assert state_mbqc.isclose(state)