diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cdab9f23..4801c8dae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- #484: J & CZ transpilation. + - Replaced `Circuit.transpile()` with a new approach based decomposing circuits into J & CZ gates. + - Added `Circuit.transpile_to_cflow()` to produce `CausalFlow` using the same decomposition. + - Added `instruction.J` class. + - Added `Circuit.transpile_j_to_rzh()` method to prepare circuits with J gates for export to OpenQASM. + - #490: Introduced new `Instruction` and `Command` namespace classes for instruction and command instantiation. - #476 Introduced new methods `OpenGraph.extract_circuit`, `CliffordMap.to_tableau` and new function `graphix.circ_ext.compilation.cm_berg_pass`. Circuit extraction can be done natively in Graphix. @@ -17,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- #484: Added `transpile` argument to `qasm3_exporter.circuit_to_qasm3` and `qasm3_exporter.circuit_to_qasm3_lines`, which defaults to true and applies `Circuit.transpile_j_to_rzh` and `Circuit.transpile_measurements_to_z_axis` methods. + - #490: Exposed more common classes and methods to top level `__init__.py`. - Renamed `Instruction`, `InstructionWithoutRZZ` and `Command` to `InstructionType`, `InstructionTypeWithoutRZZ` and `CommandType` respectively. - Moved `InstructionType`, `InstructionTypeWithoutRZZ`, `CommandType`, `Correction` and `CommandOrNoise` to `TYPE_CHECKING` blocks. diff --git a/docs/source/generator.rst b/docs/source/generator.rst index 1a9915952..f9fac960f 100644 --- a/docs/source/generator.rst +++ b/docs/source/generator.rst @@ -16,6 +16,8 @@ Pattern Generation .. automethod:: simulate_statevector + .. automethod:: cz + .. automethod:: cnot .. automethod:: h @@ -34,6 +36,8 @@ Pattern Generation .. automethod:: rz + .. automethod:: j + .. automethod:: ccx .. automethod:: m diff --git a/examples/deutsch_jozsa.py b/examples/deutsch_jozsa.py index e5f82d056..7603d54cc 100644 --- a/examples/deutsch_jozsa.py +++ b/examples/deutsch_jozsa.py @@ -74,6 +74,7 @@ # Now we preprocess all Pauli measurements, which requires that we move inputs to N commands pattern.remove_input_nodes() +pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() print( pattern.to_ascii( diff --git a/examples/ghz_with_tn.py b/examples/ghz_with_tn.py index 3e73e8364..d02949d81 100644 --- a/examples/ghz_with_tn.py +++ b/examples/ghz_with_tn.py @@ -15,8 +15,9 @@ import networkx as nx from graphix import Circuit +from graphix.transpiler import transpile_swaps -n = 100 +n = 50 print(f"{n}-qubit GHZ state generation") circuit = Circuit(n) @@ -32,7 +33,7 @@ # %% # Transpile into pattern -pattern = circuit.transpile().pattern +pattern = transpile_swaps(circuit).circuit.transpile().pattern pattern.standardize() graph = pattern.extract_graph() diff --git a/examples/mbqc_vqe.py b/examples/mbqc_vqe.py index fce62723a..eece69ed6 100644 --- a/examples/mbqc_vqe.py +++ b/examples/mbqc_vqe.py @@ -33,6 +33,7 @@ from graphix import Circuit from graphix.parameter import Placeholder from graphix.simulator import PatternSimulator +from graphix.transpiler import transpile_swaps if TYPE_CHECKING: from collections.abc import Iterable @@ -89,11 +90,14 @@ def __init__(self, n_qubits: int, hamiltonian: npt.NDArray[np.float64]): # Function to build the MBQC pattern def build_mbqc_pattern(self, params: Iterable[ParameterizedAngle]) -> Pattern: circuit = build_vqe_circuit(self.n_qubits, params) + circuit = transpile_swaps(circuit).circuit pattern = circuit.transpile().pattern pattern.standardize() pattern.shift_signals() pattern.remove_input_nodes() + pattern = pattern.infer_pauli_measurements() # Infer Pauli measurements to determine measurement planes pattern.perform_pauli_measurements() # Perform Pauli measurements + pattern.minimize_space() return pattern # %% @@ -156,7 +160,7 @@ def cost_function(params: Iterable[float]) -> float: # %% # Perform the optimization using COBYLA def compute() -> OptimizeResult: - return minimize(cost_function, initial_params, method="COBYLA", options={"maxiter": 100}) + return minimize(cost_function, initial_params, method="COBYLA", options={"maxiter": 20}) result = compute() @@ -173,9 +177,9 @@ def compute() -> OptimizeResult: # Compare performances between using parameterized circuits (with placeholders) or not mbqc_vqe = MBQCVQEWithPlaceholders(n_qubits, hamiltonian) -time_with_placeholders = timeit(compute, number=2) +time_with_placeholders = timeit(compute, number=1) print(f"Time with placeholders: {time_with_placeholders}") mbqc_vqe = MBQCVQE(n_qubits, hamiltonian) -time_without_placeholders = timeit(compute, number=2) +time_without_placeholders = timeit(compute, number=1) print(f"Time without placeholders: {time_without_placeholders}") diff --git a/examples/qaoa.py b/examples/qaoa.py index 0e57f3b66..f81036d84 100644 --- a/examples/qaoa.py +++ b/examples/qaoa.py @@ -41,6 +41,7 @@ # perform Pauli measurements and plot the new (minimal) graph to perform the same quantum computation pattern.remove_input_nodes() +pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() pattern.draw(flow_from_pattern=False) diff --git a/examples/qft_with_tn.py b/examples/qft_with_tn.py index eb92a6b3e..06ac29913 100644 --- a/examples/qft_with_tn.py +++ b/examples/qft_with_tn.py @@ -67,6 +67,7 @@ def qft(circuit: Circuit, n: int) -> None: # Using efficient graph state simulator `graphix.graphsim`, we can classically preprocess Pauli measurements. # We are currently improving the speed of this process by using rust-based graph manipulation backend. pattern.remove_input_nodes() +pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() diff --git a/examples/tn_simulation.py b/examples/tn_simulation.py index 8de62c998..3213479e6 100644 --- a/examples/tn_simulation.py +++ b/examples/tn_simulation.py @@ -85,6 +85,7 @@ def ansatz( # %% # Optimizing by performing Pauli measurements in the pattern using efficient stabilizer simulator. pattern.remove_input_nodes() +pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() # %% @@ -203,6 +204,7 @@ def cost( pattern.standardize() pattern.shift_signals() pattern.remove_input_nodes() + pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() mbqc_tn = pattern.simulate_pattern(backend="tensornetwork", graph_prep="parallel") exp_val: float = 0 diff --git a/examples/visualization.py b/examples/visualization.py index 0aa642b2e..5651b7629 100644 --- a/examples/visualization.py +++ b/examples/visualization.py @@ -38,6 +38,7 @@ # %% # next, show the gflow: pattern.remove_input_nodes() +pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() pattern.draw(flow_from_pattern=False, measurement_labels=True) diff --git a/graphix/instruction.py b/graphix/instruction.py index f608ccf8f..86be7e347 100644 --- a/graphix/instruction.py +++ b/graphix/instruction.py @@ -49,6 +49,7 @@ class InstructionKind(Enum): X = enum.auto() Y = enum.auto() Z = enum.auto() + J = enum.auto() I = enum.auto() M = enum.auto() RX = enum.auto() @@ -290,6 +291,19 @@ def visit(self, visitor: InstructionVisitor) -> RZ: return RZ(visitor.visit_qubit(self.target), visitor.visit_angle(self.angle)) +@dataclass(repr=False) +class J(_KindChecker, BaseInstruction): + """J circuit instruction.""" + + target: int + angle: ParameterizedAngle = field(metadata={"repr": repr_angle}) + kind: ClassVar[Literal[InstructionKind.J]] = field(default=InstructionKind.J, init=False) + + @override + def visit(self, visitor: InstructionVisitor) -> J: + return J(visitor.visit_qubit(self.target), visitor.visit_angle(self.angle)) + + class InstructionWithoutRZZ: """Grouping of all instructions except RZZ for namespace exposure. @@ -313,6 +327,7 @@ class InstructionWithoutRZZ: RX: TypeAlias = RX RY: TypeAlias = RY RZ: TypeAlias = RZ + J: TypeAlias = J def __init__(self) -> None: raise TypeError("InstructionWithoutRZZ is a namespace, not a class.") @@ -334,5 +349,5 @@ def __init__(self) -> None: if TYPE_CHECKING: - InstructionTypeWithoutRZZ = CCX | CNOT | SWAP | CZ | H | S | X | Y | Z | I | M | RX | RY | RZ + InstructionTypeWithoutRZZ = CCX | CNOT | SWAP | CZ | H | S | X | Y | Z | I | M | RX | RY | RZ | J InstructionType = InstructionTypeWithoutRZZ | RZZ diff --git a/graphix/ops.py b/graphix/ops.py index 075273da7..381a212df 100644 --- a/graphix/ops.py +++ b/graphix/ops.py @@ -167,6 +167,35 @@ def rz(theta: ParameterizedAngle) -> npt.NDArray[np.complex128] | npt.NDArray[np """ return Ops._cast_array([[exp(-1j * angle_to_rad(theta) / 2), 0], [0, exp(1j * angle_to_rad(theta) / 2)]], theta) + @overload + @staticmethod + def j(theta: Angle) -> npt.NDArray[np.complex128]: ... + + @overload + @staticmethod + def j(theta: Expression) -> npt.NDArray[np.object_]: ... + + @staticmethod + def j(theta: ParameterizedAngle) -> npt.NDArray[np.complex128] | npt.NDArray[np.object_]: + """J operation. + + Parameters + ---------- + theta : Angle | Expression + rotation angle in units of π + + Returns + ------- + operator : 2*2 np.asarray + """ + return Ops._cast_array( + [ + [1 / np.sqrt(2), (1 / np.sqrt(2)) * exp(1j * angle_to_rad(theta))], + [1 / np.sqrt(2), (-1 / np.sqrt(2)) * exp(1j * angle_to_rad(theta))], + ], + theta, + ) + @overload @staticmethod def rzz(theta: Angle) -> npt.NDArray[np.complex128]: ... diff --git a/graphix/pattern.py b/graphix/pattern.py index c28bf7555..9133b71d7 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -414,16 +414,16 @@ def to_ascii( >>> circuit.ccx(0, 1, 2) >>> pattern = circuit.transpile().pattern >>> pattern.to_ascii() - 'Z(16,{11}) Z(18,{13}) Z(20,{7}) X(16,{15}) X(18,{14}) X(20,{19}) {17}[M(19)]{7} {10}[M(15,-pi/4)]{11} {17,12}[M(14)]{13} {17,9}[M(11)]{10} {0,1,5,10,13}[M(7,-pi/4)]{17} {1}[M(13,-7pi/4)]{12} {8}[M(10,-7pi/4)]{9} {17}[M(12)]{1} {6}[M(9)]{8} {8,3}[M(1,-pi/4)] {5}[M(8,-pi/4)]{6} {17,4}[M(6)]{5} [M(17)]{0} {3}[M(5,-7pi/4)]{4} M(0) {2}[M(4)]{3} [M(3)]{2} M(2) E(19,20) E(15,16) E(14,18) E(11,15) E(7,19) E(7,14) E(7,11) E(13,14) E(10,11) E(12,13) E(12,7) E(9,10) E(1,12) E(1,9) E(8,9)...(27 more commands)' + 'X(24,{21}) Z(24,{20}) X(32,{31}) Z(32,{28}) X(30,{29}) Z(30,{0}) {27}[M(31,0)]{28} E(31,32) N(32) [M(29,0)]{0} E(29,30) N(30) {26}[M(28,3pi/2)]{27} E(28,31) N(31) {17}[M(21,0)]{20} E(21,24) N(24) {25}[M(27,0)]{26} E(27,28) N(28) {26,19,15,7}[M(0,7pi/4)] E(0,27) E(0,29) N(29) {16}[M(20,0)]{17} E(20,21) N(21) {23}[M(26,0)]{25} E(26,27) N(27) {22}[M(25,0)]{23} E(25,26) N(26) {15}[M(17,7pi/4)]{16} E(17,20) N(20) {19}[M(23,pi/4)]{22} E(23,25)...(63 more commands)' >>> pattern.to_ascii(left_to_right=True) - 'N(3) N(4) N(5) N(6) N(7) N(8) N(9) N(10) N(11) N(12) N(13) N(14) N(15) N(16) N(17) N(18) N(19) N(20) E(2,3) E(3,4) E(4,5) E(4,1) E(0,17) E(5,6) E(17,7) E(6,8) E(6,7) E(8,9) E(1,9) E(1,12) E(9,10) E(12,7) E(12,13) E(10,11) E(13,14) E(7,11) E(7,14) E(7,19) E(11,15)...(27 more commands)' + 'N(3) E(2,3) M(2,0) N(4) E(3,4) [M(3,0)]{2} E(4,1) N(5) E(4,5) {2}[M(4,0)]{3} N(6) E(5,6) {3}[M(5,pi/4)]{4} N(7) E(6,7) {4}[M(6,0)]{5} N(8) E(7,8) {5}[M(7,0)]{6} E(8,0) N(9) E(8,9) {6}[M(8,0)]{7} N(10) E(9,10) {7}[M(9,7pi/4)]{8} N(11) E(10,11) {8}[M(10,0)]{9} N(12) E(11,12) {9}[M(11,0)]{10} N(18) E(1,18) E(1,12) {11,3}[M(1,pi/4)] N(13) E(12,13) {10}[M(12,0)]{11}...(63 more commands)' >>> pattern.to_ascii(limit=None) - 'Z(16,{11}) Z(18,{13}) Z(20,{7}) X(16,{15}) X(18,{14}) X(20,{19}) {17}[M(19)]{7} {10}[M(15,-pi/4)]{11} {17,12}[M(14)]{13} {17,9}[M(11)]{10} {0,1,5,10,13}[M(7,-pi/4)]{17} {1}[M(13,-7pi/4)]{12} {8}[M(10,-7pi/4)]{9} {17}[M(12)]{1} {6}[M(9)]{8} {8,3}[M(1,-pi/4)] {5}[M(8,-pi/4)]{6} {17,4}[M(6)]{5} [M(17)]{0} {3}[M(5,-7pi/4)]{4} M(0) {2}[M(4)]{3} [M(3)]{2} M(2) E(19,20) E(15,16) E(14,18) E(11,15) E(7,19) E(7,14) E(7,11) E(13,14) E(10,11) E(12,13) E(12,7) E(9,10) E(1,12) E(1,9) E(8,9) E(6,7) E(6,8) E(17,7) E(5,6) E(0,17) E(4,1) E(4,5) E(3,4) E(2,3) N(20) N(19) N(18) N(17) N(16) N(15) N(14) N(13) N(12) N(11) N(10) N(9) N(8) N(7) N(6) N(5) N(4) N(3)' + 'X(24,{21}) Z(24,{20}) X(32,{31}) Z(32,{28}) X(30,{29}) Z(30,{0}) {27}[M(31,0)]{28} E(31,32) N(32) [M(29,0)]{0} E(29,30) N(30) {26}[M(28,3pi/2)]{27} E(28,31) N(31) {17}[M(21,0)]{20} E(21,24) N(24) {25}[M(27,0)]{26} E(27,28) N(28) {26,19,15,7}[M(0,7pi/4)] E(0,27) E(0,29) N(29) {16}[M(20,0)]{17} E(20,21) N(21) {23}[M(26,0)]{25} E(26,27) N(27) {22}[M(25,0)]{23} E(25,26) N(26) {15}[M(17,7pi/4)]{16} E(17,20) N(20) {19}[M(23,pi/4)]{22} E(23,25) N(25) {14}[M(16,0)]{15} E(16,0) E(16,17) N(17) {13}[M(15,0)]{14} E(15,16) N(16) {18}[M(22,0)]{19} E(22,23) N(23) E(22,0) {12}[M(14,0)]{13} E(14,15) N(15) {1}[M(19,0)]{18} E(19,22) N(22) {11}[M(13,pi/4)]{12} E(13,14) N(14) [M(18,0)]{1} E(18,19) N(19) {10}[M(12,0)]{11} E(12,13) N(13) {11,3}[M(1,pi/4)] E(1,12) E(1,18) N(18) {9}[M(11,0)]{10} E(11,12) N(12) {8}[M(10,0)]{9} E(10,11) N(11) {7}[M(9,7pi/4)]{8} E(9,10) N(10) {6}[M(8,0)]{7} E(8,9) N(9) E(8,0) {5}[M(7,0)]{6} E(7,8) N(8) {4}[M(6,0)]{5} E(6,7) N(7) {3}[M(5,pi/4)]{4} E(5,6) N(6) {2}[M(4,0)]{3} E(4,5) N(5) E(4,1) [M(3,0)]{2} E(3,4) N(4) M(2,0) E(2,3) N(3)' >>> from graphix.command import CommandKind >>> pattern.to_ascii(target={CommandKind.M}) - '{17}[M(19)]{7} {10}[M(15,-pi/4)]{11} {17,12}[M(14)]{13} {17,9}[M(11)]{10} {0,1,5,10,13}[M(7,-pi/4)]{17} {1}[M(13,-7pi/4)]{12} {8}[M(10,-7pi/4)]{9} {17}[M(12)]{1} {6}[M(9)]{8} {8,3}[M(1,-pi/4)] {5}[M(8,-pi/4)]{6} {17,4}[M(6)]{5} [M(17)]{0} {3}[M(5,-7pi/4)]{4} M(0) {2}[M(4)]{3} [M(3)]{2} M(2)' + '{27}[M(31,0)]{28} [M(29,0)]{0} {26}[M(28,3pi/2)]{27} {17}[M(21,0)]{20} {25}[M(27,0)]{26} {26,19,15,7}[M(0,7pi/4)] {16}[M(20,0)]{17} {23}[M(26,0)]{25} {22}[M(25,0)]{23} {15}[M(17,7pi/4)]{16} {19}[M(23,pi/4)]{22} {14}[M(16,0)]{15} {13}[M(15,0)]{14} {18}[M(22,0)]{19} {12}[M(14,0)]{13} {1}[M(19,0)]{18} {11}[M(13,pi/4)]{12} [M(18,0)]{1} {10}[M(12,0)]{11} {11,3}[M(1,pi/4)] {9}[M(11,0)]{10} {8}[M(10,0)]{9} {7}[M(9,7pi/4)]{8} {6}[M(8,0)]{7} {5}[M(7,0)]{6} {4}[M(6,0)]{5} {3}[M(5,pi/4)]{4} {2}[M(4,0)]{3} [M(3,0)]{2} M(2,0)' >>> pattern.to_ascii(target={CommandKind.X, CommandKind.Z}) - 'Z(16,{11}) Z(18,{13}) Z(20,{7}) X(16,{15}) X(18,{14}) X(20,{19})' + 'X(24,{21}) Z(24,{20}) X(32,{31}) Z(32,{28}) X(30,{29}) Z(30,{0})' """ return pattern_to_str(self, OutputFormat.ASCII, left_to_right, limit, target) @@ -452,13 +452,14 @@ def to_latex( >>> circuit.ccx(0, 1, 2) >>> pattern = circuit.transpile().pattern >>> pattern.to_latex() - '\\(Z_{16}^{11}\\,Z_{18}^{13}\\,Z_{20}^{7}\\,X_{16}^{15}\\,X_{18}^{14}\\,X_{20}^{19}\\,{}_{17}[M_{19}]^{7}\\,{}_{10}[M_{15}^{-\\frac{\\pi}{4}}]^{11}\\,{}_{17,12}[M_{14}]^{13}\\,{}_{17,9}[M_{11}]^{10}\\,{}_{0,1,5,10,13}[M_{7}^{-\\frac{\\pi}{4}}]^{17}\\,{}_{1}[M_{13}^{-\\frac{7\\pi}{4}}]^{12}\\,{}_{8}[M_{10}^{-\\frac{7\\pi}{4}}]^{9}\\,{}_{17}[M_{12}]^{1}\\,{}_{6}[M_{9}]^{8}\\,{}_{8,3}[M_{1}^{-\\frac{\\pi}{4}}]\\,{}_{5}[M_{8}^{-\\frac{\\pi}{4}}]^{6}\\,{}_{17,4}[M_{6}]^{5}\\,[M_{17}]^{0}\\,{}_{3}[M_{5}^{-\\frac{7\\pi}{4}}]^{4}\\,M_{0}\\,{}_{2}[M_{4}]^{3}\\,[M_{3}]^{2}\\,M_{2}\\,E_{19,20}\\,E_{15,16}\\,E_{14,18}\\,E_{11,15}\\,E_{7,19}\\,E_{7,14}\\,E_{7,11}\\,E_{13,14}\\,E_{10,11}\\,E_{12,13}\\,E_{12,7}\\,E_{9,10}\\,E_{1,12}\\,E_{1,9}\\,E_{8,9}\\)...(27 more commands)' + '\\(X_{24}^{21}\\,Z_{24}^{20}\\,X_{32}^{31}\\,Z_{32}^{28}\\,X_{30}^{29}\\,Z_{30}^{0}\\,{}_{27}[M_{31}^{0}]^{28}\\,E_{31,32}\\,N_{32}\\,[M_{29}^{0}]^{0}\\,E_{29,30}\\,N_{30}\\,{}_{26}[M_{28}^{\\frac{3\\pi}{2}}]^{27}\\,E_{28,31}\\,N_{31}\\,{}_{17}[M_{21}^{0}]^{20}\\,E_{21,24}\\,N_{24}\\,{}_{25}[M_{27}^{0}]^{26}\\,E_{27,28}\\,N_{28}\\,{}_{26,19,15,7}[M_{0}^{\\frac{7\\pi}{4}}]\\,E_{0,27}\\,E_{0,29}\\,N_{29}\\,{}_{16}[M_{20}^{0}]^{17}\\,E_{20,21}\\,N_{21}\\,{}_{23}[M_{26}^{0}]^{25}\\,E_{26,27}\\,N_{27}\\,{}_{22}[M_{25}^{0}]^{23}\\,E_{25,26}\\,N_{26}\\,{}_{15}[M_{17}^{\\frac{7\\pi}{4}}]^{16}\\,E_{17,20}\\,N_{20}\\,{}_{19}[M_{23}^{\\frac{\\pi}{4}}]^{22}\\,E_{23,25}\\)...(63 more commands)' >>> pattern.to_latex(left_to_right=True) - '\\(N_{3}\\,N_{4}\\,N_{5}\\,N_{6}\\,N_{7}\\,N_{8}\\,N_{9}\\,N_{10}\\,N_{11}\\,N_{12}\\,N_{13}\\,N_{14}\\,N_{15}\\,N_{16}\\,N_{17}\\,N_{18}\\,N_{19}\\,N_{20}\\,E_{2,3}\\,E_{3,4}\\,E_{4,5}\\,E_{4,1}\\,E_{0,17}\\,E_{5,6}\\,E_{17,7}\\,E_{6,8}\\,E_{6,7}\\,E_{8,9}\\,E_{1,9}\\,E_{1,12}\\,E_{9,10}\\,E_{12,7}\\,E_{12,13}\\,E_{10,11}\\,E_{13,14}\\,E_{7,11}\\,E_{7,14}\\,E_{7,19}\\,E_{11,15}\\)...(27 more commands)' + '\\(N_{3}\\,E_{2,3}\\,M_{2}^{0}\\,N_{4}\\,E_{3,4}\\,[M_{3}^{0}]^{2}\\,E_{4,1}\\,N_{5}\\,E_{4,5}\\,{}_{2}[M_{4}^{0}]^{3}\\,N_{6}\\,E_{5,6}\\,{}_{3}[M_{5}^{\\frac{\\pi}{4}}]^{4}\\,N_{7}\\,E_{6,7}\\,{}_{4}[M_{6}^{0}]^{5}\\,N_{8}\\,E_{7,8}\\,{}_{5}[M_{7}^{0}]^{6}\\,E_{8,0}\\,N_{9}\\,E_{8,9}\\,{}_{6}[M_{8}^{0}]^{7}\\,N_{10}\\,E_{9,10}\\,{}_{7}[M_{9}^{\\frac{7\\pi}{4}}]^{8}\\,N_{11}\\,E_{10,11}\\,{}_{8}[M_{10}^{0}]^{9}\\,N_{12}\\,E_{11,12}\\,{}_{9}[M_{11}^{0}]^{10}\\,N_{18}\\,E_{1,18}\\,E_{1,12}\\,{}_{11,3}[M_{1}^{\\frac{\\pi}{4}}]\\,N_{13}\\,E_{12,13}\\,{}_{10}[M_{12}^{0}]^{11}\\)...(63 more commands)' + >>> from graphix.command import CommandKind >>> pattern.to_latex(target={CommandKind.M}) - '\\({}_{17}[M_{19}]^{7}\\,{}_{10}[M_{15}^{-\\frac{\\pi}{4}}]^{11}\\,{}_{17,12}[M_{14}]^{13}\\,{}_{17,9}[M_{11}]^{10}\\,{}_{0,1,5,10,13}[M_{7}^{-\\frac{\\pi}{4}}]^{17}\\,{}_{1}[M_{13}^{-\\frac{7\\pi}{4}}]^{12}\\,{}_{8}[M_{10}^{-\\frac{7\\pi}{4}}]^{9}\\,{}_{17}[M_{12}]^{1}\\,{}_{6}[M_{9}]^{8}\\,{}_{8,3}[M_{1}^{-\\frac{\\pi}{4}}]\\,{}_{5}[M_{8}^{-\\frac{\\pi}{4}}]^{6}\\,{}_{17,4}[M_{6}]^{5}\\,[M_{17}]^{0}\\,{}_{3}[M_{5}^{-\\frac{7\\pi}{4}}]^{4}\\,M_{0}\\,{}_{2}[M_{4}]^{3}\\,[M_{3}]^{2}\\,M_{2}\\)' + '\\({}_{27}[M_{31}^{0}]^{28}\\,[M_{29}^{0}]^{0}\\,{}_{26}[M_{28}^{\\frac{3\\pi}{2}}]^{27}\\,{}_{17}[M_{21}^{0}]^{20}\\,{}_{25}[M_{27}^{0}]^{26}\\,{}_{26,19,15,7}[M_{0}^{\\frac{7\\pi}{4}}]\\,{}_{16}[M_{20}^{0}]^{17}\\,{}_{23}[M_{26}^{0}]^{25}\\,{}_{22}[M_{25}^{0}]^{23}\\,{}_{15}[M_{17}^{\\frac{7\\pi}{4}}]^{16}\\,{}_{19}[M_{23}^{\\frac{\\pi}{4}}]^{22}\\,{}_{14}[M_{16}^{0}]^{15}\\,{}_{13}[M_{15}^{0}]^{14}\\,{}_{18}[M_{22}^{0}]^{19}\\,{}_{12}[M_{14}^{0}]^{13}\\,{}_{1}[M_{19}^{0}]^{18}\\,{}_{11}[M_{13}^{\\frac{\\pi}{4}}]^{12}\\,[M_{18}^{0}]^{1}\\,{}_{10}[M_{12}^{0}]^{11}\\,{}_{11,3}[M_{1}^{\\frac{\\pi}{4}}]\\,{}_{9}[M_{11}^{0}]^{10}\\,{}_{8}[M_{10}^{0}]^{9}\\,{}_{7}[M_{9}^{\\frac{7\\pi}{4}}]^{8}\\,{}_{6}[M_{8}^{0}]^{7}\\,{}_{5}[M_{7}^{0}]^{6}\\,{}_{4}[M_{6}^{0}]^{5}\\,{}_{3}[M_{5}^{\\frac{\\pi}{4}}]^{4}\\,{}_{2}[M_{4}^{0}]^{3}\\,[M_{3}^{0}]^{2}\\,M_{2}^{0}\\)' >>> pattern.to_latex(target={CommandKind.X, CommandKind.Z}) - '\\(Z_{16}^{11}\\,Z_{18}^{13}\\,Z_{20}^{7}\\,X_{16}^{15}\\,X_{18}^{14}\\,X_{20}^{19}\\)' + '\\(X_{24}^{21}\\,Z_{24}^{20}\\,X_{32}^{31}\\,Z_{32}^{28}\\,X_{30}^{29}\\,Z_{30}^{0}\\)' """ return pattern_to_str(self, OutputFormat.LaTeX, left_to_right, limit, target) @@ -487,13 +488,14 @@ def to_unicode( >>> circuit.ccx(0, 1, 2) >>> pattern = circuit.transpile().pattern >>> pattern.to_unicode() - 'Z₁₆¹¹ Z₁₈¹³ Z₂₀⁷ X₁₆¹⁵ X₁₈¹⁴ X₂₀¹⁹ ₁₇[M₁₉]⁷ ₁₀[M₁₅(-π/4)]¹¹ ₁₇₊₁₂[M₁₄]¹³ ₁₇₊₉[M₁₁]¹⁰ ₀₊₁₊₅₊₁₀₊₁₃[M₇(-π/4)]¹⁷ ₁[M₁₃(-7π/4)]¹² ₈[M₁₀(-7π/4)]⁹ ₁₇[M₁₂]¹ ₆[M₉]⁸ ₈₊₃[M₁(-π/4)] ₅[M₈(-π/4)]⁶ ₁₇₊₄[M₆]⁵ [M₁₇]⁰ ₃[M₅(-7π/4)]⁴ M₀ ₂[M₄]³ [M₃]² M₂ E₁₉₋₂₀ E₁₅₋₁₆ E₁₄₋₁₈ E₁₁₋₁₅ E₇₋₁₉ E₇₋₁₄ E₇₋₁₁ E₁₃₋₁₄ E₁₀₋₁₁ E₁₂₋₁₃ E₁₂₋₇ E₉₋₁₀ E₁₋₁₂ E₁₋₉ E₈₋₉...(27 more commands)' + 'X₂₄²¹ Z₂₄²⁰ X₃₂³¹ Z₃₂²⁸ X₃₀²⁹ Z₃₀⁰ ₂₇[M₃₁(0)]²⁸ E₃₁₋₃₂ N₃₂ [M₂₉(0)]⁰ E₂₉₋₃₀ N₃₀ ₂₆[M₂₈(3π/2)]²⁷ E₂₈₋₃₁ N₃₁ ₁₇[M₂₁(0)]²⁰ E₂₁₋₂₄ N₂₄ ₂₅[M₂₇(0)]²⁶ E₂₇₋₂₈ N₂₈ ₂₆₊₁₉₊₁₅₊₇[M₀(7π/4)] E₀₋₂₇ E₀₋₂₉ N₂₉ ₁₆[M₂₀(0)]¹⁷ E₂₀₋₂₁ N₂₁ ₂₃[M₂₆(0)]²⁵ E₂₆₋₂₇ N₂₇ ₂₂[M₂₅(0)]²³ E₂₅₋₂₆ N₂₆ ₁₅[M₁₇(7π/4)]¹⁶ E₁₇₋₂₀ N₂₀ ₁₉[M₂₃(π/4)]²² E₂₃₋₂₅...(63 more commands)' >>> pattern.to_unicode(left_to_right=True) - 'N₃ N₄ N₅ N₆ N₇ N₈ N₉ N₁₀ N₁₁ N₁₂ N₁₃ N₁₄ N₁₅ N₁₆ N₁₇ N₁₈ N₁₉ N₂₀ E₂₋₃ E₃₋₄ E₄₋₅ E₄₋₁ E₀₋₁₇ E₅₋₆ E₁₇₋₇ E₆₋₈ E₆₋₇ E₈₋₉ E₁₋₉ E₁₋₁₂ E₉₋₁₀ E₁₂₋₇ E₁₂₋₁₃ E₁₀₋₁₁ E₁₃₋₁₄ E₇₋₁₁ E₇₋₁₄ E₇₋₁₉ E₁₁₋₁₅...(27 more commands)' + 'N₃ E₂₋₃ M₂(0) N₄ E₃₋₄ [M₃(0)]² E₄₋₁ N₅ E₄₋₅ ₂[M₄(0)]³ N₆ E₅₋₆ ₃[M₅(π/4)]⁴ N₇ E₆₋₇ ₄[M₆(0)]⁵ N₈ E₇₋₈ ₅[M₇(0)]⁶ E₈₋₀ N₉ E₈₋₉ ₆[M₈(0)]⁷ N₁₀ E₉₋₁₀ ₇[M₉(7π/4)]⁸ N₁₁ E₁₀₋₁₁ ₈[M₁₀(0)]⁹ N₁₂ E₁₁₋₁₂ ₉[M₁₁(0)]¹⁰ N₁₈ E₁₋₁₈ E₁₋₁₂ ₁₁₊₃[M₁(π/4)] N₁₃ E₁₂₋₁₃ ₁₀[M₁₂(0)]¹¹...(63 more commands)' + >>> from graphix.command import CommandKind >>> pattern.to_unicode(target={CommandKind.M}) - '₁₇[M₁₉]⁷ ₁₀[M₁₅(-π/4)]¹¹ ₁₇₊₁₂[M₁₄]¹³ ₁₇₊₉[M₁₁]¹⁰ ₀₊₁₊₅₊₁₀₊₁₃[M₇(-π/4)]¹⁷ ₁[M₁₃(-7π/4)]¹² ₈[M₁₀(-7π/4)]⁹ ₁₇[M₁₂]¹ ₆[M₉]⁸ ₈₊₃[M₁(-π/4)] ₅[M₈(-π/4)]⁶ ₁₇₊₄[M₆]⁵ [M₁₇]⁰ ₃[M₅(-7π/4)]⁴ M₀ ₂[M₄]³ [M₃]² M₂' + '₂₇[M₃₁(0)]²⁸ [M₂₉(0)]⁰ ₂₆[M₂₈(3π/2)]²⁷ ₁₇[M₂₁(0)]²⁰ ₂₅[M₂₇(0)]²⁶ ₂₆₊₁₉₊₁₅₊₇[M₀(7π/4)] ₁₆[M₂₀(0)]¹⁷ ₂₃[M₂₆(0)]²⁵ ₂₂[M₂₅(0)]²³ ₁₅[M₁₇(7π/4)]¹⁶ ₁₉[M₂₃(π/4)]²² ₁₄[M₁₆(0)]¹⁵ ₁₃[M₁₅(0)]¹⁴ ₁₈[M₂₂(0)]¹⁹ ₁₂[M₁₄(0)]¹³ ₁[M₁₉(0)]¹⁸ ₁₁[M₁₃(π/4)]¹² [M₁₈(0)]¹ ₁₀[M₁₂(0)]¹¹ ₁₁₊₃[M₁(π/4)] ₉[M₁₁(0)]¹⁰ ₈[M₁₀(0)]⁹ ₇[M₉(7π/4)]⁸ ₆[M₈(0)]⁷ ₅[M₇(0)]⁶ ₄[M₆(0)]⁵ ₃[M₅(π/4)]⁴ ₂[M₄(0)]³ [M₃(0)]² M₂(0)' >>> pattern.to_unicode(target={CommandKind.X, CommandKind.Z}) - 'Z₁₆¹¹ Z₁₈¹³ Z₂₀⁷ X₁₆¹⁵ X₁₈¹⁴ X₂₀¹⁹' + 'X₂₄²¹ Z₂₄²⁰ X₃₂³¹ Z₃₂²⁸ X₃₀²⁹ Z₃₀⁰' """ return pattern_to_str(self, OutputFormat.Unicode, left_to_right, limit, target) @@ -557,7 +559,7 @@ def shift_signals(self, method: str = "direct") -> dict[int, set[int]]: ---------- method : str, optional 'direct' shift_signals is executed on a conventional Pattern sequence. - 'mc' shift_signals is done using the original algorithm on the measurement calculus paper. + 'mc' shift_signals is done using the original algorithm in the measurement calculus paper. Returns ------- diff --git a/graphix/qasm3_exporter.py b/graphix/qasm3_exporter.py index 8b0066a4b..039d95d32 100644 --- a/graphix/qasm3_exporter.py +++ b/graphix/qasm3_exporter.py @@ -22,7 +22,7 @@ from graphix.instruction import InstructionType -def circuit_to_qasm3(circuit: Circuit) -> str: +def circuit_to_qasm3(circuit: Circuit, transpile: bool = True) -> str: """Export circuit instructions to OpenQASM 3.0 representation. Returns @@ -30,10 +30,12 @@ def circuit_to_qasm3(circuit: Circuit) -> str: str The OpenQASM 3.0 string representation of the circuit. """ - return "\n".join(circuit_to_qasm3_lines(circuit)) + if transpile: + circuit = circuit.transpile_j_to_rzh().transpile_measurements_to_z_axis() + return "\n".join(circuit_to_qasm3_lines(circuit, transpile)) -def circuit_to_qasm3_lines(circuit: Circuit) -> Iterator[str]: +def circuit_to_qasm3_lines(circuit: Circuit, transpile: bool = True) -> Iterator[str]: """Export circuit instructions to line-by-line OpenQASM 3.0 representation. Returns @@ -41,6 +43,10 @@ def circuit_to_qasm3_lines(circuit: Circuit) -> Iterator[str]: Iterator[str] The OpenQASM 3.0 lines that represent the circuit. """ + if transpile: + circuit = circuit.transpile_j_to_rzh().transpile_measurements_to_z_axis() + if any(instr.kind == InstructionKind.J for instr in circuit.instruction): + raise ValueError("J gates must be decomposed before QASM3 export using `Circuit.transpile_j_to_rzh`.") yield "OPENQASM 3;" yield 'include "stdgates.inc";' yield f"qubit[{circuit.width}] q;" @@ -72,7 +78,23 @@ def angle_to_qasm3(angle: ParameterizedAngle) -> str: def instruction_to_qasm3(instruction: InstructionType) -> str: - """Get the OpenQASM3 representation of a single circuit instruction.""" + """Get the OpenQASM3 representation of a single circuit instruction. + + Parameters + ---------- + instruction : Instruction + The instruction to convert. + + Returns + ------- + str + The OpenQASM3 representation of the instruction. + + Raises + ------ + ValueError + If the instruction is not supported by OpenQASM3. + """ match instruction.kind: case InstructionKind.M: if instruction.axis != Axis.Z: @@ -85,6 +107,8 @@ def instruction_to_qasm3(instruction: InstructionType) -> str: return qasm3_gate_call( instruction.kind.name.lower(), args=[angle], operands=[qasm3_qubit(instruction.target)] ) + case InstructionKind.J: + raise ValueError("J gate should have been removed by `_decompose_j_gates`.") case InstructionKind.H | InstructionKind.S | InstructionKind.X | InstructionKind.Y | InstructionKind.Z: return qasm3_gate_call(instruction.kind.name.lower(), [qasm3_qubit(instruction.target)]) case InstructionKind.I: diff --git a/graphix/random_objects.py b/graphix/random_objects.py index d5c89f788..c966c4ae3 100644 --- a/graphix/random_objects.py +++ b/graphix/random_objects.py @@ -377,6 +377,7 @@ def rand_circuit( depth: int, rng: Generator | None = None, *, + use_j: bool = True, use_cz: bool = True, use_rzz: bool = False, use_ccx: bool = False, @@ -393,6 +394,8 @@ 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_j : bool, optional + If ``True`` add J gates in each layer (default: ``False``). use_cz : bool, optional If ``True`` add CZ gates in each layer (default: ``True``). use_rzz : bool, optional @@ -424,6 +427,7 @@ def rand_circuit( circuit.z, circuit.y, *parametric_gate_choice, + *((functools.partial(circuit.j, angle=ANGLE_PI / 4),) if use_j else ()), ) for _ in range(depth): for j, k in _genpair(nqubits, 2, rng): @@ -437,7 +441,7 @@ def rand_circuit( if use_ccx: for j, k, l in _gentriplet(nqubits, 2, rng): circuit.ccx(j, k, l) - for j, k in _genpair(nqubits, 4, rng): + for j, k in _genpair(nqubits, 2, rng): circuit.swap(j, k) for j in range(nqubits): ind = rng.integers(len(gate_choice)) diff --git a/graphix/sim/density_matrix.py b/graphix/sim/density_matrix.py index cb5570e2a..dd37403b7 100644 --- a/graphix/sim/density_matrix.py +++ b/graphix/sim/density_matrix.py @@ -137,6 +137,7 @@ def add_nodes(self, nqubit: int, data: Data) -> None: - A multi-qubit state vector of dimension :math:`2^n` initializes the new nodes jointly. - A density matrix must have shape :math:`2^n \times 2^n`, and is used to jointly initialize the new nodes. + - The type of nodes to be added is inferred from the type of the existing ``DensityMatrix``. Notes ----- @@ -259,6 +260,8 @@ def tensor(self, other: DensityMatrix) -> None: """ if not isinstance(other, DensityMatrix): other = DensityMatrix(other) + if self.rho.dtype == np.object_ and other.rho.dtype != np.object_: + other.rho = other.rho.astype(np.object_, copy=False) self.rho = kron(self.rho, other.rho) def cnot(self, edge: tuple[int, int]) -> None: diff --git a/graphix/sim/statevec.py b/graphix/sim/statevec.py index 1f7dd23e6..efb24bfe1 100644 --- a/graphix/sim/statevec.py +++ b/graphix/sim/statevec.py @@ -165,6 +165,7 @@ def add_nodes(self, nqubit: int, data: Data) -> None: - A single-qubit state vector will be broadcast to all nodes. - A multi-qubit state vector of dimension :math:`2^n`, where :math:`n = \mathrm{len}(nodes)`, initializes the new nodes jointly. + - The type of nodes to be added is inferred from the type of the existing ``Statevec``. Notes ----- @@ -301,6 +302,8 @@ def tensor(self, other: Statevec) -> None: psi_other = other.psi.flatten() total_num = len(self.dims()) + len(other.dims()) + if psi_self.dtype == np.object_ and psi_other.dtype != np.object_: + psi_other = psi_other.astype(np.object_, copy=False) self.psi = kron(psi_self, psi_other).reshape((2,) * total_num) def cnot(self, qubits: tuple[int, int]) -> None: diff --git a/graphix/transpiler.py b/graphix/transpiler.py index c009b62b0..d3f40fab3 100644 --- a/graphix/transpiler.py +++ b/graphix/transpiler.py @@ -7,7 +7,9 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING, SupportsFloat +from typing import TYPE_CHECKING, Generic, SupportsFloat, TypeVar + +import networkx as nx # assert_never introduced in Python 3.11 # override introduced in Python 3.12 @@ -15,12 +17,13 @@ from graphix import command, instruction, parameter from graphix.branch_selector import BranchSelector, RandomBranchSelector -from graphix.command import E, M, N, X, Z +from graphix.flow.core import CausalFlow, _corrections_to_partial_order_layers from graphix.fundamentals import ANGLE_PI, Axis from graphix.instruction import InstructionKind, InstructionVisitor -from graphix.measurements import Measurement, PauliMeasurement +from graphix.measurements import BlochMeasurement, Measurement, Outcome, PauliMeasurement +from graphix.opengraph import OpenGraph from graphix.ops import Ops -from graphix.pattern import Pattern +from graphix.optimization import StandardizedPattern from graphix.sim.statevec import Statevec, StatevectorBackend if TYPE_CHECKING: @@ -28,25 +31,39 @@ from numpy.random import Generator - from graphix.command import CommandType from graphix.fundamentals import ParameterizedAngle - from graphix.instruction import InstructionType, InstructionTypeWithoutRZZ + from graphix.instruction import InstructionType from graphix.parameter import ExpressionOrFloat, Parameter + from graphix.pattern import Pattern from graphix.sim import Data from graphix.sim.base_backend import Matrix +_R = TypeVar("_R", bound="Pattern | CausalFlow[BlochMeasurement]") +_CO = TypeVar("_CO", bound="tuple[int, ...] | dict[int, command.M]") + @dataclass -class TranspileResult: +class TranspileResult(Generic[_R, _CO]): """ The result of a transpilation. - pattern : :class:`graphix.pattern.Pattern` object - classical_outputs : tuple[int,...], index of nodes measured with *M* gates + result : :class:`graphix.pattern.Pattern` or :class:`graphix.flow.core.CausalFlow` object + classical_outputs : tuple[int, ...] | dict[int, command.M], index of nodes measured with *M* gates, with associated M commands as dictionary. + """ - pattern: Pattern - classical_outputs: tuple[int, ...] + result: _R + classical_outputs: _CO + + @property + def pattern(self: TranspileResult[Pattern, _CO]) -> Pattern: + """Return pattern from `TranspileResult` if any.""" + return self.result + + @property + def flow(self: TranspileResult[CausalFlow[BlochMeasurement], _CO]) -> CausalFlow[BlochMeasurement]: + """Return causal flow from TranspileResult if any.""" + return self.result @dataclass @@ -144,6 +161,8 @@ def add(self, instr: InstructionType) -> None: self.ry(instr.target, instr.angle) case InstructionKind.RZ: self.rz(instr.target, instr.angle) + case InstructionKind.J: + self.j(instr.target, instr.angle) case _: assert_never(instr.kind) @@ -295,6 +314,19 @@ def rz(self, qubit: int, angle: ParameterizedAngle) -> None: assert qubit in self.active_qubits self.instruction.append(instruction.RZ(target=qubit, angle=angle)) + def j(self, qubit: int, angle: ParameterizedAngle) -> None: + """Apply a J rotation gate. + + Parameters + ---------- + qubit : int + target qubit + angle : ParameterizedAngle + rotation angle in units of π + """ + assert qubit in self.active_qubits + self.instruction.append(instruction.J(target=qubit, angle=angle)) + def r(self, qubit: int, axis: Axis, angle: ParameterizedAngle) -> None: """Apply a rotation gate on the given axis. @@ -388,559 +420,91 @@ def m(self, qubit: int, axis: Axis) -> None: self.instruction.append(instruction.M(target=qubit, axis=axis)) self.active_qubits.remove(qubit) - def transpile(self) -> TranspileResult: - """Transpile the circuit to a pattern. - - Returns - ------- - result : :class:`TranspileResult` object - """ - n_node = self.width - out: list[int | None] = list(range(self.width)) - pattern = Pattern(input_nodes=list(range(self.width))) - classical_outputs = [] - for instr in _transpile_rzz(self.instruction): - match instr.kind: - case 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) - case instruction.InstructionKind.CNOT: - ancilla = [n_node, n_node + 1] - control = _check_target(out, instr.control) - target = _check_target(out, instr.target) - out[instr.control], out[instr.target], seq = self._cnot_command(control, target, ancilla) - pattern.extend(seq) - n_node += 2 - case instruction.InstructionKind.SWAP: - target0 = _check_target(out, instr.targets[0]) - target1 = _check_target(out, instr.targets[1]) - out[instr.targets[0]], out[instr.targets[1]] = ( - target1, - target0, - ) - case instruction.InstructionKind.I: - pass - case instruction.InstructionKind.H: - single_ancilla = n_node - target = _check_target(out, instr.target) - out[instr.target], seq = self._h_command(target, single_ancilla) - pattern.extend(seq) - n_node += 1 - case instruction.InstructionKind.S: - ancilla = [n_node, n_node + 1] - target = _check_target(out, instr.target) - out[instr.target], seq = self._s_command(target, ancilla) - pattern.extend(seq) - n_node += 2 - case instruction.InstructionKind.X: - ancilla = [n_node, n_node + 1] - target = _check_target(out, instr.target) - out[instr.target], seq = self._x_command(target, ancilla) - pattern.extend(seq) - n_node += 2 - case instruction.InstructionKind.Y: - ancilla = [n_node, n_node + 1, n_node + 2, n_node + 3] - target = _check_target(out, instr.target) - out[instr.target], seq = self._y_command(target, ancilla) - pattern.extend(seq) - n_node += 4 - case instruction.InstructionKind.Z: - ancilla = [n_node, n_node + 1] - target = _check_target(out, instr.target) - out[instr.target], seq = self._z_command(target, ancilla) - pattern.extend(seq) - n_node += 2 - case instruction.InstructionKind.RX: - ancilla = [n_node, n_node + 1] - target = _check_target(out, instr.target) - out[instr.target], seq = self._rx_command(target, ancilla, instr.angle) - pattern.extend(seq) - n_node += 2 - case instruction.InstructionKind.RY: - ancilla = [n_node, n_node + 1, n_node + 2, n_node + 3] - target = _check_target(out, instr.target) - out[instr.target], seq = self._ry_command(target, ancilla, instr.angle) - pattern.extend(seq) - n_node += 4 - case instruction.InstructionKind.RZ: - ancilla = [n_node, n_node + 1] - target = _check_target(out, instr.target) - out[instr.target], seq = self._rz_command(target, ancilla, instr.angle) - pattern.extend(seq) - n_node += 2 - case instruction.InstructionKind.CCX: - ancilla = [n_node + i for i in range(18)] - control0 = _check_target(out, instr.controls[0]) - control1 = _check_target(out, instr.controls[1]) - target = _check_target(out, instr.target) - ( - out[instr.controls[0]], - out[instr.controls[1]], - out[instr.target], - seq, - ) = self._ccx_command( - control0, - control1, - target, - ancilla, - ) - pattern.extend(seq) - n_node += 18 - case instruction.InstructionKind.M: - target = _check_target(out, instr.target) - seq = self._m_command(target, instr.axis) - pattern.extend(seq) - classical_outputs.append(target) - out[instr.target] = None - case _: - assert_never(instr.kind) - output_nodes = [node for node in out if node is not None] - pattern.reorder_output_nodes(output_nodes) - return TranspileResult(pattern, tuple(classical_outputs)) - - @classmethod - def _cnot_command( - cls, control_node: int, target_node: int, ancilla: Sequence[int] - ) -> tuple[int, int, list[command.CommandType]]: - """MBQC commands for CNOT gate. + def transpile_to_cflow(self) -> TranspileResult[CausalFlow[BlochMeasurement], dict[int, command.M]]: + """Transpile a circuit via J-∧z decomposition to a causal flow. Parameters ---------- - control_node : int - control node on graph - target : int - target node on graph - ancilla : list of two ints - ancilla node indices to be added to graph + self: the circuit to transpile. Returns ------- - control_out : int - control node on graph after the gate - target_out : int - target node on graph after the gate - commands : list - list of MBQC commands - """ - assert len(ancilla) == 2 - seq: list[CommandType] = [N(node=ancilla[0]), N(node=ancilla[1])] - seq.extend( - ( - E(nodes=(target_node, ancilla[0])), - E(nodes=(control_node, ancilla[0])), - E(nodes=(ancilla[0], ancilla[1])), - M(node=target_node), - M(node=ancilla[0], s_domain={target_node}), - X(node=ancilla[1], domain={ancilla[0]}), - Z(node=ancilla[1], domain={target_node}), - Z(node=control_node, domain={target_node}), - ) - ) - return control_node, ancilla[1], seq - - @classmethod - def _cz_command(cls, target_1: int, target_2: int) -> list[CommandType]: - """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, axis: Axis) -> list[CommandType]: - """MBQC commands for measuring qubit. - - Parameters - ---------- - input_node : int - target node on graph - axis : Axis - measurement basis - - Returns - ------- - commands : list - list of MBQC commands - """ - # `measurement.angle` and `M.angle` are both expressed in units of π. - return [M(input_node, PauliMeasurement(axis))] - - @classmethod - def _h_command(cls, input_node: int, ancilla: int) -> tuple[int, list[CommandType]]: - """MBQC commands for Hadamard gate. - - Parameters - ---------- - input_node : int - target node on graph - ancilla : int - ancilla node index to be added - - Returns - ------- - out_node : int - control node on graph after the gate - commands : list - list of MBQC commands - """ - seq: list[CommandType] = [N(node=ancilla)] - seq.extend((E(nodes=(input_node, ancilla)), M(node=input_node), X(node=ancilla, domain={input_node}))) - return ancilla, seq - - @classmethod - def _s_command(cls, input_node: int, ancilla: Sequence[int]) -> tuple[int, list[command.CommandType]]: - """MBQC commands for S gate. - - Parameters - ---------- - input_node : int - input node index - ancilla : list of two ints - ancilla node indices to be added to graph - - Returns - ------- - out_node : int - control node on graph after the gate - commands : list - list of MBQC commands - """ - assert len(ancilla) == 2 - seq: list[CommandType] = [N(node=ancilla[0]), command.N(node=ancilla[1])] - seq.extend( - ( - E(nodes=(input_node, ancilla[0])), - E(nodes=(ancilla[0], ancilla[1])), - M(input_node, -Measurement.Y), - M(node=ancilla[0], s_domain={input_node}), - X(node=ancilla[1], domain={ancilla[0]}), - Z(node=ancilla[1], domain={input_node}), - ) - ) - return ancilla[1], seq - - @classmethod - def _x_command(cls, input_node: int, ancilla: Sequence[int]) -> tuple[int, list[command.CommandType]]: - """MBQC commands for Pauli X gate. - - Parameters - ---------- - input_node : int - input node index - ancilla : list of two ints - ancilla node indices to be added to graph - - Returns - ------- - out_node : int - control node on graph after the gate - commands : list - list of MBQC commands - """ - assert len(ancilla) == 2 - seq: list[CommandType] = [N(node=ancilla[0]), N(node=ancilla[1])] - seq.extend( - ( - E(nodes=(input_node, ancilla[0])), - E(nodes=(ancilla[0], ancilla[1])), - M(node=input_node), - M(ancilla[0], -Measurement.X, s_domain={input_node}), - X(node=ancilla[1], domain={ancilla[0]}), - Z(node=ancilla[1], domain={input_node}), - ) - ) - return ancilla[1], seq - - @classmethod - def _y_command(cls, input_node: int, ancilla: Sequence[int]) -> tuple[int, list[command.CommandType]]: - """MBQC commands for Pauli Y gate. - - Parameters - ---------- - input_node : int - input node index - ancilla : list of four ints - ancilla node indices to be added to graph - - Returns - ------- - out_node : int - control node on graph after the gate - commands : list - list of MBQC commands - """ - assert len(ancilla) == 4 - seq: list[CommandType] = [N(node=ancilla[0]), N(node=ancilla[1])] - seq.extend([N(node=ancilla[2]), N(node=ancilla[3])]) - seq.extend( - ( - E(nodes=(input_node, ancilla[0])), - E(nodes=(ancilla[0], ancilla[1])), - E(nodes=(ancilla[1], ancilla[2])), - E(nodes=(ancilla[2], ancilla[3])), - M(input_node, Measurement.Y), - M(ancilla[0], -Measurement.X, s_domain={input_node}), - M(ancilla[1], -Measurement.Y, s_domain={ancilla[0]}, t_domain={input_node}), - M(node=ancilla[2], s_domain={ancilla[1]}, t_domain={ancilla[0]}), - X(node=ancilla[3], domain={ancilla[2]}), - Z(node=ancilla[3], domain={ancilla[1]}), - ) - ) - return ancilla[3], seq - - @classmethod - def _z_command(cls, input_node: int, ancilla: Sequence[int]) -> tuple[int, list[command.CommandType]]: - """MBQC commands for Pauli Z gate. - - Parameters - ---------- - input_node : int - input node index - ancilla : list of two ints - ancilla node indices to be added to graph - - Returns - ------- - out_node : int - control node on graph after the gate - commands : list - list of MBQC commands - """ - assert len(ancilla) == 2 - seq: list[CommandType] = [N(node=ancilla[0]), N(node=ancilla[1])] - seq.extend( - ( - E(nodes=(input_node, ancilla[0])), - E(nodes=(ancilla[0], ancilla[1])), - M(input_node, -Measurement.X), - M(node=ancilla[0], s_domain={input_node}), - X(node=ancilla[1], domain={ancilla[0]}), - Z(node=ancilla[1], domain={input_node}), - ) - ) - return ancilla[1], seq - - @classmethod - def _rx_command( - cls, input_node: int, ancilla: Sequence[int], angle: ParameterizedAngle - ) -> tuple[int, list[command.CommandType]]: - """MBQC commands for X rotation gate. - - Parameters - ---------- - input_node : int - input node index - ancilla : list of two ints - ancilla node indices to be added to graph - angle : ParameterizedAngle - measurement angle in units of π - - Returns - ------- - out_node : int - control node on graph after the gate - commands : list - list of MBQC commands - """ - assert len(ancilla) == 2 - seq: list[CommandType] = [N(node=ancilla[0]), N(node=ancilla[1])] - seq.extend( - ( - E(nodes=(input_node, ancilla[0])), - E(nodes=(ancilla[0], ancilla[1])), - M(node=input_node), - M(ancilla[0], Measurement.XY(-angle), s_domain={input_node}), - X(node=ancilla[1], domain={ancilla[0]}), - Z(node=ancilla[1], domain={input_node}), - ) - ) - return ancilla[1], seq - - @classmethod - def _ry_command( - cls, input_node: int, ancilla: Sequence[int], angle: ParameterizedAngle - ) -> tuple[int, list[command.CommandType]]: - """MBQC commands for Y rotation gate. - - Parameters - ---------- - input_node : int - input node index - ancilla : list of four ints - ancilla node indices to be added to graph - angle : ParameterizedAngle - rotation angle in units of π - - Returns - ------- - out_node : int - control node on graph after the gate - commands : list - list of MBQC commands - """ - assert len(ancilla) == 4 - seq: list[CommandType] = [N(node=ancilla[0]), N(node=ancilla[1])] - seq.extend([N(node=ancilla[2]), N(node=ancilla[3])]) - seq.extend( - ( - E(nodes=(input_node, ancilla[0])), - E(nodes=(ancilla[0], ancilla[1])), - E(nodes=(ancilla[1], ancilla[2])), - E(nodes=(ancilla[2], ancilla[3])), - M(input_node, Measurement.Y), - M(ancilla[0], Measurement.XY(-angle), s_domain={input_node}), - M(ancilla[1], -Measurement.Y, s_domain={ancilla[0]}, t_domain={input_node}), - M(node=ancilla[2], s_domain={ancilla[1]}, t_domain={ancilla[0]}), - X(node=ancilla[3], domain={ancilla[2]}), - Z(node=ancilla[3], domain={ancilla[1]}), - ) - ) - return ancilla[3], seq - - @classmethod - def _rz_command( - cls, input_node: int, ancilla: Sequence[int], angle: ParameterizedAngle - ) -> tuple[int, list[command.CommandType]]: - """MBQC commands for Z rotation gate. - - Parameters - ---------- - input_node : int - input node index - ancilla : list of two ints - ancilla node indices to be added to graph - angle : ParameterizedAngle - measurement angle in units of π - - Returns - ------- - out_node : int - node on graph after the gate - commands : list - list of MBQC commands - """ - assert len(ancilla) == 2 - seq: list[CommandType] = [N(node=ancilla[0]), N(node=ancilla[1])] # assign new qubit labels - seq.extend( - ( - E(nodes=(input_node, ancilla[0])), - E(nodes=(ancilla[0], ancilla[1])), - M(input_node, Measurement.XY(-angle)), - M(node=ancilla[0], s_domain={input_node}), - X(node=ancilla[1], domain={ancilla[0]}), - Z(node=ancilla[1], domain={input_node}), - ) + the result of the transpilation: a causal flow and classical outputs. + + Raises + ------ + IllformedCircuitError: if the pattern is ill-formed (operation on already measured node) + """ + indices: list[int | None] = list(range(self.width)) + n_nodes = self.width + measurements: dict[int, BlochMeasurement] = {} + classical_outputs: dict[int, command.M] = {} + inputs = list(range(n_nodes)) + graph: nx.Graph[int] = nx.Graph() + graph.add_nodes_from(inputs) + x_corrections: dict[int, set[int]] = {} + for instr in instructions_to_jcz(self.instruction): + match instr.kind: + case InstructionKind.M: + target = indices[instr.target] + if target is None: + raise IllformedCircuitError + classical_outputs[target] = command.M(target, PauliMeasurement(instr.axis)) + indices[instr.target] = None + continue + case InstructionKind.J: + target = indices[instr.target] + if target is None: + raise IllformedCircuitError + graph.add_edge(target, n_nodes) # Also adds nodes + measurements[target] = Measurement.XY(normalize_angle(-instr.angle)) + indices[instr.target] = n_nodes + x_corrections[target] = {n_nodes} # X correction on ancilla + n_nodes += 1 + continue + case InstructionKind.CZ: + t0, t1 = instr.targets + i0, i1 = indices[t0], indices[t1] + if i0 is None or i1 is None: + raise IllformedCircuitError + # If edge exists, remove it; else, add it + if graph.has_edge(i0, i1): + graph.remove_edge(i0, i1) + else: + graph.add_edge(i0, i1) + continue + case _: + assert_never(instr.kind) + outputs = [i for i in indices if i is not None] + outputs.extend(classical_outputs.keys()) # Necessary for flow-finding step + og = OpenGraph( + graph=graph, + input_nodes=inputs, + output_nodes=outputs, + measurements=measurements, ) - return ancilla[1], seq - - @classmethod - def _ccx_command( - cls, - control_node1: int, - control_node2: int, - target_node: int, - ancilla: Sequence[int], - ) -> tuple[int, int, int, list[command.CommandType]]: - """MBQC commands for CCX gate. + z_corrections: dict[int, set[int]] = {} + for node, correctors in x_corrections.items(): + (corrector,) = correctors + z_targets = set(graph.neighbors(corrector)) - {node} + if z_targets: + z_corrections[node] = z_targets + partial_order_layers = _corrections_to_partial_order_layers(og, x_corrections, z_corrections) + f: CausalFlow[BlochMeasurement] = CausalFlow(og, x_corrections, partial_order_layers) + return TranspileResult(f, classical_outputs) + + def transpile(self) -> TranspileResult[Pattern, tuple[int, ...]]: + """Transpile a circuit via J-∧z decomposition to a pattern. Parameters ---------- - control_node1 : int - first control node on graph - control_node2 : int - second control node on graph - target_node : int - target node on graph - ancilla : list of int - ancilla node indices to be added to graph + self: the circuit to transpile. Returns ------- - control_out1 : int - first control node on graph after the gate - control_out2 : int - second control node on graph after the gate - target_out : int - target node on graph after the gate - commands : list - list of MBQC commands + the result of the transpilation: a pattern and classical outputs. """ - assert len(ancilla) == 18 - seq: list[CommandType] = [N(node=ancilla[i]) for i in range(18)] # assign new qubit labels - seq.extend( - ( - E(nodes=(target_node, ancilla[0])), - E(nodes=(ancilla[0], ancilla[1])), - E(nodes=(ancilla[1], ancilla[2])), - E(nodes=(ancilla[1], control_node2)), - E(nodes=(control_node1, ancilla[14])), - E(nodes=(ancilla[2], ancilla[3])), - E(nodes=(ancilla[14], ancilla[4])), - E(nodes=(ancilla[3], ancilla[5])), - E(nodes=(ancilla[3], ancilla[4])), - E(nodes=(ancilla[5], ancilla[6])), - E(nodes=(control_node2, ancilla[6])), - E(nodes=(control_node2, ancilla[9])), - E(nodes=(ancilla[6], ancilla[7])), - E(nodes=(ancilla[9], ancilla[4])), - E(nodes=(ancilla[9], ancilla[10])), - E(nodes=(ancilla[7], ancilla[8])), - E(nodes=(ancilla[10], ancilla[11])), - E(nodes=(ancilla[4], ancilla[8])), - E(nodes=(ancilla[4], ancilla[11])), - E(nodes=(ancilla[4], ancilla[16])), - E(nodes=(ancilla[8], ancilla[12])), - E(nodes=(ancilla[11], ancilla[15])), - E(nodes=(ancilla[12], ancilla[13])), - E(nodes=(ancilla[16], ancilla[17])), - M(node=target_node), - M(node=ancilla[0], s_domain={target_node}), - M(node=ancilla[1], s_domain={ancilla[0]}, t_domain={target_node}), - M(node=control_node1), - M(ancilla[2], Measurement.XY(-7 * ANGLE_PI / 4), s_domain={ancilla[1]}, t_domain={ancilla[0]}), - M(node=ancilla[14], s_domain={control_node1}), - M(node=ancilla[3], s_domain={ancilla[2]}, t_domain={ancilla[1], ancilla[14]}), - M(ancilla[5], Measurement.XY(-ANGLE_PI / 4), s_domain={ancilla[3]}, t_domain={ancilla[2]}), - M(control_node2, Measurement.XY(-ANGLE_PI / 4), t_domain={ancilla[5], ancilla[0]}), - M(node=ancilla[6], s_domain={ancilla[5]}, t_domain={ancilla[3]}), - M(node=ancilla[9], s_domain={control_node2}, t_domain={ancilla[14]}), - M(ancilla[7], Measurement.XY(-7 * ANGLE_PI / 4), s_domain={ancilla[6]}, t_domain={ancilla[5]}), - M(ancilla[10], Measurement.XY(-7 * ANGLE_PI / 4), s_domain={ancilla[9]}, t_domain={control_node2}), - M( - ancilla[4], - Measurement.XY(-ANGLE_PI / 4), - s_domain={ancilla[14]}, - t_domain={control_node1, control_node2, ancilla[2], ancilla[7], ancilla[10]}, - ), - M(node=ancilla[8], s_domain={ancilla[7]}, t_domain={ancilla[14], ancilla[6]}), - M(node=ancilla[11], s_domain={ancilla[10]}, t_domain={ancilla[9], ancilla[14]}), - M(ancilla[12], Measurement.XY(-ANGLE_PI / 4), s_domain={ancilla[8]}, t_domain={ancilla[7]}), - M( - node=ancilla[16], - s_domain={ancilla[4]}, - t_domain={ancilla[14]}, - ), - X(node=ancilla[17], domain={ancilla[16]}), - X(node=ancilla[15], domain={ancilla[11]}), - X(node=ancilla[13], domain={ancilla[12]}), - Z(node=ancilla[17], domain={ancilla[4]}), - Z(node=ancilla[15], domain={ancilla[10]}), - Z(node=ancilla[13], domain={ancilla[8]}), - ) - ) - return ancilla[17], ancilla[15], ancilla[13], seq + return _transpile_cflow_to_pattern(self.transpile_to_cflow()) def simulate_statevector( self, @@ -976,7 +540,7 @@ def simulate_statevector( else: backend.add_nodes(range(self.width), input_state) - classical_measures = [] + classical_measures: list[Outcome] = [] for i in range(len(self.instruction)): instr = self.instruction[i] @@ -1016,6 +580,8 @@ def evolve(op: Matrix, qargs: Iterable[int]) -> None: evolve_single(Ops.ry(instr.angle), instr.target) case instruction.InstructionKind.RZ: evolve_single(Ops.rz(instr.angle), instr.target) + case instruction.InstructionKind.J: + evolve_single(Ops.j(instr.angle), instr.target) case instruction.InstructionKind.RZZ: evolve(Ops.rzz(instr.angle), [instr.control, instr.target]) case instruction.InstructionKind.CCX: @@ -1085,15 +651,255 @@ def transpile_measurements_to_z_axis(self) -> Circuit: circuit.add(instr) return circuit + def transpile_j_to_rzh(self) -> Circuit: + """Return an equivalent circuit where all J gates have been replaced with RZ and H gates.""" + new_circuit = Circuit(self.width) + for instr in self.instruction: + match instr.kind: + case InstructionKind.J: + new_circuit.add(instruction.RZ(target=instr.target, angle=instr.angle)) + new_circuit.add(instruction.H(target=instr.target)) + case _: + new_circuit.add(instr) + return new_circuit -def _transpile_rzz(instructions: Iterable[InstructionType]) -> Iterator[InstructionTypeWithoutRZZ]: - for instr in instructions: - if instr.kind == InstructionKind.RZZ: - yield instruction.CNOT(control=instr.control, target=instr.target) - yield instruction.RZ(target=instr.target, angle=instr.angle) - yield instruction.CNOT(control=instr.control, target=instr.target) - else: - yield instr + +def decompose_rzz(instr: instruction.RZZ) -> Iterator[instruction.CNOT | instruction.RZ]: + """Yield a decomposition of RZZ(α) gate as CNOT(control, target)·Rz(target, α)·CNOT(control, target). + + Parameters + ---------- + instr: the RZZ instruction to decompose. + + Returns + ------- + the decomposition. + + """ + yield instruction.CNOT(target=instr.target, control=instr.control) + yield instruction.RZ(instr.target, instr.angle) + yield instruction.CNOT(target=instr.target, control=instr.control) + + +def decompose_ccx( + instr: instruction.CCX, +) -> Iterator[instruction.H | instruction.CNOT | instruction.RZ]: + """Yield a decomposition of the CCX gate into H, CNOT, T and T-dagger gates. + + This decomposition of the Toffoli gate can be found in + Michael A. Nielsen and Isaac L. Chuang, + Quantum Computation and Quantum Information, + Cambridge University Press, 2000 + (p. 182 in the 10th Anniversary Edition). + + Parameters + ---------- + instr: the CCX instruction to decompose. + + Returns + ------- + the decomposition. + + """ + c0, c1, t = instr.controls[0], instr.controls[1], instr.target + yield instruction.H(t) + yield instruction.CNOT(control=c1, target=t) + yield instruction.RZ(t, -ANGLE_PI / 4) + yield instruction.CNOT(control=c0, target=t) + yield instruction.RZ(t, ANGLE_PI / 4) + yield instruction.CNOT(control=c1, target=t) + yield instruction.RZ(t, -ANGLE_PI / 4) + yield instruction.CNOT(control=c0, target=t) + yield instruction.RZ(c1, -ANGLE_PI / 4) + yield instruction.RZ(t, ANGLE_PI / 4) + yield instruction.CNOT(control=c0, target=c1) + yield instruction.H(t) + yield instruction.RZ(c1, -ANGLE_PI / 4) + yield instruction.CNOT(control=c0, target=c1) + yield instruction.RZ(c0, ANGLE_PI / 4) + yield instruction.RZ(c1, ANGLE_PI / 2) + + +def decompose_cnot(instr: instruction.CNOT) -> Iterator[instruction.H | instruction.CZ]: + """Yield a decomposition of the CNOT gate as H·∧z·H. + + Vincent Danos, Elham Kashefi, Prakash Panangaden, The Measurement Calculus, 2007. + + Parameters + ---------- + instr: the CNOT instruction to decompose. + + Returns + ------- + the decomposition. + + """ + yield instruction.H(instr.target) + yield instruction.CZ((instr.control, instr.target)) + yield instruction.H(instr.target) + + +def decompose_swap(instr: instruction.SWAP) -> Iterator[instruction.CNOT]: + """Yield a decomposition of the SWAP gate as CNOT(0, 1)·CNOT(1, 0)·CNOT(0, 1). + + Michael A. Nielsen and Isaac L. Chuang, + Quantum Computation and Quantum Information, + Cambridge University Press, 2000 + (p. 23 in the 10th Anniversary Edition). + + Parameters + ---------- + instr: the SWAP instruction to decompose. + + Returns + ------- + the decomposition. + + """ + yield instruction.CNOT(control=instr.targets[0], target=instr.targets[1]) + yield instruction.CNOT(control=instr.targets[1], target=instr.targets[0]) + yield instruction.CNOT(control=instr.targets[0], target=instr.targets[1]) + + +def decompose_y(instr: instruction.Y) -> Iterator[instruction.X | instruction.Z]: + """Return a decomposition of the Y gate as X·Z. + + Parameters + ---------- + instr: the Y instruction to decompose. + + Returns + ------- + the decomposition. + + """ + yield instruction.Z(instr.target) + yield instruction.X(instr.target) + + +def decompose_rx(instr: instruction.RX) -> Iterator[instruction.J]: + """Yield a J decomposition of the RX gate. + + The Rx(α) gate is decomposed into J(α)·H (that is to say, J(α)·J(0)). + Vincent Danos, Elham Kashefi, Prakash Panangaden, The Measurement Calculus, 2007. + + Parameters + ---------- + instr: the RX instruction to decompose. + + Returns + ------- + the decomposition. + + """ + yield instruction.J(instr.target, 0) + yield instruction.J(instr.target, instr.angle) + + +def decompose_ry(instr: instruction.RY) -> Iterator[instruction.J]: + """Yield a J decomposition of the RY gate. + + The Ry(α) gate is decomposed into J(0)·J(π/2)·J(α)·J(-π/2). + Vincent Danos, Elham Kashefi, Prakash Panangaden, Robust and parsimonious realisations of unitaries in the one-way + model, 2004. + + Parameters + ---------- + instr: the RY instruction to decompose. + + Returns + ------- + the decomposition. + + """ + yield instruction.J(target=instr.target, angle=-ANGLE_PI / 2) + yield instruction.J(target=instr.target, angle=instr.angle) + yield instruction.J(target=instr.target, angle=ANGLE_PI / 2) + yield instruction.J(target=instr.target, angle=0) + + +def decompose_rz(instr: instruction.RZ) -> Iterator[instruction.J]: + """Yield a J decomposition of the RZ gate. + + The Rz(α) gate is decomposed into H·J(α) (that is to say, J(0)·J(α)). + Vincent Danos, Elham Kashefi, Prakash Panangaden, The Measurement Calculus, 2007. + + Parameters + ---------- + instr: the RZ instruction to decompose. + + Returns + ------- + the decomposition. + + """ + yield instruction.J(target=instr.target, angle=instr.angle) + yield instruction.J(target=instr.target, angle=0) + + +def instructions_to_jcz(instrs: Iterable[InstructionType]) -> Iterator[instruction.J | instruction.CZ | instruction.M]: + """Yield a J-∧z decomposition of the instruction. + + Parameters + ---------- + instr: the instruction to decompose. + + Returns + ------- + the decomposition. + + """ + for instr in instrs: + match instr.kind: + case InstructionKind.J | InstructionKind.CZ | InstructionKind.M: + yield instr + case InstructionKind.I: + return + case InstructionKind.H: + yield instruction.J(instr.target, 0) + case InstructionKind.S: + yield from decompose_rz(instruction.RZ(instr.target, ANGLE_PI / 2)) + case InstructionKind.X: + yield from decompose_rx(instruction.RX(instr.target, ANGLE_PI)) + case InstructionKind.Y: + yield from instructions_to_jcz(decompose_y(instr)) + case InstructionKind.Z: + yield from decompose_rz(instruction.RZ(instr.target, ANGLE_PI)) + case InstructionKind.RX: + yield from decompose_rx(instr) + case InstructionKind.RY: + yield from decompose_ry(instr) + case InstructionKind.RZ: + yield from decompose_rz(instr) + case InstructionKind.CCX: + yield from instructions_to_jcz(decompose_ccx(instr)) + case InstructionKind.RZZ: + yield from instructions_to_jcz(decompose_rzz(instr)) + case InstructionKind.CNOT: + yield from instructions_to_jcz(decompose_cnot(instr)) + case InstructionKind.SWAP: + yield from instructions_to_jcz(decompose_swap(instr)) + case _: + assert_never(instr.kind) + + +def normalize_angle(angle: ParameterizedAngle) -> ParameterizedAngle: + r"""Return an equivalent angle in range :math:`[0, 2 \cdot \pi)` if ``angle`` is instantiated. + + Parameters + ---------- + angle: ParameterizedAngle + An angle. + + Returns + ------- + ParameterizedAngle + An equivalent angle in range :math:`[0, 2 \cdot \pi)` if ``angle`` is instantiated. + If ``angle`` is parameterized, ``angle`` is returned unchanged. + """ + if isinstance(angle, float): + return angle % (2 * ANGLE_PI) + return angle @dataclass(frozen=True) @@ -1153,3 +959,19 @@ def transpile_swaps(circuit: Circuit) -> TranspileSwapsResult: if instr.kind == InstructionKind.M: visitor.qubits[instr.target] = None return TranspileSwapsResult(new_circuit, tuple(visitor.qubits)) + + +def _transpile_cflow_to_pattern( + tr: TranspileResult[CausalFlow[BlochMeasurement], dict[int, command.M]], +) -> TranspileResult[Pattern, tuple[int, ...]]: + pattern = StandardizedPattern.from_pattern(tr.flow.to_corrections().to_pattern()).to_space_optimal_pattern() + pattern.extend(tr.classical_outputs.values()) + return TranspileResult(pattern, tuple(tr.classical_outputs.keys())) + + +class IllformedCircuitError(Exception): + """Raised if the circuit is ill-formed.""" + + def __init__(self) -> None: + """Build the exception.""" + super().__init__("Ill-formed pattern") diff --git a/noxfile.py b/noxfile.py index a2e5f253c..0a5c232f5 100644 --- a/noxfile.py +++ b/noxfile.py @@ -108,7 +108,7 @@ class ReverseDependency: @nox.parametrize( "package", [ - ReverseDependency("https://github.com/thierry-martinez/graphix-stim-backend", branch="fix/graphix_namespace"), + ReverseDependency("https://github.com/emlynsg/graphix-stim-backend", branch="jcz"), ReverseDependency( "https://github.com/TeamGraphix/graphix-symbolic", ), diff --git a/tests/test_opengraph.py b/tests/test_opengraph.py index 1fd2cac83..58354aed9 100644 --- a/tests/test_opengraph.py +++ b/tests/test_opengraph.py @@ -17,6 +17,7 @@ from graphix.fundamentals import ANGLE_PI, Axis, Plane from graphix.measurements import Measurement from graphix.opengraph import OpenGraph, OpenGraphError +from graphix.optimization import StandardizedPattern from graphix.parameter import Placeholder from graphix.pattern import Pattern from graphix.random_objects import rand_circuit @@ -942,7 +943,9 @@ def test_from_to_pattern(self, fx_rng: Generator) -> None: depth = 2 circuit = rand_circuit(n_qubits, depth, fx_rng) pattern_ref = circuit.transpile().pattern - pattern = pattern_ref.extract_opengraph().to_pattern() + pattern = StandardizedPattern.from_pattern( + pattern_ref.extract_opengraph().to_pattern() + ).to_space_optimal_pattern() for plane in {Plane.XY, Plane.XZ, Plane.YZ}: alpha = 2 * ANGLE_PI * fx_rng.random() diff --git a/tests/test_optimization.py b/tests/test_optimization.py index 29bc663c9..419903e7b 100644 --- a/tests/test_optimization.py +++ b/tests/test_optimization.py @@ -67,8 +67,11 @@ def test_incorporate_pauli_results(fx_bg: PCG64, jumps: int) -> None: pattern.standardize() pattern.shift_signals() pattern.remove_input_nodes() + pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() + pattern = StandardizedPattern.from_pattern(pattern).to_space_optimal_pattern() pattern2 = incorporate_pauli_results(pattern) + pattern2 = StandardizedPattern.from_pattern(pattern2).to_space_optimal_pattern() state = pattern.simulate_pattern(rng=rng) state2 = pattern2.simulate_pattern(rng=rng) assert state.isclose(state2) @@ -85,6 +88,7 @@ def test_flow_after_pauli_preprocessing(fx_bg: PCG64, jumps: int) -> None: pattern.shift_signals() # pattern.move_pauli_measurements_to_the_front() pattern.remove_input_nodes() + pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() pattern2 = incorporate_pauli_results(pattern) gflow = pattern2.extract_gflow() @@ -101,8 +105,11 @@ def test_remove_useless_domains(fx_bg: PCG64, jumps: int) -> None: pattern.standardize() pattern.shift_signals() pattern.remove_input_nodes() + pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() + pattern = StandardizedPattern.from_pattern(pattern).to_space_optimal_pattern() pattern2 = remove_useless_domains(pattern) + pattern2 = StandardizedPattern.from_pattern(pattern2).to_space_optimal_pattern() state = pattern.simulate_pattern(rng=rng) state2 = pattern2.simulate_pattern(rng=rng) assert state.isclose(state2) diff --git a/tests/test_parameter.py b/tests/test_parameter.py index f33040634..8c3b4afc5 100644 --- a/tests/test_parameter.py +++ b/tests/test_parameter.py @@ -171,6 +171,7 @@ def test_random_circuit_with_parameters(fx_bg: PCG64, jumps: int, use_xreplace: pattern.standardize() pattern.shift_signals() pattern.remove_input_nodes() + pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() pattern.minimize_space() assignment: dict[Parameter, float] = {alpha: rng.uniform(high=2), beta: rng.uniform(high=2)} diff --git a/tests/test_pattern.py b/tests/test_pattern.py index 948c5d9ca..5d6ffbb70 100644 --- a/tests/test_pattern.py +++ b/tests/test_pattern.py @@ -19,6 +19,7 @@ from graphix.fundamentals import ANGLE_PI, Angle, Plane from graphix.measurements import BlochMeasurement, Measurement, Outcome, PauliMeasurement from graphix.opengraph import OpenGraph +from graphix.optimization import StandardizedPattern from graphix.pattern import Pattern, PatternError, RunnabilityError, RunnabilityErrorReason, shift_outcomes from graphix.random_objects import rand_circuit, rand_gate from graphix.sim.density_matrix import DensityMatrix @@ -70,7 +71,9 @@ def test_standardize(self, fx_rng: Generator) -> None: depth = 1 circuit = rand_circuit(nqubits, depth, fx_rng) pattern = circuit.transpile().pattern - + pattern = pattern.infer_pauli_measurements() + pattern.remove_input_nodes() + pattern.perform_pauli_measurements() pattern.standardize() assert pattern.is_standard() state = circuit.simulate_statevector().statevec @@ -155,6 +158,7 @@ def test_minimize_space_with_gflow(self, fx_bg: PCG64, jumps: int) -> None: pattern.standardize() pattern.shift_signals(method="mc") pattern.remove_input_nodes() + pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() pattern.minimize_space() state = circuit.simulate_statevector().statevec @@ -215,6 +219,7 @@ def test_shift_signals(self, fx_bg: PCG64, jumps: int) -> None: pattern.standardize() pattern.shift_signals(method="mc") assert pattern.is_standard() + pattern = StandardizedPattern.from_pattern(pattern).to_space_optimal_pattern() state = circuit.simulate_statevector().statevec state_mbqc = pattern.simulate_pattern(rng=rng) assert state_mbqc.isclose(state) @@ -234,6 +239,7 @@ def test_pauli_measurement_random_circuit( pattern.standardize() pattern.shift_signals(method="mc") pattern.remove_input_nodes() + pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() pattern.minimize_space() state = circuit.simulate_statevector().statevec @@ -253,6 +259,7 @@ def test_pauli_measurement_random_circuit_all_paulis( pattern.standardize() pattern.shift_signals(method="mc") pattern.remove_input_nodes() + pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements(ignore_pauli_with_deps=ignore_pauli_with_deps) assert ignore_pauli_with_deps or not any( cmd.measurement.try_to_pauli() is not None for cmd in pattern if cmd.kind == CommandKind.M @@ -289,10 +296,11 @@ def test_pauli_measurement(self) -> None: pattern.standardize() pattern.shift_signals(method="mc") pattern.remove_input_nodes() + pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() isolated_nodes = pattern.extract_isolated_nodes() - # 42-node is the isolated and output node. - isolated_nodes_ref = {42} + # 48-node is the isolated and output node. + isolated_nodes_ref = {48} assert isolated_nodes == isolated_nodes_ref def test_pauli_measurement_error(self, fx_rng: Generator) -> None: @@ -333,9 +341,10 @@ def test_pauli_measured_against_nonmeasured(self, fx_bg: PCG64, jumps: int, igno depth = 2 circuit = rand_circuit(nqubits, depth, rng) pattern = circuit.transpile().pattern - pattern.standardize() + pattern.minimize_space() pattern1 = copy.deepcopy(pattern) pattern1.remove_input_nodes() + pattern1 = pattern1.infer_pauli_measurements() pattern1.perform_pauli_measurements(ignore_pauli_with_deps=ignore_pauli_with_deps) state = pattern.simulate_pattern(rng=rng) state1 = pattern1.simulate_pattern(rng=rng) @@ -349,6 +358,7 @@ def test_pauli_repeated_measurement(self, fx_bg: PCG64, jumps: int) -> None: circuit = rand_circuit(nqubits, depth, rng, use_ccx=False) pattern = circuit.transpile().pattern pattern.remove_input_nodes() + pattern = pattern.infer_pauli_measurements() assert not pattern.results pattern.perform_pauli_measurements() assert pattern.results @@ -371,9 +381,12 @@ def test_pauli_repeated_measurement_compose(self, fx_bg: PCG64, jumps: int) -> N pattern1.remove_input_nodes() assert not pattern.results assert not pattern1.results + pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() + pattern1 = pattern1.infer_pauli_measurements() pattern1.perform_pauli_measurements() composed_pattern.remove_input_nodes() + composed_pattern = composed_pattern.infer_pauli_measurements() composed_pattern.perform_pauli_measurements() assert abs(len(composed_pattern.results) - len(pattern.results) - len(pattern1.results)) <= 2 @@ -478,6 +491,7 @@ def test_pauli_measurement_then_standardize(self, fx_bg: PCG64, jumps: int) -> N circuit = rand_circuit(nqubits, depth, rng) pattern = circuit.transpile().pattern pattern.remove_input_nodes() + pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() pattern.standardize() pattern.minimize_space() @@ -716,6 +730,7 @@ def test_compose_5(self, fx_rng: Generator) -> None: p2 = circuit_2.transpile().pattern # inputs: [0] p, _ = p1.compose(p2, mapping={0: 1, 1: 2, 2: 3}) + p = StandardizedPattern.from_pattern(p).to_space_optimal_pattern() circuit_12 = Circuit(1) circuit_12.h(0) @@ -754,6 +769,7 @@ def test_compose_7(self, fx_rng: Generator) -> None: circuit_1.rz(0, alpha) p1 = circuit_1.transpile().pattern p1.remove_input_nodes() + p1 = p1.infer_pauli_measurements() p1.perform_pauli_measurements() circuit_2 = Circuit(1) @@ -881,10 +897,12 @@ def test_extract_partial_order_layers_results(self) -> None: c.rz(0, 0.2) p = c.transpile().pattern p.remove_input_nodes() + p = p.infer_pauli_measurements() p.perform_pauli_measurements() assert p.extract_partial_order_layers() == (frozenset({2}), frozenset({0})) p = Pattern(cmds=[N(0), N(1), N(2), M(0), E((1, 2)), X(1, {0}), M(2, Measurement.XY(0.3))]) + p = p.infer_pauli_measurements() p.perform_pauli_measurements() assert p.extract_partial_order_layers() == (frozenset({1}), frozenset({2})) @@ -1008,6 +1026,8 @@ def test_extract_causal_flow_rnd_circuit(self, fx_bg: PCG64, jumps: int) -> None p_ref.remove_input_nodes() p_test.remove_input_nodes() + p_ref = p_ref.infer_pauli_measurements() + p_test = p_test.infer_pauli_measurements() p_ref.perform_pauli_measurements() p_test.perform_pauli_measurements() @@ -1027,6 +1047,8 @@ def test_extract_gflow_rnd_circuit(self, fx_bg: PCG64, jumps: int) -> None: p_ref.remove_input_nodes() p_test.remove_input_nodes() + p_ref = p_ref.infer_pauli_measurements() + p_test = p_test.infer_pauli_measurements() p_ref.perform_pauli_measurements() p_test.perform_pauli_measurements() @@ -1121,9 +1143,12 @@ def test_extract_xzc_rnd_circuit(self, fx_bg: PCG64, jumps: int) -> None: xzc.check_well_formed() p_test = xzc.to_pattern() - for p in [p_ref, p_test]: - p.remove_input_nodes() - p.perform_pauli_measurements() + p_ref.remove_input_nodes() + p_test.remove_input_nodes() + p_ref = p_ref.infer_pauli_measurements() + p_test = p_test.infer_pauli_measurements() + p_ref.perform_pauli_measurements() + p_test.perform_pauli_measurements() s_ref = p_ref.simulate_pattern(rng=rng) s_test = p_test.simulate_pattern(rng=rng) diff --git a/tests/test_pretty_print.py b/tests/test_pretty_print.py index 28dedce55..6bf690d2b 100644 --- a/tests/test_pretty_print.py +++ b/tests/test_pretty_print.py @@ -119,7 +119,7 @@ def test_flow_pretty_print_random( flow_extractor: Callable[[OpenGraph[Measurement]], PauliFlow[Measurement]], ) -> None: rng = Generator(fx_bg.jumped(jumps)) - rand_og = rand_circuit(5, 5, rng=rng).transpile().pattern.extract_opengraph() + rand_og = rand_circuit(5, 5, rng=rng).transpile().pattern.infer_pauli_measurements().extract_opengraph() flow = flow_extractor(rand_og) flow.to_ascii() diff --git a/tests/test_qasm3_exporter.py b/tests/test_qasm3_exporter.py index f9751d0de..b23965f1f 100644 --- a/tests/test_qasm3_exporter.py +++ b/tests/test_qasm3_exporter.py @@ -56,6 +56,7 @@ def test_to_qasm3_random_circuit(fx_bg: PCG64, jumps: int) -> None: circuit = rand_circuit(nqubits, depth, rng=rng) pattern = circuit.transpile().pattern pattern.remove_input_nodes() + pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() pattern.minimize_space() _qasm3 = pattern_to_qasm3(pattern) diff --git a/tests/test_qasm3_exporter_to_graphix_parser.py b/tests/test_qasm3_exporter_to_graphix_parser.py index cd2421b06..9e31e9eb9 100644 --- a/tests/test_qasm3_exporter_to_graphix_parser.py +++ b/tests/test_qasm3_exporter_to_graphix_parser.py @@ -31,9 +31,10 @@ def check_round_trip(circuit: Circuit) -> None: qasm = circuit_to_qasm3(circuit) + check_circuit = circuit.transpile_j_to_rzh() parser = OpenQASMParser() parsed_circuit = parser.parse_str(qasm) - assert parsed_circuit.instruction == circuit.instruction + assert parsed_circuit.instruction == check_circuit.instruction @pytest.mark.parametrize("jumps", range(1, 11)) @@ -42,7 +43,7 @@ def test_circuit_to_qasm3(fx_bg: PCG64, jumps: int) -> None: nqubits = 5 depth = 4 # See https://github.com/TeamGraphix/graphix-qasm-parser/pull/5 - check_round_trip(rand_circuit(nqubits, depth, rng, use_cz=False)) + check_round_trip(rand_circuit(nqubits, depth, rng, use_j=True, use_cz=True)) @pytest.mark.parametrize( @@ -52,8 +53,7 @@ def test_circuit_to_qasm3(fx_bg: PCG64, jumps: int) -> None: instruction.RZZ(target=0, control=1, angle=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.CZ(targets=(0, 1)), instruction.H(target=0), instruction.S(target=0), instruction.X(target=0), @@ -63,7 +63,22 @@ def test_circuit_to_qasm3(fx_bg: PCG64, jumps: int) -> None: instruction.RX(target=0, angle=ANGLE_PI / 4), instruction.RY(target=0, angle=ANGLE_PI / 4), instruction.RZ(target=0, angle=ANGLE_PI / 4), + instruction.J(target=0, angle=ANGLE_PI / 4), ], ) def test_instruction_to_qasm3(instruction: InstructionType) -> None: check_round_trip(Circuit(3, instr=[instruction])) + + +def test_j_to_qasm3() -> None: + circuit = Circuit(3, instr=[instruction.J(target=0, angle=ANGLE_PI / 4)]) + qasm = circuit_to_qasm3(circuit) + parser = OpenQASMParser() + parsed_circuit = parser.parse_str(qasm) + assert parsed_circuit.instruction == circuit.transpile_j_to_rzh().instruction + + +def test_j_to_qasm3_failure() -> None: + circuit = Circuit(3, instr=[instruction.J(target=0, angle=ANGLE_PI / 4)]) + with pytest.raises(ValueError): + circuit_to_qasm3(circuit, transpile=False) diff --git a/tests/test_qasm3_exporter_to_qiskit.py b/tests/test_qasm3_exporter_to_qiskit.py index a857daa7d..03273fe35 100644 --- a/tests/test_qasm3_exporter_to_qiskit.py +++ b/tests/test_qasm3_exporter_to_qiskit.py @@ -117,9 +117,10 @@ def test_to_qasm3_random_circuit(fx_bg: PCG64, jumps: int) -> None: rng = Generator(fx_bg.jumped(jumps)) nqubits = 5 depth = 5 - circuit = rand_circuit(nqubits, depth, rng=rng) + circuit = rand_circuit(nqubits, depth, rng=rng, use_j=True) pattern = circuit.transpile().pattern pattern.remove_input_nodes() + pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() pattern.minimize_space() diff --git a/tests/test_tnsim.py b/tests/test_tnsim.py index 9827576af..7f312821d 100644 --- a/tests/test_tnsim.py +++ b/tests/test_tnsim.py @@ -336,6 +336,7 @@ def test_with_graphtrans(self, fx_bg: PCG64, jumps: int, fx_rng: Generator) -> N pattern.standardize() pattern.shift_signals() pattern.remove_input_nodes() + pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() state = circuit.simulate_statevector().statevec tn_mbqc = pattern.simulate_pattern(backend="tensornetwork", rng=fx_rng) @@ -355,6 +356,7 @@ def test_with_graphtrans_sequential(self, fx_bg: PCG64, jumps: int, fx_rng: Gene pattern.standardize() pattern.shift_signals() pattern.remove_input_nodes() + pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() state = circuit.simulate_statevector().statevec tn_mbqc = pattern.simulate_pattern(backend="tensornetwork", graph_prep="sequential", rng=fx_rng) @@ -404,6 +406,7 @@ def test_evolve(self, fx_bg: PCG64, jumps: int, fx_rng: Generator) -> None: pattern.standardize() pattern.shift_signals() pattern.remove_input_nodes() + pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() state = circuit.simulate_statevector().statevec tn_mbqc = pattern.simulate_pattern(backend="tensornetwork", rng=fx_rng) diff --git a/tests/test_transpiler.py b/tests/test_transpiler.py index d73f9133d..061fbf2b4 100644 --- a/tests/test_transpiler.py +++ b/tests/test_transpiler.py @@ -7,12 +7,13 @@ from numpy.random import PCG64, Generator from graphix import instruction -from graphix.branch_selector import ConstBranchSelector +from graphix.branch_selector import ConstBranchSelector, FixedBranchSelector from graphix.fundamentals import ANGLE_PI, Axis, Sign from graphix.instruction import I, InstructionKind from graphix.random_objects import rand_circuit, rand_gate, rand_state_vector +from graphix.simulator import DefaultMeasureMethod from graphix.states import BasicStates -from graphix.transpiler import Circuit, transpile_swaps +from graphix.transpiler import Circuit, decompose_ccx, transpile_swaps from tests.test_branch_selector import CheckedBranchSelector if TYPE_CHECKING: @@ -39,111 +40,28 @@ lambda rng: instruction.RX(0, rng.random() * 2 * ANGLE_PI), lambda rng: instruction.RY(0, rng.random() * 2 * ANGLE_PI), lambda rng: instruction.RZ(0, rng.random() * 2 * ANGLE_PI), + lambda rng: instruction.J(0, rng.random() * 2 * ANGLE_PI), ] 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 state_mbqc.isclose(state) - - def test_cnot(self, fx_rng: Generator) -> None: - circuit = Circuit(2) - circuit.cnot(0, 1) - pattern = circuit.transpile().pattern - state = circuit.simulate_statevector(rng=fx_rng).statevec - state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert state_mbqc.isclose(state) - - def test_hadamard(self, fx_rng: Generator) -> None: - circuit = Circuit(1) - circuit.h(0) - pattern = circuit.transpile().pattern - state = circuit.simulate_statevector(rng=fx_rng).statevec - state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert state_mbqc.isclose(state) - - def test_s(self, fx_rng: Generator) -> None: - circuit = Circuit(1) - circuit.s(0) - pattern = circuit.transpile().pattern - state = circuit.simulate_statevector(rng=fx_rng).statevec - state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert state_mbqc.isclose(state) - - def test_x(self, fx_rng: Generator) -> None: - circuit = Circuit(1) - circuit.x(0) - pattern = circuit.transpile().pattern - state = circuit.simulate_statevector(rng=fx_rng).statevec - state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert state_mbqc.isclose(state) - - def test_y(self, fx_rng: Generator) -> None: - circuit = Circuit(1) - circuit.y(0) - pattern = circuit.transpile().pattern - state = circuit.simulate_statevector(rng=fx_rng).statevec - state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert state_mbqc.isclose(state) - - def test_z(self, fx_rng: Generator) -> None: - circuit = Circuit(1) - circuit.z(0) - pattern = circuit.transpile().pattern - state = circuit.simulate_statevector(rng=fx_rng).statevec - state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert state_mbqc.isclose(state) - - def test_rx(self, fx_rng: Generator) -> None: - theta = fx_rng.uniform() * 2 * ANGLE_PI - circuit = Circuit(1) - circuit.rx(0, theta) - pattern = circuit.transpile().pattern - state = circuit.simulate_statevector(rng=fx_rng).statevec - state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert state_mbqc.isclose(state) - - def test_ry(self, fx_rng: Generator) -> None: - theta = fx_rng.uniform() * 2 * ANGLE_PI - circuit = Circuit(1) - circuit.ry(0, theta) - pattern = circuit.transpile().pattern - state = circuit.simulate_statevector(rng=fx_rng).statevec - state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert state_mbqc.isclose(state) - - def test_rz(self, fx_rng: Generator) -> None: - theta = fx_rng.uniform() * 2 * ANGLE_PI - circuit = Circuit(1) - circuit.rz(0, theta) - pattern = circuit.transpile().pattern - state = circuit.simulate_statevector(rng=fx_rng).statevec - state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert state_mbqc.isclose(state) - - def test_i(self, fx_rng: Generator) -> None: - circuit = Circuit(1) - circuit.i(0) + @pytest.mark.parametrize("instruction", INSTRUCTION_TEST_CASES) + def test_instruction_flow(self, fx_rng: Generator, instruction: InstructionTestCase) -> None: + circuit = Circuit(3, instr=[instruction(fx_rng)]) pattern = circuit.transpile().pattern - state = circuit.simulate_statevector(rng=fx_rng).statevec - state_mbqc = pattern.simulate_pattern(rng=fx_rng) - assert state_mbqc.isclose(state) + circuit.transpile_to_cflow().flow.check_well_formed() + flow = pattern.to_bloch().extract_causal_flow() + flow.check_well_formed() @pytest.mark.parametrize("jumps", range(1, 11)) - def test_ccx(self, fx_bg: PCG64, jumps: int) -> None: + @pytest.mark.parametrize("instruction", INSTRUCTION_TEST_CASES) + def test_instructions(self, fx_bg: PCG64, jumps: int, instruction: InstructionTestCase) -> None: rng = Generator(fx_bg.jumped(jumps)) - nqubits = 4 - depth = 6 - circuit = rand_circuit(nqubits, depth, rng, use_ccx=True) + circuit = Circuit(3, instr=[instruction(rng)]) pattern = circuit.transpile().pattern - pattern.minimize_space() - state = circuit.simulate_statevector(rng=rng).statevec - state_mbqc = pattern.simulate_pattern(rng=rng) + 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 state_mbqc.isclose(state) def test_transpiled(self, fx_rng: Generator) -> None: @@ -171,6 +89,21 @@ def test_measure(self, fx_bg: PCG64, jumps: int, axis: Axis, outcome: Outcome) - state_mbqc = pattern.simulate_pattern(rng=rng, input_state=input_state, branch_selector=branch_selector) assert state_mbqc.isclose(state) + @pytest.mark.parametrize("jumps", range(1, 11)) + @pytest.mark.parametrize("axis", [Axis.X, Axis.Y, Axis.Z]) + @pytest.mark.parametrize("outcome", [0, 1]) + def test_measure_early(self, fx_bg: PCG64, jumps: int, axis: Axis, outcome: Outcome) -> None: + rng = Generator(fx_bg.jumped(jumps)) + circuit = Circuit(3) + circuit.m(0, axis) + circuit.cnot(1, 2) + input_state = rand_state_vector(3, rng=rng) + branch_selector = ConstBranchSelector(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 state_mbqc.isclose(state) + @pytest.mark.parametrize("input_axis", [Axis.X, Axis.Y, Axis.Z]) @pytest.mark.parametrize("input_sign", [Sign.PLUS, Sign.MINUS]) @pytest.mark.parametrize("measurement_axis", [Axis.X, Axis.Y, Axis.Z]) @@ -213,6 +146,146 @@ def test_transpile_measurements_to_z_axis(self, fx_bg: PCG64, jumps: int, axis: ).statevec assert state_z.isclose(state) + @pytest.mark.parametrize("jumps", range(1, 11)) + def test_transpile_swaps(self, fx_bg: PCG64, jumps: int) -> None: + rng = Generator(fx_bg.jumped(jumps)) + nqubits = 4 + depth = 6 + circuit = rand_circuit(nqubits, depth, rng, use_ccx=True, use_rzz=True) + assert any(instr.kind == InstructionKind.SWAP for instr in circuit.instruction) + transpiled_swaps = transpile_swaps(circuit) + circuit2 = transpiled_swaps.circuit + assert not any(instr.kind == InstructionKind.SWAP for instr in circuit2.instruction) + state = circuit.simulate_statevector(rng=rng).statevec + state2 = circuit2.simulate_statevector(rng=rng).statevec + qubits: list[int] = [] + for qubit in transpiled_swaps.qubits: + assert qubit is not None + qubits.append(qubit) + state2.psi = np.transpose(state2.psi, qubits) + assert state.isclose(state2) + + @pytest.mark.parametrize("jumps", range(1, 11)) + def test_transpile_j_to_rzh(self, fx_bg: PCG64, jumps: int) -> None: + rng = Generator(fx_bg.jumped(jumps)) + nqubits = 3 + depth = 2 + circuit = rand_circuit(nqubits, depth, rng, use_j=True, use_ccx=True, use_rzz=True) + circuit.j(0, 0.5) # Ensure that there is at least one J instruction + assert any(instr.kind == InstructionKind.J for instr in circuit.instruction) + circuit2 = circuit.transpile_j_to_rzh() + assert not any(instr.kind == InstructionKind.J for instr in circuit2.instruction) + state = circuit.simulate_statevector(rng=rng).statevec + state2 = circuit2.simulate_statevector(rng=rng).statevec + print(state.fidelity(state2)) + assert state.fidelity(state2) == pytest.approx(1) + + @pytest.mark.parametrize("jumps", range(1, 11)) + @pytest.mark.parametrize("axis", [Axis.X, Axis.Y, Axis.Z]) + @pytest.mark.parametrize("outcome", [0, 1]) + def test_transpile_swaps_with_measurements(self, fx_bg: PCG64, jumps: int, axis: Axis, outcome: Outcome) -> None: + rng = Generator(fx_bg.jumped(jumps)) + circuit = Circuit(3) + circuit.swap(0, 1) + circuit.swap(0, 2) + circuit.cnot(1, 2) + circuit.m(1, axis) + circuit.i(0) + transpiled_swaps = transpile_swaps(circuit) + circuit2 = transpiled_swaps.circuit + assert not any(instr.kind == InstructionKind.SWAP for instr in circuit2.instruction) + assert I(2) in circuit2.instruction + input_state = rand_state_vector(3, rng=rng) + branch_selector = ConstBranchSelector(outcome) + state = circuit.simulate_statevector(rng=rng, input_state=input_state, branch_selector=branch_selector).statevec + state2 = circuit2.simulate_statevector( + rng=rng, input_state=input_state, branch_selector=branch_selector + ).statevec + assert transpiled_swaps.qubits == (2, None, 1) + state2.swap((0, 1)) + assert state.isclose(state2) + + def test_cz_ccx(self, fx_rng: Generator) -> None: + """Test case reported in issue #2. + + https://github.com/qat-inria/graphix-jcz-transpiler/issues/2 + """ + circuit = Circuit(width=3) + circuit.cz(2, 0) + circuit.ccx(0, 1, 2) + ref_state = circuit.simulate_statevector(rng=fx_rng).statevec + pattern = circuit.transpile().pattern + state = pattern.simulate_pattern(rng=fx_rng) + assert state.isclose(ref_state) + + def test_ccx_decomposition(self) -> None: + circuit = Circuit(width=3) + circuit.cz(2, 0) + circuit.ccx(0, 1, 2) + circuit2 = Circuit(width=3) + circuit2.cz(2, 0) + circuit2.extend(decompose_ccx(instruction.CCX(controls=(0, 1), target=2))) + state = circuit.simulate_statevector().statevec + state2 = circuit2.simulate_statevector().statevec + assert state.isclose(state2) + + def test_cnot_cz(self, fx_rng: Generator) -> None: + """Test regression about output node reordering.""" + circuit = Circuit(width=3, instr=[instruction.CNOT(0, 1), instruction.CZ((0, 1))]) + state = circuit.simulate_statevector(rng=fx_rng).statevec + pattern = circuit.transpile().pattern + state_mbqc = pattern.simulate_pattern(rng=fx_rng) + assert state.isclose(state_mbqc) + + @pytest.mark.parametrize("jumps", range(1, 6)) + @pytest.mark.parametrize("axes", [[Axis.X, Axis.Y], [Axis.X, Axis.Y, Axis.Z]]) + def test_classical_outputs_consistency(self, fx_bg: PCG64, jumps: int, axes: list[Axis]) -> None: + """Check that `classical_outputs` are in the same order as `classical_measures`.""" + rng = Generator(fx_bg.jumped(jumps)) + n = len(axes) + width = n + 1 + circuit = Circuit(width) + for q in range(n): + circuit.cnot(q, q + 1) + for q, axis in enumerate(axes): + circuit.m(q, axis) + + transpile_result = circuit.transpile() + pattern = transpile_result.pattern + expected_outcomes: list[Outcome] = [1 if q % 2 else 0 for q in range(n)] + results_circuit: dict[int, Outcome] = dict(zip(range(n), expected_outcomes, strict=False)) + m_outcomes = dict(zip(transpile_result.classical_outputs, expected_outcomes, strict=False)) + non_output_nodes = pattern.extract_nodes() - set(pattern.output_nodes) + results_pattern: dict[int, Outcome] = {node: m_outcomes.get(node, 0) for node in non_output_nodes} + input_state = rand_state_vector(width, rng=rng) + measure_method = DefaultMeasureMethod() + circuit_result = circuit.simulate_statevector( + rng=rng, + input_state=input_state, + branch_selector=FixedBranchSelector(results=results_circuit), + ) + pattern.simulate_pattern( + rng=rng, + input_state=input_state, + branch_selector=FixedBranchSelector(results=results_pattern), + measure_method=measure_method, + ) + assert len(transpile_result.classical_outputs) == len(circuit_result.classical_measures) + pattern_measures = [measure_method.results[node] for node in transpile_result.classical_outputs] + assert pattern_measures == list(circuit_result.classical_measures) + assert pattern_measures == expected_outcomes + + def test_classical_outputs_empty(self) -> None: + """Circuits with no M instructions produce empty classical_outputs.""" + circuit = Circuit(2) + circuit.cnot(0, 1) + circuit.h(0) + result = circuit.transpile() + assert len(result.classical_outputs) == 0 + assert len(circuit.simulate_statevector().classical_measures) == 0 + + +class TestCircuits: def test_add_extend(self) -> None: circuit = Circuit(3) circuit.ccx(0, 1, 2) @@ -232,75 +305,3 @@ def test_add_extend(self) -> None: circuit.rz(1, 0.5) circuit2 = Circuit(3, instr=circuit.instruction) assert circuit.instruction == circuit2.instruction - - @pytest.mark.parametrize("instruction", INSTRUCTION_TEST_CASES) - def test_instruction_flow(self, fx_rng: Generator, instruction: InstructionTestCase) -> None: - circuit = Circuit(3, instr=[instruction(fx_rng)]) - pattern = circuit.transpile().pattern - flow = pattern.to_bloch().extract_causal_flow() - flow.check_well_formed() - - @pytest.mark.parametrize("jumps", range(1, 11)) - @pytest.mark.parametrize("instruction", INSTRUCTION_TEST_CASES) - def test_instructions(self, fx_bg: PCG64, jumps: int, instruction: InstructionTestCase) -> None: - rng = Generator(fx_bg.jumped(jumps)) - circuit = Circuit(3, instr=[instruction(rng)]) - pattern = circuit.transpile().pattern - 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 state_mbqc.isclose(state) - - def test_simple(self) -> None: - rng = np.random.default_rng(420) - circuit = Circuit(3, instr=[instruction.CCX(0, (1, 2))]) - pattern = circuit.transpile().pattern - pattern.minimize_space() - 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 state_mbqc.isclose(state) - - -@pytest.mark.parametrize("jumps", range(1, 11)) -def test_transpile_swaps(fx_bg: PCG64, jumps: int) -> None: - rng = Generator(fx_bg.jumped(jumps)) - nqubits = 4 - depth = 6 - circuit = rand_circuit(nqubits, depth, rng, use_ccx=True, use_rzz=True) - assert any(instr.kind == InstructionKind.SWAP for instr in circuit.instruction) - transpiled_swaps = transpile_swaps(circuit) - circuit2 = transpiled_swaps.circuit - assert not any(instr.kind == InstructionKind.SWAP for instr in circuit2.instruction) - state = circuit.simulate_statevector(rng=rng).statevec - state2 = circuit2.simulate_statevector(rng=rng).statevec - qubits: list[int] = [] - for qubit in transpiled_swaps.qubits: - assert qubit is not None - qubits.append(qubit) - state2.psi = np.transpose(state2.psi, qubits) - assert state.isclose(state2) - - -@pytest.mark.parametrize("jumps", range(1, 11)) -@pytest.mark.parametrize("axis", [Axis.X, Axis.Y, Axis.Z]) -@pytest.mark.parametrize("outcome", [0, 1]) -def test_transpile_swaps_with_measurements(fx_bg: PCG64, jumps: int, axis: Axis, outcome: Outcome) -> None: - rng = Generator(fx_bg.jumped(jumps)) - circuit = Circuit(3) - circuit.swap(0, 1) - circuit.swap(0, 2) - circuit.cnot(1, 2) - circuit.m(1, axis) - circuit.i(0) - transpiled_swaps = transpile_swaps(circuit) - circuit2 = transpiled_swaps.circuit - assert not any(instr.kind == InstructionKind.SWAP for instr in circuit2.instruction) - assert I(2) in circuit2.instruction - input_state = rand_state_vector(3, rng=rng) - branch_selector = ConstBranchSelector(outcome) - state = circuit.simulate_statevector(rng=rng, input_state=input_state, branch_selector=branch_selector).statevec - state2 = circuit2.simulate_statevector(rng=rng, input_state=input_state, branch_selector=branch_selector).statevec - assert transpiled_swaps.qubits == (2, None, 1) - state2.swap((0, 1)) - assert state.isclose(state2) diff --git a/tests/test_visualization.py b/tests/test_visualization.py index e1ee898b7..ce5a2bf97 100644 --- a/tests/test_visualization.py +++ b/tests/test_visualization.py @@ -159,6 +159,7 @@ def example_hadamard() -> Pattern: def example_local_clifford() -> Pattern: pattern = example_hadamard() pattern.remove_input_nodes() + pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() return pattern @@ -259,6 +260,7 @@ def test_draw_graph_reference(flow_and_not_pauli_presimulate: bool) -> Figure: pattern = pattern.to_bloch() else: pattern.remove_input_nodes() + pattern = pattern.infer_pauli_measurements() pattern.perform_pauli_measurements() pattern.standardize() pattern.draw(