diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index 79659a27..fab3839c 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -11,7 +11,9 @@ on: jobs: lint: runs-on: ubuntu-latest + continue-on-error: true strategy: + fail-fast: false matrix: python-version: [ "3.9", "3.10" ] outputs: diff --git a/pauliopt/clifford/tableau.py b/pauliopt/clifford/tableau.py index b59d0e1f..a3698eb9 100644 --- a/pauliopt/clifford/tableau.py +++ b/pauliopt/clifford/tableau.py @@ -61,8 +61,8 @@ def mult_paulis(p1, p2, sign1, sign2, n_qubits): class CliffordTableau: """ - Class for storing and manipulating Clifford clifford. - A Clifford clifford is a representation of a Clifford circuit as a + Class for storing and manipulating Clifford tableau. + A Clifford tableau is a representation of a Clifford circuit as a 2n x 2n binary matrix, where n is the number of qubits. The first n rows represent the stabilizers, and the last n rows represent the destabilizers. The first n columns represent the X operators, and the last n columns diff --git a/pauliopt/clifford/tableau_synthesis.py b/pauliopt/clifford/tableau_synthesis.py index 76004882..e336d7be 100644 --- a/pauliopt/clifford/tableau_synthesis.py +++ b/pauliopt/clifford/tableau_synthesis.py @@ -342,10 +342,10 @@ def get_non_cutting_vertex(G, pivot_col, swappable_nodes): def synthesize_tableau(tableau: CliffordTableau, topo: Topology, include_swaps=True): """ - Architecture aware synthesis of a Clifford clifford. + Architecture aware synthesis of a Clifford tableau. This is the implementation of the algorithm described in Winderl et. al. [1] - :param tableau: The Clifford clifford + :param tableau: The Clifford tableau :param topo: The topology :param include_swaps: Whether to allow initial and final measurement permutations diff --git a/pauliopt/gates.py b/pauliopt/gates.py index 06baa751..6938c85d 100644 --- a/pauliopt/gates.py +++ b/pauliopt/gates.py @@ -140,7 +140,7 @@ def __repr__(self) -> str: return f"{self.name}({self.phase}, {', '.join(args)})" def get_phase_as_float(self): - return self.phase if not isinstance(self.phase, Angle) else self.phase.to_qiskit + return self.phase if not isinstance(self.phase, Angle) else float(self.phase) def get_phase_as_angle(self): return self.phase if isinstance(self.phase, Angle) else Angle(self.phase) @@ -193,7 +193,7 @@ def __init__(self, *qubits): @abstractmethod def get_h_s_cx_decomposition(self) -> List[Union["H", "S", "CX"]]: """ - Every clifford must me decomposable into a list of H, S and CX gates. + Every clifford must be decomposable into a list of H, S and CX gates. Returns: """ @@ -695,10 +695,7 @@ class Rx(PhaseGate): @property def decomp(self): (q,) = self.qubits - if isinstance(self.phase, Angle): - return [XHead(self.get_phase_as_angle()) @ {q}] - else: - return [XHead(Angle(self.get_phase_as_angle())) @ {q}] + return [XHead(self.get_phase_as_angle()) @ {q}] def to_qiskit(self): try: diff --git a/pauliopt/pauli/pauli_gadget.py b/pauliopt/pauli/pauli_gadget.py index 658808e3..b9396055 100644 --- a/pauliopt/pauli/pauli_gadget.py +++ b/pauliopt/pauli/pauli_gadget.py @@ -8,6 +8,7 @@ from pauliopt.gates import H, V, Rz, CX, Vdg from pauliopt.pauli_strings import Pauli, X, Y, Z, I from pauliopt.topologies import Topology +from pauliopt.utils import AngleExpr def decompose_cnot_ladder_z(ctrl: int, trg: int, arch: Topology): @@ -59,9 +60,9 @@ def find_minimal_cx_assignment(column: np.array, arch: Topology, q0=None): class PPhase: - _angle: float + _angle: AngleExpr - def __init__(self, angle: float): + def __init__(self, angle: AngleExpr): self._angle = angle def __matmul__(self, paulis: List[Pauli]): @@ -72,7 +73,7 @@ def __str__(self): class PauliGadget: - def __init__(self, angle: float, paulis: List[Pauli]): + def __init__(self, angle: AngleExpr, paulis: List[Pauli]): self.angle = angle self.paulis = paulis diff --git a/pauliopt/pauli/pauli_polynomial.py b/pauliopt/pauli/pauli_polynomial.py index 0b59e2cb..db2e58ef 100644 --- a/pauliopt/pauli/pauli_polynomial.py +++ b/pauliopt/pauli/pauli_polynomial.py @@ -91,12 +91,12 @@ def __irshift__(self, gadget: PauliGadget): f"Pauli Polynomial has {self.num_qubits} qubits, but Pauli gadget has: " f"{len(gadget)} qubits" ) - self.append_pauli_gadget(gadget) + self.pauli_gadgets.append(gadget) return self def __rshift__(self, pauli_polynomial: "PauliPolynomial"): for gadget in pauli_polynomial.pauli_gadgets: - self.append_pauli_gadget(gadget) + self.pauli_gadgets.append(gadget) return self def __repr__(self): @@ -134,8 +134,6 @@ def num_legs(self): legs += gadget.num_legs() return legs - def append_pauli_gadget(self, pauli_gadget: PauliGadget): - self.pauli_gadgets.append(pauli_gadget) def assign_time(self, time: float): for gadet in self.pauli_gadgets: @@ -239,7 +237,7 @@ def to_svg( # width of the text of the phases # TODO round floats (!!) text_width = int(math.ceil(50 * vscale)) - bend_degree = int(math.ceil(10)) + bend_degree = 10 # margins between the angle and the legs margin_angle_x = int(math.ceil(20 * hscale)) @@ -249,8 +247,6 @@ def to_svg( margin_x = int(math.ceil(10 * hscale)) margin_y = int(math.ceil(10 * hscale)) - font_size = int(10) - width = ( num_gadgets * (square_width + margin_x + margin_angle_x + text_width) + margin_x diff --git a/requirements-dev.in b/requirements-dev.in index a7a4c755..695214d1 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -4,5 +4,5 @@ pytket-qiskit==0.34.0 qiskit==0.39.0 galois==0.3.7 parameterized==0.9.0 -notebook==7.0.7 +notebook~=7.0.7 black==24.3.0 diff --git a/requirements-dev.txt b/requirements-dev.txt index b3b6d952..57f4004d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,139 +2,433 @@ # This file is autogenerated by pip-compile with Python 3.10 # by the following command: # -# pip-compile --allow-unsafe --no-annotate requirements-dev.in +# pip-compile --allow-unsafe requirements-dev.in # anyio==4.4.0 + # via + # httpx + # jupyter-server appnope==0.1.4 + # via ipykernel argon2-cffi==23.1.0 + # via jupyter-server argon2-cffi-bindings==21.2.0 + # via argon2-cffi arrow==1.3.0 + # via isoduration asttokens==2.4.1 + # via stack-data async-lru==2.0.4 + # via jupyterlab attrs==23.2.0 + # via + # jsonschema + # referencing babel==2.15.0 + # via jupyterlab-server beautifulsoup4==4.12.3 + # via nbconvert black==24.3.0 + # via -r requirements-dev.in bleach==6.1.0 + # via nbconvert certifi==2024.7.4 + # via + # httpcore + # httpx + # requests cffi==1.16.0 + # via + # argon2-cffi-bindings + # cryptography charset-normalizer==3.3.2 + # via requests click==8.1.7 + # via black comm==0.2.2 + # via ipykernel cryptography==43.0.0 + # via + # pyspnego + # requests-ntlm debugpy==1.8.2 + # via ipykernel decorator==5.1.1 + # via ipython defusedxml==0.7.1 + # via nbconvert dill==0.3.8 + # via qiskit-terra exceptiongroup==1.2.2 + # via + # anyio + # ipython executing==2.0.1 + # via stack-data fastjsonschema==2.20.0 + # via nbformat fqdn==1.5.1 + # via jsonschema galois==0.3.7 + # via -r requirements-dev.in graphviz==0.20.3 + # via pytket h11==0.14.0 + # via httpcore httpcore==1.0.5 + # via httpx httpx==0.27.0 + # via jupyterlab ibm-cloud-sdk-core==3.20.3 + # via ibm-platform-services ibm-platform-services==0.55.1 + # via qiskit-ibm-runtime idna==3.7 + # via + # anyio + # httpx + # jsonschema + # requests ipykernel==6.29.5 + # via jupyterlab ipython==8.26.0 + # via ipykernel isoduration==20.11.0 + # via jsonschema jedi==0.19.1 + # via ipython jinja2==3.1.4 + # via + # jupyter-server + # jupyterlab + # jupyterlab-server + # nbconvert + # pytket json5==0.9.25 + # via jupyterlab-server jsonpointer==3.0.0 + # via jsonschema jsonschema[format-nongpl]==4.23.0 + # via + # jupyter-events + # jupyterlab-server + # nbformat jsonschema-specifications==2023.12.1 + # via jsonschema jupyter-client==8.6.2 + # via + # ipykernel + # jupyter-server + # nbclient jupyter-core==5.7.2 + # via + # ipykernel + # jupyter-client + # jupyter-server + # jupyterlab + # nbclient + # nbconvert + # nbformat jupyter-events==0.10.0 + # via jupyter-server jupyter-lsp==2.2.5 + # via jupyterlab jupyter-server==2.13.0 + # via + # jupyter-lsp + # jupyterlab + # jupyterlab-server + # notebook + # notebook-shim jupyter-server-terminals==0.5.3 + # via jupyter-server jupyterlab==4.2.4 + # via notebook jupyterlab-pygments==0.3.0 + # via nbconvert jupyterlab-server==2.27.3 + # via + # jupyterlab + # notebook lark-parser==0.12.0 + # via pytket llvmlite==0.41.1 + # via numba markupsafe==2.1.5 + # via + # jinja2 + # nbconvert matplotlib-inline==0.1.7 + # via + # ipykernel + # ipython mistune==3.0.2 + # via nbconvert mpmath==1.3.0 + # via sympy mypy-extensions==1.0.0 + # via black nbclient==0.10.0 + # via nbconvert nbconvert==7.16.4 + # via jupyter-server nbformat==5.10.4 + # via + # jupyter-server + # nbclient + # nbconvert nest-asyncio==1.6.0 + # via ipykernel networkx==2.8.8 + # via + # -r requirements.in + # pytket notebook==7.0.7 + # via -r requirements-dev.in notebook-shim==0.2.4 + # via + # jupyterlab + # notebook numba==0.58.1 + # via galois numpy==1.25.2 + # via + # -r requirements.in + # galois + # numba + # pytket + # qiskit-aer + # qiskit-ibm-runtime + # qiskit-ibmq-provider + # qiskit-terra + # retworkx + # rustworkx + # scipy overrides==7.7.0 + # via jupyter-server packaging==24.1 + # via + # black + # ipykernel + # jupyter-server + # jupyterlab + # jupyterlab-server + # nbconvert pandocfilters==1.5.1 + # via nbconvert parameterized==0.9.0 + # via -r requirements-dev.in parso==0.8.4 + # via jedi pathspec==0.12.1 + # via black pbr==6.0.0 + # via stevedore pexpect==4.9.0 + # via ipython platformdirs==4.2.2 + # via + # black + # jupyter-core ply==3.11 + # via qiskit-terra prometheus-client==0.20.0 + # via jupyter-server prompt-toolkit==3.0.47 + # via ipython psutil==6.0.0 + # via + # ipykernel + # qiskit-terra ptyprocess==0.7.0 + # via + # pexpect + # terminado pure-eval==0.2.3 + # via stack-data pycparser==2.22 + # via cffi pygments==2.18.0 + # via + # ipython + # nbconvert pyjwt==2.8.0 + # via ibm-cloud-sdk-core pyspnego==0.11.0 + # via requests-ntlm python-dateutil==2.9.0.post0 + # via + # arrow + # ibm-cloud-sdk-core + # jupyter-client + # qiskit-ibm-runtime + # qiskit-ibmq-provider + # qiskit-terra python-json-logger==2.0.7 + # via jupyter-events pytket==1.11.0 + # via + # -r requirements-dev.in + # pytket-qiskit pytket-qiskit==0.34.0 + # via -r requirements-dev.in pyyaml==6.0.1 + # via jupyter-events pyzmq==26.0.3 + # via + # ipykernel + # jupyter-client + # jupyter-server qiskit==0.39.0 + # via + # -r requirements-dev.in + # pytket-qiskit qiskit-aer==0.11.0 + # via qiskit qiskit-ibm-runtime==0.8.0 + # via pytket-qiskit qiskit-ibmq-provider==0.19.2 + # via qiskit qiskit-terra==0.22.0 + # via + # qiskit + # qiskit-aer + # qiskit-ibm-runtime + # qiskit-ibmq-provider qwasm==1.0.1 + # via pytket referencing==0.35.1 + # via + # jsonschema + # jsonschema-specifications + # jupyter-events requests==2.32.3 + # via + # ibm-cloud-sdk-core + # jupyterlab-server + # qiskit-ibm-runtime + # qiskit-ibmq-provider + # requests-ntlm requests-ntlm==1.3.0 + # via + # qiskit-ibm-runtime + # qiskit-ibmq-provider retworkx==0.15.1 + # via qiskit-terra rfc3339-validator==0.1.4 + # via + # jsonschema + # jupyter-events rfc3986-validator==0.1.1 + # via + # jsonschema + # jupyter-events rpds-py==0.19.0 + # via + # jsonschema + # referencing rustworkx==0.15.1 + # via retworkx scipy==1.14.0 + # via + # pytket + # qiskit-aer + # qiskit-terra send2trash==1.8.3 + # via jupyter-server six==1.16.0 + # via + # asttokens + # bleach + # python-dateutil + # rfc3339-validator sniffio==1.3.1 + # via + # anyio + # httpx soupsieve==2.5 + # via beautifulsoup4 stack-data==0.6.3 + # via ipython stevedore==5.2.0 + # via qiskit-terra symengine==0.11.0 + # via qiskit-terra sympy==1.13.1 + # via + # pytket + # qiskit-terra terminado==0.18.1 + # via + # jupyter-server + # jupyter-server-terminals tinycss2==1.3.0 + # via nbconvert tomli==2.0.1 + # via + # black + # jupyterlab tornado==6.4.1 + # via + # ipykernel + # jupyter-client + # jupyter-server + # jupyterlab + # notebook + # terminado traitlets==5.14.3 + # via + # comm + # ipykernel + # ipython + # jupyter-client + # jupyter-core + # jupyter-events + # jupyter-server + # jupyterlab + # matplotlib-inline + # nbclient + # nbconvert + # nbformat types-pkg-resources==0.1.3 + # via pytket types-python-dateutil==2.9.0.20240316 + # via arrow typing-extensions==4.12.2 + # via + # anyio + # async-lru + # black + # galois + # ipython + # pytket + # qiskit-ibm-runtime uri-template==1.3.0 + # via jsonschema urllib3==2.2.2 + # via + # ibm-cloud-sdk-core + # qiskit-ibm-runtime + # qiskit-ibmq-provider + # requests wcwidth==0.2.13 + # via prompt-toolkit webcolors==24.6.0 + # via jsonschema webencodings==0.5.1 + # via + # bleach + # tinycss2 websocket-client==1.3.3 + # via + # jupyter-server + # qiskit-ibm-runtime + # qiskit-ibmq-provider websockets==12.0 + # via qiskit-ibmq-provider # The following packages are considered to be unsafe in a requirements file: setuptools==71.1.0 + # via + # jupyterlab + # qwasm diff --git a/tests/clifford/test_clifford_synthesis.py b/tests/clifford/test_clifford_synthesis.py index 0614e452..4f19d67a 100644 --- a/tests/clifford/test_clifford_synthesis.py +++ b/tests/clifford/test_clifford_synthesis.py @@ -34,6 +34,6 @@ def test_clifford_synthesis(self, _, n_qubits, n_gates, topo, include_swaps): qc = qc.to_qiskit() self.assertTrue( - verify_equality(circuit, qc), + verify_equality(circuit.to_qiskit(), qc), "The Synthesized circuit does not equal to original", ) diff --git a/tests/pauli/test_pauli_synthesis.py b/tests/pauli/test_pauli_synthesis.py deleted file mode 100644 index 5fc30811..00000000 --- a/tests/pauli/test_pauli_synthesis.py +++ /dev/null @@ -1,110 +0,0 @@ -import unittest - -from pauliopt.pauli.synthesis import PauliSynthesizer, SynthMethod -from pauliopt.topologies import Topology -from tests.pauli.utils import generate_random_pauli_polynomial - - -class TestPauliSynthesis(unittest.TestCase): - def test_uccds(self): - for num_gadgets in [100, 200]: - for topo in [ - Topology.line(4), - Topology.line(8), - Topology.cycle(4), - Topology.cycle(8), - Topology.grid(2, 3), - ]: - print(topo._named) - pp = generate_random_pauli_polynomial(topo.num_qubits, num_gadgets) - synthesizer = PauliSynthesizer(pp, SynthMethod.UCCDS, topo) - synthesizer.synthesize() - self.assertTrue( - synthesizer.check_circuit_equivalence(), "Circuits did not match" - ) - self.assertTrue( - synthesizer.check_connectivity_predicate(), - "Connectivity predicate not satisfied", - ) - - def test_divide_and_conquer(self): - for num_gadgets in [10, 30]: - for topo in [ - Topology.line(4), - Topology.cycle(4), - Topology.complete(4), - Topology.grid(2, 3), - ]: - print(topo._named) - pp = generate_random_pauli_polynomial(topo.num_qubits, num_gadgets) - synthesizer = PauliSynthesizer(pp, SynthMethod.DIVIDE_AND_CONQUER, topo) - synthesizer.synthesize() - self.assertTrue( - synthesizer.check_circuit_equivalence(), "Circuits did not match" - ) - self.assertTrue( - synthesizer.check_connectivity_predicate(), - "Connectivity predicate not satisfied", - ) - - def test_steiner_gray_nc(self): - for num_gadgets in [5]: - for topo in [ - Topology.line(4), - Topology.line(6), - Topology.cycle(4), - Topology.grid(2, 4), - ]: - print(topo._named) - pp = generate_random_pauli_polynomial(topo.num_qubits, num_gadgets) - synthesizer = PauliSynthesizer(pp, SynthMethod.STEINER_GRAY_NC, topo) - synthesizer.synthesize() - self.assertTrue( - synthesizer.check_circuit_equivalence(), "Circuits did not match" - ) - self.assertTrue( - synthesizer.check_connectivity_predicate(), - "Connectivity predicate not satisfied", - ) - - def test_steiner_gray_clifford(self): - for num_gadgets in [100, 200]: - for topo in [ - Topology.complete(4), - Topology.line(4), - Topology.line(6), - Topology.cycle(4), - Topology.grid(2, 4), - ]: - pp = generate_random_pauli_polynomial(topo.num_qubits, num_gadgets) - synthesizer = PauliSynthesizer( - pp, SynthMethod.STEINER_GRAY_CLIFFORD, topo - ) - synthesizer.synthesize() - self.assertTrue( - synthesizer.check_circuit_equivalence(), "Circuits did not match" - ) - self.assertTrue( - synthesizer.check_connectivity_predicate(), - "Connectivity predicate not satisfied", - ) - - def test_pauli_annealing(self): - for num_gadgets in [100, 200]: - for topo in [ - Topology.line(4), - Topology.line(6), - Topology.cycle(4), - Topology.grid(2, 4), - ]: - print(topo._named) - pp = generate_random_pauli_polynomial(topo.num_qubits, num_gadgets) - synthesizer = PauliSynthesizer(pp, SynthMethod.ANNEAL, topo) - synthesizer.synthesize() - self.assertTrue( - synthesizer.check_circuit_equivalence(), "Circuits did not match" - ) - self.assertTrue( - synthesizer.check_connectivity_predicate(), - "Connectivity predicate not satisfied", - ) diff --git a/tests/test_pauli_annealing.py b/tests/test_pauli_annealing.py deleted file mode 100644 index 3ae9461f..00000000 --- a/tests/test_pauli_annealing.py +++ /dev/null @@ -1,108 +0,0 @@ -import itertools -import unittest - -import networkx as nx -import numpy as np -import pytket -from pytket._tket.circuit import PauliExpBox -from pytket._tket.pauli import Pauli -from pytket._tket.transform import Transform -from pytket.extensions.qiskit.qiskit_convert import tk_to_qiskit -from qiskit import QuantumCircuit - -from pauliopt.pauli.anneal import anneal -from pauliopt.pauli.pauli_gadget import PPhase -from pauliopt.pauli.pauli_polynomial import PauliPolynomial -from pauliopt.pauli.utils import X, Y, Z, I -from pauliopt.topologies import Topology -from pauliopt.utils import pi - -PAULI_TO_TKET = {X: Pauli.X, Y: Pauli.Y, Z: Pauli.Z, I: Pauli.I} - - -def tket_to_qiskit(circuit: pytket.Circuit) -> QuantumCircuit: - return tk_to_qiskit(circuit) - - -def pauli_poly_to_tket(pp: PauliPolynomial): - circuit = pytket.Circuit(pp.num_qubits) - for gadget in pp.pauli_gadgets: - circuit.add_pauliexpbox( - PauliExpBox( - [PAULI_TO_TKET[p] for p in gadget.paulis], - gadget.angle.to_qiskit / np.pi, - ), - list(range(pp.num_qubits)), - ) - Transform.DecomposeBoxes().apply(circuit) - return tket_to_qiskit(circuit) - - -def verify_equality(qc_in, qc_out): - """ - Verify the equality up to a global phase - :param qc_in: - :param qc_out: - :return: - """ - try: - from qiskit.quantum_info import Statevector - except: - raise Exception("Please install qiskit to compare to quantum circuits") - return Statevector.from_instruction(qc_in).equiv( - Statevector.from_instruction(qc_out) - ) - - -def generate_all_combination_pauli_polynomial(n_qubits=2): - allowed_angels = [2 * pi, pi, pi / 2, pi / 4, pi / 8] - pp = PauliPolynomial(n_qubits) - for comb in itertools.product([X, Y, Z, I], repeat=n_qubits): - pp >>= PPhase(np.random.choice(allowed_angels)) @ list(comb) - return pp - - -def check_matching_architecture(qc: QuantumCircuit, G: nx.Graph): - for gate in qc: - if gate.operation.num_qubits == 2: - ctrl, target = gate.qubits - ctrl, target = ( - ctrl._index, - target._index, - ) # TODO refactor this to a non deprecated way - if not G.has_edge(ctrl, target): - return False - return True - - -def get_two_qubit_count(circ: QuantumCircuit): - ops = circ.count_ops() - two_qubit_count = 0 - two_qubit_ops = ["cx", "cy", "cz"] - for op_key in two_qubit_ops: - if op_key in ops.keys(): - two_qubit_count += ops[op_key] - - return two_qubit_count - - -class TestPauliAnnealing(unittest.TestCase): - def test_simulated_annealing_pauli(self): - """ - Checks in this Unit test: - 1) If one constructs the Pauli Polynomial with our libary the circuits should match the ones of tket - 2) When synthesizing onto a different architecture the circuits should match the ones of tket - 3) Check that our to_qiskit method exports the Pauli Polynomial according to an architecture - """ - for num_qubits in [2, 3]: - for topo_creation in [Topology.line, Topology.complete]: - pp = generate_all_combination_pauli_polynomial(n_qubits=num_qubits) - - topology = topo_creation(pp.num_qubits) - tket_pp = pauli_poly_to_tket(pp) - our_synth = anneal(pp, topology) - - self.assertTrue( - verify_equality(tket_pp, our_synth), - "The annealing version returned a wrong circuit", - )