diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 76bb98914..b97b4b510 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -31,6 +31,7 @@ jobs: - run: | python -m pip install --upgrade pip pip install -e .[dev,extra] + pip install --no-deps -e .[no-deps] - run: mypy diff --git a/CHANGELOG.md b/CHANGELOG.md index a5a60c0d5..b3396d617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added + - #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`. @@ -18,17 +19,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Introduced new module `graphix.flow.exceptions` grouping flow exceptions. - Introduced new methods `graphix.flow.core.PauliFlow.get_measurement_label` and `graphix.flow.core.GFlow.get_measurement_label` which return the measurement label of a given node following same criteria employed in the flow-finding algorithms. -- #374: +- #379: Added a new instruction `CZ` which can be added as a circuit gate using `circuit.cz`. - Introduced new method `graphix.opengraph.OpenGraph.is_equal_structurally` which compares the underlying structure of two open graphs. - Added new method `isclose` to `graphix.fundamentals.AbstractMeasurement` which defaults to `==` comparison. -- #383: Simulators are now parameterized by `PrepareMethod` (which - defaults to `DefaultPrepareMethod`) to customize how `N` commands are - handled, and the class `BaseN` can be used as a base class for - custom preparation commands. +- #383: Simulators are now parameterized by `PrepareMethod` (which defaults to `DefaultPrepareMethod`) to customize how `N` commands are handled, and the class `BaseN` can be used as a base class for custom preparation commands. ### Fixed +- #379: Removed unnecessary `meas_index` from API for rotation instructions `RZ`, `RY` and `RX`. + - #347: Adapted existing method `graphix.opengraph.OpenGraph.isclose` to the new API introduced in #358. - #349, #362: Patterns transpiled from circuits always have causal flow. diff --git a/graphix/instruction.py b/graphix/instruction.py index 81c149957..f84ae96a6 100644 --- a/graphix/instruction.py +++ b/graphix/instruction.py @@ -40,6 +40,7 @@ class InstructionKind(Enum): RZZ = enum.auto() CNOT = enum.auto() SWAP = enum.auto() + CZ = enum.auto() H = enum.auto() S = enum.auto() X = enum.auto() @@ -80,9 +81,6 @@ class RZZ(_KindChecker, DataclassReprMixin): target: int control: int angle: ExpressionOrFloat = field(metadata={"repr": repr_angle}) - # FIXME: Remove `| None` from `meas_index` - # - `None` makes codes messy/type-unsafe - meas_index: int | None = None kind: ClassVar[Literal[InstructionKind.RZZ]] = field(default=InstructionKind.RZZ, init=False) @@ -95,6 +93,14 @@ class CNOT(_KindChecker, DataclassReprMixin): kind: ClassVar[Literal[InstructionKind.CNOT]] = field(default=InstructionKind.CNOT, init=False) +@dataclass(repr=False) +class CZ(_KindChecker, DataclassReprMixin): + """CZ circuit instruction.""" + + targets: tuple[int, int] + kind: ClassVar[Literal[InstructionKind.CZ]] = field(default=InstructionKind.CZ, init=False) + + @dataclass(repr=False) class SWAP(_KindChecker, DataclassReprMixin): """SWAP circuit instruction.""" @@ -167,7 +173,6 @@ class RX(_KindChecker, DataclassReprMixin): target: int angle: ExpressionOrFloat = field(metadata={"repr": repr_angle}) - meas_index: int | None = None kind: ClassVar[Literal[InstructionKind.RX]] = field(default=InstructionKind.RX, init=False) @@ -177,7 +182,6 @@ class RY(_KindChecker, DataclassReprMixin): target: int angle: ExpressionOrFloat = field(metadata={"repr": repr_angle}) - meas_index: int | None = None kind: ClassVar[Literal[InstructionKind.RY]] = field(default=InstructionKind.RY, init=False) @@ -187,7 +191,6 @@ class RZ(_KindChecker, DataclassReprMixin): target: int angle: ExpressionOrFloat = field(metadata={"repr": repr_angle}) - meas_index: int | None = None kind: ClassVar[Literal[InstructionKind.RZ]] = field(default=InstructionKind.RZ, init=False) @@ -209,4 +212,4 @@ class _ZC(_KindChecker): kind: ClassVar[Literal[InstructionKind._ZC]] = field(default=InstructionKind._ZC, init=False) -Instruction = CCX | RZZ | CNOT | SWAP | H | S | X | Y | Z | I | M | RX | RY | RZ | _XC | _ZC +Instruction = CCX | RZZ | CNOT | SWAP | CZ | H | S | X | Y | Z | I | M | RX | RY | RZ | _XC | _ZC diff --git a/graphix/qasm3_exporter.py b/graphix/qasm3_exporter.py index 60e9dec5f..372fb71b0 100644 --- a/graphix/qasm3_exporter.py +++ b/graphix/qasm3_exporter.py @@ -100,6 +100,8 @@ def instruction_to_qasm3(instruction: Instruction) -> str: return qasm3_gate_call("cx", [qasm3_qubit(instruction.control), qasm3_qubit(instruction.target)]) if instruction.kind == InstructionKind.SWAP: return qasm3_gate_call("swap", [qasm3_qubit(instruction.targets[i]) for i in (0, 1)]) + if instruction.kind == InstructionKind.CZ: + return qasm3_gate_call("cz", [qasm3_qubit(instruction.targets[i]) for i in (0, 1)]) if instruction.kind == InstructionKind.RZZ: angle = angle_to_qasm3(instruction.angle) return qasm3_gate_call( diff --git a/graphix/random_objects.py b/graphix/random_objects.py index d123ab486..d18aae637 100644 --- a/graphix/random_objects.py +++ b/graphix/random_objects.py @@ -367,6 +367,7 @@ def rand_circuit( depth: int, rng: Generator | None = None, *, + use_cz: bool = True, use_rzz: bool = False, use_ccx: bool = False, parameters: Iterable[Parameter] | None = None, @@ -381,10 +382,12 @@ def rand_circuit( Number of alternating entangling and single-qubit layers. rng : numpy.random.Generator, optional Random number generator. A default generator is created if ``None``. + use_cz : bool, optional + If ``True`` add CZ gates in each layer (default: ``True``). use_rzz : bool, optional - If ``True`` add :math:`R_{ZZ}` gates in each layer. + If ``True`` add :math:`R_{ZZ}` gates in each layer (default: ``False``). use_ccx : bool, optional - If ``True`` add CCX gates in each layer. + If ``True`` add CCX gates in each layer (default: ``False``). parameters : Iterable[Parameter], optional Parameters used for randomly chosen rotation gates. @@ -414,6 +417,9 @@ def rand_circuit( for _ in range(depth): for j, k in _genpair(nqubits, 2, rng): circuit.cnot(j, k) + if use_cz: + for j, k in _genpair(nqubits, 2, rng): + circuit.cz(j, k) if use_rzz: for j, k in _genpair(nqubits, 2, rng): circuit.rzz(j, k, np.pi / 4) diff --git a/graphix/transpiler.py b/graphix/transpiler.py index 1160abdda..9af57b4e7 100644 --- a/graphix/transpiler.py +++ b/graphix/transpiler.py @@ -109,6 +109,8 @@ def add(self, instr: Instruction) -> None: self.cnot(instr.control, instr.target) elif instr.kind == InstructionKind.SWAP: self.swap(instr.targets[0], instr.targets[1]) + elif instr.kind == InstructionKind.CZ: + self.cz(instr.targets[0], instr.targets[1]) elif instr.kind == InstructionKind.H: self.h(instr.target) elif instr.kind == InstructionKind.S: @@ -174,6 +176,21 @@ def swap(self, qubit1: int, qubit2: int) -> None: assert qubit1 != qubit2 self.instruction.append(instruction.SWAP(targets=(qubit1, qubit2))) + def cz(self, qubit1: int, qubit2: int) -> None: + """Apply a CNOT gate. + + Parameters + ---------- + qubit1 : int + control qubit + qubit2 : int + target qubit + """ + assert qubit1 in self.active_qubits + assert qubit2 in self.active_qubits + assert qubit1 != qubit2 + self.instruction.append(instruction.CZ(targets=(qubit1, qubit2))) + def h(self, qubit: int) -> None: """Apply a Hadamard gate. @@ -351,7 +368,12 @@ def transpile(self) -> TranspileResult: pattern = Pattern(input_nodes=list(range(self.width))) classical_outputs = [] for instr in _transpile_rzz(self.instruction): - if instr.kind == instruction.InstructionKind.CNOT: + if instr.kind == instruction.InstructionKind.CZ: + target0 = _check_target(out, instr.targets[0]) + target1 = _check_target(out, instr.targets[1]) + seq = self._cz_command(target0, target1) + pattern.extend(seq) + elif instr.kind == instruction.InstructionKind.CNOT: ancilla = [n_node, n_node + 1] control = _check_target(out, instr.control) target = _check_target(out, instr.target) @@ -485,6 +507,24 @@ def _cnot_command( ) return control_node, ancilla[1], seq + @classmethod + def _cz_command(cls, target_1: int, target_2: int) -> list[Command]: + """MBQC commands for CZ gate. + + Parameters + ---------- + target_1 : int + target node on graph + target_2 : int + other target node on graph + + Returns + ------- + commands : list + list of MBQC commands + """ + return [E(nodes=(target_1, target_2))] + @classmethod def _m_command(cls, input_node: int, plane: Plane, angle: Angle) -> list[Command]: """MBQC commands for measuring qubit. @@ -901,6 +941,8 @@ def simulate_statevector( state.cnot((instr.control, instr.target)) elif instr.kind == instruction.InstructionKind.SWAP: state.swap(instr.targets) + elif instr.kind == instruction.InstructionKind.CZ: + state.entangle(instr.targets) elif instr.kind == instruction.InstructionKind.I: pass elif instr.kind == instruction.InstructionKind.S: diff --git a/requirements-dev.txt b/requirements-dev.txt index 3683792cf..1e09ee45f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -24,4 +24,5 @@ pytest-mpl qiskit>=1.0 qiskit-aer +openqasm-parser>=3.1.0 graphix-qasm-parser diff --git a/tests/test_qasm3_exporter.py b/tests/test_qasm3_exporter.py index 22202e388..52f6d9908 100644 --- a/tests/test_qasm3_exporter.py +++ b/tests/test_qasm3_exporter.py @@ -17,7 +17,7 @@ from graphix.instruction import Instruction try: - from graphix_qasm_parser import OpenQASMParser + from graphix_qasm_parser import OpenQASMParser # type: ignore[import-not-found, unused-ignore] except ImportError: pytestmark = pytest.mark.skip(reason="graphix-qasm-parser not installed") @@ -42,7 +42,8 @@ def test_circuit_to_qasm3(fx_bg: PCG64, jumps: int) -> None: rng = Generator(fx_bg.jumped(jumps)) nqubits = 5 depth = 4 - check_round_trip(rand_circuit(nqubits, depth, rng)) + # See https://github.com/TeamGraphix/graphix-qasm-parser/pull/5 + check_round_trip(rand_circuit(nqubits, depth, rng, use_cz=False)) @pytest.mark.parametrize( @@ -52,6 +53,8 @@ def test_circuit_to_qasm3(fx_bg: PCG64, jumps: int) -> None: instruction.RZZ(target=0, control=1, angle=pi / 4), instruction.CNOT(target=0, control=1), instruction.SWAP(targets=(0, 1)), + # See https://github.com/TeamGraphix/graphix-qasm-parser/pull/5 + # instruction.CZ(targets=(0, 1)), instruction.H(target=0), instruction.S(target=0), instruction.X(target=0), diff --git a/tests/test_statevec_backend.py b/tests/test_statevec_backend.py index 9d14f372b..a4906bd60 100644 --- a/tests/test_statevec_backend.py +++ b/tests/test_statevec_backend.py @@ -93,7 +93,7 @@ def test_clifford(self) -> None: backend.apply_clifford(node=0, clifford=clifford) np.testing.assert_allclose(vec.psi, backend.state.psi) - def test_deterministic_measure_one(self, fx_rng: Generator): + def test_deterministic_measure_one(self, fx_rng: Generator) -> None: # plus state & zero state (default), but with tossed coins for _ in range(10): backend = StatevectorBackend() @@ -112,7 +112,7 @@ def test_deterministic_measure_one(self, fx_rng: Generator): result = backend.measure(node=node_to_measure, measurement=measurement) assert result == expected_result - def test_deterministic_measure(self): + def test_deterministic_measure(self) -> None: """Entangle |+> state with N |0> states, the (XY,0) measurement yields the outcome 0 with probability 1.""" for _ in range(10): # plus state (default) @@ -130,7 +130,7 @@ def test_deterministic_measure(self): assert result == 0 assert list(backend.node_index) == list(range(1, n_neighbors + 1)) - def test_deterministic_measure_many(self): + def test_deterministic_measure_many(self) -> None: """Entangle |+> state with N |0> states, the (XY,0) measurement yields the outcome 0 with probability 1.""" for _ in range(10): # plus state (default) @@ -161,7 +161,7 @@ def test_deterministic_measure_many(self): assert list(backend.node_index) == list(range(n_traps, n_neighbors + n_traps + n_whatever)) - def test_deterministic_measure_with_coin(self, fx_rng: Generator): + def test_deterministic_measure_with_coin(self, fx_rng: Generator) -> None: """Entangle |+> state with N |0> states, the (XY,0) measurement yields the outcome 0 with probability 1. We add coin toss to that. diff --git a/tests/test_tnsim.py b/tests/test_tnsim.py index cc669a590..dd5ac3906 100644 --- a/tests/test_tnsim.py +++ b/tests/test_tnsim.py @@ -288,6 +288,18 @@ def test_i(self, fx_rng: Generator) -> None: value2 = tn_mbqc.expectation_value(random_op1, [0]) assert value1 == pytest.approx(value2) + def test_cz(self, fx_rng: Generator) -> None: + circuit = Circuit(2) + circuit.cz(0, 1) + pattern = circuit.transpile().pattern + pattern.standardize() + state = circuit.simulate_statevector().statevec + tn_mbqc = pattern.simulate_pattern(backend="tensornetwork", rng=fx_rng) + random_op2 = random_op(2, np.complex128, fx_rng) + value1 = state.expectation_value(random_op2, [0, 1]) + value2 = tn_mbqc.expectation_value(random_op2, [0, 1]) + assert value1 == pytest.approx(value2) + def test_cnot(self, fx_rng: Generator) -> None: circuit = Circuit(2) circuit.cnot(0, 1) diff --git a/tests/test_transpiler.py b/tests/test_transpiler.py index d12d0a21e..e823c15d2 100644 --- a/tests/test_transpiler.py +++ b/tests/test_transpiler.py @@ -23,6 +23,7 @@ INSTRUCTION_TEST_CASES: list[InstructionTestCase] = [ lambda _rng: instruction.CCX(0, (1, 2)), lambda rng: instruction.RZZ(0, 1, rng.random() * 2 * np.pi), + lambda _rng: instruction.CZ((0, 1)), lambda _rng: instruction.CNOT(0, 1), lambda _rng: instruction.SWAP((0, 1)), lambda _rng: instruction.H(0), @@ -38,11 +39,19 @@ class TestTranspilerUnitGates: + def test_cz(self, fx_rng: Generator) -> None: + circuit = Circuit(2) + circuit.cz(0, 1) + 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) + def test_cnot(self, fx_rng: Generator) -> None: circuit = Circuit(2) circuit.cnot(0, 1) pattern = circuit.transpile().pattern - state = circuit.simulate_statevector().statevec + 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) @@ -50,7 +59,7 @@ def test_hadamard(self, fx_rng: Generator) -> None: circuit = Circuit(1) circuit.h(0) pattern = circuit.transpile().pattern - state = circuit.simulate_statevector().statevec + 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) @@ -58,7 +67,7 @@ def test_s(self, fx_rng: Generator) -> None: circuit = Circuit(1) circuit.s(0) pattern = circuit.transpile().pattern - state = circuit.simulate_statevector().statevec + 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) @@ -66,7 +75,7 @@ def test_x(self, fx_rng: Generator) -> None: circuit = Circuit(1) circuit.x(0) pattern = circuit.transpile().pattern - state = circuit.simulate_statevector().statevec + 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) @@ -74,7 +83,7 @@ def test_y(self, fx_rng: Generator) -> None: circuit = Circuit(1) circuit.y(0) pattern = circuit.transpile().pattern - state = circuit.simulate_statevector().statevec + 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) @@ -82,7 +91,7 @@ def test_z(self, fx_rng: Generator) -> None: circuit = Circuit(1) circuit.z(0) pattern = circuit.transpile().pattern - state = circuit.simulate_statevector().statevec + 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) @@ -91,7 +100,7 @@ def test_rx(self, fx_rng: Generator) -> None: circuit = Circuit(1) circuit.rx(0, theta) pattern = circuit.transpile().pattern - state = circuit.simulate_statevector().statevec + 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) @@ -100,7 +109,7 @@ def test_ry(self, fx_rng: Generator) -> None: circuit = Circuit(1) circuit.ry(0, theta) pattern = circuit.transpile().pattern - state = circuit.simulate_statevector().statevec + 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) @@ -109,7 +118,7 @@ def test_rz(self, fx_rng: Generator) -> None: circuit = Circuit(1) circuit.rz(0, theta) pattern = circuit.transpile().pattern - state = circuit.simulate_statevector().statevec + 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) @@ -117,7 +126,7 @@ def test_i(self, fx_rng: Generator) -> None: circuit = Circuit(1) circuit.i(0) pattern = circuit.transpile().pattern - state = circuit.simulate_statevector().statevec + 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) @@ -129,7 +138,7 @@ def test_ccx(self, fx_bg: PCG64, jumps: int) -> None: circuit = rand_circuit(nqubits, depth, rng, use_ccx=True) pattern = circuit.transpile().pattern pattern.minimize_space() - state = circuit.simulate_statevector().statevec + 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) @@ -139,7 +148,7 @@ def test_transpiled(self, fx_rng: Generator) -> None: pairs = [(i, np.mod(i + 1, nqubits)) for i in range(nqubits)] circuit = rand_gate(nqubits, depth, pairs, fx_rng, use_rzz=True) pattern = circuit.transpile().pattern - state = circuit.simulate_statevector().statevec + 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) @@ -163,6 +172,7 @@ def test_add_extend(self) -> None: circuit = Circuit(3) circuit.ccx(0, 1, 2) circuit.rzz(0, 1, 2) + circuit.cz(0, 1) circuit.cnot(0, 1) circuit.swap(0, 1) circuit.h(0)