From 8ee1d0c690e1a0c0e8731f8713ae504927d35d7b Mon Sep 17 00:00:00 2001 From: daehiff Date: Thu, 20 Apr 2023 14:29:49 +0200 Subject: [PATCH 01/30] added pycharm to gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0752b089..8c147a27 100644 --- a/.gitignore +++ b/.gitignore @@ -128,6 +128,6 @@ dmypy.json # Pyre type checker .pyre/ - +.idea .vscode .DS_Store From dd230b21287d60a9f13a03b2d96f426db08c1f87 Mon Sep 17 00:00:00 2001 From: daehiff Date: Mon, 1 May 2023 13:07:10 +0200 Subject: [PATCH 02/30] final commit --- pauliopt/pauli/anneal.py | 48 +++++ pauliopt/pauli/clifford_gates.py | 274 +++++++++++++++++++++++++++++ pauliopt/pauli/clifford_region.py | 60 +++++++ pauliopt/pauli/pauli_gadget.py | 147 ++++++++++++++++ pauliopt/pauli/pauli_polynomial.py | 68 +++++++ pauliopt/pauli/utils.py | 27 +++ pauliopt/phase/phase_circuits.py | 218 +++++++++++++---------- pauliopt/topologies.py | 133 ++++++++------ pauliopt/utils.py | 109 ++++++------ test.py | 88 +++++++++ tests/test_pauli_propagation.py | 117 ++++++++++++ 11 files changed, 1088 insertions(+), 201 deletions(-) create mode 100644 pauliopt/pauli/anneal.py create mode 100644 pauliopt/pauli/clifford_gates.py create mode 100644 pauliopt/pauli/clifford_region.py create mode 100644 pauliopt/pauli/pauli_gadget.py create mode 100644 pauliopt/pauli/pauli_polynomial.py create mode 100644 pauliopt/pauli/utils.py create mode 100644 test.py create mode 100644 tests/test_pauli_propagation.py diff --git a/pauliopt/pauli/anneal.py b/pauliopt/pauli/anneal.py new file mode 100644 index 00000000..b4c240fd --- /dev/null +++ b/pauliopt/pauli/anneal.py @@ -0,0 +1,48 @@ +import networkx as nx +import numpy as np +from qiskit import QuantumCircuit + +from .clifford_gates import CX, CY, CZ, CliffordGate +from .clifford_region import CliffordRegion +from .pauli_polynomial import PauliPolynomial +from ..phase.optimized_circuits import _validate_temp_schedule +from ..topologies import Topology + + +def pick_random_gate(num_qubits, G: nx.Graph, gate_set=None): + if gate_set is None: + gate_set = [CX, CY, CZ] + + gate = np.random.choice(gate_set) + + return gate.generate_random(num_qubits) + + +def compute_effect(pp: PauliPolynomial, gate: CliffordGate, topology: Topology, leg_chache=None): + pp_ = pp.copy() + pp_.propagate(gate) + + return pp_.two_qubit_count(topology, leg_chache=leg_chache) - pp.two_qubit_count(topology, leg_chache=leg_chache) + + +def anneal(pp: PauliPolynomial, topology, schedule=("geometric", 1.0, 0.1), nr_iterations=100) -> QuantumCircuit: + leg_cache = {} + clifford_region = CliffordRegion() + + schedule = _validate_temp_schedule(schedule) + random_nrs = np.random.uniform(0.0, 1.0, size=(nr_iterations,)) + num_qubits = pp.num_qubits + for it in range(nr_iterations): + t = schedule(it, nr_iterations) + gate = pick_random_gate(num_qubits, topology.to_nx) + effect = 2 + compute_effect(pp, gate, topology, leg_chache=leg_cache) + accept_step = effect < 0 or random_nrs[it] < np.exp(-np.log(2) * effect / t) + if accept_step: + clifford_region.add_gate(gate) # TODO optimize clifford regions + pp.propagate(gate) + + qc = QuantumCircuit(pp.num_qubits) + qc.compose(clifford_region.to_qiskit(), inplace=True) # TODO route on architecture + qc.compose(pp.to_qiskit(topology), inplace=True) + qc.compose(clifford_region.to_qiskit().inverse(), inplace=True) + return qc diff --git a/pauliopt/pauli/clifford_gates.py b/pauliopt/pauli/clifford_gates.py new file mode 100644 index 00000000..00f1d761 --- /dev/null +++ b/pauliopt/pauli/clifford_gates.py @@ -0,0 +1,274 @@ +from enum import Enum +from typing import List, Collection +import numpy as np +from .utils import _pauli_to_string, X, Y, Z, I, Pauli + +from .pauli_gadget import PauliGadget + + +class CLiffordType(Enum): + CX = "cx" + CY = "cy" + CZ = "cz" + H = "h" + S = "s" + V = "v" + + +class CliffordGate: + def __init__(self, c_type): + self.c_type = c_type + + def propagate_pauli(self, gadget: PauliGadget): + raise Exception(f"This method is not implemented on object: {self}") + + def to_qiskit(self): + raise Exception(f"This method is not implemented on object: {self}") + + @property + def num_qubits(self): + raise Exception(f"This property is not implemented on object: {self}") + + @staticmethod + def generate_random(num_qubits): + raise Exception(f"This method is not implemented") + + @property + def to_hash(self): + raise Exception(f"This property is not implemented on object: {self}") + + +class SingleQubitGate(CliffordGate): + rules = None + + def __init__(self, type, qubit): + super().__init__(type) + self.qubit = qubit + + def propagate_pauli(self, gadget: PauliGadget): + if self.rules is None: + raise Exception(f"{self} has no rules defined for propagation!") + p_string = _pauli_to_string(gadget.paulis[self.qubit]) + new_p, phase_change = self.rules[p_string] + gadget.paulis[self.qubit] = new_p + gadget.angle *= phase_change + return gadget + + @property + def num_qubits(self): + return self.qubit + 1 + + +class ControlGate(CliffordGate): + rules = None + + def __init__(self, type, control, target): + super().__init__(type) + self.control = control + self.target = target + + def propagate_pauli(self, gadget: PauliGadget): + if self.rules is None: + raise Exception(f"{self} has no rules defined for propagation!") + pauli_size = len(gadget) + if self.control >= pauli_size or self.target >= pauli_size: + raise Exception(f"Control: {self.control} or Target {self.target} out of bounds: {pauli_size}") + p_string = _pauli_to_string(gadget.paulis[self.control]) + _pauli_to_string(gadget.paulis[self.target]) + p_c, p_t, phase_change = self.rules[p_string] + gadget.paulis[self.control] = p_c + gadget.paulis[self.target] = p_t + gadget.angle *= phase_change + return gadget + + @property + def num_qubits(self): + return max(self.control, self.target) + 1 + + +class CX(ControlGate): + rules = {'XX': (X, I, 1), + 'XY': (Y, Z, 1), + 'XZ': (Y, Y, -1), + 'XI': (X, X, 1), + 'YX': (Y, I, 1), + 'YY': (X, Z, -1), + 'YZ': (X, Y, 1), + 'YI': (Y, X, 1), + 'ZX': (Z, X, 1), + 'ZY': (I, Y, 1), + 'ZZ': (I, Z, 1), + 'ZI': (Z, I, 1), + 'IX': (I, X, 1), + 'IY': (Z, Y, 1), + 'IZ': (Z, Z, 1), + 'II': (I, I, 1)} + + def __init__(self, control, target): + super().__init__(CLiffordType.CX, control, target) + + def to_qiskit(self): + try: + from qiskit import QuantumCircuit + except: + raise Exception("Please install qiskit to export Clifford Gates") + qc = QuantumCircuit(self.num_qubits) + qc.cx(self.control, self.target) + return qc + + @staticmethod + def generate_random(num_qubits): + control = np.random.choice(list(range(num_qubits))) + target = np.random.choice([i for i in range(num_qubits) if i != control]) + return CX(control, target) + + +class CZ(ControlGate): + rules = {'XX': (Y, Y, 1), + 'XY': (Y, X, -1), + 'XZ': (X, I, 1), + 'XI': (X, Z, 1), + 'YX': (X, Y, -1), + 'YY': (X, X, 1), + 'YZ': (Y, I, 1), + 'YI': (Y, Z, 1), + 'ZX': (I, X, 1), + 'ZY': (I, Y, 1), + 'ZZ': (Z, Z, 1), + 'ZI': (Z, I, 1), + 'IX': (Z, X, 1), + 'IY': (Z, Y, 1), + 'IZ': (I, Z, 1), + 'II': (I, I, 1)} + + def __init__(self, control, target): + super().__init__(CLiffordType.CZ, control, target) + + def to_qiskit(self): + try: + from qiskit import QuantumCircuit + except: + raise Exception("Please install qiskit to export Clifford Gates") + qc = QuantumCircuit(self.num_qubits) + qc.cz(self.control, self.target) + return qc + + @staticmethod + def generate_random(num_qubits): + control = np.random.choice(list(range(num_qubits))) + target = np.random.choice([i for i in range(num_qubits) if i != control]) + return CZ(control, target) + + +class CY(ControlGate): + rules = {'XX': (Y, Z, -1), + 'XY': (X, I, 1), + 'XZ': (Y, X, 1), + 'XI': (X, Y, 1), + 'YX': (X, Z, 1), + 'YY': (Y, I, 1), + 'YZ': (X, X, -1), + 'YI': (Y, Y, 1), + 'ZX': (I, X, 1), + 'ZY': (Z, Y, 1), + 'ZZ': (I, Z, 1), + 'ZI': (Z, I, 1), + 'IX': (Z, X, 1), + 'IY': (I, Y, 1), + 'IZ': (Z, Z, 1), + 'II': (I, I, 1)} + + def __init__(self, control, target): + super().__init__(CLiffordType.CY, control, target) + + def to_qiskit(self): + try: + from qiskit import QuantumCircuit + except: + raise Exception("Please install qiskit to export Clifford Gates") + qc = QuantumCircuit(self.num_qubits) + qc.cy(self.control, self.target) + return qc + + @staticmethod + def generate_random(num_qubits): + control = np.random.choice(list(range(num_qubits))) + target = np.random.choice([i for i in range(num_qubits) if i != control]) + return CY(control, target) + + +# +class H(SingleQubitGate): + rules = {'X': (Z, 1), + 'Y': (Y, -1), + 'Z': (X, 1), + 'I': (I, 1)} + + def __init__(self, qubit): + super().__init__(CLiffordType.H, qubit) + + @staticmethod + def generate_random(num_qubits): + qubit = np.random.choice(list(range(num_qubits))) + return H(qubit) + + def to_qiskit(self): + try: + from qiskit import QuantumCircuit + except: + raise Exception("Please install qiskit to export Clifford Gates") + qc = QuantumCircuit(self.num_qubits) + qc.h(self.qubit) + return qc + + +class S(SingleQubitGate): + rules = {'X': (Y, -1), + 'Y': (X, 1), + 'Z': (Z, 1), + 'I': (I, 1)} + + def __init__(self, qubit): + super().__init__(CLiffordType.S, qubit) + + @staticmethod + def generate_random(num_qubits): + qubit = np.random.choice(list(range(num_qubits))) + + return S(qubit) + + def to_qiskit(self): + try: + from qiskit import QuantumCircuit + except: + raise Exception("Please install qiskit to export Clifford Gates") + qc = QuantumCircuit(self.num_qubits) + qc.s(self.qubit) + return qc + + +class V(SingleQubitGate): + rules = {'X': (X, 1), + 'Y': (Z, -1), + 'Z': (Y, 1), + 'I': (I, 1)} + + def __init__(self, qubit): + super().__init__(CLiffordType.V, qubit) + + @staticmethod + def generate_random(num_qubits): + qubit = np.random.choice(list(range(num_qubits))) + + return V(qubit) + + def to_qiskit(self): + try: + from qiskit import QuantumCircuit + except: + raise Exception("Please install qiskit to export Clifford Gates") + qc = QuantumCircuit(self.num_qubits) + qc.sx(self.qubit) + return qc + +# For the gates X, Y, Z there won't be a change of Pauli matrices +# Refused to implement "higher order gates" like NCX, SWAP, DCX, ... but with this structure this can easily be done diff --git a/pauliopt/pauli/clifford_region.py b/pauliopt/pauli/clifford_region.py new file mode 100644 index 00000000..79289c70 --- /dev/null +++ b/pauliopt/pauli/clifford_region.py @@ -0,0 +1,60 @@ +from qiskit import QuantumCircuit + +from .clifford_gates import * +import qiskit.quantum_info as qi + + +class CliffordRegion: + commutation_rule_set = {('V', 1, 0, 'H', 0, 1), + ('H', 0, 1, 'S', 1, 0), + ('S', 1, 0, 'CX', 1, 0), + ('CX', 0, 1, 'S', 0, 1), + ('H', 1, 0, 'V', 0, 1), + ('S', 0, 1, 'CX', 0, 1), + ('S', 1, 0, 'CZ', 0, 1), + ('S', 1, 0, 'CZ', 1, 0), + ('S', 0, 1, 'H', 1, 0), + ('H', 0, 1, 'V', 1, 0), + ('CX', 0, 1, 'V', 1, 0), + ('V', 0, 1, 'H', 1, 0), + ('S', 1, 0, 'H', 0, 1), + ('V', 1, 0, 'CX', 0, 1), + ('CX', 1, 0, 'V', 0, 1), + ('S', 0, 1, 'V', 1, 0), + ('H', 1, 0, 'S', 0, 1), + ('S', 0, 1, 'CY', 0, 1), + ('CX', 1, 0, 'S', 1, 0), + ('S', 1, 0, 'CY', 1, 0), + ('CZ', 1, 0, 'S', 1, 0), + ('V', 0, 1, 'CX', 1, 0), + ('S', 0, 1, 'CZ', 1, 0), + ('CY', 1, 0, 'S', 1, 0), + ('S', 1, 0, 'V', 0, 1), + ('V', 0, 1, 'S', 1, 0), + ('CY', 0, 1, 'S', 0, 1), + ('CZ', 0, 1, 'S', 0, 1), + ('S', 0, 1, 'CZ', 0, 1), + ('CZ', 0, 1, 'S', 1, 0), + ('CZ', 1, 0, 'S', 0, 1), + ('V', 1, 0, 'S', 0, 1)} + + def __init__(self, gates=None): + if gates is None: + gates = [] + self.gates: [CliffordGate] = gates + self.num_qubits = 1 + + def add_gate(self, gate: CliffordGate): + self.num_qubits = max(self.num_qubits, gate.num_qubits) + self.gates.append(gate) + + def add_gate_simplify(self, gate: CliffordGate): + for idx, other in enumerate(self.gates): + pass + + def to_qiskit(self): + qc = QuantumCircuit(self.num_qubits) + for gate in self.gates: + qc.compose(gate.to_qiskit(), inplace=True) + + return qc diff --git a/pauliopt/pauli/pauli_gadget.py b/pauliopt/pauli/pauli_gadget.py new file mode 100644 index 00000000..328a19ec --- /dev/null +++ b/pauliopt/pauli/pauli_gadget.py @@ -0,0 +1,147 @@ +from collections import deque +from typing import List + +from qiskit import QuantumCircuit + +from pauliopt.pauli.utils import _pauli_to_string, Pauli, X, Y, Z, I +from pauliopt.topologies import Topology +from pauliopt.utils import AngleExpr +import numpy as np +import networkx as nx + + +def decompose_cnot_ladder_z(ctrl: int, trg: int, arch: Topology): + cnot_ladder = [] + shortest_path = arch.shortest_path(ctrl, trg) + + prev = ctrl + for current in shortest_path[1:-1]: + cnot_ladder.append((current, prev)) + cnot_ladder.append((prev, current)) + prev = current + cnot_ladder.append((shortest_path[-2], trg)) + return reversed(cnot_ladder) + + +def find_minimal_cx_assignment(column: np.array, arch: Topology): + if not np.all(np.isin(column, [0, 1])): + raise Exception(f"Expected binary array as column, got: {column}") + + G = nx.Graph() + for i in range(len(column)): + G.add_node(i) + + for i in range(len(column)): + for j in range(len(column)): + if column[i] != 0 and column[j] != 0 and i != j: + G.add_edge(i, j, weight=4 * arch.dist(i, j) - 2) + + # Algorithm by Gogioso et. al. (https://arxiv.org/pdf/2206.11839.pdf) to find qubit assignment with MST + mst_branches = list(nx.minimum_spanning_edges(G, data=False, algorithm="prim")) + incident = {q: set() for q in range(len(column))} + for fst, snd in mst_branches: + incident[fst].add((fst, snd)) + incident[snd].add((snd, fst)) + + q0 = np.argmax(column) # Assume that 0 is always the first qubit aka first non zero + visited = set() + queue = deque([q0]) + cnot_ladder = [] + while queue: + q = queue.popleft() + visited.add(q) + for tail, head in incident[q]: + if head not in visited: + cnot_ladder += decompose_cnot_ladder_z(head, tail, arch) + queue.append(head) + return cnot_ladder, q0 + + +class PPhase: + _angle: AngleExpr + + def __init__(self, angle: AngleExpr): + self._angle = angle + + def __matmul__(self, paulis: List[Pauli]): + return PauliGadget(self._angle, paulis) + + +class PauliGadget: + def __init__(self, angle: AngleExpr, paulis: List[Pauli]): + self.angle = angle + self.paulis = paulis + + def __len__(self): + return len(self.paulis) + + def __repr__(self): + return f"({self.angle}) @ {{ {', '.join([_pauli_to_string(pauli) for pauli in self.paulis])} }}" + + def copy(self): + return PauliGadget(self.angle, self.paulis.copy()) + + def two_qubit_count(self, topology, leg_chache=None): + if leg_chache is None: + leg_chache = {} + + column = np.asarray(self.paulis) + col_binary = np.where(column == Pauli.I, 0, 1) + col_id = "".join([str(int(el)) for el in col_binary]) + if col_id in leg_chache.keys(): + return leg_chache[col_id] + else: + cnot_amount = len(find_minimal_cx_assignment(col_binary, topology)[0]) + leg_chache[col_id] = cnot_amount + return cnot_amount + + def to_qiskit(self, topology=None): + num_qubits = len(self.paulis) + if topology is None: + topology = Topology.complete(num_qubits) + + circ = QuantumCircuit(num_qubits) + + column = np.asarray(self.paulis) + column_binary = np.where(column == I, 0, 1) + if np.all(column_binary == 0): + circ.global_phase += self.angle + return circ + + cnot_ladder, q0 = find_minimal_cx_assignment(column_binary, topology) + for pauli_idx in range(len(column)): + if column[pauli_idx] == I: + pass + elif column[pauli_idx] == X: + circ.h(pauli_idx) # Had + elif column[pauli_idx] == Y: + circ.rx(0.5 * np.pi, pauli_idx) # V = Rx(0.5) + elif column[pauli_idx] == Z: # Z + pass + else: + raise Exception(f"unknown column type: {column[pauli_idx]}") + + if len(cnot_ladder) > 0: + for (pauli_idx, target) in reversed(cnot_ladder): + circ.cx(pauli_idx, target) + + circ.rz(self.angle, q0) + + for (pauli_idx, target) in cnot_ladder: + circ.cx(pauli_idx, target) + else: + target = np.argmax(column_binary) + circ.rz(self.angle, target) + + for pauli_idx in range(len(column)): + if column[pauli_idx] == Pauli.I: + pass + elif column[pauli_idx] == Pauli.X: + circ.h(pauli_idx) # Had + elif column[pauli_idx] == Pauli.Y: + circ.rx(-0.5 * np.pi, pauli_idx) # Vdg = Rx(-0.5) + elif column[pauli_idx] == Pauli.Z: + pass + else: + raise Exception(f"unknown column type: {column[pauli_idx]}") + return circ diff --git a/pauliopt/pauli/pauli_polynomial.py b/pauliopt/pauli/pauli_polynomial.py new file mode 100644 index 00000000..a1611679 --- /dev/null +++ b/pauliopt/pauli/pauli_polynomial.py @@ -0,0 +1,68 @@ +from .clifford_gates import CliffordGate +from .pauli_gadget import PauliGadget + +from qiskit import QuantumCircuit + +from pauliopt.topologies import Topology + + +class PauliPolynomial: + def __init__(self): + self.pauli_gadgets = [] + + def __irshift__(self, gadget: PauliGadget): + self.pauli_gadgets.append(gadget) + return self + + def __rshift__(self, pauli_polynomial): + for gadget in pauli_polynomial.pauli_gadgets: + self.pauli_gadgets.append(gadget) + return self + + def __repr__(self): + rep_ = "" + for gadet in self.pauli_gadgets: + rep_ += str(gadet) + "\n" + return rep_ + + def __len__(self): + return self.size + + @property + def size(self): + return len(self.pauli_gadgets) + + @property + def num_qubits(self): + return max([len(gadget) for gadget in self.pauli_gadgets]) + + def to_qiskit(self, topology=None): + num_qubits = self.num_qubits + if topology is None: + topology = Topology.complete(num_qubits) + + qc = QuantumCircuit(num_qubits) + for gadget in self.pauli_gadgets: + qc.compose(gadget.to_qiskit(topology), inplace=True) + + return qc + + def propagate(self, gate: CliffordGate): + pp_ = PauliPolynomial() + for gadget in self.pauli_gadgets: + pp_ >>= gate.propagate_pauli(gadget) + return pp_ + + def copy(self): + pp_ = PauliPolynomial() + for gadget in self.pauli_gadgets: + pp_ >>= gadget.copy() + return pp_ + + def two_qubit_count(self, topology, leg_chache=None): + if leg_chache is None: + leg_chache = {} + count = 0 + for gadget in self.pauli_gadgets: + count += gadget.two_qubit_count(topology, leg_chache=leg_chache) + return count diff --git a/pauliopt/pauli/utils.py b/pauliopt/pauli/utils.py new file mode 100644 index 00000000..d3d17c14 --- /dev/null +++ b/pauliopt/pauli/utils.py @@ -0,0 +1,27 @@ +from enum import Enum + + +class Pauli(Enum): + I = 0 + X = 1 + Y = 2 + Z = 3 + + +I = Pauli.I +X = Pauli.X +Y = Pauli.Y +Z = Pauli.Z + + +def _pauli_to_string(pauli: Pauli): + if pauli == Pauli.I: + return "I" + elif pauli == Pauli.X: + return "X" + elif pauli == Pauli.Y: + return "Y" + elif pauli == Pauli.Z: + return "Z" + else: + raise Exception(f"{pauli} is not a Paulimatrix") diff --git a/pauliopt/phase/phase_circuits.py b/pauliopt/phase/phase_circuits.py index 75a51723..88ee6148 100644 --- a/pauliopt/phase/phase_circuits.py +++ b/pauliopt/phase/phase_circuits.py @@ -13,12 +13,14 @@ from pauliopt.topologies import Topology from pauliopt.utils import Angle, AngleExpr, AngleVar, SVGBuilder, pi + def _frozenset_to_int(s: FrozenSet[int]) -> int: i = 0 for x in s: - i |= (2**x) + i |= (2 ** x) return i + def _int_to_frozenset(i: int) -> FrozenSet[int]: s: List[int] = [] x = 0 @@ -29,6 +31,7 @@ def _int_to_frozenset(i: int) -> FrozenSet[int]: x += 1 return frozenset(s) + def _int_to_iterator(i: int) -> Iterator[int]: x = 0 while i != 0: @@ -37,6 +40,7 @@ def _int_to_iterator(i: int) -> Iterator[int]: i //= 2 x += 1 + def _prims_algorithm_weight(nodes: Collection[int], weight: Callable[[int, int], int], inf: int) -> int: """ @@ -59,8 +63,8 @@ def _prims_algorithm_weight(nodes: Collection[int], weight: Callable[[int, int], } while to_visit: # Look for the node to be visited which is nearest to the visited set: - nearest_node = 0 # dummy value - nearest_dist: int = inf # dummy value + nearest_node = 0 # dummy value + nearest_dist: int = inf # dummy value for n in to_visit: n_dist: int = dist_from_visited[n] if n_dist < nearest_dist: @@ -76,6 +80,7 @@ def _prims_algorithm_weight(nodes: Collection[int], weight: Callable[[int, int], dist_from_visited[n] = dist_nearest_n return mst_length + def _prims_algorithm_branches(nodes: Collection[int], weight: Callable[[int, int], int], inf: int) -> Sequence[Tuple[int, int]]: # pylint: disable = too-many-locals @@ -96,8 +101,8 @@ def _prims_algorithm_branches(nodes: Collection[int], weight: Callable[[int, int } while to_visit: # Look for the node to be visited which is nearest to the visited set: - nearest_node = 0 # dummy value - nearest_dist: int = inf # dummy value + nearest_node = 0 # dummy value + nearest_dist: int = inf # dummy value for n in to_visit: n_dist: int = dist_from_visited[n] if n_dist < nearest_dist: @@ -114,6 +119,7 @@ def _prims_algorithm_branches(nodes: Collection[int], weight: Callable[[int, int edge_from_visited[n] = (nearest_node, n) return mst_branches + def _prims_algorithm_full(nodes: Collection[int], weight: Callable[[int, int], int], inf: int) -> Tuple[int, Sequence[int], Sequence[Tuple[int, int]]]: # pylint: disable = too-many-locals @@ -136,8 +142,8 @@ def _prims_algorithm_full(nodes: Collection[int], weight: Callable[[int, int], i } while to_visit: # Look for the node to be visited which is nearest to the visited set: - nearest_node = 0 # dummy value - nearest_dist: int = inf # dummy value + nearest_node = 0 # dummy value + nearest_dist: int = inf # dummy value for n in to_visit: n_dist: int = dist_from_visited[n] if n_dist < nearest_dist: @@ -225,9 +231,9 @@ def cx_count(self, topology: Topology, *, topology = topology.mapped_fwd({ mapping[i]: i for i in mapping }) - return _prims_algorithm_weight(self._qubits, - lambda u, v: 4*topology.dist(u, v)-2, - 4*len(topology.qubits)-2) + return _prims_algorithm_weight(self._qubits, + lambda u, v: 4 * topology.dist(u, v) - 2, + 4 * len(topology.qubits) - 2) def on_qiskit_circuit(self, topology: Topology, circuit: Any) -> None: """ @@ -243,7 +249,7 @@ def on_qiskit_circuit(self, topology: Topology, circuit: Any) -> None: # TODO: currently uses CX ladder, must change into balanced tree! (same CX count) try: # pylint: disable = import-outside-toplevel - from qiskit.circuit import QuantumCircuit # type: ignore + from qiskit.circuit import QuantumCircuit # type: ignore except ModuleNotFoundError as e: raise ModuleNotFoundError("You must install the 'qiskit' library.") from e if not isinstance(circuit, QuantumCircuit): @@ -253,8 +259,8 @@ def on_qiskit_circuit(self, topology: Topology, circuit: Any) -> None: raise TypeError(f"Expected Topology, found {type(topology)}.") # Build MST data structure: mst_branches = _prims_algorithm_branches(self._qubits, - lambda u, v: 4*topology.dist(u, v)-2, - 4*len(topology.qubits)-2) + lambda u, v: 4 * topology.dist(u, v) - 2, + 4 * len(topology.qubits) - 2) upper_ladder: List[Tuple[int, int]] = [] if len(self._qubits) == 1: q0 = next(iter(self._qubits)) @@ -298,8 +304,8 @@ def print_impl_info(self, topology: Topology) -> None: raise TypeError(f"Expected Topology, found {type(topology)}.") mst_length, mst_branch_lengths, mst_branches = \ _prims_algorithm_full(self._qubits, - lambda u, v: 4*topology.dist(u, v)-2, - 4*len(topology.qubits)-2) + lambda u, v: 4 * topology.dist(u, v) - 2, + 4 * len(topology.qubits) - 2) print(f"MST implementation info for {str(self)}:") print(f" - Overall CX count for gadget: {mst_length}") print(f" - MST branches: {mst_branches}") @@ -370,87 +376,107 @@ def __matmul__(self, qubits: Collection[int]) -> PhaseGadget: def _rx(qubit: int, angle: Angle) -> List[PhaseGadget]: return [X(angle) @ {qubit}] + def _rz(qubit: int, angle: Angle) -> List[PhaseGadget]: return [Z(angle) @ {qubit}] + def _ry(qubit: int, angle: Angle) -> List[PhaseGadget]: """ Phase gadget implementation of single-qubit Y rotation. """ - return _rx(qubit, +pi/2) + _rz(qubit, angle) + _rx(qubit, -pi/2) + return _rx(qubit, +pi / 2) + _rz(qubit, angle) + _rx(qubit, -pi / 2) + def _i(qubit: int) -> List[PhaseGadget]: return [] + def _x(qubit: int) -> List[PhaseGadget]: return _rx(qubit, pi) + def _z(qubit: int) -> List[PhaseGadget]: """ Phase gadget implementation of single-qubit X gate. """ return _rz(qubit, pi) + def _y(qubit: int) -> List[PhaseGadget]: """ Phase gadget implementation of single-qubit Y gate. """ return _z(qubit) + _x(qubit) + def _s(qubit: int) -> List[PhaseGadget]: """ Phase gadget implementation of single-qubit S gate. """ - return _rz(qubit, pi/2) + return _rz(qubit, pi / 2) + def _sdg(qubit: int) -> List[PhaseGadget]: """ Phase gadget implementation of single-qubit S gate. """ - return _rz(qubit, -pi/2) + return _rz(qubit, -pi / 2) + def _v(qubit: int) -> List[PhaseGadget]: """ Phase gadget implementation of single-qubit S gate. """ - return _rx(qubit, pi/2) + return _rx(qubit, pi / 2) + def _vdg(qubit: int) -> List[PhaseGadget]: """ Phase gadget implementation of single-qubit S gate. """ - return _rx(qubit, -pi/2) + return _rx(qubit, -pi / 2) + def _t(qubit: int) -> List[PhaseGadget]: """ Phase gadget implementation of single-qubit T gate. """ - return _rz(qubit, pi/4) + return _rz(qubit, pi / 4) + def _h(qubit: int, basis: Literal["Z", "X"] = "Z", - sign: Literal[1, -1]=1) -> List[PhaseGadget]: + sign: Literal[1, -1] = 1) -> List[PhaseGadget]: """ Phase gadget implementation of single-qubit Hadamard gate. """ if basis not in ("Z", "X"): raise TypeError(f"Invalid basis {basis}.") if sign not in (1, -1): raise TypeError(f"Invalid sign {sign}.") if basis == "Z": - return _rx(qubit, sign*pi/2) + _rz(qubit, sign*pi/2) + _rx(qubit, sign*pi/2) - return _rz(qubit, sign*pi/2) + _rx(qubit, sign*pi/2) + _rz(qubit, sign*pi/2) + return _rx(qubit, sign * pi / 2) + _rz(qubit, sign * pi / 2) + _rx(qubit, sign * pi / 2) + return _rz(qubit, sign * pi / 2) + _rx(qubit, sign * pi / 2) + _rz(qubit, sign * pi / 2) + def _cu1(ctrl: int, tgt: int, angle: Angle) -> List[PhaseGadget]: """ Phase gadget implementation of CU1 gate. """ return [Z(-angle) @ {ctrl, tgt}] + _rz(ctrl, angle) + _rz(tgt, angle) + def _crz(ctrl: int, tgt: int, angle: Angle) -> List[PhaseGadget]: """ Phase gadget implementation of CRZ gate. """ return [Z(-angle / 2) @ {ctrl, tgt}] + _rz(tgt, angle / 2) + def _cry(ctrl: int, tgt: int, angle: Angle) -> List[PhaseGadget]: """ Phase gadget implementation of CRY gate. """ return _v(tgt) + _crz(ctrl, tgt, angle) + _vdg(tgt) + def _crx(ctrl: int, tgt: int, angle: Angle) -> List[PhaseGadget]: """ Phase gadget implementation of CRX gate. """ return _h(tgt) + _crz(ctrl, tgt, angle) + _h(tgt, sign=-1) + def _cz(leg1: int, leg2: int) -> List[PhaseGadget]: """ Phase gadget implementation of CZ gate. """ return _cu1(leg1, leg2, pi / 2) + def _cy(leg1: int, leg2: int) -> List[PhaseGadget]: """ Phase gadget implementation of CY gate. """ return _v(leg2) + _cz(leg1, leg2) + _vdg(leg2) + def _cx(ctrl: int, tgt: int) -> List[PhaseGadget]: """ Phase gadget implementation of CX gate. """ return _h(tgt) + _cz(ctrl, tgt) + _h(tgt, sign=-1) + def _u3(qubit: int, theta: Angle, phi: Angle, lam: Angle) -> List[PhaseGadget]: """ Phase gadget implementation of U3 gate. """ return _rz(qubit, lam) + _ry(qubit, theta) + _rz(qubit, phi) @@ -511,7 +537,7 @@ def __init__(self, num_qubits: int, gadgets: Sequence[PhaseGadget] = tuple()): if not isinstance(num_qubits, int) or num_qubits <= 0: raise TypeError("Number of qubits must be a positive integer.") if (not isinstance(gadgets, Sequence) - or not all(isinstance(g, PhaseGadget) for g in gadgets)): # pylint: disable = C0330 + or not all(isinstance(g, PhaseGadget) for g in gadgets)): # pylint: disable = C0330 raise TypeError("Gadgets should be a sequence of PhaseGadget.") self._num_qubits = num_qubits # Fills the lists of original indices and angles for the gadgets: @@ -523,7 +549,7 @@ def __init__(self, num_qubits: int, gadgets: Sequence[PhaseGadget] = tuple()): self._gadget_idxs[gadget.basis].append(i) angle = gadget.angle if isinstance(angle, Angle): - angle %= 2*pi + angle %= 2 * pi self._angles.append(angle) self._matrix = {} self._gadget_legs_cache = {} @@ -585,8 +611,8 @@ def set_angles(self, angles: Sequence[AngleExpr]) -> None: if len(angles) != len(self._angles): raise ValueError(f"Expected {len(self._angles)} angles, " f"found {len(angles)} instead.") - pi2 = 2*pi - self._angles = [angle%pi2 if isinstance(angle, Angle) else angle + pi2 = 2 * pi + self._angles = [angle % pi2 if isinstance(angle, Angle) else angle for angle in angles] def refresh_angle_vars(self, params: Union[str, Callable[[int], AngleVar]]) -> None: @@ -614,9 +640,9 @@ def rz(self, qubit: int, angle: AngleExpr) -> "PhaseCircuit": def ry(self, qubit: int, angle: AngleExpr) -> "PhaseCircuit": """ Phase gadget implementation of single-qubit Y rotation. """ - self.rx(qubit, +pi/2) + self.rx(qubit, +pi / 2) self.rz(qubit, angle) - self.rx(qubit, -pi/2) + self.rx(qubit, -pi / 2) return self def i(self, qubit: int) -> "PhaseCircuit": @@ -641,45 +667,45 @@ def y(self, qubit: int) -> "PhaseCircuit": def s(self, qubit: int) -> "PhaseCircuit": """ Phase gadget implementation of single-qubit S gate. """ - self.rz(qubit, pi/2) + self.rz(qubit, pi / 2) return self def sdg(self, qubit: int) -> "PhaseCircuit": """ Phase gadget implementation of single-qubit Sdg gate. """ - self.rz(qubit, -pi/2) + self.rz(qubit, -pi / 2) return self def v(self, qubit: int) -> "PhaseCircuit": """ Phase gadget implementation of single-qubit S gate. """ - self.rx(qubit, pi/2) + self.rx(qubit, pi / 2) return self def vdg(self, qubit: int) -> "PhaseCircuit": """ Phase gadget implementation of single-qubit Sdg gate. """ - self.rx(qubit, -pi/2) + self.rx(qubit, -pi / 2) return self def t(self, qubit: int) -> "PhaseCircuit": """ Phase gadget implementation of single-qubit T gate. """ - self.rz(qubit, pi/4) + self.rz(qubit, pi / 4) return self def h(self, qubit: int, basis: Literal["Z", "X"] = "Z", - sign: Literal[1, -1]=1) -> "PhaseCircuit": + sign: Literal[1, -1] = 1) -> "PhaseCircuit": """ Phase gadget implementation of single-qubit Hadamard gate. """ if basis not in ("Z", "X"): raise TypeError(f"Invalid basis {basis}.") if sign not in (1, -1): raise TypeError(f"Invalid sign {sign}.") if basis == "Z": - self.rx(qubit, sign*pi/2) - self.rz(qubit, sign*pi/2) - self.rx(qubit, sign*pi/2) + self.rx(qubit, sign * pi / 2) + self.rz(qubit, sign * pi / 2) + self.rx(qubit, sign * pi / 2) else: - self.rz(qubit, sign*pi/2) - self.rx(qubit, sign*pi/2) - self.rz(qubit, sign*pi/2) + self.rz(qubit, sign * pi / 2) + self.rx(qubit, sign * pi / 2) + self.rz(qubit, sign * pi / 2) return self def cu1(self, ctrl: int, tgt: int, angle: AngleExpr) -> "PhaseCircuit": @@ -801,7 +827,7 @@ def cx_count(self, topology: Topology, *, i: mapping[i] for i in range(len(mapping)) } if mapping is not None and set(mapping.values()) != set(range(self.num_qubits)): - raise TypeError(f"Expected mapping images [0, ..., {self.num_qubits-1}], " + raise TypeError(f"Expected mapping images [0, ..., {self.num_qubits - 1}], " f"found {sorted(set(mapping.values()))}") if mapping is not None: # use the reverse mapping on the topology @@ -912,7 +938,7 @@ def to_svg(self, *, raise TypeError("Keyword argument 'scale' must be positive float.") return self._to_svg(zcolor=zcolor, xcolor=xcolor, hscale=hscale, vscale=vscale, scale=scale, - svg_code_only=svg_code_only) # type: ignore[call-overload] + svg_code_only=svg_code_only) # type: ignore[call-overload] @overload def _to_svg(self, *, @@ -948,61 +974,61 @@ def _to_svg(self, *, hscale *= scale gadgets = self.gadgets num_digits = int(ceil(log10(num_qubits))) - line_height = int(ceil(30*vscale)) - row_width = int(ceil(120*hscale)) - pad_x = int(ceil(10*hscale)) - margin_x = int(ceil(40*hscale)) - pad_y = int(ceil(20*vscale)) - r = pad_y//2-2 - font_size = 2*r - pad_x += font_size*(num_digits+1) - delta_fst = row_width//4 - delta_snd = 2*row_width//4 - width = 2*pad_x + 2*margin_x + row_width*len(gadgets) - height = pad_y + line_height*(num_qubits+1) + line_height = int(ceil(30 * vscale)) + row_width = int(ceil(120 * hscale)) + pad_x = int(ceil(10 * hscale)) + margin_x = int(ceil(40 * hscale)) + pad_y = int(ceil(20 * vscale)) + r = pad_y // 2 - 2 + font_size = 2 * r + pad_x += font_size * (num_digits + 1) + delta_fst = row_width // 4 + delta_snd = 2 * row_width // 4 + width = 2 * pad_x + 2 * margin_x + row_width * len(gadgets) + height = pad_y + line_height * (num_qubits + 1) builder = SVGBuilder(width, height) levels: List[int] = [0 for _ in range(num_qubits)] max_lvl = 0 for gadget in gadgets: fill = zcolor if gadget.basis == "Z" else xcolor other_fill = xcolor if gadget.basis == "Z" else zcolor - qubit_span = range(min(gadget.qubits), max(gadget.qubits)+1) + qubit_span = range(min(gadget.qubits), max(gadget.qubits) + 1) lvl = max(levels[q] for q in qubit_span) max_lvl = max(max_lvl, lvl) x = pad_x + margin_x + lvl * row_width for q in qubit_span: - levels[q] = lvl+1 + levels[q] = lvl + 1 if len(gadget.qubits) > 1: - text_y = pad_y+min(gadget.qubits)*line_height+line_height//2 + text_y = pad_y + min(gadget.qubits) * line_height + line_height // 2 for q in gadget.qubits: - y = pad_y + (q+1)*line_height - builder.line((x, y), (x+delta_fst, text_y)) + y = pad_y + (q + 1) * line_height + builder.line((x, y), (x + delta_fst, text_y)) for q in gadget.qubits: - y = pad_y + (q+1)*line_height + y = pad_y + (q + 1) * line_height builder.circle((x, y), r, fill) - builder.line((x+delta_fst, text_y), (x+delta_snd, text_y)) - builder.circle((x+delta_fst, text_y), r, other_fill) - builder.circle((x+delta_snd, text_y), r, fill) - builder.text((x+delta_snd+2*r, text_y), str(gadget.angle), font_size=font_size) + builder.line((x + delta_fst, text_y), (x + delta_snd, text_y)) + builder.circle((x + delta_fst, text_y), r, other_fill) + builder.circle((x + delta_snd, text_y), r, fill) + builder.text((x + delta_snd + 2 * r, text_y), str(gadget.angle), font_size=font_size) else: for q in gadget.qubits: - y = pad_y + (q+1)*line_height + y = pad_y + (q + 1) * line_height builder.circle((x, y), r, fill) - builder.text((x+r, y-line_height//3), str(gadget.angle), font_size=font_size) - width = 2*pad_x + 2*margin_x + row_width*(2*max_lvl+1)//2 + builder.text((x + r, y - line_height // 3), str(gadget.angle), font_size=font_size) + width = 2 * pad_x + 2 * margin_x + row_width * (2 * max_lvl + 1) // 2 _builder = SVGBuilder(width, height) for q in range(num_qubits): - y = pad_y + (q+1) * line_height - _builder.line((pad_x, y), (width-pad_x, y)) + y = pad_y + (q + 1) * line_height + _builder.line((pad_x, y), (width - pad_x, y)) _builder.text((0, y), f"{str(q):>{num_digits}}", font_size=font_size) - _builder.text((width-pad_x+r, y), f"{str(q):>{num_digits}}", font_size=font_size) + _builder.text((width - pad_x + r, y), f"{str(q):>{num_digits}}", font_size=font_size) _builder >>= builder svg_code = repr(_builder) if svg_code_only: return svg_code try: # pylint: disable = import-outside-toplevel - from IPython.core.display import SVG # type: ignore + from IPython.core.display import SVG # type: ignore except ModuleNotFoundError as e: raise ModuleNotFoundError("You must install the 'IPython' library.") from e return SVG(svg_code) @@ -1174,15 +1200,15 @@ def simplified(self) -> "PhaseCircuit": # Perform all commutations, fusions and pi gadget simplifications # TODO: explain with comments how the jumplist works # jumplist: Dict[int, Optional[Dict[int, int]]] = {} - for i, (basis, angles) in enumerate(groups): # pylint: disable = too-many-nested-blocks + for i, (basis, angles) in enumerate(groups): # pylint: disable = too-many-nested-blocks # Try commuting all gadgets to the left as much as possible for qubits, angle in angles.items(): if angle == 0: # Skip zeroed gadgets continue # Try to commute the gadget to the left as much as possible - j = i # j is the current group to which the gadget has been commuted - obstacle_found = False # this records whether we found an obstacle + j = i # j is the current group to which the gadget has been commuted + obstacle_found = False # this records whether we found an obstacle while not obstacle_found and j >= 2: # if j in jumplist: # j_jump = jumplist[j] @@ -1190,13 +1216,13 @@ def simplified(self) -> "PhaseCircuit": # print("Jump", i, j, j_jump[qubits], bin(qubits)) # j = j_jump[qubits] # break - _, angles_commute = groups[j-1] # angles to commute through + _, angles_commute = groups[j - 1] # angles to commute through for qubits_commute, angle_commute in angles_commute.items(): if angle_commute.is_zero: # Zero angle gadget, not an obstable continue # https://stackoverflow.com/questions/9829578/fast-way-of-counting-non-zero-bits-in-positive-integer - if bin(qubits&qubits_commute).count("1") % 2 != 0: + if bin(qubits & qubits_commute).count("1") % 2 != 0: # Odd number of shared legs, obstacle found obstacle_found = True break @@ -1225,11 +1251,11 @@ def simplified(self) -> "PhaseCircuit": angles_fuse[qubits] = angle if angles_fuse[qubits].is_pi: # This is a pi gadget, further simplification to be performed - angles_fuse[qubits] = Angle.zero # Remove gadget from this group + angles_fuse[qubits] = Angle.zero # Remove gadget from this group pi_gadget = True elif angle.is_pi: # We didn't manage to commute the gadget, but it is a pi gadget - angles[qubits] = Angle.zero # Remove gadget from this group + angles[qubits] = Angle.zero # Remove gadget from this group pi_gadget = True if pi_gadget: # pi gadget @@ -1237,7 +1263,7 @@ def simplified(self) -> "PhaseCircuit": # Commute through gadgets below of other basis, flipping sign if necessary _, angles_k = groups[k] for qubits_k in angles_k: - if bin(qubits_k&qubits).count("1")%2 == 1: + if bin(qubits_k & qubits).count("1") % 2 == 1: # Odd number of legs in comon: flip sign angles_k[qubits_k] *= -1 for q in _int_to_iterator(qubits): @@ -1246,18 +1272,18 @@ def simplified(self) -> "PhaseCircuit": # Create the new list of gadgets new_gadgets: List[PhaseGadget] = [] for q in range(num_qubits): - if pi_gates["Z"][q]%2 == 1: + if pi_gates["Z"][q] % 2 == 1: # Single-qubit pi Z gate new_gadgets.append(PhaseGadget("Z", pi, {q})) for q in range(num_qubits): - if pi_gates["X"][q]%2 == 1: + if pi_gates["X"][q] % 2 == 1: # Single-qubit pi X gate new_gadgets.append(PhaseGadget("X", pi, {q})) for basis, angles in groups: for qubits, angle in angles.items(): if isinstance(angle, Angle): - angle = angle % (2*pi) - if angle != 0: # skip zero angle gadgets + angle = angle % (2 * pi) + if angle != 0: # skip zero angle gadgets new_gadgets.append(PhaseGadget(basis, angle, _int_to_frozenset(qubits))) # Return a new phase circuit. return PhaseCircuit(num_qubits, new_gadgets) @@ -1345,8 +1371,8 @@ def _cx_count(self, topology: Topology, cache: Dict[int, Dict[Tuple[int, ...], i """ # pylint: disable = too-many-locals num_qubits = self._num_qubits - weight = lambda u, v: 4*topology.dist(u, v)-2 - inf = 4*num_qubits-2 + weight = lambda u, v: 4 * topology.dist(u, v) - 2 + inf = 4 * num_qubits - 2 count = 0 for basis in ("Z", "X"): basis = cast(Literal["Z", "X"], basis) @@ -1423,22 +1449,22 @@ def random(num_qubits: int, num_gadgets: int, *, s = parametric parametric = lambda i: AngleVar(f"{s}[{i}]", f"{s}_{i}") rng = np.random.default_rng(seed=rng_seed) - angle_rng_seed = int(rng.integers(65536)) # type: ignore[attr-defined] - basis_idxs = rng.integers(2, size=num_gadgets) # type: ignore[attr-defined] - num_legs = rng.integers(min_legs, max_legs+1, size=num_gadgets) # type: ignore[attr-defined] + angle_rng_seed = int(rng.integers(65536)) # type: ignore[attr-defined] + basis_idxs = rng.integers(2, size=num_gadgets) # type: ignore[attr-defined] + num_legs = rng.integers(min_legs, max_legs + 1, size=num_gadgets) # type: ignore[attr-defined] legs_list: List[npt.NDArray[int]] = [ rng.choice(num_qubits, num_legs[i], replace=False) for i in range(num_gadgets) ] angle_rng = np.random.default_rng(seed=angle_rng_seed) angles: List[Union[Angle, AngleVar]] - angles = [int(x)*pi/angle_subdivision - for x in angle_rng.integers(1, 2*angle_subdivision, # type: ignore[attr-defined] + angles = [int(x) * pi / angle_subdivision + for x in angle_rng.integers(1, 2 * angle_subdivision, # type: ignore[attr-defined] size=num_gadgets)] if parametric is not None: angles = [parametric(i) for i in range(num_gadgets)] bases = cast(Sequence[Literal["Z", "X"]], ("Z", "X")) gadgets: List[PhaseGadget] = [ - PhaseGadget(bases[(basis_idx+i)%2], + PhaseGadget(bases[(basis_idx + i) % 2], angle, [int(x) for x in legs]) for i, (basis_idx, angle, legs) in enumerate(zip(basis_idxs, @@ -1595,7 +1621,7 @@ def from_qasm(qasm: Union[str, QASM], *, raise ValueError(f"Expected {num_params} angles for {gate_name}, " f"found {len(gate_params)}") for qs in gate_qubits: - gadgets += m(*qs, *gate_params) # type: ignore # TODO: fix this! + gadgets += m(*qs, *gate_params) # type: ignore # TODO: fix this! break continue raise ValueError(f"Unsupported QASM statement: {statement}") @@ -1682,7 +1708,7 @@ def to_svg(self, *, return self._circuit.to_svg(zcolor=zcolor, xcolor=xcolor, hscale=hscale, vscale=vscale, scale=scale, - svg_code_only=svg_code_only) # type: ignore[call-overload] + svg_code_only=svg_code_only) # type: ignore[call-overload] def cloned(self) -> PhaseCircuit: """ @@ -1704,4 +1730,4 @@ def _repr_svg_(self) -> Any: Magic method for IPython/Jupyter pretty-printing. See https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html """ - return self._circuit._repr_svg_() # pylint: disable = protected-access + return self._circuit._repr_svg_() # pylint: disable = protected-access diff --git a/pauliopt/topologies.py b/pauliopt/topologies.py index e429cc44..a9253bfa 100644 --- a/pauliopt/topologies.py +++ b/pauliopt/topologies.py @@ -8,11 +8,13 @@ import numpy as np import numpy.typing as npt + class Coupling(FrozenSet[int]): """ Type for couplings in a qubit topology, i.e. unordered pairs of adjacent qubits. """ + def __new__(cls, fst: int, snd: int) -> "Coupling": if not isinstance(fst, int): raise TypeError(f"Expected integer, found {fst}") @@ -21,14 +23,14 @@ def __new__(cls, fst: int, snd: int) -> "Coupling": if len({fst, snd}) != 2: raise ValueError("Expected a pair of distinct qubits.") # see https://github.com/python/mypy/issues/6061 - return super(Coupling, cls).__new__(cls, {fst, snd}) # type: ignore + return super(Coupling, cls).__new__(cls, {fst, snd}) # type: ignore @property def as_pair(self) -> Tuple[int, int]: """ Returns the coupling as a (increasingly) ordered pair. """ - return (min(*self), max(*self)) # pylint: disable = not-an-iterable + return (min(*self), max(*self)) # pylint: disable = not-an-iterable def __str__(self) -> str: fst, snd = sorted(self) @@ -85,34 +87,36 @@ class TopologyDict(TypedDict, total=True): """ -def _floyd_warshall(topology: "Topology") -> np.ndarray: - """ - Runs the Floyd–Warshall to compute a matrix of distances between all pairs - of qubits in a topology. Raises `ValueError` if topology is not connected. - """ - num_qubits = topology.num_qubits - def init_dist(u, v): - if u == v: - return 0 - coupling = Coupling(u, v) - if coupling in topology.couplings: - return 1 - return num_qubits # a number surely larger than max dist in topology - dist = np.zeros(shape=(num_qubits, num_qubits), dtype=np.uint32) - for u in topology.qubits: - for v in topology.qubits: - dist[u, v] = init_dist(u, v) - for w in topology.qubits: - for u in topology.qubits: - for v in topology.qubits: - upper_bound = dist[u, w] + dist[w, v] - if dist[u, v] > upper_bound: - dist[u, v] = upper_bound - for u in topology.qubits: - for v in topology.qubits: - if dist[u, v] == num_qubits: - raise ValueError("Topology is not connected.") - return dist +def fw_shortest_path(self, u, v): + if self.floyd_warshall_next[u, v] is None: + raise Exception("Unconnected Architecture") + else: + path = [u] + while u != v: + u = self.floyd_warshall_next[u, v] + path.append(u) + return path + + +def _floyd_warshall(topology: "Topology"): + next = np.ones((topology.num_qubits, topology.num_qubits), dtype=int) + dist = np.inf * np.ones((topology.num_qubits, topology.num_qubits)) + G = topology.to_nx + for (u, v) in G.edges(): + dist[u, v] = 1.0 + dist[v, u] = 1.0 + next[u, v] = v + next[v, u] = u + for v in G.nodes: + dist[v, v] = 0 + next[v, v] = v + for k in G.nodes: + for i in G.nodes: + for j in G.nodes: + if dist[i][j] > dist[i][k] + dist[k][j]: + dist[i][j] = dist[i][k] + dist[k][j] + next[i][j] = next[i][k] + return dist, next class Topology: @@ -137,7 +141,7 @@ def __init__(self, num_qubits: int, couplings: Collection[CouplingLike]): _adjacent[fst].add(snd) _adjacent[snd].add(fst) self._adjacent = tuple(frozenset(n) for n in _adjacent) - self._dist = _floyd_warshall(self) + self._dist, self._next = _floyd_warshall(self) @property def num_qubits(self) -> int: @@ -180,8 +184,8 @@ def is_planar(self) -> bool: """ try: # pylint: disable = import-outside-toplevel - import networkx as nx # type: ignore - except ModuleNotFoundError as e: # pylint: disable = unused-variable + import networkx as nx # type: ignore + except ModuleNotFoundError as e: # pylint: disable = unused-variable raise ModuleNotFoundError("You must install the 'networkx' library.") G = self.to_nx is_planar, _ = nx.check_planarity(G) @@ -193,7 +197,7 @@ def available_nx_layouts(self) -> Tuple[str, ...]: Readonly property returning the available layouts for this qubit topology. """ if self.is_planar: - return Layouts+("planar",) + return Layouts + ("planar",) return Layouts @property @@ -204,7 +208,7 @@ def to_nx(self): """ try: # pylint: disable = import-outside-toplevel - import networkx as nx # type: ignore + import networkx as nx # type: ignore except ModuleNotFoundError as _: raise ModuleNotFoundError("You must install the 'networkx' library.") g = nx.Graph() @@ -228,12 +232,12 @@ def draw(self, layout: str = "kamada_kawai", *, """ try: # pylint: disable = import-outside-toplevel - import networkx as nx # type: ignore + import networkx as nx # type: ignore except ModuleNotFoundError as _: raise ModuleNotFoundError("You must install the 'networkx' library.") try: # pylint: disable = import-outside-toplevel - import matplotlib.pyplot as plt # type: ignore + import matplotlib.pyplot as plt # type: ignore except ModuleNotFoundError as _: raise ModuleNotFoundError("You must install the 'matplotlib' library.") G = self.to_nx @@ -243,7 +247,7 @@ def draw(self, layout: str = "kamada_kawai", *, if layout not in layouts: raise ValueError(f"Invalid layout found: {layout}. " f"Valid layouts: {', '.join(repr(l) for l in layouts)}") - kwargs["pos"] = getattr(nx, layout+"_layout")(G) + kwargs["pos"] = getattr(nx, layout + "_layout")(G) if "node_color" not in kwargs: kwargs["node_color"] = "#dddddd" plt.figure(figsize=figsize) @@ -285,6 +289,19 @@ def dist(self, fro: int, to: int) -> int: raise TypeError(f"Expected a valid qubit, found {to}.") return self._dist[fro, to] + def shortest_path(self, fro: int, to: int): + """ + Compute the shortest path using the next lookup table from the Floyd–Warshall algorithm + """ + if self._next[fro, to] is None: + raise Exception("Unconnected Architecture") + else: + path = [fro] + while fro != to: + fro = self._next[fro, to] + path.append(fro) + return path + def mapped_fwd(self, mapping: Union[Sequence[int], Dict[int, int]]) -> "Topology": """ Returns a topology with the same couplings, but remapping the qubits using @@ -405,9 +422,9 @@ def line(num_qubits: int) -> "Topology": """ if not isinstance(num_qubits, int) or num_qubits <= 0: raise TypeError("Number of qubits must be positive integer.") - couplings = [[i, i+1] for i in range(num_qubits-1)] + couplings = [[i, i + 1] for i in range(num_qubits - 1)] top = Topology(num_qubits, couplings) - top._named = f"line({num_qubits})" # pylint: disable = protected-access + top._named = f"line({num_qubits})" # pylint: disable = protected-access return top @staticmethod @@ -417,9 +434,9 @@ def cycle(num_qubits: int) -> "Topology": """ if not isinstance(num_qubits, int) or num_qubits <= 0: raise TypeError("Number of qubits must be positive integer.") - couplings = [[i, (i+1)%num_qubits] for i in range(num_qubits)] + couplings = [[i, (i + 1) % num_qubits] for i in range(num_qubits)] top = Topology(num_qubits, couplings) - top._named = f"cycle({num_qubits})" # pylint: disable = protected-access + top._named = f"cycle({num_qubits})" # pylint: disable = protected-access return top @staticmethod @@ -429,9 +446,9 @@ def complete(num_qubits: int) -> "Topology": """ if not isinstance(num_qubits, int) or num_qubits <= 0: raise TypeError("Number of qubits must be positive integer.") - couplings = [[i, j] for i in range(num_qubits) for j in range(i+1, num_qubits)] + couplings = [[i, j] for i in range(num_qubits) for j in range(i + 1, num_qubits)] top = Topology(num_qubits, couplings) - top._named = f"complete({num_qubits})" # pylint: disable = protected-access + top._named = f"complete({num_qubits})" # pylint: disable = protected-access return top @staticmethod @@ -445,17 +462,19 @@ def grid(num_rows: int, num_cols: int) -> "Topology": if not isinstance(num_cols, int) or num_cols <= 0: raise TypeError("Number of cols must be positive integer.") num_qubits = num_rows * num_cols + def qubit(r, c): - return num_cols*r + c + return num_cols * r + c + couplings: List[List[int]] = [] for r in range(num_rows): for c in range(num_cols): - if r < num_rows-1: - couplings.append([qubit(r, c), qubit(r+1, c)]) - if c < num_cols-1: - couplings.append([qubit(r, c), qubit(r, c+1)]) + if r < num_rows - 1: + couplings.append([qubit(r, c), qubit(r + 1, c)]) + if c < num_cols - 1: + couplings.append([qubit(r, c), qubit(r, c + 1)]) top = Topology(num_qubits, couplings) - top._named = f"grid({num_rows},{num_cols})" # pylint: disable = protected-access + top._named = f"grid({num_rows},{num_cols})" # pylint: disable = protected-access return top @staticmethod @@ -469,15 +488,17 @@ def periodic_grid(num_rows: int, num_cols: int) -> "Topology": if not isinstance(num_cols, int) or num_cols <= 0: raise TypeError("Number of cols must be positive integer.") num_qubits = num_rows * num_cols + def qubit(r, c): - return num_cols*r + c + return num_cols * r + c + couplings: List[List[int]] = [] for r in range(num_rows): for c in range(num_cols): - couplings.append([qubit(r, c), qubit((r+1)%num_rows, c)]) - couplings.append([qubit(r, c), qubit(r, (c+1)%num_cols)]) + couplings.append([qubit(r, c), qubit((r + 1) % num_rows, c)]) + couplings.append([qubit(r, c), qubit(r, (c + 1) % num_cols)]) top = Topology(num_qubits, couplings) - top._named = f"periodic_grid({num_rows},{num_cols})" # pylint: disable = protected-access + top._named = f"periodic_grid({num_rows},{num_cols})" # pylint: disable = protected-access return top @staticmethod @@ -492,7 +513,7 @@ def from_qiskit_config(config) -> "Topology": """ try: # pylint: disable = import-outside-toplevel - from qiskit.providers.models import QasmBackendConfiguration # type: ignore + from qiskit.providers.models import QasmBackendConfiguration # type: ignore except ModuleNotFoundError as _: raise ModuleNotFoundError("You must install the 'qiskit' library.") if not isinstance(config, QasmBackendConfiguration): @@ -515,7 +536,7 @@ def from_qiskit_backend(backend) -> "Topology": """ try: # pylint: disable = import-outside-toplevel, unused-import - from qiskit.providers import Backend # type: ignore + from qiskit.providers import Backend # type: ignore except ModuleNotFoundError as _: raise ModuleNotFoundError("You must install the 'qiskit' library.") if not isinstance(backend, Backend): diff --git a/pauliopt/utils.py b/pauliopt/utils.py index 568bc64e..003d6b2d 100644 --- a/pauliopt/utils.py +++ b/pauliopt/utils.py @@ -1,7 +1,7 @@ """ Utility classes and functions for the `pauliopt` library. """ - +import itertools from abc import ABC, abstractmethod import math from decimal import Decimal @@ -13,6 +13,7 @@ AngleInitT = Union[int, Fraction, str, Decimal] + class AngleExpr(ABC): """ A container class for angle expressions. @@ -51,7 +52,7 @@ def __rmul__(self, other: int) -> "AngleExpr": def __truediv__(self, other: int) -> "AngleExpr": if isinstance(other, int): - return SumprodAngleExpr(self, coeffs=Fraction(1,other)) + return SumprodAngleExpr(self, coeffs=Fraction(1, other)) return NotImplemented @abstractmethod @@ -96,7 +97,7 @@ def _repr_latex_(self) -> str: Magic method for IPython/Jupyter pretty-printing. See https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html """ - return "$%s$"%self.repr_latex + return "$%s$" % self.repr_latex @abstractmethod def __eq__(self, other: Any) -> bool: @@ -122,7 +123,7 @@ def value(self) -> Fraction: """ The value of this angle as a fraction of PI. """ - return self._value%2 + return self._value % 2 @property def as_root_of_unity(self) -> Tuple[int, int]: @@ -133,8 +134,8 @@ def as_root_of_unity(self) -> Tuple[int, int]: """ num = self.value.numerator den = self.value.denominator - a: int = num//2 if num%2 == 0 else num - order: int = den if num % 2 == 0 else 2*den + a: int = num // 2 if num % 2 == 0 else num + order: int = den if num % 2 == 0 else 2 * den return (a, order) @property @@ -144,7 +145,7 @@ def order(self) -> int: """ num = self.value.numerator den = self.value.denominator - return den if num % 2 == 0 else 2*den + return den if num % 2 == 0 else 2 * den @property def is_zero_or_pi(self) -> bool: @@ -162,7 +163,7 @@ def is_zero(self) -> bool: """ num = self.value.numerator den = self.value.denominator - return num % (2*den) == 0 + return num % (2 * den) == 0 @property def is_pi(self) -> bool: @@ -171,7 +172,7 @@ def is_pi(self) -> bool: """ num = self.value.numerator den = self.value.denominator - return num % den == 0 and not num % (2*den) == 0 + return num % den == 0 and not num % (2 * den) == 0 @property def to_qiskit(self) -> float: @@ -223,7 +224,7 @@ def __mod__(self, other: "AngleExpr") -> "AngleExpr": return super().__mod__(other) def _mul(self, other: Union[int, Fraction]) -> "Angle": - return Angle(self._value*other) + return Angle(self._value * other) def __mul__(self, other: int) -> "Angle": if isinstance(other, int): @@ -251,8 +252,8 @@ def __str__(self) -> str: if num == 1: if den == 1: return "π" - return "π/%d"%den - return "%dπ/%d"%(num, den) + return "π/%d" % den + return "%dπ/%d" % (num, den) def __repr__(self) -> str: num = self.value.numerator @@ -262,8 +263,8 @@ def __repr__(self) -> str: if num == 1: if den == 1: return "pi" - return "pi/%d"%den - return "%d*pi/%d"%(num, den) + return "pi/%d" % den + return "%d*pi/%d" % (num, den) @property def repr_latex(self) -> str: @@ -277,15 +278,15 @@ def repr_latex(self) -> str: if num == 1: if den == 1: return "\\pi" - return "\\frac{\\pi}{%d}"%den - return "\\frac{%d\\pi}{%d}"%(num, den) + return "\\frac{\\pi}{%d}" % den + return "\\frac{%d\\pi}{%d}" % (num, den) def _repr_latex_(self) -> str: """ Magic method for IPython/Jupyter pretty-printing. See https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html """ - return "$%s$"%self.repr_latex + return "$%s$" % self.repr_latex def __eq__(self, other: Any) -> bool: if other == 0: @@ -295,12 +296,12 @@ def __eq__(self, other: Any) -> bool: return self.value == other.value def __float__(self) -> float: - return float(self.value)*math.pi + return float(self.value) * math.pi @overload @staticmethod def random(subdivision: int = 4, *, - size: Literal[1]=1, + size: Literal[1] = 1, rng_seed: Optional[int] = None, nonzero: bool = False) -> "Angle": ... @@ -335,35 +336,35 @@ def random(subdivision: int = 4, *, size: int = 1, raise TypeError("RNG seed must be integer or 'None'.") rng = np.random.default_rng(seed=rng_seed) if nonzero: - rs = 1+rng.integers(2*subdivision-1, size=size) # type: ignore[attr-defined] + rs = 1 + rng.integers(2 * subdivision - 1, size=size) # type: ignore[attr-defined] else: - rs = rng.integers(2*subdivision, size=size) # type: ignore[attr-defined] + rs = rng.integers(2 * subdivision, size=size) # type: ignore[attr-defined] if size == 1: return Angle(Fraction(int(rs[0]), subdivision)) return tuple(Angle(Fraction(int(r), subdivision)) for r in rs) - zero: Final["Angle"] # type: ignore + zero: Final["Angle"] # type: ignore """ A constant for the angle 0. """ - pi: Final["Angle"] # type: ignore + pi: Final["Angle"] # type: ignore """ A constant for the angle pi. """ -# Set static constants for Angle: -Angle.zero = Angle(0) # type: ignore -Angle.pi = Angle(1) # type: ignore +# Set static constants for Angle: +Angle.zero = Angle(0) # type: ignore +Angle.pi = Angle(1) # type: ignore pi: Final[Angle] = Angle.pi """ Constant for `Angle.pi`. """ -π: Final[Angle] = Angle.pi # pylint: disable=non-ascii-name +π: Final[Angle] = Angle.pi # pylint: disable=non-ascii-name """ Constant for `Angle.pi`. """ def SumprodAngleExpr(*exprs: AngleExpr, coeffs: Union[int, Fraction, Sequence[Union[int, Fraction]]] = 1 - ) -> AngleExpr: + ) -> AngleExpr: if not isinstance(coeffs, Sequence): coeffs = (coeffs,) if len(coeffs) != len(exprs): @@ -373,10 +374,10 @@ def SumprodAngleExpr(*exprs: AngleExpr, _const: Angle = Angle.zero for e, c in zip(exprs, coeffs): if isinstance(e, Angle): - _const += e._mul(c) # pylint: disable = protected-access + _const += e._mul(c) # pylint: disable = protected-access elif isinstance(e, _SumprodAngleExpr): for sub_e, sub_c in e.coeffs.items(): - new_c = c*sub_c + new_c = c * sub_c if sub_e in _coeffs: new_c += _coeffs[sub_e] _coeffs[sub_e] = new_c @@ -426,7 +427,7 @@ def is_pi(self) -> bool: @property def to_qiskit(self) -> Any: - return sum((c*e.to_qiskit for e, c in self.coeffs.items()), self.const.to_qiskit) + return sum((c * e.to_qiskit for e, c in self.coeffs.items()), self.const.to_qiskit) def __hash__(self) -> int: return hash((_SumprodAngleExpr, tuple(self.coeffs.items()), self.const)) @@ -437,12 +438,12 @@ def _str_repr(self, f: Callable[[Union[Angle, AngleExpr]], str]) -> str: pos_sub_e = {e: c for e, c in self.coeffs.items() if c > 0} neg_sub_e = {e: c for e, c in self.coeffs.items() if c < 0} s = "+".join( - ("" if c == 1 else str(c))+f(e) + ("" if c == 1 else str(c)) + f(e) for e, c in pos_sub_e.items() ) if neg_sub_e: - s += "-"+"".join( - ("-" if c == -1 else str(c))+f(e) + s += "-" + "".join( + ("-" if c == -1 else str(c)) + f(e) for e, c in pos_sub_e.items() ) if self.const != 0: @@ -469,11 +470,13 @@ def __eq__(self, other: Any) -> bool: return False return NotImplemented + def ModAngleExpr(expr: AngleExpr, mod: AngleExpr) -> AngleExpr: if isinstance(expr, Angle) and isinstance(mod, Angle): - return expr%mod + return expr % mod return _ModAngleExpr(expr, mod) + class _ModAngleExpr(AngleExpr): _expr: AngleExpr _mod: AngleExpr @@ -498,7 +501,7 @@ def is_zero(self) -> bool: @property def to_qiskit(self) -> Any: - return self.expr.to_qiskit%self.mod.to_qiskit + return self.expr.to_qiskit % self.mod.to_qiskit def __hash__(self) -> int: return hash((_ModAngleExpr, self.expr, self.mod)) @@ -523,6 +526,7 @@ def __eq__(self, other: Any) -> bool: return False return NotImplemented + class AngleVar(AngleExpr): _global_id: ClassVar[int] = 0 _qiskit_bindings: ClassVar[Dict[int, Any]] @@ -544,7 +548,7 @@ def to_qiskit(self) -> Any: return AngleVar._qiskit_bindings[self._id] try: # pylint: disable = import-outside-toplevel - from qiskit.circuit import Parameter # type: ignore + from qiskit.circuit import Parameter # type: ignore except ModuleNotFoundError as e: raise ModuleNotFoundError("You must install the 'qiskit' library.") from e p = Parameter(self._repr_latex_) @@ -575,13 +579,13 @@ def __eq__(self, other: Any) -> bool: return NotImplemented - def _validate_vec2(vec2: Tuple[int, int]) -> None: if not isinstance(vec2, tuple) or len(vec2) != 2: raise TypeError("Expected pair.") if not all(isinstance(x, int) for x in vec2): raise TypeError("Expected pair of integers.") + class SVGBuilder: """ Utility class for building certain SVG images. @@ -676,7 +680,7 @@ def text(self, pos: Tuple[int, int], text: str, *, font_size: int = 10) -> "SVGB if not isinstance(font_size, int) or font_size <= 0: raise TypeError("Font size must be positive integer.") x, y = pos - tag = f'{text}' + tag = f'{text}' self._tags.append(tag) return self @@ -709,6 +713,7 @@ class TempSchedule(Protocol): def __call__(self, it: int, num_iters: int) -> float: ... + @runtime_checkable class TempScheduleProvider(Protocol): """ @@ -729,8 +734,10 @@ def linear_temp_schedule(t_init: Union[int, float], t_final: Union[int, float]) raise TypeError(f"Expected int or float, found {type(t_init)}.") if not isinstance(t_final, (int, float)): raise TypeError(f"Expected int or float, found {type(t_final)}.") + def temp_schedule(it: int, num_iters: int) -> float: - return t_init + (t_final-t_init)*it/(num_iters-1) + return t_init + (t_final - t_init) * it / (num_iters - 1) + return temp_schedule @@ -743,8 +750,10 @@ def geometric_temp_schedule(t_init: Union[int, float], t_final: Union[int, float raise TypeError(f"Expected int or float, found {type(t_init)}.") if not isinstance(t_final, (int, float)): raise TypeError(f"Expected int or float, found {type(t_final)}.") + def temp_schedule(it: int, num_iters: int) -> float: - return t_init * ((t_final/t_init)**(it/(num_iters-1.0))) # type: ignore[no-any-return] + return t_init * ((t_final / t_init) ** (it / (num_iters - 1.0))) # type: ignore[no-any-return] + return temp_schedule @@ -757,10 +766,12 @@ def reciprocal_temp_schedule(t_init: Union[int, float], t_final: Union[int, floa raise TypeError(f"Expected int or float, found {type(t_init)}.") if not isinstance(t_final, (int, float)): raise TypeError(f"Expected int or float, found {type(t_final)}.") + def temp_schedule(it: int, num_iters: int) -> float: - num = t_init*t_final*(num_iters-1) - denom = (t_final*num_iters-t_init)+(t_init-t_final)*(it+1) - return num/denom + num = t_init * t_final * (num_iters - 1) + denom = (t_final * num_iters - t_init) + (t_init - t_final) * (it + 1) + return num / denom + return temp_schedule @@ -773,10 +784,12 @@ def log_temp_schedule(t_init: Union[int, float], t_final: Union[int, float]) -> raise TypeError(f"Expected int or float, found {type(t_init)}.") if not isinstance(t_final, (int, float)): raise TypeError(f"Expected int or float, found {type(t_final)}.") + def temp_schedule(it: int, num_iters: int) -> float: - num = t_init*t_final*(math.log(num_iters+1)-math.log(2)) - denom = (t_final*math.log(num_iters+1)-t_init*math.log(2))+(t_init-t_final)*math.log(it+2) - return num/denom + num = t_init * t_final * (math.log(num_iters + 1) - math.log(2)) + denom = (t_final * math.log(num_iters + 1) - t_init * math.log(2)) + (t_init - t_final) * math.log(it + 2) + return num / denom + return temp_schedule @@ -785,14 +798,12 @@ def temp_schedule(it: int, num_iters: int) -> float: Names of the standard temperature schedules. """ - StandardTempSchedule = Tuple[StandardTempScheduleName, Union[int, float], Union[int, float]] """ Type for standard temperature schedules. """ - StandardTempSchedules: Final[Mapping[StandardTempScheduleName, TempScheduleProvider]] = { "linear": linear_temp_schedule, "geometric": geometric_temp_schedule, diff --git a/test.py b/test.py new file mode 100644 index 00000000..fe08edf5 --- /dev/null +++ b/test.py @@ -0,0 +1,88 @@ +import networkx as nx +from matplotlib import pyplot as plt + +from pauliopt.pauli.anneal import anneal +from pauliopt.pauli.clifford_gates import CX, H, CY, CZ +from pauliopt.pauli.pauli_gadget import PPhase +from pauliopt.pauli.pauli_polynomial import * +import numpy as np + +from pauliopt.pauli.utils import Pauli, _pauli_to_string + + +def two_qubit_count(count_ops): + count = 0 + count += count_ops["cx"] if "cx" in count_ops.keys() else 0 + count += count_ops["cy"] if "cy" in count_ops.keys() else 0 + count += count_ops["cz"] if "cz" in count_ops.keys() else 0 + return count + + +def create_random_phase_gadget(num_qubits, min_legs, max_legs, allowed_angels): + angle = np.random.choice(allowed_angels) + nr_legs = np.random.randint(min_legs, max_legs) + legs = np.random.choice([i for i in range(num_qubits)], size=nr_legs, replace=False) + phase_gadget = [Pauli.I for _ in range(num_qubits)] + for leg in legs: + phase_gadget[leg] = np.random.choice([Pauli.X, Pauli.Y, Pauli.Z]) + return PPhase(angle) @ phase_gadget + + +def generate_random_pauli_polynomial(num_qubits: int, num_gadgets: int, min_legs=None, + max_legs=None, allowed_angels=None): + if min_legs is None: + min_legs = 1 + if max_legs is None: + max_legs = num_qubits + if allowed_angels is None: + allowed_angels = [2 * np.pi, np.pi, 0.5 * np.pi, 0.25 * np.pi, 0.125 * np.pi] + + pp = PauliPolynomial() + for _ in range(num_gadgets): + pp >>= create_random_phase_gadget(num_qubits, min_legs, max_legs, allowed_angels) + + return pp + + +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 main(num_qubits=3): + topo = Topology.line(num_qubits) + pp = generate_random_pauli_polynomial(num_qubits, 5) + init_circ = pp.to_qiskit(topo) + print(pp) + print(init_circ) + #print(res_circ) + res_circ = anneal(pp.copy(), topo, nr_iterations=1000) + print((two_qubit_count(init_circ.count_ops()) - two_qubit_count(res_circ.count_ops())) / two_qubit_count( + init_circ.count_ops())) + print(res_circ) + print(verify_equality(res_circ, init_circ)) + + +def create_rules_graph(rules): + rules_tuple = [] + for k, v in rules.items(): + res_key = _pauli_to_string(v[0]) + _pauli_to_string(v[1]) + rules_tuple.append((k, res_key)) + G = nx.Graph() + print(rules_tuple) + G.add_edges_from(rules_tuple) + return G + + +if __name__ == '__main__': + main() diff --git a/tests/test_pauli_propagation.py b/tests/test_pauli_propagation.py new file mode 100644 index 00000000..61ecef42 --- /dev/null +++ b/tests/test_pauli_propagation.py @@ -0,0 +1,117 @@ +import itertools +import unittest + +import networkx as nx +import numpy as np +import pytket +from pytket._tket.circuit import PauliExpBox +from pytket._tket.transform import Transform +from pytket.extensions.qiskit.qiskit_convert import tk_to_qiskit, qiskit_to_tk + +from qiskit import QuantumCircuit + +from pauliopt.pauli.clifford_gates import CX, CY, CZ, H, S, V +from pauliopt.pauli.pauli_gadget import PPhase +from pauliopt.pauli.pauli_polynomial import PauliPolynomial +from pauliopt.pauli.utils import X, Y, Z, I +from pytket._tket.pauli import Pauli + +from pauliopt.topologies import Topology + + +def pauli_to_tket_pauli(pauli): + if pauli == X: + return Pauli.X + elif pauli == Y: + return Pauli.Y + elif pauli == Z: + return Pauli.Z + elif pauli == I: + return Pauli.I + else: + raise Exception("Unknown Pauli Matrix") + + +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_pauli(p) for p in gadget.paulis], gadget.angle / 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 * np.pi, np.pi, 0.5 * np.pi, 0.25 * np.pi, 0.125 * np.pi] + pp = PauliPolynomial() + 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 + + +class TestPauliConversion(unittest.TestCase): + def test_circuit_construction(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, 4]: + 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 = pp.to_qiskit(topology) + self.assertTrue(verify_equality(tket_pp, our_synth), + "The resulting Quantum Circuits were not equivalent") + self.assertTrue(check_matching_architecture(our_synth, topology.to_nx), + "The Pauli Polynomial did not match the architecture") + + def test_gate_propagation(self): + """ + Checks if the clifford Propagation rules are sound for 3, 4, 5 qubits + """ + for num_qubits in [2, 3, 4]: + pp = generate_all_combination_pauli_polynomial(n_qubits=num_qubits) + inital_qc = pp.to_qiskit() + + for gate_class in [CX, CY, CZ, H, S, V]: + gate = gate_class.generate_random(num_qubits) + pp_ = pp.propagate(gate) + qc = QuantumCircuit(num_qubits) + qc.compose(gate.to_qiskit(), inplace=True) + qc.compose(pp_.to_qiskit()) + qc.compose(gate.to_qiskit().inverse(), inplace=True) + + self.assertTrue(verify_equality(inital_qc, qc), "The resulting Quantum Circuits were not equivalent") From 60beee6bd1d43ddf68a3b50bd937e256e7d08821 Mon Sep 17 00:00:00 2001 From: daehiff Date: Tue, 2 May 2023 15:08:59 +0200 Subject: [PATCH 03/30] added unit tests --- README.md | 9 +++ requirements.txt | 122 ++++++++++++++++++++++++++++++++ test.py | 88 ----------------------- tests/__init__.py | 0 tests/test_pauli_propagation.py | 15 ++-- 5 files changed, 138 insertions(+), 96 deletions(-) create mode 100644 requirements.txt delete mode 100644 test.py create mode 100644 tests/__init__.py diff --git a/README.md b/README.md index d70abebb..76c30cb8 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,12 @@ pip install --upgrade pauliopt **Step 4.** Run a few iterations of simulated annealing and look at the simplified circuit. + +## Unit tests + + +To run the unit tests, install the additional requirements using our `requirements.txt` (recommended python: 3.9), then to launch then, run: + +```bash +python -m unittest discover -s ./tests/ -p "test_*.py" +``` \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..49ca5091 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,122 @@ +anyio==3.6.2 +appnope==0.1.3 +argon2-cffi==21.3.0 +argon2-cffi-bindings==21.2.0 +arrow==1.2.3 +asttokens==2.2.1 +attrs==23.1.0 +backcall==0.2.0 +beautifulsoup4==4.12.2 +bleach==6.0.0 +certifi==2022.12.7 +cffi==1.15.1 +charset-normalizer==3.1.0 +comm==0.1.3 +contourpy==1.0.7 +cryptography==40.0.2 +cycler==0.11.0 +debugpy==1.6.7 +decorator==5.1.1 +defusedxml==0.7.1 +dill==0.3.6 +executing==1.2.0 +fastjsonschema==2.16.3 +fonttools==4.39.3 +fqdn==1.5.1 +graphviz==0.20.1 +ibm-cloud-sdk-core==3.16.5 +ibm-platform-services==0.34.0 +idna==3.4 +importlib-metadata==6.5.0 +importlib-resources==5.12.0 +ipykernel==6.22.0 +ipython==8.12.0 +ipython-genutils==0.2.0 +isoduration==20.11.0 +jedi==0.18.2 +Jinja2==3.1.2 +jsonpointer==2.3 +jsonschema==4.17.3 +jupyter-events==0.6.3 +jupyter_client==8.2.0 +jupyter_core==5.3.0 +jupyter_server==2.5.0 +jupyter_server_terminals==0.4.4 +jupyterlab-pygments==0.2.2 +kiwisolver==1.4.4 +lark-parser==0.12.0 +MarkupSafe==2.1.2 +matplotlib==3.7.1 +matplotlib-inline==0.1.6 +mistune==2.0.5 +mpmath==1.3.0 +nbclassic==0.5.5 +nbclient==0.7.3 +nbconvert==7.3.1 +nbformat==5.8.0 +nest-asyncio==1.5.6 +networkx==2.8.8 +notebook==6.5.4 +notebook_shim==0.2.2 +ntlm-auth==1.5.0 +numpy==1.23.5 +packaging==23.1 +pandocfilters==1.5.0 +parso==0.8.3 +pbr==5.11.1 +pexpect==4.8.0 +pickleshare==0.7.5 +Pillow==9.5.0 +platformdirs==3.2.0 +ply==3.11 +prometheus-client==0.16.0 +prompt-toolkit==3.0.38 +psutil==5.9.5 +ptyprocess==0.7.0 +pure-eval==0.2.2 +pycparser==2.21 +Pygments==2.15.1 +PyJWT==2.6.0 +pyparsing==3.0.9 +pyrsistent==0.19.3 +python-dateutil==2.8.2 +python-json-logger==2.0.7 +pytket==1.10.0 +pytket-qiskit==0.33.0 +PyYAML==6.0 +pyzmq==25.0.2 +qiskit==0.39.0 +qiskit-aer==0.11.0 +qiskit-ibm-runtime==0.8.0 +qiskit-ibmq-provider==0.19.2 +qiskit-terra==0.22.0 +qwasm==1.0.1 +requests==2.28.2 +requests-ntlm==1.1.0 +rfc3339-validator==0.1.4 +rfc3986-validator==0.1.1 +rustworkx==0.12.1 +scipy==1.10.1 +Send2Trash==1.8.0 +six==1.16.0 +sniffio==1.3.0 +soupsieve==2.4.1 +stack-data==0.6.2 +stevedore==5.0.0 +stim==1.11.0 +symengine==0.10.0 +sympy==1.11.1 +terminado==0.17.1 +tinycss2==1.2.1 +tornado==6.3 +traitlets==5.9.0 +types-pkg-resources==0.1.3 +typing_extensions==4.5.0 +uri-template==1.2.0 +urllib3==1.26.15 +wcwidth==0.2.6 +webcolors==1.13 +webencodings==0.5.1 +websocket-client==1.3.3 +websockets==11.0.2 +zipp==3.15.0 diff --git a/test.py b/test.py deleted file mode 100644 index fe08edf5..00000000 --- a/test.py +++ /dev/null @@ -1,88 +0,0 @@ -import networkx as nx -from matplotlib import pyplot as plt - -from pauliopt.pauli.anneal import anneal -from pauliopt.pauli.clifford_gates import CX, H, CY, CZ -from pauliopt.pauli.pauli_gadget import PPhase -from pauliopt.pauli.pauli_polynomial import * -import numpy as np - -from pauliopt.pauli.utils import Pauli, _pauli_to_string - - -def two_qubit_count(count_ops): - count = 0 - count += count_ops["cx"] if "cx" in count_ops.keys() else 0 - count += count_ops["cy"] if "cy" in count_ops.keys() else 0 - count += count_ops["cz"] if "cz" in count_ops.keys() else 0 - return count - - -def create_random_phase_gadget(num_qubits, min_legs, max_legs, allowed_angels): - angle = np.random.choice(allowed_angels) - nr_legs = np.random.randint(min_legs, max_legs) - legs = np.random.choice([i for i in range(num_qubits)], size=nr_legs, replace=False) - phase_gadget = [Pauli.I for _ in range(num_qubits)] - for leg in legs: - phase_gadget[leg] = np.random.choice([Pauli.X, Pauli.Y, Pauli.Z]) - return PPhase(angle) @ phase_gadget - - -def generate_random_pauli_polynomial(num_qubits: int, num_gadgets: int, min_legs=None, - max_legs=None, allowed_angels=None): - if min_legs is None: - min_legs = 1 - if max_legs is None: - max_legs = num_qubits - if allowed_angels is None: - allowed_angels = [2 * np.pi, np.pi, 0.5 * np.pi, 0.25 * np.pi, 0.125 * np.pi] - - pp = PauliPolynomial() - for _ in range(num_gadgets): - pp >>= create_random_phase_gadget(num_qubits, min_legs, max_legs, allowed_angels) - - return pp - - -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 main(num_qubits=3): - topo = Topology.line(num_qubits) - pp = generate_random_pauli_polynomial(num_qubits, 5) - init_circ = pp.to_qiskit(topo) - print(pp) - print(init_circ) - #print(res_circ) - res_circ = anneal(pp.copy(), topo, nr_iterations=1000) - print((two_qubit_count(init_circ.count_ops()) - two_qubit_count(res_circ.count_ops())) / two_qubit_count( - init_circ.count_ops())) - print(res_circ) - print(verify_equality(res_circ, init_circ)) - - -def create_rules_graph(rules): - rules_tuple = [] - for k, v in rules.items(): - res_key = _pauli_to_string(v[0]) + _pauli_to_string(v[1]) - rules_tuple.append((k, res_key)) - G = nx.Graph() - print(rules_tuple) - G.add_edges_from(rules_tuple) - return G - - -if __name__ == '__main__': - main() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_pauli_propagation.py b/tests/test_pauli_propagation.py index 61ecef42..554ab259 100644 --- a/tests/test_pauli_propagation.py +++ b/tests/test_pauli_propagation.py @@ -7,14 +7,13 @@ from pytket._tket.circuit import PauliExpBox from pytket._tket.transform import Transform from pytket.extensions.qiskit.qiskit_convert import tk_to_qiskit, qiskit_to_tk - +from pytket._tket.pauli import Pauli from qiskit import QuantumCircuit from pauliopt.pauli.clifford_gates import CX, CY, CZ, H, S, V from pauliopt.pauli.pauli_gadget import PPhase from pauliopt.pauli.pauli_polynomial import PauliPolynomial from pauliopt.pauli.utils import X, Y, Z, I -from pytket._tket.pauli import Pauli from pauliopt.topologies import Topology @@ -100,18 +99,18 @@ def test_circuit_construction(self): def test_gate_propagation(self): """ - Checks if the clifford Propagation rules are sound for 3, 4, 5 qubits + Checks if the clifford Propagation rules are sound for 2, 3, 4 qubits """ for num_qubits in [2, 3, 4]: pp = generate_all_combination_pauli_polynomial(n_qubits=num_qubits) inital_qc = pp.to_qiskit() - for gate_class in [CX, CY, CZ, H, S, V]: gate = gate_class.generate_random(num_qubits) - pp_ = pp.propagate(gate) + print(gate.to_qiskit()) + pp_ = pp.copy().propagate(gate) qc = QuantumCircuit(num_qubits) - qc.compose(gate.to_qiskit(), inplace=True) - qc.compose(pp_.to_qiskit()) qc.compose(gate.to_qiskit().inverse(), inplace=True) - + qc.compose(pp_.to_qiskit(), inplace=True) + qc.compose(gate.to_qiskit(), inplace=True) + # print(qc) self.assertTrue(verify_equality(inital_qc, qc), "The resulting Quantum Circuits were not equivalent") From 4da183ca4f31554fb55b0e42e79ae05408e89bdf Mon Sep 17 00:00:00 2001 From: daehiff Date: Tue, 2 May 2023 15:19:09 +0200 Subject: [PATCH 04/30] added clifford tableau --- pauliopt/pauli/clifford_tableau.py | 18 ++++++ test.py | 88 ++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 pauliopt/pauli/clifford_tableau.py create mode 100644 test.py diff --git a/pauliopt/pauli/clifford_tableau.py b/pauliopt/pauli/clifford_tableau.py new file mode 100644 index 00000000..0a83881b --- /dev/null +++ b/pauliopt/pauli/clifford_tableau.py @@ -0,0 +1,18 @@ +import numpy as np + + +class CliffordTableau: + def __init__(self, n_qubits: int = None, tableau: np.array = None): + if n_qubits is None and tableau is None: + raise Exception("Either Tableau or number of qubits must be defined") + if tableau is None: + self.tableau = np.eye(2 * n_qubits) + self.n_qubits = n_qubits + else: + if tableau.shape[1] == tableau.shape[1]: + raise Exception("Must be a 2nx2n Tableau!") + self.tableau_x = tableau + self.n_qubits = int(tableau.shape[1] / 2.0) + + def apply_h(self, qubit): + pass diff --git a/test.py b/test.py new file mode 100644 index 00000000..a6a95baa --- /dev/null +++ b/test.py @@ -0,0 +1,88 @@ +import networkx as nx +from matplotlib import pyplot as plt + +from pauliopt.pauli.anneal import anneal +from pauliopt.pauli.clifford_gates import CX, H, CY, CZ +from pauliopt.pauli.pauli_gadget import PPhase +from pauliopt.pauli.pauli_polynomial import * +import numpy as np + +from pauliopt.pauli.utils import Pauli, _pauli_to_string +import stim + +def two_qubit_count(count_ops): + count = 0 + count += count_ops["cx"] if "cx" in count_ops.keys() else 0 + count += count_ops["cy"] if "cy" in count_ops.keys() else 0 + count += count_ops["cz"] if "cz" in count_ops.keys() else 0 + return count + + +def create_random_phase_gadget(num_qubits, min_legs, max_legs, allowed_angels): + angle = np.random.choice(allowed_angels) + nr_legs = np.random.randint(min_legs, max_legs) + legs = np.random.choice([i for i in range(num_qubits)], size=nr_legs, replace=False) + phase_gadget = [Pauli.I for _ in range(num_qubits)] + for leg in legs: + phase_gadget[leg] = np.random.choice([Pauli.X, Pauli.Y, Pauli.Z]) + return PPhase(angle) @ phase_gadget + + +def generate_random_pauli_polynomial(num_qubits: int, num_gadgets: int, min_legs=None, + max_legs=None, allowed_angels=None): + if min_legs is None: + min_legs = 1 + if max_legs is None: + max_legs = num_qubits + if allowed_angels is None: + allowed_angels = [2 * np.pi, np.pi, 0.5 * np.pi, 0.25 * np.pi, 0.125 * np.pi] + + pp = PauliPolynomial() + for _ in range(num_gadgets): + pp >>= create_random_phase_gadget(num_qubits, min_legs, max_legs, allowed_angels) + + return pp + + +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 main(num_qubits=3): + tableau = stim.Tableau(3) + print(tableau) + x2x, x2z, z2x, z2z, x_signs, z_signs = tableau.to_numpy(bit_packed=False) + print(x2x) + print(x2z) + x_arr = np.concatenate([x2x, x2z]) + print(x_arr) + + + + + + +def create_rules_graph(rules): + rules_tuple = [] + for k, v in rules.items(): + res_key = _pauli_to_string(v[0]) + _pauli_to_string(v[1]) + rules_tuple.append((k, res_key)) + G = nx.Graph() + print(rules_tuple) + G.add_edges_from(rules_tuple) + return G + + +if __name__ == '__main__': + main() From 2aa2d1c8bf232d841a45f0b7635280cff89ccd67 Mon Sep 17 00:00:00 2001 From: daehiff Date: Sun, 7 May 2023 12:51:07 +0200 Subject: [PATCH 05/30] removed path method of the FW algorithm --- pauliopt/topologies.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pauliopt/topologies.py b/pauliopt/topologies.py index a9253bfa..c739230b 100644 --- a/pauliopt/topologies.py +++ b/pauliopt/topologies.py @@ -87,17 +87,6 @@ class TopologyDict(TypedDict, total=True): """ -def fw_shortest_path(self, u, v): - if self.floyd_warshall_next[u, v] is None: - raise Exception("Unconnected Architecture") - else: - path = [u] - while u != v: - u = self.floyd_warshall_next[u, v] - path.append(u) - return path - - def _floyd_warshall(topology: "Topology"): next = np.ones((topology.num_qubits, topology.num_qubits), dtype=int) dist = np.inf * np.ones((topology.num_qubits, topology.num_qubits)) From e29fa742776e3773f9afe0acdd5758ee0ad51df3 Mon Sep 17 00:00:00 2001 From: daehiff Date: Sun, 7 May 2023 12:52:53 +0200 Subject: [PATCH 06/30] some cleanup: Removed clifford Tableaus, and test.py --- pauliopt/pauli/clifford_tableau.py | 18 ------ test.py | 88 ------------------------------ 2 files changed, 106 deletions(-) delete mode 100644 pauliopt/pauli/clifford_tableau.py delete mode 100644 test.py diff --git a/pauliopt/pauli/clifford_tableau.py b/pauliopt/pauli/clifford_tableau.py deleted file mode 100644 index 0a83881b..00000000 --- a/pauliopt/pauli/clifford_tableau.py +++ /dev/null @@ -1,18 +0,0 @@ -import numpy as np - - -class CliffordTableau: - def __init__(self, n_qubits: int = None, tableau: np.array = None): - if n_qubits is None and tableau is None: - raise Exception("Either Tableau or number of qubits must be defined") - if tableau is None: - self.tableau = np.eye(2 * n_qubits) - self.n_qubits = n_qubits - else: - if tableau.shape[1] == tableau.shape[1]: - raise Exception("Must be a 2nx2n Tableau!") - self.tableau_x = tableau - self.n_qubits = int(tableau.shape[1] / 2.0) - - def apply_h(self, qubit): - pass diff --git a/test.py b/test.py deleted file mode 100644 index a6a95baa..00000000 --- a/test.py +++ /dev/null @@ -1,88 +0,0 @@ -import networkx as nx -from matplotlib import pyplot as plt - -from pauliopt.pauli.anneal import anneal -from pauliopt.pauli.clifford_gates import CX, H, CY, CZ -from pauliopt.pauli.pauli_gadget import PPhase -from pauliopt.pauli.pauli_polynomial import * -import numpy as np - -from pauliopt.pauli.utils import Pauli, _pauli_to_string -import stim - -def two_qubit_count(count_ops): - count = 0 - count += count_ops["cx"] if "cx" in count_ops.keys() else 0 - count += count_ops["cy"] if "cy" in count_ops.keys() else 0 - count += count_ops["cz"] if "cz" in count_ops.keys() else 0 - return count - - -def create_random_phase_gadget(num_qubits, min_legs, max_legs, allowed_angels): - angle = np.random.choice(allowed_angels) - nr_legs = np.random.randint(min_legs, max_legs) - legs = np.random.choice([i for i in range(num_qubits)], size=nr_legs, replace=False) - phase_gadget = [Pauli.I for _ in range(num_qubits)] - for leg in legs: - phase_gadget[leg] = np.random.choice([Pauli.X, Pauli.Y, Pauli.Z]) - return PPhase(angle) @ phase_gadget - - -def generate_random_pauli_polynomial(num_qubits: int, num_gadgets: int, min_legs=None, - max_legs=None, allowed_angels=None): - if min_legs is None: - min_legs = 1 - if max_legs is None: - max_legs = num_qubits - if allowed_angels is None: - allowed_angels = [2 * np.pi, np.pi, 0.5 * np.pi, 0.25 * np.pi, 0.125 * np.pi] - - pp = PauliPolynomial() - for _ in range(num_gadgets): - pp >>= create_random_phase_gadget(num_qubits, min_legs, max_legs, allowed_angels) - - return pp - - -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 main(num_qubits=3): - tableau = stim.Tableau(3) - print(tableau) - x2x, x2z, z2x, z2z, x_signs, z_signs = tableau.to_numpy(bit_packed=False) - print(x2x) - print(x2z) - x_arr = np.concatenate([x2x, x2z]) - print(x_arr) - - - - - - -def create_rules_graph(rules): - rules_tuple = [] - for k, v in rules.items(): - res_key = _pauli_to_string(v[0]) + _pauli_to_string(v[1]) - rules_tuple.append((k, res_key)) - G = nx.Graph() - print(rules_tuple) - G.add_edges_from(rules_tuple) - return G - - -if __name__ == '__main__': - main() From 5aec29e545a5f3d5dfe53fed3e99122aec9477b1 Mon Sep 17 00:00:00 2001 From: daehiff Date: Sun, 7 May 2023 13:16:27 +0200 Subject: [PATCH 07/30] removed formatting issues from topologies.py --- pauliopt/topologies.py | 90 ++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 47 deletions(-) diff --git a/pauliopt/topologies.py b/pauliopt/topologies.py index c739230b..8aa06559 100644 --- a/pauliopt/topologies.py +++ b/pauliopt/topologies.py @@ -8,13 +8,11 @@ import numpy as np import numpy.typing as npt - class Coupling(FrozenSet[int]): """ Type for couplings in a qubit topology, i.e. unordered pairs of adjacent qubits. """ - def __new__(cls, fst: int, snd: int) -> "Coupling": if not isinstance(fst, int): raise TypeError(f"Expected integer, found {fst}") @@ -23,14 +21,14 @@ def __new__(cls, fst: int, snd: int) -> "Coupling": if len({fst, snd}) != 2: raise ValueError("Expected a pair of distinct qubits.") # see https://github.com/python/mypy/issues/6061 - return super(Coupling, cls).__new__(cls, {fst, snd}) # type: ignore + return super(Coupling, cls).__new__(cls, {fst, snd}) # type: ignore @property def as_pair(self) -> Tuple[int, int]: """ Returns the coupling as a (increasingly) ordered pair. """ - return (min(*self), max(*self)) # pylint: disable = not-an-iterable + return (min(*self), max(*self)) # pylint: disable = not-an-iterable def __str__(self) -> str: fst, snd = sorted(self) @@ -173,8 +171,8 @@ def is_planar(self) -> bool: """ try: # pylint: disable = import-outside-toplevel - import networkx as nx # type: ignore - except ModuleNotFoundError as e: # pylint: disable = unused-variable + import networkx as nx # type: ignore + except ModuleNotFoundError as e: # pylint: disable = unused-variable raise ModuleNotFoundError("You must install the 'networkx' library.") G = self.to_nx is_planar, _ = nx.check_planarity(G) @@ -186,7 +184,7 @@ def available_nx_layouts(self) -> Tuple[str, ...]: Readonly property returning the available layouts for this qubit topology. """ if self.is_planar: - return Layouts + ("planar",) + return Layouts+("planar",) return Layouts @property @@ -197,7 +195,7 @@ def to_nx(self): """ try: # pylint: disable = import-outside-toplevel - import networkx as nx # type: ignore + import networkx as nx # type: ignore except ModuleNotFoundError as _: raise ModuleNotFoundError("You must install the 'networkx' library.") g = nx.Graph() @@ -221,12 +219,12 @@ def draw(self, layout: str = "kamada_kawai", *, """ try: # pylint: disable = import-outside-toplevel - import networkx as nx # type: ignore + import networkx as nx # type: ignore except ModuleNotFoundError as _: raise ModuleNotFoundError("You must install the 'networkx' library.") try: # pylint: disable = import-outside-toplevel - import matplotlib.pyplot as plt # type: ignore + import matplotlib.pyplot as plt # type: ignore except ModuleNotFoundError as _: raise ModuleNotFoundError("You must install the 'matplotlib' library.") G = self.to_nx @@ -236,7 +234,7 @@ def draw(self, layout: str = "kamada_kawai", *, if layout not in layouts: raise ValueError(f"Invalid layout found: {layout}. " f"Valid layouts: {', '.join(repr(l) for l in layouts)}") - kwargs["pos"] = getattr(nx, layout + "_layout")(G) + kwargs["pos"] = getattr(nx, layout+"_layout")(G) if "node_color" not in kwargs: kwargs["node_color"] = "#dddddd" plt.figure(figsize=figsize) @@ -278,19 +276,6 @@ def dist(self, fro: int, to: int) -> int: raise TypeError(f"Expected a valid qubit, found {to}.") return self._dist[fro, to] - def shortest_path(self, fro: int, to: int): - """ - Compute the shortest path using the next lookup table from the Floyd–Warshall algorithm - """ - if self._next[fro, to] is None: - raise Exception("Unconnected Architecture") - else: - path = [fro] - while fro != to: - fro = self._next[fro, to] - path.append(fro) - return path - def mapped_fwd(self, mapping: Union[Sequence[int], Dict[int, int]]) -> "Topology": """ Returns a topology with the same couplings, but remapping the qubits using @@ -317,7 +302,22 @@ def mapped_fwd(self, mapping: Union[Sequence[int], Dict[int, int]]) -> "Topology mapped_couplings = [{_mapping[x] for x in coupling} for coupling in self._couplings] return Topology(self.num_qubits, mapped_couplings) - def mapped_bwd(self, mapping: Union[Sequence[int], Dict[int, int]]) -> "Topology": + def shortest_path(self, fro: int, to: int): + """ + Computes the shortest path using the next lookup table from the Floyd–Warshall algorithm + """ + if self._next[fro, to] is None: + raise Exception("Unconnected Architecture") + else: + path = [fro] + while fro != to: + fro = self._next[fro, to] + path.append(fro) + return path + + + +def mapped_bwd(self, mapping: Union[Sequence[int], Dict[int, int]]) -> "Topology": """ Returns a topology with the same couplings, but remapping the qubits using the inverse of the given mapping. @@ -411,9 +411,9 @@ def line(num_qubits: int) -> "Topology": """ if not isinstance(num_qubits, int) or num_qubits <= 0: raise TypeError("Number of qubits must be positive integer.") - couplings = [[i, i + 1] for i in range(num_qubits - 1)] + couplings = [[i, i+1] for i in range(num_qubits-1)] top = Topology(num_qubits, couplings) - top._named = f"line({num_qubits})" # pylint: disable = protected-access + top._named = f"line({num_qubits})" # pylint: disable = protected-access return top @staticmethod @@ -423,9 +423,9 @@ def cycle(num_qubits: int) -> "Topology": """ if not isinstance(num_qubits, int) or num_qubits <= 0: raise TypeError("Number of qubits must be positive integer.") - couplings = [[i, (i + 1) % num_qubits] for i in range(num_qubits)] + couplings = [[i, (i+1)%num_qubits] for i in range(num_qubits)] top = Topology(num_qubits, couplings) - top._named = f"cycle({num_qubits})" # pylint: disable = protected-access + top._named = f"cycle({num_qubits})" # pylint: disable = protected-access return top @staticmethod @@ -435,9 +435,9 @@ def complete(num_qubits: int) -> "Topology": """ if not isinstance(num_qubits, int) or num_qubits <= 0: raise TypeError("Number of qubits must be positive integer.") - couplings = [[i, j] for i in range(num_qubits) for j in range(i + 1, num_qubits)] + couplings = [[i, j] for i in range(num_qubits) for j in range(i+1, num_qubits)] top = Topology(num_qubits, couplings) - top._named = f"complete({num_qubits})" # pylint: disable = protected-access + top._named = f"complete({num_qubits})" # pylint: disable = protected-access return top @staticmethod @@ -451,19 +451,17 @@ def grid(num_rows: int, num_cols: int) -> "Topology": if not isinstance(num_cols, int) or num_cols <= 0: raise TypeError("Number of cols must be positive integer.") num_qubits = num_rows * num_cols - def qubit(r, c): - return num_cols * r + c - + return num_cols*r + c couplings: List[List[int]] = [] for r in range(num_rows): for c in range(num_cols): - if r < num_rows - 1: - couplings.append([qubit(r, c), qubit(r + 1, c)]) - if c < num_cols - 1: - couplings.append([qubit(r, c), qubit(r, c + 1)]) + if r < num_rows-1: + couplings.append([qubit(r, c), qubit(r+1, c)]) + if c < num_cols-1: + couplings.append([qubit(r, c), qubit(r, c+1)]) top = Topology(num_qubits, couplings) - top._named = f"grid({num_rows},{num_cols})" # pylint: disable = protected-access + top._named = f"grid({num_rows},{num_cols})" # pylint: disable = protected-access return top @staticmethod @@ -477,17 +475,15 @@ def periodic_grid(num_rows: int, num_cols: int) -> "Topology": if not isinstance(num_cols, int) or num_cols <= 0: raise TypeError("Number of cols must be positive integer.") num_qubits = num_rows * num_cols - def qubit(r, c): - return num_cols * r + c - + return num_cols*r + c couplings: List[List[int]] = [] for r in range(num_rows): for c in range(num_cols): - couplings.append([qubit(r, c), qubit((r + 1) % num_rows, c)]) - couplings.append([qubit(r, c), qubit(r, (c + 1) % num_cols)]) + couplings.append([qubit(r, c), qubit((r+1)%num_rows, c)]) + couplings.append([qubit(r, c), qubit(r, (c+1)%num_cols)]) top = Topology(num_qubits, couplings) - top._named = f"periodic_grid({num_rows},{num_cols})" # pylint: disable = protected-access + top._named = f"periodic_grid({num_rows},{num_cols})" # pylint: disable = protected-access return top @staticmethod @@ -502,7 +498,7 @@ def from_qiskit_config(config) -> "Topology": """ try: # pylint: disable = import-outside-toplevel - from qiskit.providers.models import QasmBackendConfiguration # type: ignore + from qiskit.providers.models import QasmBackendConfiguration # type: ignore except ModuleNotFoundError as _: raise ModuleNotFoundError("You must install the 'qiskit' library.") if not isinstance(config, QasmBackendConfiguration): @@ -525,7 +521,7 @@ def from_qiskit_backend(backend) -> "Topology": """ try: # pylint: disable = import-outside-toplevel, unused-import - from qiskit.providers import Backend # type: ignore + from qiskit.providers import Backend # type: ignore except ModuleNotFoundError as _: raise ModuleNotFoundError("You must install the 'qiskit' library.") if not isinstance(backend, Backend): From baca52ed65d74dc44a512c5d90ad99328ea1ca10 Mon Sep 17 00:00:00 2001 From: daehiff Date: Sun, 7 May 2023 13:18:22 +0200 Subject: [PATCH 08/30] restored utils, fixed issue in topologies.py --- pauliopt/topologies.py | 4 +- pauliopt/utils.py | 109 ++++++++++++++++++----------------------- 2 files changed, 51 insertions(+), 62 deletions(-) diff --git a/pauliopt/topologies.py b/pauliopt/topologies.py index 8aa06559..0af833c7 100644 --- a/pauliopt/topologies.py +++ b/pauliopt/topologies.py @@ -302,6 +302,7 @@ def mapped_fwd(self, mapping: Union[Sequence[int], Dict[int, int]]) -> "Topology mapped_couplings = [{_mapping[x] for x in coupling} for coupling in self._couplings] return Topology(self.num_qubits, mapped_couplings) + def shortest_path(self, fro: int, to: int): """ Computes the shortest path using the next lookup table from the Floyd–Warshall algorithm @@ -316,8 +317,7 @@ def shortest_path(self, fro: int, to: int): return path - -def mapped_bwd(self, mapping: Union[Sequence[int], Dict[int, int]]) -> "Topology": + def mapped_bwd(self, mapping: Union[Sequence[int], Dict[int, int]]) -> "Topology": """ Returns a topology with the same couplings, but remapping the qubits using the inverse of the given mapping. diff --git a/pauliopt/utils.py b/pauliopt/utils.py index 003d6b2d..568bc64e 100644 --- a/pauliopt/utils.py +++ b/pauliopt/utils.py @@ -1,7 +1,7 @@ """ Utility classes and functions for the `pauliopt` library. """ -import itertools + from abc import ABC, abstractmethod import math from decimal import Decimal @@ -13,7 +13,6 @@ AngleInitT = Union[int, Fraction, str, Decimal] - class AngleExpr(ABC): """ A container class for angle expressions. @@ -52,7 +51,7 @@ def __rmul__(self, other: int) -> "AngleExpr": def __truediv__(self, other: int) -> "AngleExpr": if isinstance(other, int): - return SumprodAngleExpr(self, coeffs=Fraction(1, other)) + return SumprodAngleExpr(self, coeffs=Fraction(1,other)) return NotImplemented @abstractmethod @@ -97,7 +96,7 @@ def _repr_latex_(self) -> str: Magic method for IPython/Jupyter pretty-printing. See https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html """ - return "$%s$" % self.repr_latex + return "$%s$"%self.repr_latex @abstractmethod def __eq__(self, other: Any) -> bool: @@ -123,7 +122,7 @@ def value(self) -> Fraction: """ The value of this angle as a fraction of PI. """ - return self._value % 2 + return self._value%2 @property def as_root_of_unity(self) -> Tuple[int, int]: @@ -134,8 +133,8 @@ def as_root_of_unity(self) -> Tuple[int, int]: """ num = self.value.numerator den = self.value.denominator - a: int = num // 2 if num % 2 == 0 else num - order: int = den if num % 2 == 0 else 2 * den + a: int = num//2 if num%2 == 0 else num + order: int = den if num % 2 == 0 else 2*den return (a, order) @property @@ -145,7 +144,7 @@ def order(self) -> int: """ num = self.value.numerator den = self.value.denominator - return den if num % 2 == 0 else 2 * den + return den if num % 2 == 0 else 2*den @property def is_zero_or_pi(self) -> bool: @@ -163,7 +162,7 @@ def is_zero(self) -> bool: """ num = self.value.numerator den = self.value.denominator - return num % (2 * den) == 0 + return num % (2*den) == 0 @property def is_pi(self) -> bool: @@ -172,7 +171,7 @@ def is_pi(self) -> bool: """ num = self.value.numerator den = self.value.denominator - return num % den == 0 and not num % (2 * den) == 0 + return num % den == 0 and not num % (2*den) == 0 @property def to_qiskit(self) -> float: @@ -224,7 +223,7 @@ def __mod__(self, other: "AngleExpr") -> "AngleExpr": return super().__mod__(other) def _mul(self, other: Union[int, Fraction]) -> "Angle": - return Angle(self._value * other) + return Angle(self._value*other) def __mul__(self, other: int) -> "Angle": if isinstance(other, int): @@ -252,8 +251,8 @@ def __str__(self) -> str: if num == 1: if den == 1: return "π" - return "π/%d" % den - return "%dπ/%d" % (num, den) + return "π/%d"%den + return "%dπ/%d"%(num, den) def __repr__(self) -> str: num = self.value.numerator @@ -263,8 +262,8 @@ def __repr__(self) -> str: if num == 1: if den == 1: return "pi" - return "pi/%d" % den - return "%d*pi/%d" % (num, den) + return "pi/%d"%den + return "%d*pi/%d"%(num, den) @property def repr_latex(self) -> str: @@ -278,15 +277,15 @@ def repr_latex(self) -> str: if num == 1: if den == 1: return "\\pi" - return "\\frac{\\pi}{%d}" % den - return "\\frac{%d\\pi}{%d}" % (num, den) + return "\\frac{\\pi}{%d}"%den + return "\\frac{%d\\pi}{%d}"%(num, den) def _repr_latex_(self) -> str: """ Magic method for IPython/Jupyter pretty-printing. See https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html """ - return "$%s$" % self.repr_latex + return "$%s$"%self.repr_latex def __eq__(self, other: Any) -> bool: if other == 0: @@ -296,12 +295,12 @@ def __eq__(self, other: Any) -> bool: return self.value == other.value def __float__(self) -> float: - return float(self.value) * math.pi + return float(self.value)*math.pi @overload @staticmethod def random(subdivision: int = 4, *, - size: Literal[1] = 1, + size: Literal[1]=1, rng_seed: Optional[int] = None, nonzero: bool = False) -> "Angle": ... @@ -336,35 +335,35 @@ def random(subdivision: int = 4, *, size: int = 1, raise TypeError("RNG seed must be integer or 'None'.") rng = np.random.default_rng(seed=rng_seed) if nonzero: - rs = 1 + rng.integers(2 * subdivision - 1, size=size) # type: ignore[attr-defined] + rs = 1+rng.integers(2*subdivision-1, size=size) # type: ignore[attr-defined] else: - rs = rng.integers(2 * subdivision, size=size) # type: ignore[attr-defined] + rs = rng.integers(2*subdivision, size=size) # type: ignore[attr-defined] if size == 1: return Angle(Fraction(int(rs[0]), subdivision)) return tuple(Angle(Fraction(int(r), subdivision)) for r in rs) - zero: Final["Angle"] # type: ignore + zero: Final["Angle"] # type: ignore """ A constant for the angle 0. """ - pi: Final["Angle"] # type: ignore + pi: Final["Angle"] # type: ignore """ A constant for the angle pi. """ - # Set static constants for Angle: -Angle.zero = Angle(0) # type: ignore -Angle.pi = Angle(1) # type: ignore +Angle.zero = Angle(0) # type: ignore +Angle.pi = Angle(1) # type: ignore + pi: Final[Angle] = Angle.pi """ Constant for `Angle.pi`. """ -π: Final[Angle] = Angle.pi # pylint: disable=non-ascii-name +π: Final[Angle] = Angle.pi # pylint: disable=non-ascii-name """ Constant for `Angle.pi`. """ def SumprodAngleExpr(*exprs: AngleExpr, coeffs: Union[int, Fraction, Sequence[Union[int, Fraction]]] = 1 - ) -> AngleExpr: + ) -> AngleExpr: if not isinstance(coeffs, Sequence): coeffs = (coeffs,) if len(coeffs) != len(exprs): @@ -374,10 +373,10 @@ def SumprodAngleExpr(*exprs: AngleExpr, _const: Angle = Angle.zero for e, c in zip(exprs, coeffs): if isinstance(e, Angle): - _const += e._mul(c) # pylint: disable = protected-access + _const += e._mul(c) # pylint: disable = protected-access elif isinstance(e, _SumprodAngleExpr): for sub_e, sub_c in e.coeffs.items(): - new_c = c * sub_c + new_c = c*sub_c if sub_e in _coeffs: new_c += _coeffs[sub_e] _coeffs[sub_e] = new_c @@ -427,7 +426,7 @@ def is_pi(self) -> bool: @property def to_qiskit(self) -> Any: - return sum((c * e.to_qiskit for e, c in self.coeffs.items()), self.const.to_qiskit) + return sum((c*e.to_qiskit for e, c in self.coeffs.items()), self.const.to_qiskit) def __hash__(self) -> int: return hash((_SumprodAngleExpr, tuple(self.coeffs.items()), self.const)) @@ -438,12 +437,12 @@ def _str_repr(self, f: Callable[[Union[Angle, AngleExpr]], str]) -> str: pos_sub_e = {e: c for e, c in self.coeffs.items() if c > 0} neg_sub_e = {e: c for e, c in self.coeffs.items() if c < 0} s = "+".join( - ("" if c == 1 else str(c)) + f(e) + ("" if c == 1 else str(c))+f(e) for e, c in pos_sub_e.items() ) if neg_sub_e: - s += "-" + "".join( - ("-" if c == -1 else str(c)) + f(e) + s += "-"+"".join( + ("-" if c == -1 else str(c))+f(e) for e, c in pos_sub_e.items() ) if self.const != 0: @@ -470,13 +469,11 @@ def __eq__(self, other: Any) -> bool: return False return NotImplemented - def ModAngleExpr(expr: AngleExpr, mod: AngleExpr) -> AngleExpr: if isinstance(expr, Angle) and isinstance(mod, Angle): - return expr % mod + return expr%mod return _ModAngleExpr(expr, mod) - class _ModAngleExpr(AngleExpr): _expr: AngleExpr _mod: AngleExpr @@ -501,7 +498,7 @@ def is_zero(self) -> bool: @property def to_qiskit(self) -> Any: - return self.expr.to_qiskit % self.mod.to_qiskit + return self.expr.to_qiskit%self.mod.to_qiskit def __hash__(self) -> int: return hash((_ModAngleExpr, self.expr, self.mod)) @@ -526,7 +523,6 @@ def __eq__(self, other: Any) -> bool: return False return NotImplemented - class AngleVar(AngleExpr): _global_id: ClassVar[int] = 0 _qiskit_bindings: ClassVar[Dict[int, Any]] @@ -548,7 +544,7 @@ def to_qiskit(self) -> Any: return AngleVar._qiskit_bindings[self._id] try: # pylint: disable = import-outside-toplevel - from qiskit.circuit import Parameter # type: ignore + from qiskit.circuit import Parameter # type: ignore except ModuleNotFoundError as e: raise ModuleNotFoundError("You must install the 'qiskit' library.") from e p = Parameter(self._repr_latex_) @@ -579,13 +575,13 @@ def __eq__(self, other: Any) -> bool: return NotImplemented + def _validate_vec2(vec2: Tuple[int, int]) -> None: if not isinstance(vec2, tuple) or len(vec2) != 2: raise TypeError("Expected pair.") if not all(isinstance(x, int) for x in vec2): raise TypeError("Expected pair of integers.") - class SVGBuilder: """ Utility class for building certain SVG images. @@ -680,7 +676,7 @@ def text(self, pos: Tuple[int, int], text: str, *, font_size: int = 10) -> "SVGB if not isinstance(font_size, int) or font_size <= 0: raise TypeError("Font size must be positive integer.") x, y = pos - tag = f'{text}' + tag = f'{text}' self._tags.append(tag) return self @@ -713,7 +709,6 @@ class TempSchedule(Protocol): def __call__(self, it: int, num_iters: int) -> float: ... - @runtime_checkable class TempScheduleProvider(Protocol): """ @@ -734,10 +729,8 @@ def linear_temp_schedule(t_init: Union[int, float], t_final: Union[int, float]) raise TypeError(f"Expected int or float, found {type(t_init)}.") if not isinstance(t_final, (int, float)): raise TypeError(f"Expected int or float, found {type(t_final)}.") - def temp_schedule(it: int, num_iters: int) -> float: - return t_init + (t_final - t_init) * it / (num_iters - 1) - + return t_init + (t_final-t_init)*it/(num_iters-1) return temp_schedule @@ -750,10 +743,8 @@ def geometric_temp_schedule(t_init: Union[int, float], t_final: Union[int, float raise TypeError(f"Expected int or float, found {type(t_init)}.") if not isinstance(t_final, (int, float)): raise TypeError(f"Expected int or float, found {type(t_final)}.") - def temp_schedule(it: int, num_iters: int) -> float: - return t_init * ((t_final / t_init) ** (it / (num_iters - 1.0))) # type: ignore[no-any-return] - + return t_init * ((t_final/t_init)**(it/(num_iters-1.0))) # type: ignore[no-any-return] return temp_schedule @@ -766,12 +757,10 @@ def reciprocal_temp_schedule(t_init: Union[int, float], t_final: Union[int, floa raise TypeError(f"Expected int or float, found {type(t_init)}.") if not isinstance(t_final, (int, float)): raise TypeError(f"Expected int or float, found {type(t_final)}.") - def temp_schedule(it: int, num_iters: int) -> float: - num = t_init * t_final * (num_iters - 1) - denom = (t_final * num_iters - t_init) + (t_init - t_final) * (it + 1) - return num / denom - + num = t_init*t_final*(num_iters-1) + denom = (t_final*num_iters-t_init)+(t_init-t_final)*(it+1) + return num/denom return temp_schedule @@ -784,12 +773,10 @@ def log_temp_schedule(t_init: Union[int, float], t_final: Union[int, float]) -> raise TypeError(f"Expected int or float, found {type(t_init)}.") if not isinstance(t_final, (int, float)): raise TypeError(f"Expected int or float, found {type(t_final)}.") - def temp_schedule(it: int, num_iters: int) -> float: - num = t_init * t_final * (math.log(num_iters + 1) - math.log(2)) - denom = (t_final * math.log(num_iters + 1) - t_init * math.log(2)) + (t_init - t_final) * math.log(it + 2) - return num / denom - + num = t_init*t_final*(math.log(num_iters+1)-math.log(2)) + denom = (t_final*math.log(num_iters+1)-t_init*math.log(2))+(t_init-t_final)*math.log(it+2) + return num/denom return temp_schedule @@ -798,12 +785,14 @@ def temp_schedule(it: int, num_iters: int) -> float: Names of the standard temperature schedules. """ + StandardTempSchedule = Tuple[StandardTempScheduleName, Union[int, float], Union[int, float]] """ Type for standard temperature schedules. """ + StandardTempSchedules: Final[Mapping[StandardTempScheduleName, TempScheduleProvider]] = { "linear": linear_temp_schedule, "geometric": geometric_temp_schedule, From 82e9a72824d7a601fd2e5d34b9c46b77339aa437 Mon Sep 17 00:00:00 2001 From: daehiff Date: Sun, 7 May 2023 13:21:22 +0200 Subject: [PATCH 09/30] removed phase circuit --- pauliopt/phase/phase_circuits.py | 218 ++++++++++++++----------------- 1 file changed, 96 insertions(+), 122 deletions(-) diff --git a/pauliopt/phase/phase_circuits.py b/pauliopt/phase/phase_circuits.py index 88ee6148..75a51723 100644 --- a/pauliopt/phase/phase_circuits.py +++ b/pauliopt/phase/phase_circuits.py @@ -13,14 +13,12 @@ from pauliopt.topologies import Topology from pauliopt.utils import Angle, AngleExpr, AngleVar, SVGBuilder, pi - def _frozenset_to_int(s: FrozenSet[int]) -> int: i = 0 for x in s: - i |= (2 ** x) + i |= (2**x) return i - def _int_to_frozenset(i: int) -> FrozenSet[int]: s: List[int] = [] x = 0 @@ -31,7 +29,6 @@ def _int_to_frozenset(i: int) -> FrozenSet[int]: x += 1 return frozenset(s) - def _int_to_iterator(i: int) -> Iterator[int]: x = 0 while i != 0: @@ -40,7 +37,6 @@ def _int_to_iterator(i: int) -> Iterator[int]: i //= 2 x += 1 - def _prims_algorithm_weight(nodes: Collection[int], weight: Callable[[int, int], int], inf: int) -> int: """ @@ -63,8 +59,8 @@ def _prims_algorithm_weight(nodes: Collection[int], weight: Callable[[int, int], } while to_visit: # Look for the node to be visited which is nearest to the visited set: - nearest_node = 0 # dummy value - nearest_dist: int = inf # dummy value + nearest_node = 0 # dummy value + nearest_dist: int = inf # dummy value for n in to_visit: n_dist: int = dist_from_visited[n] if n_dist < nearest_dist: @@ -80,7 +76,6 @@ def _prims_algorithm_weight(nodes: Collection[int], weight: Callable[[int, int], dist_from_visited[n] = dist_nearest_n return mst_length - def _prims_algorithm_branches(nodes: Collection[int], weight: Callable[[int, int], int], inf: int) -> Sequence[Tuple[int, int]]: # pylint: disable = too-many-locals @@ -101,8 +96,8 @@ def _prims_algorithm_branches(nodes: Collection[int], weight: Callable[[int, int } while to_visit: # Look for the node to be visited which is nearest to the visited set: - nearest_node = 0 # dummy value - nearest_dist: int = inf # dummy value + nearest_node = 0 # dummy value + nearest_dist: int = inf # dummy value for n in to_visit: n_dist: int = dist_from_visited[n] if n_dist < nearest_dist: @@ -119,7 +114,6 @@ def _prims_algorithm_branches(nodes: Collection[int], weight: Callable[[int, int edge_from_visited[n] = (nearest_node, n) return mst_branches - def _prims_algorithm_full(nodes: Collection[int], weight: Callable[[int, int], int], inf: int) -> Tuple[int, Sequence[int], Sequence[Tuple[int, int]]]: # pylint: disable = too-many-locals @@ -142,8 +136,8 @@ def _prims_algorithm_full(nodes: Collection[int], weight: Callable[[int, int], i } while to_visit: # Look for the node to be visited which is nearest to the visited set: - nearest_node = 0 # dummy value - nearest_dist: int = inf # dummy value + nearest_node = 0 # dummy value + nearest_dist: int = inf # dummy value for n in to_visit: n_dist: int = dist_from_visited[n] if n_dist < nearest_dist: @@ -231,9 +225,9 @@ def cx_count(self, topology: Topology, *, topology = topology.mapped_fwd({ mapping[i]: i for i in mapping }) - return _prims_algorithm_weight(self._qubits, - lambda u, v: 4 * topology.dist(u, v) - 2, - 4 * len(topology.qubits) - 2) + return _prims_algorithm_weight(self._qubits, + lambda u, v: 4*topology.dist(u, v)-2, + 4*len(topology.qubits)-2) def on_qiskit_circuit(self, topology: Topology, circuit: Any) -> None: """ @@ -249,7 +243,7 @@ def on_qiskit_circuit(self, topology: Topology, circuit: Any) -> None: # TODO: currently uses CX ladder, must change into balanced tree! (same CX count) try: # pylint: disable = import-outside-toplevel - from qiskit.circuit import QuantumCircuit # type: ignore + from qiskit.circuit import QuantumCircuit # type: ignore except ModuleNotFoundError as e: raise ModuleNotFoundError("You must install the 'qiskit' library.") from e if not isinstance(circuit, QuantumCircuit): @@ -259,8 +253,8 @@ def on_qiskit_circuit(self, topology: Topology, circuit: Any) -> None: raise TypeError(f"Expected Topology, found {type(topology)}.") # Build MST data structure: mst_branches = _prims_algorithm_branches(self._qubits, - lambda u, v: 4 * topology.dist(u, v) - 2, - 4 * len(topology.qubits) - 2) + lambda u, v: 4*topology.dist(u, v)-2, + 4*len(topology.qubits)-2) upper_ladder: List[Tuple[int, int]] = [] if len(self._qubits) == 1: q0 = next(iter(self._qubits)) @@ -304,8 +298,8 @@ def print_impl_info(self, topology: Topology) -> None: raise TypeError(f"Expected Topology, found {type(topology)}.") mst_length, mst_branch_lengths, mst_branches = \ _prims_algorithm_full(self._qubits, - lambda u, v: 4 * topology.dist(u, v) - 2, - 4 * len(topology.qubits) - 2) + lambda u, v: 4*topology.dist(u, v)-2, + 4*len(topology.qubits)-2) print(f"MST implementation info for {str(self)}:") print(f" - Overall CX count for gadget: {mst_length}") print(f" - MST branches: {mst_branches}") @@ -376,107 +370,87 @@ def __matmul__(self, qubits: Collection[int]) -> PhaseGadget: def _rx(qubit: int, angle: Angle) -> List[PhaseGadget]: return [X(angle) @ {qubit}] - def _rz(qubit: int, angle: Angle) -> List[PhaseGadget]: return [Z(angle) @ {qubit}] - def _ry(qubit: int, angle: Angle) -> List[PhaseGadget]: """ Phase gadget implementation of single-qubit Y rotation. """ - return _rx(qubit, +pi / 2) + _rz(qubit, angle) + _rx(qubit, -pi / 2) - + return _rx(qubit, +pi/2) + _rz(qubit, angle) + _rx(qubit, -pi/2) def _i(qubit: int) -> List[PhaseGadget]: return [] - def _x(qubit: int) -> List[PhaseGadget]: return _rx(qubit, pi) - def _z(qubit: int) -> List[PhaseGadget]: """ Phase gadget implementation of single-qubit X gate. """ return _rz(qubit, pi) - def _y(qubit: int) -> List[PhaseGadget]: """ Phase gadget implementation of single-qubit Y gate. """ return _z(qubit) + _x(qubit) - def _s(qubit: int) -> List[PhaseGadget]: """ Phase gadget implementation of single-qubit S gate. """ - return _rz(qubit, pi / 2) - + return _rz(qubit, pi/2) def _sdg(qubit: int) -> List[PhaseGadget]: """ Phase gadget implementation of single-qubit S gate. """ - return _rz(qubit, -pi / 2) - + return _rz(qubit, -pi/2) def _v(qubit: int) -> List[PhaseGadget]: """ Phase gadget implementation of single-qubit S gate. """ - return _rx(qubit, pi / 2) - + return _rx(qubit, pi/2) def _vdg(qubit: int) -> List[PhaseGadget]: """ Phase gadget implementation of single-qubit S gate. """ - return _rx(qubit, -pi / 2) - + return _rx(qubit, -pi/2) def _t(qubit: int) -> List[PhaseGadget]: """ Phase gadget implementation of single-qubit T gate. """ - return _rz(qubit, pi / 4) - + return _rz(qubit, pi/4) def _h(qubit: int, basis: Literal["Z", "X"] = "Z", - sign: Literal[1, -1] = 1) -> List[PhaseGadget]: + sign: Literal[1, -1]=1) -> List[PhaseGadget]: """ Phase gadget implementation of single-qubit Hadamard gate. """ if basis not in ("Z", "X"): raise TypeError(f"Invalid basis {basis}.") if sign not in (1, -1): raise TypeError(f"Invalid sign {sign}.") if basis == "Z": - return _rx(qubit, sign * pi / 2) + _rz(qubit, sign * pi / 2) + _rx(qubit, sign * pi / 2) - return _rz(qubit, sign * pi / 2) + _rx(qubit, sign * pi / 2) + _rz(qubit, sign * pi / 2) - + return _rx(qubit, sign*pi/2) + _rz(qubit, sign*pi/2) + _rx(qubit, sign*pi/2) + return _rz(qubit, sign*pi/2) + _rx(qubit, sign*pi/2) + _rz(qubit, sign*pi/2) def _cu1(ctrl: int, tgt: int, angle: Angle) -> List[PhaseGadget]: """ Phase gadget implementation of CU1 gate. """ return [Z(-angle) @ {ctrl, tgt}] + _rz(ctrl, angle) + _rz(tgt, angle) - def _crz(ctrl: int, tgt: int, angle: Angle) -> List[PhaseGadget]: """ Phase gadget implementation of CRZ gate. """ return [Z(-angle / 2) @ {ctrl, tgt}] + _rz(tgt, angle / 2) - def _cry(ctrl: int, tgt: int, angle: Angle) -> List[PhaseGadget]: """ Phase gadget implementation of CRY gate. """ return _v(tgt) + _crz(ctrl, tgt, angle) + _vdg(tgt) - def _crx(ctrl: int, tgt: int, angle: Angle) -> List[PhaseGadget]: """ Phase gadget implementation of CRX gate. """ return _h(tgt) + _crz(ctrl, tgt, angle) + _h(tgt, sign=-1) - def _cz(leg1: int, leg2: int) -> List[PhaseGadget]: """ Phase gadget implementation of CZ gate. """ return _cu1(leg1, leg2, pi / 2) - def _cy(leg1: int, leg2: int) -> List[PhaseGadget]: """ Phase gadget implementation of CY gate. """ return _v(leg2) + _cz(leg1, leg2) + _vdg(leg2) - def _cx(ctrl: int, tgt: int) -> List[PhaseGadget]: """ Phase gadget implementation of CX gate. """ return _h(tgt) + _cz(ctrl, tgt) + _h(tgt, sign=-1) - def _u3(qubit: int, theta: Angle, phi: Angle, lam: Angle) -> List[PhaseGadget]: """ Phase gadget implementation of U3 gate. """ return _rz(qubit, lam) + _ry(qubit, theta) + _rz(qubit, phi) @@ -537,7 +511,7 @@ def __init__(self, num_qubits: int, gadgets: Sequence[PhaseGadget] = tuple()): if not isinstance(num_qubits, int) or num_qubits <= 0: raise TypeError("Number of qubits must be a positive integer.") if (not isinstance(gadgets, Sequence) - or not all(isinstance(g, PhaseGadget) for g in gadgets)): # pylint: disable = C0330 + or not all(isinstance(g, PhaseGadget) for g in gadgets)): # pylint: disable = C0330 raise TypeError("Gadgets should be a sequence of PhaseGadget.") self._num_qubits = num_qubits # Fills the lists of original indices and angles for the gadgets: @@ -549,7 +523,7 @@ def __init__(self, num_qubits: int, gadgets: Sequence[PhaseGadget] = tuple()): self._gadget_idxs[gadget.basis].append(i) angle = gadget.angle if isinstance(angle, Angle): - angle %= 2 * pi + angle %= 2*pi self._angles.append(angle) self._matrix = {} self._gadget_legs_cache = {} @@ -611,8 +585,8 @@ def set_angles(self, angles: Sequence[AngleExpr]) -> None: if len(angles) != len(self._angles): raise ValueError(f"Expected {len(self._angles)} angles, " f"found {len(angles)} instead.") - pi2 = 2 * pi - self._angles = [angle % pi2 if isinstance(angle, Angle) else angle + pi2 = 2*pi + self._angles = [angle%pi2 if isinstance(angle, Angle) else angle for angle in angles] def refresh_angle_vars(self, params: Union[str, Callable[[int], AngleVar]]) -> None: @@ -640,9 +614,9 @@ def rz(self, qubit: int, angle: AngleExpr) -> "PhaseCircuit": def ry(self, qubit: int, angle: AngleExpr) -> "PhaseCircuit": """ Phase gadget implementation of single-qubit Y rotation. """ - self.rx(qubit, +pi / 2) + self.rx(qubit, +pi/2) self.rz(qubit, angle) - self.rx(qubit, -pi / 2) + self.rx(qubit, -pi/2) return self def i(self, qubit: int) -> "PhaseCircuit": @@ -667,45 +641,45 @@ def y(self, qubit: int) -> "PhaseCircuit": def s(self, qubit: int) -> "PhaseCircuit": """ Phase gadget implementation of single-qubit S gate. """ - self.rz(qubit, pi / 2) + self.rz(qubit, pi/2) return self def sdg(self, qubit: int) -> "PhaseCircuit": """ Phase gadget implementation of single-qubit Sdg gate. """ - self.rz(qubit, -pi / 2) + self.rz(qubit, -pi/2) return self def v(self, qubit: int) -> "PhaseCircuit": """ Phase gadget implementation of single-qubit S gate. """ - self.rx(qubit, pi / 2) + self.rx(qubit, pi/2) return self def vdg(self, qubit: int) -> "PhaseCircuit": """ Phase gadget implementation of single-qubit Sdg gate. """ - self.rx(qubit, -pi / 2) + self.rx(qubit, -pi/2) return self def t(self, qubit: int) -> "PhaseCircuit": """ Phase gadget implementation of single-qubit T gate. """ - self.rz(qubit, pi / 4) + self.rz(qubit, pi/4) return self def h(self, qubit: int, basis: Literal["Z", "X"] = "Z", - sign: Literal[1, -1] = 1) -> "PhaseCircuit": + sign: Literal[1, -1]=1) -> "PhaseCircuit": """ Phase gadget implementation of single-qubit Hadamard gate. """ if basis not in ("Z", "X"): raise TypeError(f"Invalid basis {basis}.") if sign not in (1, -1): raise TypeError(f"Invalid sign {sign}.") if basis == "Z": - self.rx(qubit, sign * pi / 2) - self.rz(qubit, sign * pi / 2) - self.rx(qubit, sign * pi / 2) + self.rx(qubit, sign*pi/2) + self.rz(qubit, sign*pi/2) + self.rx(qubit, sign*pi/2) else: - self.rz(qubit, sign * pi / 2) - self.rx(qubit, sign * pi / 2) - self.rz(qubit, sign * pi / 2) + self.rz(qubit, sign*pi/2) + self.rx(qubit, sign*pi/2) + self.rz(qubit, sign*pi/2) return self def cu1(self, ctrl: int, tgt: int, angle: AngleExpr) -> "PhaseCircuit": @@ -827,7 +801,7 @@ def cx_count(self, topology: Topology, *, i: mapping[i] for i in range(len(mapping)) } if mapping is not None and set(mapping.values()) != set(range(self.num_qubits)): - raise TypeError(f"Expected mapping images [0, ..., {self.num_qubits - 1}], " + raise TypeError(f"Expected mapping images [0, ..., {self.num_qubits-1}], " f"found {sorted(set(mapping.values()))}") if mapping is not None: # use the reverse mapping on the topology @@ -938,7 +912,7 @@ def to_svg(self, *, raise TypeError("Keyword argument 'scale' must be positive float.") return self._to_svg(zcolor=zcolor, xcolor=xcolor, hscale=hscale, vscale=vscale, scale=scale, - svg_code_only=svg_code_only) # type: ignore[call-overload] + svg_code_only=svg_code_only) # type: ignore[call-overload] @overload def _to_svg(self, *, @@ -974,61 +948,61 @@ def _to_svg(self, *, hscale *= scale gadgets = self.gadgets num_digits = int(ceil(log10(num_qubits))) - line_height = int(ceil(30 * vscale)) - row_width = int(ceil(120 * hscale)) - pad_x = int(ceil(10 * hscale)) - margin_x = int(ceil(40 * hscale)) - pad_y = int(ceil(20 * vscale)) - r = pad_y // 2 - 2 - font_size = 2 * r - pad_x += font_size * (num_digits + 1) - delta_fst = row_width // 4 - delta_snd = 2 * row_width // 4 - width = 2 * pad_x + 2 * margin_x + row_width * len(gadgets) - height = pad_y + line_height * (num_qubits + 1) + line_height = int(ceil(30*vscale)) + row_width = int(ceil(120*hscale)) + pad_x = int(ceil(10*hscale)) + margin_x = int(ceil(40*hscale)) + pad_y = int(ceil(20*vscale)) + r = pad_y//2-2 + font_size = 2*r + pad_x += font_size*(num_digits+1) + delta_fst = row_width//4 + delta_snd = 2*row_width//4 + width = 2*pad_x + 2*margin_x + row_width*len(gadgets) + height = pad_y + line_height*(num_qubits+1) builder = SVGBuilder(width, height) levels: List[int] = [0 for _ in range(num_qubits)] max_lvl = 0 for gadget in gadgets: fill = zcolor if gadget.basis == "Z" else xcolor other_fill = xcolor if gadget.basis == "Z" else zcolor - qubit_span = range(min(gadget.qubits), max(gadget.qubits) + 1) + qubit_span = range(min(gadget.qubits), max(gadget.qubits)+1) lvl = max(levels[q] for q in qubit_span) max_lvl = max(max_lvl, lvl) x = pad_x + margin_x + lvl * row_width for q in qubit_span: - levels[q] = lvl + 1 + levels[q] = lvl+1 if len(gadget.qubits) > 1: - text_y = pad_y + min(gadget.qubits) * line_height + line_height // 2 + text_y = pad_y+min(gadget.qubits)*line_height+line_height//2 for q in gadget.qubits: - y = pad_y + (q + 1) * line_height - builder.line((x, y), (x + delta_fst, text_y)) + y = pad_y + (q+1)*line_height + builder.line((x, y), (x+delta_fst, text_y)) for q in gadget.qubits: - y = pad_y + (q + 1) * line_height + y = pad_y + (q+1)*line_height builder.circle((x, y), r, fill) - builder.line((x + delta_fst, text_y), (x + delta_snd, text_y)) - builder.circle((x + delta_fst, text_y), r, other_fill) - builder.circle((x + delta_snd, text_y), r, fill) - builder.text((x + delta_snd + 2 * r, text_y), str(gadget.angle), font_size=font_size) + builder.line((x+delta_fst, text_y), (x+delta_snd, text_y)) + builder.circle((x+delta_fst, text_y), r, other_fill) + builder.circle((x+delta_snd, text_y), r, fill) + builder.text((x+delta_snd+2*r, text_y), str(gadget.angle), font_size=font_size) else: for q in gadget.qubits: - y = pad_y + (q + 1) * line_height + y = pad_y + (q+1)*line_height builder.circle((x, y), r, fill) - builder.text((x + r, y - line_height // 3), str(gadget.angle), font_size=font_size) - width = 2 * pad_x + 2 * margin_x + row_width * (2 * max_lvl + 1) // 2 + builder.text((x+r, y-line_height//3), str(gadget.angle), font_size=font_size) + width = 2*pad_x + 2*margin_x + row_width*(2*max_lvl+1)//2 _builder = SVGBuilder(width, height) for q in range(num_qubits): - y = pad_y + (q + 1) * line_height - _builder.line((pad_x, y), (width - pad_x, y)) + y = pad_y + (q+1) * line_height + _builder.line((pad_x, y), (width-pad_x, y)) _builder.text((0, y), f"{str(q):>{num_digits}}", font_size=font_size) - _builder.text((width - pad_x + r, y), f"{str(q):>{num_digits}}", font_size=font_size) + _builder.text((width-pad_x+r, y), f"{str(q):>{num_digits}}", font_size=font_size) _builder >>= builder svg_code = repr(_builder) if svg_code_only: return svg_code try: # pylint: disable = import-outside-toplevel - from IPython.core.display import SVG # type: ignore + from IPython.core.display import SVG # type: ignore except ModuleNotFoundError as e: raise ModuleNotFoundError("You must install the 'IPython' library.") from e return SVG(svg_code) @@ -1200,15 +1174,15 @@ def simplified(self) -> "PhaseCircuit": # Perform all commutations, fusions and pi gadget simplifications # TODO: explain with comments how the jumplist works # jumplist: Dict[int, Optional[Dict[int, int]]] = {} - for i, (basis, angles) in enumerate(groups): # pylint: disable = too-many-nested-blocks + for i, (basis, angles) in enumerate(groups): # pylint: disable = too-many-nested-blocks # Try commuting all gadgets to the left as much as possible for qubits, angle in angles.items(): if angle == 0: # Skip zeroed gadgets continue # Try to commute the gadget to the left as much as possible - j = i # j is the current group to which the gadget has been commuted - obstacle_found = False # this records whether we found an obstacle + j = i # j is the current group to which the gadget has been commuted + obstacle_found = False # this records whether we found an obstacle while not obstacle_found and j >= 2: # if j in jumplist: # j_jump = jumplist[j] @@ -1216,13 +1190,13 @@ def simplified(self) -> "PhaseCircuit": # print("Jump", i, j, j_jump[qubits], bin(qubits)) # j = j_jump[qubits] # break - _, angles_commute = groups[j - 1] # angles to commute through + _, angles_commute = groups[j-1] # angles to commute through for qubits_commute, angle_commute in angles_commute.items(): if angle_commute.is_zero: # Zero angle gadget, not an obstable continue # https://stackoverflow.com/questions/9829578/fast-way-of-counting-non-zero-bits-in-positive-integer - if bin(qubits & qubits_commute).count("1") % 2 != 0: + if bin(qubits&qubits_commute).count("1") % 2 != 0: # Odd number of shared legs, obstacle found obstacle_found = True break @@ -1251,11 +1225,11 @@ def simplified(self) -> "PhaseCircuit": angles_fuse[qubits] = angle if angles_fuse[qubits].is_pi: # This is a pi gadget, further simplification to be performed - angles_fuse[qubits] = Angle.zero # Remove gadget from this group + angles_fuse[qubits] = Angle.zero # Remove gadget from this group pi_gadget = True elif angle.is_pi: # We didn't manage to commute the gadget, but it is a pi gadget - angles[qubits] = Angle.zero # Remove gadget from this group + angles[qubits] = Angle.zero # Remove gadget from this group pi_gadget = True if pi_gadget: # pi gadget @@ -1263,7 +1237,7 @@ def simplified(self) -> "PhaseCircuit": # Commute through gadgets below of other basis, flipping sign if necessary _, angles_k = groups[k] for qubits_k in angles_k: - if bin(qubits_k & qubits).count("1") % 2 == 1: + if bin(qubits_k&qubits).count("1")%2 == 1: # Odd number of legs in comon: flip sign angles_k[qubits_k] *= -1 for q in _int_to_iterator(qubits): @@ -1272,18 +1246,18 @@ def simplified(self) -> "PhaseCircuit": # Create the new list of gadgets new_gadgets: List[PhaseGadget] = [] for q in range(num_qubits): - if pi_gates["Z"][q] % 2 == 1: + if pi_gates["Z"][q]%2 == 1: # Single-qubit pi Z gate new_gadgets.append(PhaseGadget("Z", pi, {q})) for q in range(num_qubits): - if pi_gates["X"][q] % 2 == 1: + if pi_gates["X"][q]%2 == 1: # Single-qubit pi X gate new_gadgets.append(PhaseGadget("X", pi, {q})) for basis, angles in groups: for qubits, angle in angles.items(): if isinstance(angle, Angle): - angle = angle % (2 * pi) - if angle != 0: # skip zero angle gadgets + angle = angle % (2*pi) + if angle != 0: # skip zero angle gadgets new_gadgets.append(PhaseGadget(basis, angle, _int_to_frozenset(qubits))) # Return a new phase circuit. return PhaseCircuit(num_qubits, new_gadgets) @@ -1371,8 +1345,8 @@ def _cx_count(self, topology: Topology, cache: Dict[int, Dict[Tuple[int, ...], i """ # pylint: disable = too-many-locals num_qubits = self._num_qubits - weight = lambda u, v: 4 * topology.dist(u, v) - 2 - inf = 4 * num_qubits - 2 + weight = lambda u, v: 4*topology.dist(u, v)-2 + inf = 4*num_qubits-2 count = 0 for basis in ("Z", "X"): basis = cast(Literal["Z", "X"], basis) @@ -1449,22 +1423,22 @@ def random(num_qubits: int, num_gadgets: int, *, s = parametric parametric = lambda i: AngleVar(f"{s}[{i}]", f"{s}_{i}") rng = np.random.default_rng(seed=rng_seed) - angle_rng_seed = int(rng.integers(65536)) # type: ignore[attr-defined] - basis_idxs = rng.integers(2, size=num_gadgets) # type: ignore[attr-defined] - num_legs = rng.integers(min_legs, max_legs + 1, size=num_gadgets) # type: ignore[attr-defined] + angle_rng_seed = int(rng.integers(65536)) # type: ignore[attr-defined] + basis_idxs = rng.integers(2, size=num_gadgets) # type: ignore[attr-defined] + num_legs = rng.integers(min_legs, max_legs+1, size=num_gadgets) # type: ignore[attr-defined] legs_list: List[npt.NDArray[int]] = [ rng.choice(num_qubits, num_legs[i], replace=False) for i in range(num_gadgets) ] angle_rng = np.random.default_rng(seed=angle_rng_seed) angles: List[Union[Angle, AngleVar]] - angles = [int(x) * pi / angle_subdivision - for x in angle_rng.integers(1, 2 * angle_subdivision, # type: ignore[attr-defined] + angles = [int(x)*pi/angle_subdivision + for x in angle_rng.integers(1, 2*angle_subdivision, # type: ignore[attr-defined] size=num_gadgets)] if parametric is not None: angles = [parametric(i) for i in range(num_gadgets)] bases = cast(Sequence[Literal["Z", "X"]], ("Z", "X")) gadgets: List[PhaseGadget] = [ - PhaseGadget(bases[(basis_idx + i) % 2], + PhaseGadget(bases[(basis_idx+i)%2], angle, [int(x) for x in legs]) for i, (basis_idx, angle, legs) in enumerate(zip(basis_idxs, @@ -1621,7 +1595,7 @@ def from_qasm(qasm: Union[str, QASM], *, raise ValueError(f"Expected {num_params} angles for {gate_name}, " f"found {len(gate_params)}") for qs in gate_qubits: - gadgets += m(*qs, *gate_params) # type: ignore # TODO: fix this! + gadgets += m(*qs, *gate_params) # type: ignore # TODO: fix this! break continue raise ValueError(f"Unsupported QASM statement: {statement}") @@ -1708,7 +1682,7 @@ def to_svg(self, *, return self._circuit.to_svg(zcolor=zcolor, xcolor=xcolor, hscale=hscale, vscale=vscale, scale=scale, - svg_code_only=svg_code_only) # type: ignore[call-overload] + svg_code_only=svg_code_only) # type: ignore[call-overload] def cloned(self) -> PhaseCircuit: """ @@ -1730,4 +1704,4 @@ def _repr_svg_(self) -> Any: Magic method for IPython/Jupyter pretty-printing. See https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html """ - return self._circuit._repr_svg_() # pylint: disable = protected-access + return self._circuit._repr_svg_() # pylint: disable = protected-access From c1dc86ab1898fab1c49e0c65299a003c75ee70ae Mon Sep 17 00:00:00 2001 From: daehiff Date: Sun, 7 May 2023 13:24:19 +0200 Subject: [PATCH 10/30] removed .idea folder from .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8c147a27..25eb619b 100644 --- a/.gitignore +++ b/.gitignore @@ -128,6 +128,5 @@ dmypy.json # Pyre type checker .pyre/ -.idea .vscode .DS_Store From e57363eec2c50c4229b7d2473c1d88a37ee52123 Mon Sep 17 00:00:00 2001 From: daehiff Date: Sun, 7 May 2023 14:06:28 +0200 Subject: [PATCH 11/30] added requirements --- Dockerfile | 0 requirements.txt | 127 ++--------------------------------------------- 2 files changed, 5 insertions(+), 122 deletions(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..e69de29b diff --git a/requirements.txt b/requirements.txt index 49ca5091..57cf7f26 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,122 +1,5 @@ -anyio==3.6.2 -appnope==0.1.3 -argon2-cffi==21.3.0 -argon2-cffi-bindings==21.2.0 -arrow==1.2.3 -asttokens==2.2.1 -attrs==23.1.0 -backcall==0.2.0 -beautifulsoup4==4.12.2 -bleach==6.0.0 -certifi==2022.12.7 -cffi==1.15.1 -charset-normalizer==3.1.0 -comm==0.1.3 -contourpy==1.0.7 -cryptography==40.0.2 -cycler==0.11.0 -debugpy==1.6.7 -decorator==5.1.1 -defusedxml==0.7.1 -dill==0.3.6 -executing==1.2.0 -fastjsonschema==2.16.3 -fonttools==4.39.3 -fqdn==1.5.1 -graphviz==0.20.1 -ibm-cloud-sdk-core==3.16.5 -ibm-platform-services==0.34.0 -idna==3.4 -importlib-metadata==6.5.0 -importlib-resources==5.12.0 -ipykernel==6.22.0 -ipython==8.12.0 -ipython-genutils==0.2.0 -isoduration==20.11.0 -jedi==0.18.2 -Jinja2==3.1.2 -jsonpointer==2.3 -jsonschema==4.17.3 -jupyter-events==0.6.3 -jupyter_client==8.2.0 -jupyter_core==5.3.0 -jupyter_server==2.5.0 -jupyter_server_terminals==0.4.4 -jupyterlab-pygments==0.2.2 -kiwisolver==1.4.4 -lark-parser==0.12.0 -MarkupSafe==2.1.2 -matplotlib==3.7.1 -matplotlib-inline==0.1.6 -mistune==2.0.5 -mpmath==1.3.0 -nbclassic==0.5.5 -nbclient==0.7.3 -nbconvert==7.3.1 -nbformat==5.8.0 -nest-asyncio==1.5.6 -networkx==2.8.8 -notebook==6.5.4 -notebook_shim==0.2.2 -ntlm-auth==1.5.0 -numpy==1.23.5 -packaging==23.1 -pandocfilters==1.5.0 -parso==0.8.3 -pbr==5.11.1 -pexpect==4.8.0 -pickleshare==0.7.5 -Pillow==9.5.0 -platformdirs==3.2.0 -ply==3.11 -prometheus-client==0.16.0 -prompt-toolkit==3.0.38 -psutil==5.9.5 -ptyprocess==0.7.0 -pure-eval==0.2.2 -pycparser==2.21 -Pygments==2.15.1 -PyJWT==2.6.0 -pyparsing==3.0.9 -pyrsistent==0.19.3 -python-dateutil==2.8.2 -python-json-logger==2.0.7 -pytket==1.10.0 -pytket-qiskit==0.33.0 -PyYAML==6.0 -pyzmq==25.0.2 -qiskit==0.39.0 -qiskit-aer==0.11.0 -qiskit-ibm-runtime==0.8.0 -qiskit-ibmq-provider==0.19.2 -qiskit-terra==0.22.0 -qwasm==1.0.1 -requests==2.28.2 -requests-ntlm==1.1.0 -rfc3339-validator==0.1.4 -rfc3986-validator==0.1.1 -rustworkx==0.12.1 -scipy==1.10.1 -Send2Trash==1.8.0 -six==1.16.0 -sniffio==1.3.0 -soupsieve==2.4.1 -stack-data==0.6.2 -stevedore==5.0.0 -stim==1.11.0 -symengine==0.10.0 -sympy==1.11.1 -terminado==0.17.1 -tinycss2==1.2.1 -tornado==6.3 -traitlets==5.9.0 -types-pkg-resources==0.1.3 -typing_extensions==4.5.0 -uri-template==1.2.0 -urllib3==1.26.15 -wcwidth==0.2.6 -webcolors==1.13 -webencodings==0.5.1 -websocket-client==1.3.3 -websockets==11.0.2 -zipp==3.15.0 +networkx==2.6.2 +numpy>=1.21.0 +pytket==1.11.0 +pytket-qiskit==0.34.0 +qiskit==0.39.0 \ No newline at end of file From 0e2c5eb3116b6fc9a650b846a04308325aaa4309 Mon Sep 17 00:00:00 2001 From: daehiff Date: Sun, 7 May 2023 14:12:03 +0200 Subject: [PATCH 12/30] added Dockerfile to make tests reproducable --- Dockerfile | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Dockerfile b/Dockerfile index e69de29b..e2f6f296 100644 --- a/Dockerfile +++ b/Dockerfile @@ -0,0 +1,20 @@ +# Use the official Python 3.9 image as the base image +FROM python:3.9 + +# Set the working directory in the container +WORKDIR /app + +# Copy the requirements to the container +COPY ./requirements.txt . + +# Install the Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the source code to the container +COPY . . + +# Install your library +RUN python setup.py install + +# Execute all the unit tests in the ./tests folder +CMD ["python", "-m", "unittest", "discover", "-s", "./tests/", "-p", "test_*.py"] \ No newline at end of file From 3fd911cc6711e712464ea6751eb271308a616aa2 Mon Sep 17 00:00:00 2001 From: daehiff Date: Sun, 7 May 2023 14:15:35 +0200 Subject: [PATCH 13/30] removed weird gpt-3 comment :D --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e2f6f296..8c513231 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN pip install --no-cache-dir -r requirements.txt # Copy the source code to the container COPY . . -# Install your library +# Install pauliopt RUN python setup.py install # Execute all the unit tests in the ./tests folder From 1fd6efc37cbed81210164f6496871e52f21c385c Mon Sep 17 00:00:00 2001 From: daehiff Date: Sun, 7 May 2023 15:34:17 +0200 Subject: [PATCH 14/30] reverted .gitignore --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 25eb619b..b6e47617 100644 --- a/.gitignore +++ b/.gitignore @@ -127,6 +127,3 @@ dmypy.json # Pyre type checker .pyre/ - -.vscode -.DS_Store From cc2fe616331aae526e5b77db3ccc0d3b0b2cf66a Mon Sep 17 00:00:00 2001 From: daehiff Date: Sun, 7 May 2023 15:35:49 +0200 Subject: [PATCH 15/30] reverted .gitignore #2 --- .gitignore | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/.gitignore b/.gitignore index b6e47617..36215b85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,36 @@ + +Skip to content +Pull requests +Issues +Codespaces +Marketplace +Explore +@daehiff +sg495 / +pauliopt +Public + +Fork your own copy of sg495/pauliopt + +Code +Issues 6 +Pull requests 2 +Actions +Projects +Security + + Insights + +Beta Try the new code view +pauliopt/.gitignore +@y-richie-y +y-richie-y gitignore +Latest commit 21e20e7 Jan 2, 2021 +History +2 contributors +@sg495 +@y-richie-y +133 lines (107 sloc) 1.78 KB # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -127,3 +160,24 @@ dmypy.json # Pyre type checker .pyre/ + + +.vscode +.DS_Store +Footer +© 2023 GitHub, Inc. +Footer navigation + + Terms + Privacy + Security + Status + Docs + Contact GitHub + Pricing + API + Training + Blog + About + +pauliopt/.gitignore at main · sg495/pauliopt From 97050bdef93698b586ae83fee2bc5f3c2fe90d70 Mon Sep 17 00:00:00 2001 From: daehiff Date: Sun, 7 May 2023 15:37:25 +0200 Subject: [PATCH 16/30] reverted .gitignore --- .gitignore | 50 -------------------------------------------------- 1 file changed, 50 deletions(-) diff --git a/.gitignore b/.gitignore index 36215b85..0752b089 100644 --- a/.gitignore +++ b/.gitignore @@ -1,36 +1,3 @@ - -Skip to content -Pull requests -Issues -Codespaces -Marketplace -Explore -@daehiff -sg495 / -pauliopt -Public - -Fork your own copy of sg495/pauliopt - -Code -Issues 6 -Pull requests 2 -Actions -Projects -Security - - Insights - -Beta Try the new code view -pauliopt/.gitignore -@y-richie-y -y-richie-y gitignore -Latest commit 21e20e7 Jan 2, 2021 -History -2 contributors -@sg495 -@y-richie-y -133 lines (107 sloc) 1.78 KB # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -164,20 +131,3 @@ dmypy.json .vscode .DS_Store -Footer -© 2023 GitHub, Inc. -Footer navigation - - Terms - Privacy - Security - Status - Docs - Contact GitHub - Pricing - API - Training - Blog - About - -pauliopt/.gitignore at main · sg495/pauliopt From bd2f73032da39eee1a77c471565562d6ead9c9d6 Mon Sep 17 00:00:00 2001 From: daehiff Date: Mon, 8 May 2023 11:41:39 +0200 Subject: [PATCH 17/30] fixed Clifford typo + import conventsion --- pauliopt/pauli/anneal.py | 21 +++++++++-------- pauliopt/pauli/clifford_gates.py | 24 +++++++++++--------- pauliopt/pauli/clifford_region.py | 36 +----------------------------- pauliopt/pauli/pauli_polynomial.py | 4 ++-- 4 files changed, 28 insertions(+), 57 deletions(-) diff --git a/pauliopt/pauli/anneal.py b/pauliopt/pauli/anneal.py index b4c240fd..15b09a55 100644 --- a/pauliopt/pauli/anneal.py +++ b/pauliopt/pauli/anneal.py @@ -2,11 +2,11 @@ import numpy as np from qiskit import QuantumCircuit -from .clifford_gates import CX, CY, CZ, CliffordGate -from .clifford_region import CliffordRegion -from .pauli_polynomial import PauliPolynomial -from ..phase.optimized_circuits import _validate_temp_schedule -from ..topologies import Topology +from pauliopt.pauli.clifford_gates import CX, CY, CZ, CliffordGate +from pauliopt.pauli.clifford_region import CliffordRegion +from pauliopt.pauli.pauli_polynomial import PauliPolynomial +from pauliopt.phase.optimized_circuits import _validate_temp_schedule +from pauliopt.topologies import Topology def pick_random_gate(num_qubits, G: nx.Graph, gate_set=None): @@ -18,14 +18,17 @@ def pick_random_gate(num_qubits, G: nx.Graph, gate_set=None): return gate.generate_random(num_qubits) -def compute_effect(pp: PauliPolynomial, gate: CliffordGate, topology: Topology, leg_chache=None): +def compute_effect(pp: PauliPolynomial, gate: CliffordGate, topology: Topology, + leg_chache=None): pp_ = pp.copy() pp_.propagate(gate) - return pp_.two_qubit_count(topology, leg_chache=leg_chache) - pp.two_qubit_count(topology, leg_chache=leg_chache) + return pp_.two_qubit_count(topology, leg_chache=leg_chache) - pp.two_qubit_count( + topology, leg_chache=leg_chache) -def anneal(pp: PauliPolynomial, topology, schedule=("geometric", 1.0, 0.1), nr_iterations=100) -> QuantumCircuit: +def anneal(pp: PauliPolynomial, topology, schedule=("geometric", 1.0, 0.1), + nr_iterations=100) -> QuantumCircuit: leg_cache = {} clifford_region = CliffordRegion() @@ -38,7 +41,7 @@ def anneal(pp: PauliPolynomial, topology, schedule=("geometric", 1.0, 0.1), nr_i effect = 2 + compute_effect(pp, gate, topology, leg_chache=leg_cache) accept_step = effect < 0 or random_nrs[it] < np.exp(-np.log(2) * effect / t) if accept_step: - clifford_region.add_gate(gate) # TODO optimize clifford regions + clifford_region.add_gate(gate) # TODO optimize clifford regions pp.propagate(gate) qc = QuantumCircuit(pp.num_qubits) diff --git a/pauliopt/pauli/clifford_gates.py b/pauliopt/pauli/clifford_gates.py index 00f1d761..056cf75f 100644 --- a/pauliopt/pauli/clifford_gates.py +++ b/pauliopt/pauli/clifford_gates.py @@ -1,12 +1,12 @@ from enum import Enum from typing import List, Collection import numpy as np -from .utils import _pauli_to_string, X, Y, Z, I, Pauli +from pauliopt.pauli.utils import _pauli_to_string, X, Y, Z, I, Pauli -from .pauli_gadget import PauliGadget +from pauliopt.pauli.pauli_gadget import PauliGadget -class CLiffordType(Enum): +class CliffordType(Enum): CX = "cx" CY = "cy" CZ = "cz" @@ -72,8 +72,10 @@ def propagate_pauli(self, gadget: PauliGadget): raise Exception(f"{self} has no rules defined for propagation!") pauli_size = len(gadget) if self.control >= pauli_size or self.target >= pauli_size: - raise Exception(f"Control: {self.control} or Target {self.target} out of bounds: {pauli_size}") - p_string = _pauli_to_string(gadget.paulis[self.control]) + _pauli_to_string(gadget.paulis[self.target]) + raise Exception( + f"Control: {self.control} or Target {self.target} out of bounds: {pauli_size}") + p_string = _pauli_to_string(gadget.paulis[self.control]) + _pauli_to_string( + gadget.paulis[self.target]) p_c, p_t, phase_change = self.rules[p_string] gadget.paulis[self.control] = p_c gadget.paulis[self.target] = p_t @@ -104,7 +106,7 @@ class CX(ControlGate): 'II': (I, I, 1)} def __init__(self, control, target): - super().__init__(CLiffordType.CX, control, target) + super().__init__(CliffordType.CX, control, target) def to_qiskit(self): try: @@ -141,7 +143,7 @@ class CZ(ControlGate): 'II': (I, I, 1)} def __init__(self, control, target): - super().__init__(CLiffordType.CZ, control, target) + super().__init__(CliffordType.CZ, control, target) def to_qiskit(self): try: @@ -178,7 +180,7 @@ class CY(ControlGate): 'II': (I, I, 1)} def __init__(self, control, target): - super().__init__(CLiffordType.CY, control, target) + super().__init__(CliffordType.CY, control, target) def to_qiskit(self): try: @@ -204,7 +206,7 @@ class H(SingleQubitGate): 'I': (I, 1)} def __init__(self, qubit): - super().__init__(CLiffordType.H, qubit) + super().__init__(CliffordType.H, qubit) @staticmethod def generate_random(num_qubits): @@ -228,7 +230,7 @@ class S(SingleQubitGate): 'I': (I, 1)} def __init__(self, qubit): - super().__init__(CLiffordType.S, qubit) + super().__init__(CliffordType.S, qubit) @staticmethod def generate_random(num_qubits): @@ -253,7 +255,7 @@ class V(SingleQubitGate): 'I': (I, 1)} def __init__(self, qubit): - super().__init__(CLiffordType.V, qubit) + super().__init__(CliffordType.V, qubit) @staticmethod def generate_random(num_qubits): diff --git a/pauliopt/pauli/clifford_region.py b/pauliopt/pauli/clifford_region.py index 79289c70..76939ffc 100644 --- a/pauliopt/pauli/clifford_region.py +++ b/pauliopt/pauli/clifford_region.py @@ -1,43 +1,9 @@ from qiskit import QuantumCircuit -from .clifford_gates import * -import qiskit.quantum_info as qi +from pauliopt.pauli.clifford_gates import * class CliffordRegion: - commutation_rule_set = {('V', 1, 0, 'H', 0, 1), - ('H', 0, 1, 'S', 1, 0), - ('S', 1, 0, 'CX', 1, 0), - ('CX', 0, 1, 'S', 0, 1), - ('H', 1, 0, 'V', 0, 1), - ('S', 0, 1, 'CX', 0, 1), - ('S', 1, 0, 'CZ', 0, 1), - ('S', 1, 0, 'CZ', 1, 0), - ('S', 0, 1, 'H', 1, 0), - ('H', 0, 1, 'V', 1, 0), - ('CX', 0, 1, 'V', 1, 0), - ('V', 0, 1, 'H', 1, 0), - ('S', 1, 0, 'H', 0, 1), - ('V', 1, 0, 'CX', 0, 1), - ('CX', 1, 0, 'V', 0, 1), - ('S', 0, 1, 'V', 1, 0), - ('H', 1, 0, 'S', 0, 1), - ('S', 0, 1, 'CY', 0, 1), - ('CX', 1, 0, 'S', 1, 0), - ('S', 1, 0, 'CY', 1, 0), - ('CZ', 1, 0, 'S', 1, 0), - ('V', 0, 1, 'CX', 1, 0), - ('S', 0, 1, 'CZ', 1, 0), - ('CY', 1, 0, 'S', 1, 0), - ('S', 1, 0, 'V', 0, 1), - ('V', 0, 1, 'S', 1, 0), - ('CY', 0, 1, 'S', 0, 1), - ('CZ', 0, 1, 'S', 0, 1), - ('S', 0, 1, 'CZ', 0, 1), - ('CZ', 0, 1, 'S', 1, 0), - ('CZ', 1, 0, 'S', 0, 1), - ('V', 1, 0, 'S', 0, 1)} - def __init__(self, gates=None): if gates is None: gates = [] diff --git a/pauliopt/pauli/pauli_polynomial.py b/pauliopt/pauli/pauli_polynomial.py index a1611679..186771c8 100644 --- a/pauliopt/pauli/pauli_polynomial.py +++ b/pauliopt/pauli/pauli_polynomial.py @@ -1,5 +1,5 @@ -from .clifford_gates import CliffordGate -from .pauli_gadget import PauliGadget +from pauliopt.pauli.clifford_gates import CliffordGate +from pauliopt.pauli.pauli_gadget import PauliGadget from qiskit import QuantumCircuit From 21e6ac0c82953446658c0d2c96942e2261a9e6e3 Mon Sep 17 00:00:00 2001 From: daehiff Date: Mon, 8 May 2023 11:49:26 +0200 Subject: [PATCH 18/30] Update pauliopt/pauli/pauli_polynomial.py Co-authored-by: Richie Yeung --- pauliopt/pauli/pauli_polynomial.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pauliopt/pauli/pauli_polynomial.py b/pauliopt/pauli/pauli_polynomial.py index 186771c8..cc91702c 100644 --- a/pauliopt/pauli/pauli_polynomial.py +++ b/pauliopt/pauli/pauli_polynomial.py @@ -20,10 +20,7 @@ def __rshift__(self, pauli_polynomial): return self def __repr__(self): - rep_ = "" - for gadet in self.pauli_gadgets: - rep_ += str(gadet) + "\n" - return rep_ + return '\n'.join(map(repr, self.pauli_gadgets)) def __len__(self): return self.size From 6daa9f1d216c4078fe98347ee648164035672459 Mon Sep 17 00:00:00 2001 From: daehiff Date: Mon, 8 May 2023 11:54:53 +0200 Subject: [PATCH 19/30] changed repr --- pauliopt/pauli/clifford_gates.py | 33 ++++++++++++++++---------------- pauliopt/pauli/pauli_gadget.py | 8 ++++---- pauliopt/pauli/utils.py | 21 ++++---------------- tests/test_pauli_propagation.py | 9 ++++++--- 4 files changed, 30 insertions(+), 41 deletions(-) diff --git a/pauliopt/pauli/clifford_gates.py b/pauliopt/pauli/clifford_gates.py index 056cf75f..9ee3306e 100644 --- a/pauliopt/pauli/clifford_gates.py +++ b/pauliopt/pauli/clifford_gates.py @@ -1,9 +1,10 @@ +from abc import ABC, abstractmethod from enum import Enum -from typing import List, Collection + import numpy as np -from pauliopt.pauli.utils import _pauli_to_string, X, Y, Z, I, Pauli from pauliopt.pauli.pauli_gadget import PauliGadget +from pauliopt.pauli.utils import X, Y, Z, I class CliffordType(Enum): @@ -15,30 +16,30 @@ class CliffordType(Enum): V = "v" -class CliffordGate: +class CliffordGate(ABC): def __init__(self, c_type): self.c_type = c_type + @abstractmethod def propagate_pauli(self, gadget: PauliGadget): - raise Exception(f"This method is not implemented on object: {self}") + ... + @abstractmethod def to_qiskit(self): - raise Exception(f"This method is not implemented on object: {self}") + ... @property + @abstractmethod def num_qubits(self): - raise Exception(f"This property is not implemented on object: {self}") + ... @staticmethod + @abstractmethod def generate_random(num_qubits): - raise Exception(f"This method is not implemented") - - @property - def to_hash(self): - raise Exception(f"This property is not implemented on object: {self}") + ... -class SingleQubitGate(CliffordGate): +class SingleQubitGate(CliffordGate, ABC): rules = None def __init__(self, type, qubit): @@ -48,7 +49,7 @@ def __init__(self, type, qubit): def propagate_pauli(self, gadget: PauliGadget): if self.rules is None: raise Exception(f"{self} has no rules defined for propagation!") - p_string = _pauli_to_string(gadget.paulis[self.qubit]) + p_string = gadget.paulis[self.qubit].value new_p, phase_change = self.rules[p_string] gadget.paulis[self.qubit] = new_p gadget.angle *= phase_change @@ -59,7 +60,7 @@ def num_qubits(self): return self.qubit + 1 -class ControlGate(CliffordGate): +class ControlGate(CliffordGate, ABC): rules = None def __init__(self, type, control, target): @@ -74,8 +75,7 @@ def propagate_pauli(self, gadget: PauliGadget): if self.control >= pauli_size or self.target >= pauli_size: raise Exception( f"Control: {self.control} or Target {self.target} out of bounds: {pauli_size}") - p_string = _pauli_to_string(gadget.paulis[self.control]) + _pauli_to_string( - gadget.paulis[self.target]) + p_string = gadget.paulis[self.control].value + gadget.paulis[self.target].value p_c, p_t, phase_change = self.rules[p_string] gadget.paulis[self.control] = p_c gadget.paulis[self.target] = p_t @@ -198,7 +198,6 @@ def generate_random(num_qubits): return CY(control, target) -# class H(SingleQubitGate): rules = {'X': (Z, 1), 'Y': (Y, -1), diff --git a/pauliopt/pauli/pauli_gadget.py b/pauliopt/pauli/pauli_gadget.py index 328a19ec..648da1bb 100644 --- a/pauliopt/pauli/pauli_gadget.py +++ b/pauliopt/pauli/pauli_gadget.py @@ -1,13 +1,13 @@ from collections import deque from typing import List +import networkx as nx +import numpy as np from qiskit import QuantumCircuit -from pauliopt.pauli.utils import _pauli_to_string, Pauli, X, Y, Z, I +from pauliopt.pauli.utils import Pauli, X, Y, Z, I from pauliopt.topologies import Topology from pauliopt.utils import AngleExpr -import numpy as np -import networkx as nx def decompose_cnot_ladder_z(ctrl: int, trg: int, arch: Topology): @@ -76,7 +76,7 @@ def __len__(self): return len(self.paulis) def __repr__(self): - return f"({self.angle}) @ {{ {', '.join([_pauli_to_string(pauli) for pauli in self.paulis])} }}" + return f"({self.angle}) @ {{ {', '.join([pauli.value for pauli in self.paulis])} }}" def copy(self): return PauliGadget(self.angle, self.paulis.copy()) diff --git a/pauliopt/pauli/utils.py b/pauliopt/pauli/utils.py index d3d17c14..01d990d9 100644 --- a/pauliopt/pauli/utils.py +++ b/pauliopt/pauli/utils.py @@ -2,26 +2,13 @@ class Pauli(Enum): - I = 0 - X = 1 - Y = 2 - Z = 3 + I = "I" + X = "X" + Y = "Y" + Z = "Z" I = Pauli.I X = Pauli.X Y = Pauli.Y Z = Pauli.Z - - -def _pauli_to_string(pauli: Pauli): - if pauli == Pauli.I: - return "I" - elif pauli == Pauli.X: - return "X" - elif pauli == Pauli.Y: - return "Y" - elif pauli == Pauli.Z: - return "Z" - else: - raise Exception(f"{pauli} is not a Paulimatrix") diff --git a/tests/test_pauli_propagation.py b/tests/test_pauli_propagation.py index 554ab259..16c5020a 100644 --- a/tests/test_pauli_propagation.py +++ b/tests/test_pauli_propagation.py @@ -38,8 +38,10 @@ def tket_to_qiskit(circuit: pytket.Circuit) -> QuantumCircuit: 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_pauli(p) for p in gadget.paulis], gadget.angle / np.pi), - list(range(pp.num_qubits))) + circuit.add_pauliexpbox( + PauliExpBox([pauli_to_tket_pauli(p) for p in gadget.paulis], + gadget.angle / np.pi), + list(range(pp.num_qubits))) Transform.DecomposeBoxes().apply(circuit) return tket_to_qiskit(circuit) @@ -113,4 +115,5 @@ def test_gate_propagation(self): qc.compose(pp_.to_qiskit(), inplace=True) qc.compose(gate.to_qiskit(), inplace=True) # print(qc) - self.assertTrue(verify_equality(inital_qc, qc), "The resulting Quantum Circuits were not equivalent") + self.assertTrue(verify_equality(inital_qc, qc), + "The resulting Quantum Circuits were not equivalent") From 658c0156758054ea68f85e30778d00eabbc7e742 Mon Sep 17 00:00:00 2001 From: daehiff Date: Mon, 8 May 2023 11:59:36 +0200 Subject: [PATCH 20/30] Update tests/test_pauli_propagation.py Co-authored-by: Richie Yeung --- tests/test_pauli_propagation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_pauli_propagation.py b/tests/test_pauli_propagation.py index 16c5020a..b4433356 100644 --- a/tests/test_pauli_propagation.py +++ b/tests/test_pauli_propagation.py @@ -114,6 +114,5 @@ def test_gate_propagation(self): qc.compose(gate.to_qiskit().inverse(), inplace=True) qc.compose(pp_.to_qiskit(), inplace=True) qc.compose(gate.to_qiskit(), inplace=True) - # print(qc) self.assertTrue(verify_equality(inital_qc, qc), "The resulting Quantum Circuits were not equivalent") From 6dcceb473417cc8c7ba0e2444656023db8aea9ce Mon Sep 17 00:00:00 2001 From: daehiff Date: Mon, 8 May 2023 12:01:11 +0200 Subject: [PATCH 21/30] added unit test --- tests/test_pauli_representation.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/test_pauli_representation.py diff --git a/tests/test_pauli_representation.py b/tests/test_pauli_representation.py new file mode 100644 index 00000000..9aa58f33 --- /dev/null +++ b/tests/test_pauli_representation.py @@ -0,0 +1,22 @@ +import unittest +import numpy as np + +from pauliopt.pauli.pauli_gadget import PPhase +from pauliopt.pauli.pauli_polynomial import PauliPolynomial +from pauliopt.pauli.utils import * + +_PAULI_REPR = "(0.5) @ { I, X, Y, Z }\n(0.25) @ { X, X, Y, X }" + + +class TestPauliConversion(unittest.TestCase): + def test_circuit_construction(self): + pp = PauliPolynomial() + + pp >>= PPhase(0.5) @ [I, X, Y, Z] + + self.assertEqual(pp.num_qubits, 4) + self.assertEqual(pp.size, 1) + + pp >>= PPhase(0.25) @ [X, X, Y, X] + + self.assertEqual(pp.__repr__(), _PAULI_REPR) From b086dda9ac5754c0be6bf143f0e0b9839e127bb9 Mon Sep 17 00:00:00 2001 From: daehiff Date: Mon, 8 May 2023 12:10:49 +0200 Subject: [PATCH 22/30] Update pauliopt/pauli/pauli_polynomial.py Co-authored-by: Richie Yeung --- pauliopt/pauli/pauli_polynomial.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pauliopt/pauli/pauli_polynomial.py b/pauliopt/pauli/pauli_polynomial.py index cc91702c..be493786 100644 --- a/pauliopt/pauli/pauli_polynomial.py +++ b/pauliopt/pauli/pauli_polynomial.py @@ -56,10 +56,10 @@ def copy(self): pp_ >>= gadget.copy() return pp_ - def two_qubit_count(self, topology, leg_chache=None): - if leg_chache is None: - leg_chache = {} + def two_qubit_count(self, topology, leg_cache=None): + if leg_cache is None: + leg_cache = {} count = 0 for gadget in self.pauli_gadgets: - count += gadget.two_qubit_count(topology, leg_chache=leg_chache) + count += gadget.two_qubit_count(topology, leg_cache=leg_cache) return count From 9a1ffddea712f4731f42bf1c66a66e7969f171f7 Mon Sep 17 00:00:00 2001 From: daehiff Date: Mon, 8 May 2023 12:13:33 +0200 Subject: [PATCH 23/30] fixed cache typo --- pauliopt/pauli/anneal.py | 8 ++++---- pauliopt/pauli/pauli_gadget.py | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pauliopt/pauli/anneal.py b/pauliopt/pauli/anneal.py index 15b09a55..4440b3f3 100644 --- a/pauliopt/pauli/anneal.py +++ b/pauliopt/pauli/anneal.py @@ -19,12 +19,12 @@ def pick_random_gate(num_qubits, G: nx.Graph, gate_set=None): def compute_effect(pp: PauliPolynomial, gate: CliffordGate, topology: Topology, - leg_chache=None): + leg_cache=None): pp_ = pp.copy() pp_.propagate(gate) - return pp_.two_qubit_count(topology, leg_chache=leg_chache) - pp.two_qubit_count( - topology, leg_chache=leg_chache) + return pp_.two_qubit_count(topology, leg_cache=leg_cache) - pp.two_qubit_count( + topology, leg_cache=leg_cache) def anneal(pp: PauliPolynomial, topology, schedule=("geometric", 1.0, 0.1), @@ -38,7 +38,7 @@ def anneal(pp: PauliPolynomial, topology, schedule=("geometric", 1.0, 0.1), for it in range(nr_iterations): t = schedule(it, nr_iterations) gate = pick_random_gate(num_qubits, topology.to_nx) - effect = 2 + compute_effect(pp, gate, topology, leg_chache=leg_cache) + effect = 2 + compute_effect(pp, gate, topology, leg_cache=leg_cache) accept_step = effect < 0 or random_nrs[it] < np.exp(-np.log(2) * effect / t) if accept_step: clifford_region.add_gate(gate) # TODO optimize clifford regions diff --git a/pauliopt/pauli/pauli_gadget.py b/pauliopt/pauli/pauli_gadget.py index 648da1bb..9d75c296 100644 --- a/pauliopt/pauli/pauli_gadget.py +++ b/pauliopt/pauli/pauli_gadget.py @@ -81,18 +81,18 @@ def __repr__(self): def copy(self): return PauliGadget(self.angle, self.paulis.copy()) - def two_qubit_count(self, topology, leg_chache=None): - if leg_chache is None: - leg_chache = {} + def two_qubit_count(self, topology, leg_cache=None): + if leg_cache is None: + leg_cache = {} column = np.asarray(self.paulis) col_binary = np.where(column == Pauli.I, 0, 1) col_id = "".join([str(int(el)) for el in col_binary]) - if col_id in leg_chache.keys(): - return leg_chache[col_id] + if col_id in leg_cache.keys(): + return leg_cache[col_id] else: cnot_amount = len(find_minimal_cx_assignment(col_binary, topology)[0]) - leg_chache[col_id] = cnot_amount + leg_cache[col_id] = cnot_amount return cnot_amount def to_qiskit(self, topology=None): From abcb9da1b422a0a6aad20f22f247abf1611e1d09 Mon Sep 17 00:00:00 2001 From: daehiff Date: Mon, 8 May 2023 12:17:08 +0200 Subject: [PATCH 24/30] fixed dict in tests --- tests/test_pauli_propagation.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/tests/test_pauli_propagation.py b/tests/test_pauli_propagation.py index b4433356..e8b1f617 100644 --- a/tests/test_pauli_propagation.py +++ b/tests/test_pauli_propagation.py @@ -17,18 +17,12 @@ from pauliopt.topologies import Topology - -def pauli_to_tket_pauli(pauli): - if pauli == X: - return Pauli.X - elif pauli == Y: - return Pauli.Y - elif pauli == Z: - return Pauli.Z - elif pauli == I: - return Pauli.I - else: - raise Exception("Unknown Pauli Matrix") +PAULI_TO_TKET = { + X: Pauli.X, + Y: Pauli.Y, + Z: Pauli.Z, + I: Pauli.I +} def tket_to_qiskit(circuit: pytket.Circuit) -> QuantumCircuit: @@ -39,7 +33,7 @@ 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_pauli(p) for p in gadget.paulis], + PauliExpBox([PAULI_TO_TKET[p] for p in gadget.paulis], gadget.angle / np.pi), list(range(pp.num_qubits))) Transform.DecomposeBoxes().apply(circuit) From 169f6b77b912015ef8b101fe3946628744dab59f Mon Sep 17 00:00:00 2001 From: daehiff Date: Mon, 8 May 2023 14:39:21 +0200 Subject: [PATCH 25/30] Changed PauliPolynomials to have fixed qubits, adjusted qiskit dependencies in methods, added unit tests for first annealing draft --- pauliopt/pauli/anneal.py | 21 ++-- pauliopt/pauli/clifford_gates.py | 165 +++++++++-------------------- pauliopt/pauli/clifford_region.py | 24 +++-- pauliopt/pauli/pauli_gadget.py | 9 +- pauliopt/pauli/pauli_polynomial.py | 23 ++-- pauliopt/pauli/utils.py | 3 + setup.py | 11 +- tests/test_pauli_annealing.py | 103 ++++++++++++++++++ tests/test_pauli_propagation.py | 29 +++-- tests/test_pauli_representation.py | 11 +- 10 files changed, 235 insertions(+), 164 deletions(-) create mode 100644 tests/test_pauli_annealing.py diff --git a/pauliopt/pauli/anneal.py b/pauliopt/pauli/anneal.py index 4440b3f3..4868947c 100644 --- a/pauliopt/pauli/anneal.py +++ b/pauliopt/pauli/anneal.py @@ -1,21 +1,19 @@ -import networkx as nx import numpy as np -from qiskit import QuantumCircuit -from pauliopt.pauli.clifford_gates import CX, CY, CZ, CliffordGate +from pauliopt.pauli.clifford_gates import CliffordGate, CliffordType, generate_random_clifford from pauliopt.pauli.clifford_region import CliffordRegion from pauliopt.pauli.pauli_polynomial import PauliPolynomial from pauliopt.phase.optimized_circuits import _validate_temp_schedule from pauliopt.topologies import Topology -def pick_random_gate(num_qubits, G: nx.Graph, gate_set=None): +def pick_random_gate(num_qubits, gate_set=None): if gate_set is None: - gate_set = [CX, CY, CZ] + gate_set = [CliffordType.CX, CliffordType.CY, CliffordType.CZ] gate = np.random.choice(gate_set) - return gate.generate_random(num_qubits) + return generate_random_clifford(gate, num_qubits) def compute_effect(pp: PauliPolynomial, gate: CliffordGate, topology: Topology, @@ -28,21 +26,26 @@ def compute_effect(pp: PauliPolynomial, gate: CliffordGate, topology: Topology, def anneal(pp: PauliPolynomial, topology, schedule=("geometric", 1.0, 0.1), - nr_iterations=100) -> QuantumCircuit: + nr_iterations=100): leg_cache = {} - clifford_region = CliffordRegion() + clifford_region = CliffordRegion(pp.num_qubits) schedule = _validate_temp_schedule(schedule) random_nrs = np.random.uniform(0.0, 1.0, size=(nr_iterations,)) num_qubits = pp.num_qubits for it in range(nr_iterations): t = schedule(it, nr_iterations) - gate = pick_random_gate(num_qubits, topology.to_nx) + gate = pick_random_gate(num_qubits) effect = 2 + compute_effect(pp, gate, topology, leg_cache=leg_cache) accept_step = effect < 0 or random_nrs[it] < np.exp(-np.log(2) * effect / t) if accept_step: clifford_region.add_gate(gate) # TODO optimize clifford regions pp.propagate(gate) + try: + from qiskit import QuantumCircuit + + except: + raise Exception("Please install qiskit to export the circuit") qc = QuantumCircuit(pp.num_qubits) qc.compose(clifford_region.to_qiskit(), inplace=True) # TODO route on architecture diff --git a/pauliopt/pauli/clifford_gates.py b/pauliopt/pauli/clifford_gates.py index 9ee3306e..dc53ab7b 100644 --- a/pauliopt/pauli/clifford_gates.py +++ b/pauliopt/pauli/clifford_gates.py @@ -1,10 +1,9 @@ from abc import ABC, abstractmethod from enum import Enum -import numpy as np - from pauliopt.pauli.pauli_gadget import PauliGadget from pauliopt.pauli.utils import X, Y, Z, I +import numpy as np class CliffordType(Enum): @@ -24,20 +23,6 @@ def __init__(self, c_type): def propagate_pauli(self, gadget: PauliGadget): ... - @abstractmethod - def to_qiskit(self): - ... - - @property - @abstractmethod - def num_qubits(self): - ... - - @staticmethod - @abstractmethod - def generate_random(num_qubits): - ... - class SingleQubitGate(CliffordGate, ABC): rules = None @@ -55,10 +40,6 @@ def propagate_pauli(self, gadget: PauliGadget): gadget.angle *= phase_change return gadget - @property - def num_qubits(self): - return self.qubit + 1 - class ControlGate(CliffordGate, ABC): rules = None @@ -82,10 +63,6 @@ def propagate_pauli(self, gadget: PauliGadget): gadget.angle *= phase_change return gadget - @property - def num_qubits(self): - return max(self.control, self.target) + 1 - class CX(ControlGate): rules = {'XX': (X, I, 1), @@ -108,21 +85,6 @@ class CX(ControlGate): def __init__(self, control, target): super().__init__(CliffordType.CX, control, target) - def to_qiskit(self): - try: - from qiskit import QuantumCircuit - except: - raise Exception("Please install qiskit to export Clifford Gates") - qc = QuantumCircuit(self.num_qubits) - qc.cx(self.control, self.target) - return qc - - @staticmethod - def generate_random(num_qubits): - control = np.random.choice(list(range(num_qubits))) - target = np.random.choice([i for i in range(num_qubits) if i != control]) - return CX(control, target) - class CZ(ControlGate): rules = {'XX': (Y, Y, 1), @@ -145,21 +107,6 @@ class CZ(ControlGate): def __init__(self, control, target): super().__init__(CliffordType.CZ, control, target) - def to_qiskit(self): - try: - from qiskit import QuantumCircuit - except: - raise Exception("Please install qiskit to export Clifford Gates") - qc = QuantumCircuit(self.num_qubits) - qc.cz(self.control, self.target) - return qc - - @staticmethod - def generate_random(num_qubits): - control = np.random.choice(list(range(num_qubits))) - target = np.random.choice([i for i in range(num_qubits) if i != control]) - return CZ(control, target) - class CY(ControlGate): rules = {'XX': (Y, Z, -1), @@ -182,21 +129,6 @@ class CY(ControlGate): def __init__(self, control, target): super().__init__(CliffordType.CY, control, target) - def to_qiskit(self): - try: - from qiskit import QuantumCircuit - except: - raise Exception("Please install qiskit to export Clifford Gates") - qc = QuantumCircuit(self.num_qubits) - qc.cy(self.control, self.target) - return qc - - @staticmethod - def generate_random(num_qubits): - control = np.random.choice(list(range(num_qubits))) - target = np.random.choice([i for i in range(num_qubits) if i != control]) - return CY(control, target) - class H(SingleQubitGate): rules = {'X': (Z, 1), @@ -207,20 +139,6 @@ class H(SingleQubitGate): def __init__(self, qubit): super().__init__(CliffordType.H, qubit) - @staticmethod - def generate_random(num_qubits): - qubit = np.random.choice(list(range(num_qubits))) - return H(qubit) - - def to_qiskit(self): - try: - from qiskit import QuantumCircuit - except: - raise Exception("Please install qiskit to export Clifford Gates") - qc = QuantumCircuit(self.num_qubits) - qc.h(self.qubit) - return qc - class S(SingleQubitGate): rules = {'X': (Y, -1), @@ -231,21 +149,6 @@ class S(SingleQubitGate): def __init__(self, qubit): super().__init__(CliffordType.S, qubit) - @staticmethod - def generate_random(num_qubits): - qubit = np.random.choice(list(range(num_qubits))) - - return S(qubit) - - def to_qiskit(self): - try: - from qiskit import QuantumCircuit - except: - raise Exception("Please install qiskit to export Clifford Gates") - qc = QuantumCircuit(self.num_qubits) - qc.s(self.qubit) - return qc - class V(SingleQubitGate): rules = {'X': (X, 1), @@ -256,20 +159,58 @@ class V(SingleQubitGate): def __init__(self, qubit): super().__init__(CliffordType.V, qubit) - @staticmethod - def generate_random(num_qubits): - qubit = np.random.choice(list(range(num_qubits))) - - return V(qubit) - - def to_qiskit(self): - try: - from qiskit import QuantumCircuit - except: - raise Exception("Please install qiskit to export Clifford Gates") - qc = QuantumCircuit(self.num_qubits) - qc.sx(self.qubit) - return qc # For the gates X, Y, Z there won't be a change of Pauli matrices # Refused to implement "higher order gates" like NCX, SWAP, DCX, ... but with this structure this can easily be done + + +def generate_random_clifford(c_type: CliffordType, n_qubits: int): + qubit = np.random.choice(list(range(n_qubits))) + if c_type == CliffordType.CX: + control = np.random.choice([i for i in range(n_qubits) if i != qubit]) + return CX(control, qubit) + elif c_type == CliffordType.CY: + control = np.random.choice([i for i in range(n_qubits) if i != qubit]) + return CY(control, qubit) + elif c_type == CliffordType.CZ: + control = np.random.choice([i for i in range(n_qubits) if i != qubit]) + return CZ(control, qubit) + elif c_type == CliffordType.H: + return H(qubit) + elif c_type == CliffordType.S: + return S(qubit) + elif c_type == CliffordType.V: + return V(qubit) + else: + raise TypeError(f"Unknown Clifford Type: {c_type}") + + +def clifford_to_qiskit(clifford: CliffordGate): + try: + from qiskit import QuantumCircuit + except: + raise Exception("Please install qiskit to export Clifford Gates") + + if isinstance(clifford, ControlGate): + qc = QuantumCircuit(max(clifford.control, clifford.target) + 1) + if clifford.c_type == CliffordType.CX: + qc.cx(clifford.control, clifford.target) + elif clifford.c_type == CliffordType.CY: + qc.cy(clifford.control, clifford.target) + elif clifford.c_type == CliffordType.CZ: + qc.cz(clifford.control, clifford.target) + else: + raise TypeError(f"Undefined Control gate {clifford.c_type}") + elif isinstance(clifford, SingleQubitGate): + qc = QuantumCircuit(clifford.qubit + 1) + if clifford.c_type == CliffordType.H: + qc.h(clifford.qubit) + elif clifford.c_type == CliffordType.S: + qc.s(clifford.qubit) + elif clifford.c_type == CliffordType.V: + qc.sx(clifford.qubit) + else: + raise TypeError(f"Undefined Single qubit gate: {clifford.c_type}") + else: + raise TypeError(f"Gate must be either single qubit or control") + return qc diff --git a/pauliopt/pauli/clifford_region.py b/pauliopt/pauli/clifford_region.py index 76939ffc..a73e1c83 100644 --- a/pauliopt/pauli/clifford_region.py +++ b/pauliopt/pauli/clifford_region.py @@ -1,26 +1,28 @@ -from qiskit import QuantumCircuit - from pauliopt.pauli.clifford_gates import * class CliffordRegion: - def __init__(self, gates=None): + def __init__(self, num_qubits, gates=None): if gates is None: gates = [] self.gates: [CliffordGate] = gates - self.num_qubits = 1 + self.num_qubits = num_qubits def add_gate(self, gate: CliffordGate): - self.num_qubits = max(self.num_qubits, gate.num_qubits) + if isinstance(gate, SingleQubitGate) and gate.qubit >= self.num_qubits: + raise Exception( + f"Gate with {gate.qubit} is out of bounds for Clifford Region with Qubits: {self.num_qubits}") + if isinstance(gate, ControlGate) and gate.control >= self.num_qubits and gate.target >= self.num_qubits: + raise Exception( + f"Control Gate with {gate.control}, {gate.target} is out of bounds for Clifford Region with Qubits: {self.num_qubits}") self.gates.append(gate) - def add_gate_simplify(self, gate: CliffordGate): - for idx, other in enumerate(self.gates): - pass - def to_qiskit(self): + try: + from qiskit import QuantumCircuit + except: + raise Exception("Please install qiskit to export Clifford Regions") qc = QuantumCircuit(self.num_qubits) for gate in self.gates: - qc.compose(gate.to_qiskit(), inplace=True) - + qc.compose(clifford_to_qiskit(gate), inplace=True) return qc diff --git a/pauliopt/pauli/pauli_gadget.py b/pauliopt/pauli/pauli_gadget.py index 9d75c296..fa304361 100644 --- a/pauliopt/pauli/pauli_gadget.py +++ b/pauliopt/pauli/pauli_gadget.py @@ -3,7 +3,6 @@ import networkx as nx import numpy as np -from qiskit import QuantumCircuit from pauliopt.pauli.utils import Pauli, X, Y, Z, I from pauliopt.topologies import Topology @@ -36,7 +35,6 @@ def find_minimal_cx_assignment(column: np.array, arch: Topology): if column[i] != 0 and column[j] != 0 and i != j: G.add_edge(i, j, weight=4 * arch.dist(i, j) - 2) - # Algorithm by Gogioso et. al. (https://arxiv.org/pdf/2206.11839.pdf) to find qubit assignment with MST mst_branches = list(nx.minimum_spanning_edges(G, data=False, algorithm="prim")) incident = {q: set() for q in range(len(column))} for fst, snd in mst_branches: @@ -91,7 +89,7 @@ def two_qubit_count(self, topology, leg_cache=None): if col_id in leg_cache.keys(): return leg_cache[col_id] else: - cnot_amount = len(find_minimal_cx_assignment(col_binary, topology)[0]) + cnot_amount = 2 * len(find_minimal_cx_assignment(col_binary, topology)[0]) leg_cache[col_id] = cnot_amount return cnot_amount @@ -99,7 +97,10 @@ def to_qiskit(self, topology=None): num_qubits = len(self.paulis) if topology is None: topology = Topology.complete(num_qubits) - + try: + from qiskit import QuantumCircuit + except: + raise Exception("Please install qiskit to export Clifford Regions") circ = QuantumCircuit(num_qubits) column = np.asarray(self.paulis) diff --git a/pauliopt/pauli/pauli_polynomial.py b/pauliopt/pauli/pauli_polynomial.py index be493786..37120094 100644 --- a/pauliopt/pauli/pauli_polynomial.py +++ b/pauliopt/pauli/pauli_polynomial.py @@ -1,16 +1,17 @@ from pauliopt.pauli.clifford_gates import CliffordGate from pauliopt.pauli.pauli_gadget import PauliGadget -from qiskit import QuantumCircuit - from pauliopt.topologies import Topology class PauliPolynomial: - def __init__(self): + def __init__(self, num_qubits): + self.num_qubits = num_qubits self.pauli_gadgets = [] def __irshift__(self, gadget: PauliGadget): + if not len(gadget) == self.num_qubits: + raise Exception(f"Pauli Polynomial has {self.num_qubits}, but Pauli gadget has: {len(gadget)}") self.pauli_gadgets.append(gadget) return self @@ -23,20 +24,16 @@ def __repr__(self): return '\n'.join(map(repr, self.pauli_gadgets)) def __len__(self): - return self.size - - @property - def size(self): return len(self.pauli_gadgets) - @property - def num_qubits(self): - return max([len(gadget) for gadget in self.pauli_gadgets]) - def to_qiskit(self, topology=None): num_qubits = self.num_qubits if topology is None: topology = Topology.complete(num_qubits) + try: + from qiskit import QuantumCircuit + except: + raise Exception("Please install qiskit to export Clifford Regions") qc = QuantumCircuit(num_qubits) for gadget in self.pauli_gadgets: @@ -45,13 +42,13 @@ def to_qiskit(self, topology=None): return qc def propagate(self, gate: CliffordGate): - pp_ = PauliPolynomial() + pp_ = PauliPolynomial(self.num_qubits) for gadget in self.pauli_gadgets: pp_ >>= gate.propagate_pauli(gadget) return pp_ def copy(self): - pp_ = PauliPolynomial() + pp_ = PauliPolynomial(self.num_qubits) for gadget in self.pauli_gadgets: pp_ >>= gadget.copy() return pp_ diff --git a/pauliopt/pauli/utils.py b/pauliopt/pauli/utils.py index 01d990d9..7145d099 100644 --- a/pauliopt/pauli/utils.py +++ b/pauliopt/pauli/utils.py @@ -12,3 +12,6 @@ class Pauli(Enum): X = Pauli.X Y = Pauli.Y Z = Pauli.Z + + + diff --git a/setup.py b/setup.py index 09d01318..c545722c 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ """ setup.py created according to https://packaging.python.org/tutorials/packaging-projects """ -import setuptools #type:ignore +import setuptools # type:ignore setuptools.setup( name="pauliopt", @@ -9,8 +9,8 @@ maintainer_email="sg495@users.noreply.github.com", description="A Python library to simplify quantum circuits of Pauli gadgets.", url="https://github.com/sg495/pauliopt", - packages=setuptools.find_packages(exclude=["test"]), - classifiers=[ # see https://pypi.org/classifiers/ + packages=setuptools.find_packages(exclude=["test", "tests"]), + classifiers=[ # see https://pypi.org/classifiers/ "Programming Language :: Python :: 3.8", "Operating System :: OS Independent", "Development Status :: 2 - Pre-Alpha", @@ -19,9 +19,10 @@ ], package_data={"": [], "pauliopt": ["pauliopt/py.typed"], - }, + }, include_package_data=True, install_requires=[ - "numpy", + "networkx", + "numpy" ], ) diff --git a/tests/test_pauli_annealing.py b/tests/test_pauli_annealing.py new file mode 100644 index 00000000..c090749b --- /dev/null +++ b/tests/test_pauli_annealing.py @@ -0,0 +1,103 @@ +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 + +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 / 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 * np.pi, np.pi, 0.5 * np.pi, 0.25 * np.pi, 0.125 * np.pi] + 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, 4]: + 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") diff --git a/tests/test_pauli_propagation.py b/tests/test_pauli_propagation.py index e8b1f617..4a5d0dac 100644 --- a/tests/test_pauli_propagation.py +++ b/tests/test_pauli_propagation.py @@ -10,7 +10,8 @@ from pytket._tket.pauli import Pauli from qiskit import QuantumCircuit -from pauliopt.pauli.clifford_gates import CX, CY, CZ, H, S, V +from pauliopt.pauli.clifford_gates import CX, CY, CZ, H, S, V, generate_random_clifford, CliffordType, CliffordGate, \ + ControlGate, SingleQubitGate, clifford_to_qiskit from pauliopt.pauli.pauli_gadget import PPhase from pauliopt.pauli.pauli_polynomial import PauliPolynomial from pauliopt.pauli.utils import X, Y, Z, I @@ -57,7 +58,7 @@ def verify_equality(qc_in, qc_out): def generate_all_combination_pauli_polynomial(n_qubits=2): allowed_angels = [2 * np.pi, np.pi, 0.5 * np.pi, 0.25 * np.pi, 0.125 * np.pi] - pp = PauliPolynomial() + 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 @@ -73,6 +74,17 @@ def check_matching_architecture(qc: QuantumCircuit, G: nx.Graph): 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 TestPauliConversion(unittest.TestCase): def test_circuit_construction(self): """ @@ -92,6 +104,8 @@ def test_circuit_construction(self): "The resulting Quantum Circuits were not equivalent") self.assertTrue(check_matching_architecture(our_synth, topology.to_nx), "The Pauli Polynomial did not match the architecture") + self.assertEqual(get_two_qubit_count(our_synth), pp.two_qubit_count(topology), + "Two qubit count needs to be equivalent to to two qubit count of the circuit") def test_gate_propagation(self): """ @@ -100,13 +114,14 @@ def test_gate_propagation(self): for num_qubits in [2, 3, 4]: pp = generate_all_combination_pauli_polynomial(n_qubits=num_qubits) inital_qc = pp.to_qiskit() - for gate_class in [CX, CY, CZ, H, S, V]: - gate = gate_class.generate_random(num_qubits) - print(gate.to_qiskit()) + for gate_class in [CliffordType.CX, CliffordType.CY, CliffordType.CZ, + CliffordType.H, CliffordType.S, CliffordType.V]: + gate = generate_random_clifford(gate_class, num_qubits) + print(gate_class) pp_ = pp.copy().propagate(gate) qc = QuantumCircuit(num_qubits) - qc.compose(gate.to_qiskit().inverse(), inplace=True) + qc.compose(clifford_to_qiskit(gate).inverse(), inplace=True) qc.compose(pp_.to_qiskit(), inplace=True) - qc.compose(gate.to_qiskit(), inplace=True) + qc.compose(clifford_to_qiskit(gate), inplace=True) self.assertTrue(verify_equality(inital_qc, qc), "The resulting Quantum Circuits were not equivalent") diff --git a/tests/test_pauli_representation.py b/tests/test_pauli_representation.py index 9aa58f33..5309a276 100644 --- a/tests/test_pauli_representation.py +++ b/tests/test_pauli_representation.py @@ -1,5 +1,4 @@ import unittest -import numpy as np from pauliopt.pauli.pauli_gadget import PPhase from pauliopt.pauli.pauli_polynomial import PauliPolynomial @@ -10,13 +9,19 @@ class TestPauliConversion(unittest.TestCase): def test_circuit_construction(self): - pp = PauliPolynomial() + pp = PauliPolynomial(4) pp >>= PPhase(0.5) @ [I, X, Y, Z] self.assertEqual(pp.num_qubits, 4) - self.assertEqual(pp.size, 1) + self.assertEqual(len(pp), 1) pp >>= PPhase(0.25) @ [X, X, Y, X] self.assertEqual(pp.__repr__(), _PAULI_REPR) + self.assertEqual(len(pp), 2) + + pp_ = PauliPolynomial(num_qubits=4) + pp_ >> pp + + self.assertEqual(pp.__repr__(), pp_.__repr__(), "Right shift resulted in different pauli Polynomials.") From 8d141d1214d97a0c458074a682d985cec6cc49fc Mon Sep 17 00:00:00 2001 From: daehiff Date: Wed, 31 May 2023 20:25:55 +0200 Subject: [PATCH 26/30] added SVG support --- notebooks/2. Circuits of Phase Gadgets.ipynb | 81 ++- notebooks/4. Phase Circuit Optimization.ipynb | 4 +- notebooks/6. Pauli Polynomials.ipynb | 505 ++++++++++++++++++ pauliopt/pauli/pauli_gadget.py | 12 +- pauliopt/pauli/pauli_polynomial.py | 109 +++- pauliopt/utils.py | 206 +++++-- tests/test_pauli_annealing.py | 5 +- tests/test_pauli_propagation.py | 11 +- tests/test_pauli_representation.py | 24 +- 9 files changed, 877 insertions(+), 80 deletions(-) create mode 100644 notebooks/6. Pauli Polynomials.ipynb diff --git a/notebooks/2. Circuits of Phase Gadgets.ipynb b/notebooks/2. Circuits of Phase Gadgets.ipynb index 8754c565..ed9934f4 100644 --- a/notebooks/2. Circuits of Phase Gadgets.ipynb +++ b/notebooks/2. Circuits of Phase Gadgets.ipynb @@ -31,7 +31,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Current working directory: C:\\Users\\Stefa\\Documents\\git\\pauliopt\n" + "Current working directory: /Users/davidwinderl/Documents/Workspaces/Workspace/pauliopt\n" ] } ], @@ -70,7 +70,7 @@ "name": "stderr", "output_type": "stream", "text": [ - ":4: DeprecationWarning: `set_matplotlib_formats` is deprecated since IPython 7.23, directly use `matplotlib_inline.backend_inline.set_matplotlib_formats()`\n", + "/var/folders/gx/p4btzntx3q93kpwt35k29yvr0000gn/T/ipykernel_5769/4084885951.py:4: DeprecationWarning: `set_matplotlib_formats` is deprecated since IPython 7.23, directly use `matplotlib_inline.backend_inline.set_matplotlib_formats()`\n", " set_matplotlib_formats('svg')\n" ] } @@ -210,7 +210,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "hash(gadget) = 624374175457367365\n", + "hash(gadget) = 1441341330045463407\n", "(gadget == same_gadget) = True\n", "(gadget == other_gadget) = False\n" ] @@ -291,9 +291,73 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 12, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "0\n", + "0\n", + "\n", + "1\n", + "1\n", + "\n", + "2\n", + "2\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "π/2\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "π\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "7π/4\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "π/4\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "π/2\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "from pauliopt.phase import PhaseCircuit\n", "gadgets = [\n", @@ -303,7 +367,8 @@ " Z(pi/4) @ {0, 2},\n", " X(pi/2) @ {0, 1},\n", "]\n", - "phase_circuit = PhaseCircuit(3, gadgets)" + "phase_circuit = PhaseCircuit(3, gadgets)\n", + "display(phase_circuit)" ] }, { @@ -1582,7 +1647,7 @@ "metadata": { "celltoolbar": "Slideshow", "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -1596,7 +1661,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.9.16" } }, "nbformat": 4, diff --git a/notebooks/4. Phase Circuit Optimization.ipynb b/notebooks/4. Phase Circuit Optimization.ipynb index ab0dde71..6f8f4e56 100644 --- a/notebooks/4. Phase Circuit Optimization.ipynb +++ b/notebooks/4. Phase Circuit Optimization.ipynb @@ -13401,7 +13401,7 @@ "metadata": { "celltoolbar": "Slideshow", "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -13415,7 +13415,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.9.16" } }, "nbformat": 4, diff --git a/notebooks/6. Pauli Polynomials.ipynb b/notebooks/6. Pauli Polynomials.ipynb new file mode 100644 index 00000000..e5edd004 --- /dev/null +++ b/notebooks/6. Pauli Polynomials.ipynb @@ -0,0 +1,505 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "4abbaa6c", + "metadata": {}, + "outputs": [], + "source": [ + "from pauliopt.pauli.pauli_polynomial import PauliPolynomial\n", + "from pauliopt.pauli.pauli_gadget import PPhase\n", + "from pauliopt.pauli.utils import I, Z, X, Y\n", + "from pauliopt.utils import Angle, pi\n", + "from pauliopt.topologies import Topology" + ] + }, + { + "cell_type": "markdown", + "id": "173ba092", + "metadata": {}, + "source": [ + "# Pauli Polynomials\n", + "\n", + "## Pauli Gadgets\n", + "\n", + "Pauli Gadgets are mathematical expressions defined as:\n", + "\n", + "$$\n", + "P = \\exp(-i \\frac{\\alpha}{2} \\bigotimes_i P_i)\n", + "$$\n", + "\n", + "Here, \\(P_i\\) represents one of the Pauli matrices:\n", + "\n", + "- Pauli-X:\n", + "$$\n", + "X = \\begin{bmatrix} 0 & 1 \\\\ 1 & 0 \\end{bmatrix}\n", + "$$\n", + "- Pauli-Y:\n", + "$$\n", + "Y = \\begin{bmatrix} 0 & -i \\\\ i & 0 \\end{bmatrix}\n", + "$$\n", + "- Pauli-Z:\n", + "$$\n", + "Z = \\begin{bmatrix} 1 & 0 \\\\ 0 & -1 \\end{bmatrix}\n", + "$$\n", + "- Identity:\n", + "$$\n", + "I = \\begin{bmatrix} 1 & 0 \\\\ 0 & 1 \\end{bmatrix}\n", + "$$\n", + "\n", + "A formal definition and further information on Pauli gadgets can be found [here](https://arxiv.org/abs/1906.01734).\n", + "\n", + "Within `pauliopt`, Pauli gadgets are constructed in a similar way to Phase gadgets, following these steps:\n", + "- An angle is required, which can be an instance of `pauliopt.utils.Angle` or any object that satisfies the `pauliopt.utils.AngleProtocol` protocol.\n", + "- A list of legs is defined, where each leg corresponds to one of the Pauli matrices (X, Y, Z, I).\n", + "- The qubits spanned by the Pauli gadget are determined by the length of the list. If there are no qubits to be acted on, the Identity matrix is used.\n", + "\n", + "Example:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a22d780c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(0.5) @ { I, Z, X, Y }" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pauli_gadget = PPhase(0.5) @ [I, Z, X, Y] # TODO Angle(pi)\n", + "pauli_gadget" + ] + }, + { + "cell_type": "markdown", + "id": "641c0890", + "metadata": {}, + "source": [ + "We can define a Pauli gadget on a quantum circuit by following these steps:\n", + "\n", + "1. *Apply a set of Clifford gates:*\n", + " - If the Pauli gadget is X, place the H-Gate on the corresponding qubit.\n", + " - If the Pauli gadget is Y, apply the $\\sqrt{X}$ or V-Gate on the corresponding qubit.\n", + " - If the Pauli gadget is Z or the Identity, no additional gate is applied.\n", + "2. *Create a CNOT-Ladder:*\n", + " - Implement a sequence of CNOT gates between the target qubit and the control qubits.\n", + "3. *Perform an Rz(alpha) rotation:*\n", + " - Apply the Rz(alpha) gate on the target qubit.\n", + "4. *Undo the process:*\n", + " - Reverse the CNOT-Ladder by applying the CNOT gates in the opposite order.\n", + " - Reapply the Clifford gates in the reverse order to undo their effects.\n", + "\n", + "Here's an example of a circuit showcasing these steps:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "71d6fc47", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " \n", + "q_0: ──────────────────────────────────────────────────────\n", + " ┌───┐┌─────────┐┌───┐ \n", + "q_1: ────────────────┤ X ├┤ Rz(0.5) ├┤ X ├─────────────────\n", + " ┌───┐ ┌───┐└─┬─┘└─────────┘└─┬─┘┌───┐ ┌───┐ \n", + "q_2: ───┤ H ├───┤ X ├──■───────────────■──┤ X ├───┤ H ├────\n", + " ┌──┴───┴──┐└─┬─┘ └─┬─┘┌──┴───┴───┐\n", + "q_3: ┤ Rx(π/2) ├──■─────────────────────────■──┤ Rx(-π/2) ├\n", + " └─────────┘ └──────────┘\n" + ] + } + ], + "source": [ + "print(pauli_gadget.to_qiskit(Topology.line(4)))" + ] + }, + { + "cell_type": "markdown", + "id": "49fe3a52", + "metadata": {}, + "source": [ + "## Pauli Polynomial\n", + "\n", + "To construct a Pauli polynomial, we can chain together multiple Pauli gadgets. Each Pauli gadget acts on a specific set of qubits and contributes to the overall transformation of the system (you can view this as n-dimensional rotations acting sequentially). \n", + "\n", + "Here's an example illustrating the construction of a Pauli polynomial:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "1c72d4e0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(π) @ { I, I, X, Z, Y }\n", + "(π/2) @ { X, X, I, I, Y }\n", + "(π/256) @ { X, I, I, Z, Y }\n", + "(π/8) @ { X, X, X, Z, Y }\n", + "(π/4) @ { X, Z, I, I, Y }\n", + "(π/2) @ { X, I, I, Y, Y }\n" + ] + } + ], + "source": [ + "pp = PauliPolynomial(5)\n", + "\n", + "pp >>= PPhase(Angle(pi)) @ [I, I, X, Z, Y]\n", + "pp >>= PPhase(Angle(pi/2)) @ [X, X, I, I, Y]\n", + "pp >>= PPhase(Angle(pi/256)) @ [X, I, I, Z, Y]\n", + "pp >>= PPhase(Angle(pi/8)) @ [X, X, X, Z, Y]\n", + "pp >>= PPhase(Angle(pi/4)) @ [X, Z, I, I, Y]\n", + "pp >>= PPhase(Angle(pi/2)) @ [X, I, I, Y, Y]\n", + "\n", + "# Representation in CLI applications\n", + "print(pp)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "f2007e30", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "π\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "π/2\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "π/256\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "π/8\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "π/4\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "π/2\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "(π) @ { I, I, X, Z, Y }\n", + "(π/2) @ { X, X, I, I, Y }\n", + "(π/256) @ { X, I, I, Z, Y }\n", + "(π/8) @ { X, X, X, Z, Y }\n", + "(π/4) @ { X, Z, I, I, Y }\n", + "(π/2) @ { X, I, I, Y, Y }" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# SVG representation in e.g. Jupyter notebooks\n", + "pp" + ] + }, + { + "cell_type": "markdown", + "id": "b74b67a2", + "metadata": {}, + "source": [ + "An example circuit of the Pauli Polynomial above, can be generated as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "08d1f442", + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ┌───┐ ┌───┐┌───┐»\n", + "q_0: ───┤ H ├───────────────────────────────────────────────────────┤ X ├┤ X ├»\n", + " ├───┤ └─┬─┘└─┬─┘»\n", + "q_1: ───┤ H ├─────────────────────────────────────────────────────────┼────■──»\n", + " ├───┤ ┌───┐┌───┐┌───────┐┌───┐┌───┐ ┌───┐ ┌───┐ │ »\n", + "q_2: ───┤ H ├───┤ X ├┤ X ├┤ Rz(π) ├┤ X ├┤ X ├───┤ H ├───────┤ H ├─────┼───────»\n", + " └───┘ └─┬─┘└─┬─┘└───────┘└─┬─┘└─┬─┘ └───┘ └───┘ │ »\n", + "q_3: ─────────────┼────■─────────────■────┼───────────────────────────┼───────»\n", + " ┌─────────┐ │ │ ┌──────────┐┌─────────┐ │ »\n", + "q_4: ┤ Rx(π/2) ├──■───────────────────────■──┤ Rx(-π/2) ├┤ Rx(π/2) ├──■───────»\n", + " └─────────┘ └──────────┘└─────────┘ »\n", + "« ┌─────────┐┌───┐┌───┐ ┌───┐ ┌───┐ ┌───┐┌───┐┌───────────┐┌───┐»\n", + "«q_0: ┤ Rz(π/2) ├┤ X ├┤ X ├───┤ H ├───────┤ H ├───┤ X ├┤ X ├┤ Rz(π/256) ├┤ X ├»\n", + "« └─────────┘└─┬─┘└─┬─┘ ├───┤ ├───┤ └─┬─┘└─┬─┘└───────────┘└─┬─┘»\n", + "«q_1: ─────────────■────┼─────┤ H ├───────┤ H ├─────┼────┼─────────────────┼──»\n", + "« │ └───┘ └───┘ │ │ │ »\n", + "«q_2: ──────────────────┼───────────────────────────┼────┼─────────────────┼──»\n", + "« │ │ │ │ »\n", + "«q_3: ──────────────────┼───────────────────────────┼────■─────────────────■──»\n", + "« │ ┌──────────┐┌─────────┐ │ »\n", + "«q_4: ──────────────────■──┤ Rx(-π/2) ├┤ Rx(π/2) ├──■─────────────────────────»\n", + "« └──────────┘└─────────┘ »\n", + "« ┌───┐ ┌───┐ ┌───┐ ┌───┐┌───┐┌───┐┌───┐┌─────────┐┌───┐┌───┐»\n", + "«q_0: ┤ X ├───┤ H ├───────┤ H ├───┤ X ├┤ X ├┤ X ├┤ X ├┤ Rz(π/8) ├┤ X ├┤ X ├»\n", + "« └─┬─┘ └───┘ └───┘ └─┬─┘└─┬─┘└─┬─┘└─┬─┘└─────────┘└─┬─┘└─┬─┘»\n", + "«q_1: ──┼───────────────────────────┼────┼────┼────■───────────────■────┼──»\n", + "« │ │ │ │ │ »\n", + "«q_2: ──┼───────────────────────────┼────┼────■─────────────────────────■──»\n", + "« │ │ │ »\n", + "«q_3: ──┼───────────────────────────┼────■─────────────────────────────────»\n", + "« │ ┌──────────┐┌─────────┐ │ »\n", + "«q_4: ──■──┤ Rx(-π/2) ├┤ Rx(π/2) ├──■──────────────────────────────────────»\n", + "« └──────────┘└─────────┘ »\n", + "« ┌───┐┌───┐ ┌───┐ ┌───┐ ┌───┐┌───┐┌─────────┐┌───┐┌───┐»\n", + "«q_0: ─────┤ X ├┤ X ├───┤ H ├───────┤ H ├───┤ X ├┤ X ├┤ Rz(π/4) ├┤ X ├┤ X ├»\n", + "« ┌───┐└─┬─┘└─┬─┘ └───┘ └───┘ └─┬─┘└─┬─┘└─────────┘└─┬─┘└─┬─┘»\n", + "«q_1: ┤ H ├──┼────┼───────────────────────────┼────■───────────────■────┼──»\n", + "« ├───┤ │ │ │ │ »\n", + "«q_2: ┤ H ├──┼────┼───────────────────────────┼─────────────────────────┼──»\n", + "« └───┘ │ │ ┌─────────┐ │ │ »\n", + "«q_3: ───────■────┼──┤ Rx(π/2) ├──────────────┼─────────────────────────┼──»\n", + "« │ ├─────────┴┐┌─────────┐ │ │ »\n", + "«q_4: ────────────■──┤ Rx(-π/2) ├┤ Rx(π/2) ├──■─────────────────────────■──»\n", + "« └──────────┘└─────────┘ »\n", + "« ┌───┐ ┌───┐ ┌───┐┌───┐┌─────────┐┌───┐┌───┐ ┌───┐ \n", + "«q_0: ───┤ H ├───────┤ H ├───┤ X ├┤ X ├┤ Rz(π/2) ├┤ X ├┤ X ├───┤ H ├────\n", + "« └───┘ └───┘ └─┬─┘└─┬─┘└─────────┘└─┬─┘└─┬─┘ └───┘ \n", + "«q_1: ─────────────────────────┼────┼───────────────┼────┼──────────────\n", + "« │ │ │ │ \n", + "«q_2: ─────────────────────────┼────┼───────────────┼────┼──────────────\n", + "« │ │ │ │ ┌──────────┐\n", + "«q_3: ─────────────────────────┼────■───────────────■────┼──┤ Rx(-π/2) ├\n", + "« ┌──────────┐┌─────────┐ │ │ ├──────────┤\n", + "«q_4: ┤ Rx(-π/2) ├┤ Rx(π/2) ├──■─────────────────────────■──┤ Rx(-π/2) ├\n", + "« └──────────┘└─────────┘ └──────────┘\n" + ] + } + ], + "source": [ + "print(pp.to_qiskit())" + ] + }, + { + "cell_type": "markdown", + "id": "dc2407ad", + "metadata": {}, + "source": [ + "It is also possible to route such a polynomial on a certain type of architecture" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "774fb7c8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ┌───┐ »\n", + "q_0: ───┤ H ├─────────────────────────────────────────────────────────────────»\n", + " ├───┤ »\n", + "q_1: ───┤ H ├─────────────────────────────────────────────────────────────────»\n", + " ├───┤ ┌───┐┌───────┐┌───┐┌───┐ »\n", + "q_2: ───┤ H ├────────┤ X ├┤ Rz(π) ├┤ X ├┤ H ├─────────────────────────────────»\n", + " └───┘ ┌───┐└─┬─┘└───────┘└─┬─┘├───┤ ┌───┐»\n", + "q_3: ───────────┤ X ├──■─────────────■──┤ X ├─────────────────────────■──┤ X ├»\n", + " ┌─────────┐└─┬─┘ └─┬─┘┌──────────┐┌─────────┐┌─┴─┐└─┬─┘»\n", + "q_4: ┤ Rx(π/2) ├──■───────────────────────■──┤ Rx(-π/2) ├┤ Rx(π/2) ├┤ X ├──■──»\n", + " └─────────┘ └──────────┘└─────────┘└───┘ »\n", + "« ┌───┐┌─────────┐┌───┐┌───┐┌───┐ »\n", + "«q_0: ───────────────┤ X ├┤ Rz(π/2) ├┤ X ├┤ H ├┤ H ├───────────────────────────»\n", + "« ┌───┐└─┬─┘└─────────┘└─┬─┘├───┤├───┤ »\n", + "«q_1: ──────────┤ X ├──■───────────────■──┤ X ├┤ H ├───────────────────────────»\n", + "« ┌───┐└─┬─┘ └─┬─┘├───┤ »\n", + "«q_2: ──■──┤ X ├──■─────────────────────────■──┤ X ├──■────────────────────────»\n", + "« ┌─┴─┐└─┬─┘ └─┬─┘┌─┴─┐┌───┐ »\n", + "«q_3: ┤ X ├──■───────────────────────────────────■──┤ X ├┤ X ├──■──────────────»\n", + "« └───┘ └───┘└─┬─┘┌─┴─┐┌──────────┐»\n", + "«q_4: ─────────────────────────────────────────────────────■──┤ X ├┤ Rx(-π/2) ├»\n", + "« └───┘└──────────┘»\n", + "« ┌───┐┌───────────┐┌───┐┌───┐┌───┐»\n", + "«q_0: ────────────────────────────────────┤ X ├┤ Rz(π/256) ├┤ X ├┤ H ├┤ H ├»\n", + "« ┌───┐└─┬─┘└───────────┘└─┬─┘├───┤└───┘»\n", + "«q_1: ────────────────────────────■──┤ X ├──■─────────────────■──┤ X ├──■──»\n", + "« ┌───┐┌─┴─┐└─┬─┘ └─┬─┘┌─┴─┐»\n", + "«q_2: ──────────────────■──┤ X ├┤ X ├──■───────────────────────────■──┤ X ├»\n", + "« ┌───┐┌─┴─┐└─┬─┘└───┘ └───┘»\n", + "«q_3: ───────────┤ X ├┤ X ├──■─────────────────────────────────────────────»\n", + "« ┌─────────┐└─┬─┘└───┘ »\n", + "«q_4: ┤ Rx(π/2) ├──■───────────────────────────────────────────────────────»\n", + "« └─────────┘ »\n", + "« ┌───┐┌─────────┐»\n", + "«q_0: ─────────────────────────────────────────────────────┤ X ├┤ Rz(π/8) ├»\n", + "« ┌───┐ ┌───┐└─┬─┘└─────────┘»\n", + "«q_1: ┤ H ├───────────────────────────────────────────┤ X ├──■─────────────»\n", + "« ├───┤ ┌───┐ ┌───┐└─┬─┘ »\n", + "«q_2: ┤ X ├──■──┤ H ├────────────────────────────┤ X ├──■──────────────────»\n", + "« └─┬─┘┌─┴─┐├───┤ ┌───┐└─┬─┘ »\n", + "«q_3: ──■──┤ X ├┤ X ├───────────────────────┤ X ├──■───────────────────────»\n", + "« └───┘└─┬─┘┌──────────┐┌─────────┐└─┬─┘ »\n", + "«q_4: ────────────■──┤ Rx(-π/2) ├┤ Rx(π/2) ├──■────────────────────────────»\n", + "« └──────────┘└─────────┘ »\n", + "« ┌───┐┌───┐┌───┐ ┌───┐»\n", + "«q_0: ┤ X ├┤ H ├┤ H ├─────────────────────────────────────────────────────┤ X ├»\n", + "« └─┬─┘├───┤├───┤ ┌───┐└─┬─┘»\n", + "«q_1: ──■──┤ X ├┤ H ├────────────────────────────────────────────────┤ X ├──■──»\n", + "« └─┬─┘├───┤┌───┐ ┌───┐└─┬─┘ »\n", + "«q_2: ───────■──┤ X ├┤ H ├───────────────────────────────────■──┤ X ├──■───────»\n", + "« └─┬─┘├───┤ ┌───┐┌─┴─┐└─┬─┘ »\n", + "«q_3: ────────────■──┤ X ├─────────────────────────■──┤ X ├┤ X ├──■────────────»\n", + "« └─┬─┘┌──────────┐┌─────────┐┌─┴─┐└─┬─┘└───┘ »\n", + "«q_4: ─────────────────■──┤ Rx(-π/2) ├┤ Rx(π/2) ├┤ X ├──■──────────────────────»\n", + "« └──────────┘└─────────┘└───┘ »\n", + "« ┌─────────┐┌───┐┌───┐┌───┐ »\n", + "«q_0: ┤ Rz(π/4) ├┤ X ├┤ H ├┤ H ├───────────────────────────────────────────»\n", + "« └─────────┘└─┬─┘├───┤└───┘ »\n", + "«q_1: ─────────────■──┤ X ├────────────────────────────────────────────────»\n", + "« └─┬─┘┌───┐ »\n", + "«q_2: ──────────────────■──┤ X ├──■────────────────────────────────────────»\n", + "« └─┬─┘┌─┴─┐┌───┐ ┌─────────┐ ┌───┐»\n", + "«q_3: ───────────────────────■──┤ X ├┤ X ├──■──┤ Rx(π/2) ├────────────┤ X ├»\n", + "« └───┘└─┬─┘┌─┴─┐├─────────┴┐┌─────────┐└─┬─┘»\n", + "«q_4: ─────────────────────────────────■──┤ X ├┤ Rx(-π/2) ├┤ Rx(π/2) ├──■──»\n", + "« └───┘└──────────┘└─────────┘ »\n", + "« ┌───┐┌─────────┐┌───┐┌───┐ »\n", + "«q_0: ────────────────────┤ X ├┤ Rz(π/2) ├┤ X ├┤ H ├────────────────────»\n", + "« ┌───┐└─┬─┘└─────────┘└─┬─┘├───┤ »\n", + "«q_1: ────────────■──┤ X ├──■───────────────■──┤ X ├──■─────────────────»\n", + "« ┌───┐┌─┴─┐└─┬─┘ └─┬─┘┌─┴─┐┌───┐ »\n", + "«q_2: ──■──┤ X ├┤ X ├──■─────────────────────────■──┤ X ├┤ X ├──■───────»\n", + "« ┌─┴─┐└─┬─┘└───┘ └───┘└─┬─┘┌─┴─┐┌───┐»\n", + "«q_3: ┤ X ├──■─────────────────────────────────────────────■──┤ X ├┤ X ├»\n", + "« └───┘ └───┘└─┬─┘»\n", + "«q_4: ───────────────────────────────────────────────────────────────■──»\n", + "« »\n", + "« \n", + "«q_0: ────────────\n", + "« \n", + "«q_1: ────────────\n", + "« \n", + "«q_2: ────────────\n", + "« ┌──────────┐\n", + "«q_3: ┤ Rx(-π/2) ├\n", + "« ├──────────┤\n", + "«q_4: ┤ Rx(-π/2) ├\n", + "« └──────────┘\n" + ] + } + ], + "source": [ + "print(pp.to_qiskit(Topology.line(pp.num_qubits)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a674d6c8", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.16" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pauliopt/pauli/pauli_gadget.py b/pauliopt/pauli/pauli_gadget.py index fa304361..bae6e93b 100644 --- a/pauliopt/pauli/pauli_gadget.py +++ b/pauliopt/pauli/pauli_gadget.py @@ -106,7 +106,7 @@ def to_qiskit(self, topology=None): column = np.asarray(self.paulis) column_binary = np.where(column == I, 0, 1) if np.all(column_binary == 0): - circ.global_phase += self.angle + circ.global_phase += self.angle.to_qiskit return circ cnot_ladder, q0 = find_minimal_cx_assignment(column_binary, topology) @@ -126,13 +126,19 @@ def to_qiskit(self, topology=None): for (pauli_idx, target) in reversed(cnot_ladder): circ.cx(pauli_idx, target) - circ.rz(self.angle, q0) + if isinstance(self.angle, float): + circ.rz(self.angle, q0) + elif isinstance(self.angle, AngleExpr): + circ.rz(self.angle.to_qiskit, q0) for (pauli_idx, target) in cnot_ladder: circ.cx(pauli_idx, target) else: target = np.argmax(column_binary) - circ.rz(self.angle, target) + if isinstance(self.angle, float): + circ.rz(self.angle, target) + elif isinstance(self.angle, AngleExpr): + circ.rz(self.angle.to_qiskit, target) for pauli_idx in range(len(column)): if column[pauli_idx] == Pauli.I: diff --git a/pauliopt/pauli/pauli_polynomial.py b/pauliopt/pauli/pauli_polynomial.py index 37120094..f5e9a638 100644 --- a/pauliopt/pauli/pauli_polynomial.py +++ b/pauliopt/pauli/pauli_polynomial.py @@ -2,6 +2,9 @@ from pauliopt.pauli.pauli_gadget import PauliGadget from pauliopt.topologies import Topology +import math +from pauliopt.pauli.utils import X, Y, Z, I +from pauliopt.utils import SVGBuilder class PauliPolynomial: @@ -11,7 +14,8 @@ def __init__(self, num_qubits): def __irshift__(self, gadget: PauliGadget): if not len(gadget) == self.num_qubits: - raise Exception(f"Pauli Polynomial has {self.num_qubits}, but Pauli gadget has: {len(gadget)}") + raise Exception( + f"Pauli Polynomial has {self.num_qubits}, but Pauli gadget has: {len(gadget)}") self.pauli_gadgets.append(gadget) return self @@ -26,6 +30,10 @@ def __repr__(self): def __len__(self): return len(self.pauli_gadgets) + @property + def num_gadgets(self): + return len(self.pauli_gadgets) + def to_qiskit(self, topology=None): num_qubits = self.num_qubits if topology is None: @@ -60,3 +68,102 @@ def two_qubit_count(self, topology, leg_cache=None): for gadget in self.pauli_gadgets: count += gadget.two_qubit_count(topology, leg_cache=leg_cache) return count + + def to_svg(self, hscale: float = 1.0, vscale: float = 1.0, scale: float = 1.0, + svg_code_only=False): + vscale *= scale + hscale *= scale + + x_color = "#CCFFCC" + z_color = "#FF8888" + y_color = "ycolor" + + num_qubits = self.num_qubits + num_gadgets = self.num_gadgets + + # general width and height of a square + square_width = int(math.ceil(20 * vscale)) + square_height = int(math.ceil(20 * vscale)) + + # width of the text of the phases # TODO round floats (!!) + text_width = int(math.ceil(50 * vscale)) + + bend_degree = int(math.ceil(10)) + + # margins between the angle and the legs + margin_angle_x = int(math.ceil(20 * hscale)) + margin_angle_y = int(math.ceil(20 * hscale)) + + # margins between each element + 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 + height = (num_qubits) * (square_height + margin_y) + ( + square_height + margin_y + margin_angle_y) + + builder = SVGBuilder(width, height) + builder = builder.add_diagonal_fill(x_color, z_color, y_color) + + prev_x = {qubit: 0 for qubit in range(num_qubits)} + + x = margin_x + + for gadget in self.pauli_gadgets: + paulis = gadget.paulis + y = margin_y + text_coords = (square_width + margin_x + margin_angle_x + x, y) + text_left_lower_corder = (text_coords[0], text_coords[1] + square_height) + for qubit in range(num_qubits): + if qubit == 0: + y += square_height + margin_y + margin_angle_y + else: + y += square_height + margin_y + center_coords = (x + square_width, y) + if paulis[qubit] == I: + continue + + builder.line((prev_x[qubit], y + square_height // 2), + (x, y + square_height // 2)) + prev_x[qubit] = x + square_width + builder.line_bend(text_left_lower_corder, center_coords, + degree=qubit * bend_degree) + if paulis[qubit] == X: + builder.square((x, y), square_width, square_height, x_color) + elif paulis[qubit] == Y: + builder.square((x, y), square_width, square_height, y_color) + elif paulis[qubit] == Z: + builder.square((x, y), square_width, square_height, z_color) + + builder = builder.text_with_square(text_coords, text_width, square_height, + str(gadget.angle)) + x += square_width + margin_x + text_width + margin_angle_x + y = margin_y + for qubit in range(num_qubits): + if qubit == 0: + y += square_height + margin_y + margin_angle_y + else: + y += square_height + margin_y + builder.line((prev_x[qubit], y + square_height // 2), + (width, y + square_height // 2)) + svg_code = repr(builder) + + if svg_code_only: + return svg_code + try: + # pylint: disable = import-outside-toplevel + from IPython.core.display import SVG # type: ignore + except ModuleNotFoundError as e: + raise ModuleNotFoundError("You must install the 'IPython' library.") from e + + return SVG(svg_code) + + def _repr_svg_(self): + """ + Magic method for IPython/Jupyter pretty-printing. + See https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html + """ + return self.to_svg(svg_code_only=True) diff --git a/pauliopt/utils.py b/pauliopt/utils.py index 568bc64e..77061074 100644 --- a/pauliopt/utils.py +++ b/pauliopt/utils.py @@ -7,12 +7,30 @@ from decimal import Decimal from fractions import Fraction from types import MappingProxyType -from typing import (Any, Callable, ClassVar, Dict, Final, List, Literal, Mapping, Optional, overload, +from typing import (Any, Callable, ClassVar, Dict, Final, List, Literal, Mapping, + Optional, overload, Protocol, runtime_checkable, Sequence, Tuple, Union) import numpy as np + +def calculate_orthogonal_point(a, b, d, left): + direction_vector = b - a + magnitude = np.linalg.norm(direction_vector) + normalized_direction_vector = direction_vector / magnitude + if left: + orthogonal_vector = np.array( + [-normalized_direction_vector[1], normalized_direction_vector[0]]) + else: + orthogonal_vector = np.array( + [normalized_direction_vector[1], -normalized_direction_vector[0]]) + midpoint = (a + b) / 2 + orthogonal_point = midpoint + d * orthogonal_vector + return int(orthogonal_point[0]), int(orthogonal_point[1]) + + AngleInitT = Union[int, Fraction, str, Decimal] + class AngleExpr(ABC): """ A container class for angle expressions. @@ -51,7 +69,7 @@ def __rmul__(self, other: int) -> "AngleExpr": def __truediv__(self, other: int) -> "AngleExpr": if isinstance(other, int): - return SumprodAngleExpr(self, coeffs=Fraction(1,other)) + return SumprodAngleExpr(self, coeffs=Fraction(1, other)) return NotImplemented @abstractmethod @@ -96,7 +114,7 @@ def _repr_latex_(self) -> str: Magic method for IPython/Jupyter pretty-printing. See https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html """ - return "$%s$"%self.repr_latex + return "$%s$" % self.repr_latex @abstractmethod def __eq__(self, other: Any) -> bool: @@ -122,7 +140,7 @@ def value(self) -> Fraction: """ The value of this angle as a fraction of PI. """ - return self._value%2 + return self._value % 2 @property def as_root_of_unity(self) -> Tuple[int, int]: @@ -133,8 +151,8 @@ def as_root_of_unity(self) -> Tuple[int, int]: """ num = self.value.numerator den = self.value.denominator - a: int = num//2 if num%2 == 0 else num - order: int = den if num % 2 == 0 else 2*den + a: int = num // 2 if num % 2 == 0 else num + order: int = den if num % 2 == 0 else 2 * den return (a, order) @property @@ -144,7 +162,7 @@ def order(self) -> int: """ num = self.value.numerator den = self.value.denominator - return den if num % 2 == 0 else 2*den + return den if num % 2 == 0 else 2 * den @property def is_zero_or_pi(self) -> bool: @@ -162,7 +180,7 @@ def is_zero(self) -> bool: """ num = self.value.numerator den = self.value.denominator - return num % (2*den) == 0 + return num % (2 * den) == 0 @property def is_pi(self) -> bool: @@ -171,7 +189,7 @@ def is_pi(self) -> bool: """ num = self.value.numerator den = self.value.denominator - return num % den == 0 and not num % (2*den) == 0 + return num % den == 0 and not num % (2 * den) == 0 @property def to_qiskit(self) -> float: @@ -223,7 +241,7 @@ def __mod__(self, other: "AngleExpr") -> "AngleExpr": return super().__mod__(other) def _mul(self, other: Union[int, Fraction]) -> "Angle": - return Angle(self._value*other) + return Angle(self._value * other) def __mul__(self, other: int) -> "Angle": if isinstance(other, int): @@ -251,8 +269,8 @@ def __str__(self) -> str: if num == 1: if den == 1: return "π" - return "π/%d"%den - return "%dπ/%d"%(num, den) + return "π/%d" % den + return "%dπ/%d" % (num, den) def __repr__(self) -> str: num = self.value.numerator @@ -262,8 +280,8 @@ def __repr__(self) -> str: if num == 1: if den == 1: return "pi" - return "pi/%d"%den - return "%d*pi/%d"%(num, den) + return "pi/%d" % den + return "%d*pi/%d" % (num, den) @property def repr_latex(self) -> str: @@ -277,15 +295,15 @@ def repr_latex(self) -> str: if num == 1: if den == 1: return "\\pi" - return "\\frac{\\pi}{%d}"%den - return "\\frac{%d\\pi}{%d}"%(num, den) + return "\\frac{\\pi}{%d}" % den + return "\\frac{%d\\pi}{%d}" % (num, den) def _repr_latex_(self) -> str: """ Magic method for IPython/Jupyter pretty-printing. See https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html """ - return "$%s$"%self.repr_latex + return "$%s$" % self.repr_latex def __eq__(self, other: Any) -> bool: if other == 0: @@ -295,12 +313,12 @@ def __eq__(self, other: Any) -> bool: return self.value == other.value def __float__(self) -> float: - return float(self.value)*math.pi + return float(self.value) * math.pi @overload @staticmethod def random(subdivision: int = 4, *, - size: Literal[1]=1, + size: Literal[1] = 1, rng_seed: Optional[int] = None, nonzero: bool = False) -> "Angle": ... @@ -335,35 +353,36 @@ def random(subdivision: int = 4, *, size: int = 1, raise TypeError("RNG seed must be integer or 'None'.") rng = np.random.default_rng(seed=rng_seed) if nonzero: - rs = 1+rng.integers(2*subdivision-1, size=size) # type: ignore[attr-defined] + rs = 1 + rng.integers(2 * subdivision - 1, + size=size) # type: ignore[attr-defined] else: - rs = rng.integers(2*subdivision, size=size) # type: ignore[attr-defined] + rs = rng.integers(2 * subdivision, size=size) # type: ignore[attr-defined] if size == 1: return Angle(Fraction(int(rs[0]), subdivision)) return tuple(Angle(Fraction(int(r), subdivision)) for r in rs) - zero: Final["Angle"] # type: ignore + zero: Final["Angle"] # type: ignore """ A constant for the angle 0. """ - pi: Final["Angle"] # type: ignore + pi: Final["Angle"] # type: ignore """ A constant for the angle pi. """ -# Set static constants for Angle: -Angle.zero = Angle(0) # type: ignore -Angle.pi = Angle(1) # type: ignore +# Set static constants for Angle: +Angle.zero = Angle(0) # type: ignore +Angle.pi = Angle(1) # type: ignore pi: Final[Angle] = Angle.pi """ Constant for `Angle.pi`. """ -π: Final[Angle] = Angle.pi # pylint: disable=non-ascii-name +π: Final[Angle] = Angle.pi # pylint: disable=non-ascii-name """ Constant for `Angle.pi`. """ def SumprodAngleExpr(*exprs: AngleExpr, coeffs: Union[int, Fraction, Sequence[Union[int, Fraction]]] = 1 - ) -> AngleExpr: + ) -> AngleExpr: if not isinstance(coeffs, Sequence): coeffs = (coeffs,) if len(coeffs) != len(exprs): @@ -373,10 +392,10 @@ def SumprodAngleExpr(*exprs: AngleExpr, _const: Angle = Angle.zero for e, c in zip(exprs, coeffs): if isinstance(e, Angle): - _const += e._mul(c) # pylint: disable = protected-access + _const += e._mul(c) # pylint: disable = protected-access elif isinstance(e, _SumprodAngleExpr): for sub_e, sub_c in e.coeffs.items(): - new_c = c*sub_c + new_c = c * sub_c if sub_e in _coeffs: new_c += _coeffs[sub_e] _coeffs[sub_e] = new_c @@ -426,7 +445,8 @@ def is_pi(self) -> bool: @property def to_qiskit(self) -> Any: - return sum((c*e.to_qiskit for e, c in self.coeffs.items()), self.const.to_qiskit) + return sum((c * e.to_qiskit for e, c in self.coeffs.items()), + self.const.to_qiskit) def __hash__(self) -> int: return hash((_SumprodAngleExpr, tuple(self.coeffs.items()), self.const)) @@ -437,12 +457,12 @@ def _str_repr(self, f: Callable[[Union[Angle, AngleExpr]], str]) -> str: pos_sub_e = {e: c for e, c in self.coeffs.items() if c > 0} neg_sub_e = {e: c for e, c in self.coeffs.items() if c < 0} s = "+".join( - ("" if c == 1 else str(c))+f(e) + ("" if c == 1 else str(c)) + f(e) for e, c in pos_sub_e.items() ) if neg_sub_e: - s += "-"+"".join( - ("-" if c == -1 else str(c))+f(e) + s += "-" + "".join( + ("-" if c == -1 else str(c)) + f(e) for e, c in pos_sub_e.items() ) if self.const != 0: @@ -469,18 +489,21 @@ def __eq__(self, other: Any) -> bool: return False return NotImplemented + def ModAngleExpr(expr: AngleExpr, mod: AngleExpr) -> AngleExpr: if isinstance(expr, Angle) and isinstance(mod, Angle): - return expr%mod + return expr % mod return _ModAngleExpr(expr, mod) + class _ModAngleExpr(AngleExpr): _expr: AngleExpr _mod: AngleExpr def __init__(self, expr: AngleExpr, mod: AngleExpr): if isinstance(expr, Angle) and isinstance(mod, Angle): - raise ValueError("Arguments to _ModAngleExpr constructor cannot both be Angle.") + raise ValueError( + "Arguments to _ModAngleExpr constructor cannot both be Angle.") self._expr = expr self._mod = mod @@ -498,7 +521,7 @@ def is_zero(self) -> bool: @property def to_qiskit(self) -> Any: - return self.expr.to_qiskit%self.mod.to_qiskit + return self.expr.to_qiskit % self.mod.to_qiskit def __hash__(self) -> int: return hash((_ModAngleExpr, self.expr, self.mod)) @@ -523,6 +546,7 @@ def __eq__(self, other: Any) -> bool: return False return NotImplemented + class AngleVar(AngleExpr): _global_id: ClassVar[int] = 0 _qiskit_bindings: ClassVar[Dict[int, Any]] @@ -544,7 +568,7 @@ def to_qiskit(self) -> Any: return AngleVar._qiskit_bindings[self._id] try: # pylint: disable = import-outside-toplevel - from qiskit.circuit import Parameter # type: ignore + from qiskit.circuit import Parameter # type: ignore except ModuleNotFoundError as e: raise ModuleNotFoundError("You must install the 'qiskit' library.") from e p = Parameter(self._repr_latex_) @@ -575,13 +599,13 @@ def __eq__(self, other: Any) -> bool: return NotImplemented - def _validate_vec2(vec2: Tuple[int, int]) -> None: if not isinstance(vec2, tuple) or len(vec2) != 2: raise TypeError("Expected pair.") if not all(isinstance(x, int) for x in vec2): raise TypeError("Expected pair of integers.") + class SVGBuilder: """ Utility class for building certain SVG images. @@ -599,6 +623,7 @@ def __init__(self, width: int, height: int): raise TypeError("Height should be positive integer.") self._width = width self._height = height + self._def_object_ids = [] self._tags = [] @property @@ -653,6 +678,60 @@ def line(self, fro: Tuple[int, int], to: Tuple[int, int]) -> "SVGBuilder": self._tags.append(tag) return self + def line_bend(self, fro: Tuple[int, int], to: Tuple[int, int], left=False, degree=5): + _validate_vec2(fro) + _validate_vec2(to) + + fx, fy = fro + tx, ty = to + bx, by = calculate_orthogonal_point(np.asarray(fro), np.asarray(to), d=degree, + left=left) + + tag = f'' + self._tags.append(tag) + return self + + def add_diagonal_fill(self, color_1: str, color_2: str, id: str) -> "SVGBuilder": + tag = f'' \ + f'' \ + f'' \ + f'' \ + f'' \ + f'' + + self._def_object_ids.append(id) + self._tags.append(tag) + return self + + def square(self, centre: Tuple[int, int], width: int, height: int, + fill) -> "SVGBuilder": + _validate_vec2(centre) + x, y = centre + if fill in self._def_object_ids: + tag = f'' + self._tags.append(tag) + elif isinstance(fill, str): + tag = f'' + self._tags.append(tag) + else: + raise TypeError(f"Fill must be string or a defined Tag. Got: {fill} ") + return self + + def text_with_square(self, centre: Tuple[int, int], width: int, height: int, + text: str) -> "SVGBuilder": + _validate_vec2(centre) + x, y = centre + tag = f'' \ + f'' \ + f'{text}' \ + f'' + self._tags.append(tag) + return self + def circle(self, centre: Tuple[int, int], r: int, fill: str) -> "SVGBuilder": """ Draws a circle with given centre and radius. @@ -666,7 +745,8 @@ def circle(self, centre: Tuple[int, int], r: int, fill: str) -> "SVGBuilder": self._tags.append(tag) return self - def text(self, pos: Tuple[int, int], text: str, *, font_size: int = 10) -> "SVGBuilder": + def text(self, pos: Tuple[int, int], text: str, *, + font_size: int = 10) -> "SVGBuilder": """ Draws text at the given position (stroke/fill not used). """ @@ -676,7 +756,7 @@ def text(self, pos: Tuple[int, int], text: str, *, font_size: int = 10) -> "SVGB if not isinstance(font_size, int) or font_size <= 0: raise TypeError("Font size must be positive integer.") x, y = pos - tag = f'{text}' + tag = f'{text}' self._tags.append(tag) return self @@ -709,6 +789,7 @@ class TempSchedule(Protocol): def __call__(self, it: int, num_iters: int) -> float: ... + @runtime_checkable class TempScheduleProvider(Protocol): """ @@ -716,11 +797,13 @@ class TempScheduleProvider(Protocol): from an initial and final temperatures. """ - def __call__(self, t_init: Union[int, float], t_final: Union[int, float]) -> TempSchedule: + def __call__(self, t_init: Union[int, float], + t_final: Union[int, float]) -> TempSchedule: ... -def linear_temp_schedule(t_init: Union[int, float], t_final: Union[int, float]) -> TempSchedule: +def linear_temp_schedule(t_init: Union[int, float], + t_final: Union[int, float]) -> TempSchedule: """ Returns a straight/linear temperature schedule for given initial and final temperatures, from https://link.springer.com/article/10.1007/BF00143921 @@ -729,12 +812,15 @@ def linear_temp_schedule(t_init: Union[int, float], t_final: Union[int, float]) raise TypeError(f"Expected int or float, found {type(t_init)}.") if not isinstance(t_final, (int, float)): raise TypeError(f"Expected int or float, found {type(t_final)}.") + def temp_schedule(it: int, num_iters: int) -> float: - return t_init + (t_final-t_init)*it/(num_iters-1) + return t_init + (t_final - t_init) * it / (num_iters - 1) + return temp_schedule -def geometric_temp_schedule(t_init: Union[int, float], t_final: Union[int, float]) -> TempSchedule: +def geometric_temp_schedule(t_init: Union[int, float], + t_final: Union[int, float]) -> TempSchedule: """ Returns a geometric temperature schedule for given initial and final temperatures, from https://link.springer.com/article/10.1007/BF00143921 @@ -743,12 +829,16 @@ def geometric_temp_schedule(t_init: Union[int, float], t_final: Union[int, float raise TypeError(f"Expected int or float, found {type(t_init)}.") if not isinstance(t_final, (int, float)): raise TypeError(f"Expected int or float, found {type(t_final)}.") + def temp_schedule(it: int, num_iters: int) -> float: - return t_init * ((t_final/t_init)**(it/(num_iters-1.0))) # type: ignore[no-any-return] + return t_init * ((t_final / t_init) ** ( + it / (num_iters - 1.0))) # type: ignore[no-any-return] + return temp_schedule -def reciprocal_temp_schedule(t_init: Union[int, float], t_final: Union[int, float]) -> TempSchedule: +def reciprocal_temp_schedule(t_init: Union[int, float], + t_final: Union[int, float]) -> TempSchedule: """ Returns a reciprocal temperature schedule for given initial and final temperatures, from https://link.springer.com/article/10.1007/BF00143921 @@ -757,14 +847,17 @@ def reciprocal_temp_schedule(t_init: Union[int, float], t_final: Union[int, floa raise TypeError(f"Expected int or float, found {type(t_init)}.") if not isinstance(t_final, (int, float)): raise TypeError(f"Expected int or float, found {type(t_final)}.") + def temp_schedule(it: int, num_iters: int) -> float: - num = t_init*t_final*(num_iters-1) - denom = (t_final*num_iters-t_init)+(t_init-t_final)*(it+1) - return num/denom + num = t_init * t_final * (num_iters - 1) + denom = (t_final * num_iters - t_init) + (t_init - t_final) * (it + 1) + return num / denom + return temp_schedule -def log_temp_schedule(t_init: Union[int, float], t_final: Union[int, float]) -> TempSchedule: +def log_temp_schedule(t_init: Union[int, float], + t_final: Union[int, float]) -> TempSchedule: """ Returns a logarithmic temperature schedule for given initial and final temperatures, from https://link.springer.com/article/10.1007/BF00143921 @@ -773,10 +866,13 @@ def log_temp_schedule(t_init: Union[int, float], t_final: Union[int, float]) -> raise TypeError(f"Expected int or float, found {type(t_init)}.") if not isinstance(t_final, (int, float)): raise TypeError(f"Expected int or float, found {type(t_final)}.") + def temp_schedule(it: int, num_iters: int) -> float: - num = t_init*t_final*(math.log(num_iters+1)-math.log(2)) - denom = (t_final*math.log(num_iters+1)-t_init*math.log(2))+(t_init-t_final)*math.log(it+2) - return num/denom + num = t_init * t_final * (math.log(num_iters + 1) - math.log(2)) + denom = (t_final * math.log(num_iters + 1) - t_init * math.log(2)) + ( + t_init - t_final) * math.log(it + 2) + return num / denom + return temp_schedule @@ -785,14 +881,12 @@ def temp_schedule(it: int, num_iters: int) -> float: Names of the standard temperature schedules. """ - StandardTempSchedule = Tuple[StandardTempScheduleName, Union[int, float], Union[int, float]] """ Type for standard temperature schedules. """ - StandardTempSchedules: Final[Mapping[StandardTempScheduleName, TempScheduleProvider]] = { "linear": linear_temp_schedule, "geometric": geometric_temp_schedule, diff --git a/tests/test_pauli_annealing.py b/tests/test_pauli_annealing.py index c090749b..5b4ae4f1 100644 --- a/tests/test_pauli_annealing.py +++ b/tests/test_pauli_annealing.py @@ -15,6 +15,7 @@ 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, @@ -33,7 +34,7 @@ def pauli_poly_to_tket(pp: PauliPolynomial): for gadget in pp.pauli_gadgets: circuit.add_pauliexpbox( PauliExpBox([PAULI_TO_TKET[p] for p in gadget.paulis], - gadget.angle / np.pi), + gadget.angle.to_qiskit / np.pi), list(range(pp.num_qubits))) Transform.DecomposeBoxes().apply(circuit) return tket_to_qiskit(circuit) @@ -55,7 +56,7 @@ def verify_equality(qc_in, qc_out): def generate_all_combination_pauli_polynomial(n_qubits=2): - allowed_angels = [2 * np.pi, np.pi, 0.5 * np.pi, 0.25 * np.pi, 0.125 * np.pi] + 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) diff --git a/tests/test_pauli_propagation.py b/tests/test_pauli_propagation.py index 4a5d0dac..bed248f1 100644 --- a/tests/test_pauli_propagation.py +++ b/tests/test_pauli_propagation.py @@ -10,13 +10,15 @@ from pytket._tket.pauli import Pauli from qiskit import QuantumCircuit -from pauliopt.pauli.clifford_gates import CX, CY, CZ, H, S, V, generate_random_clifford, CliffordType, CliffordGate, \ +from pauliopt.pauli.clifford_gates import CX, CY, CZ, H, S, V, generate_random_clifford, \ + CliffordType, CliffordGate, \ ControlGate, SingleQubitGate, clifford_to_qiskit 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, @@ -35,7 +37,7 @@ def pauli_poly_to_tket(pp: PauliPolynomial): for gadget in pp.pauli_gadgets: circuit.add_pauliexpbox( PauliExpBox([PAULI_TO_TKET[p] for p in gadget.paulis], - gadget.angle / np.pi), + gadget.angle.to_qiskit / np.pi), list(range(pp.num_qubits))) Transform.DecomposeBoxes().apply(circuit) return tket_to_qiskit(circuit) @@ -57,7 +59,7 @@ def verify_equality(qc_in, qc_out): def generate_all_combination_pauli_polynomial(n_qubits=2): - allowed_angels = [2 * np.pi, np.pi, 0.5 * np.pi, 0.25 * np.pi, 0.125 * np.pi] + 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) @@ -104,7 +106,8 @@ def test_circuit_construction(self): "The resulting Quantum Circuits were not equivalent") self.assertTrue(check_matching_architecture(our_synth, topology.to_nx), "The Pauli Polynomial did not match the architecture") - self.assertEqual(get_two_qubit_count(our_synth), pp.two_qubit_count(topology), + self.assertEqual(get_two_qubit_count(our_synth), + pp.two_qubit_count(topology), "Two qubit count needs to be equivalent to to two qubit count of the circuit") def test_gate_propagation(self): diff --git a/tests/test_pauli_representation.py b/tests/test_pauli_representation.py index 5309a276..11701b29 100644 --- a/tests/test_pauli_representation.py +++ b/tests/test_pauli_representation.py @@ -3,20 +3,23 @@ from pauliopt.pauli.pauli_gadget import PPhase from pauliopt.pauli.pauli_polynomial import PauliPolynomial from pauliopt.pauli.utils import * +from pauliopt.utils import Angle, pi -_PAULI_REPR = "(0.5) @ { I, X, Y, Z }\n(0.25) @ { X, X, Y, X }" +_PAULI_REPR = "(π/2) @ { I, X, Y, Z }\n(π/4) @ { X, X, Y, X }" +SVG_CODE_PAULI = '\n\n\n\n\n\n\n\n\n\n\n\nπ\n\n\n\n\n\n\n\n\n\nπ/2\n\n\n\n\n\n\n\n\n\nπ/256\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nπ/8\n\n\n\n\n\n\n\n\n\nπ/4\n\n\n\n\n\n\n\n\n\nπ/2\n\n\n\n\n\n' class TestPauliConversion(unittest.TestCase): + def test_circuit_construction(self): pp = PauliPolynomial(4) - pp >>= PPhase(0.5) @ [I, X, Y, Z] + pp >>= PPhase(Angle(pi / 2)) @ [I, X, Y, Z] self.assertEqual(pp.num_qubits, 4) self.assertEqual(len(pp), 1) - pp >>= PPhase(0.25) @ [X, X, Y, X] + pp >>= PPhase(Angle(pi / 4)) @ [X, X, Y, X] self.assertEqual(pp.__repr__(), _PAULI_REPR) self.assertEqual(len(pp), 2) @@ -24,4 +27,17 @@ def test_circuit_construction(self): pp_ = PauliPolynomial(num_qubits=4) pp_ >> pp - self.assertEqual(pp.__repr__(), pp_.__repr__(), "Right shift resulted in different pauli Polynomials.") + self.assertEqual(pp.__repr__(), pp_.__repr__(), + "Right shift resulted in different pauli Polynomials.") + + def test_circuit_visualisation_svg(self): + pp = PauliPolynomial(5) + + pp >>= PPhase(Angle(pi)) @ [I, I, X, Z, Y] + pp >>= PPhase(Angle(pi/2)) @ [X, X, I, I, Y] + pp >>= PPhase(Angle(pi/256)) @ [X, I, I, Z, Y] + pp >>= PPhase(Angle(pi/8)) @ [X, X, X, Z, Y] + pp >>= PPhase(Angle(pi/4)) @ [X, Z, I, I, Y] + pp >>= PPhase(Angle(pi/2)) @ [X, I, I, Y, Y] + + self.assertEqual(pp.to_svg(svg_code_only=True), SVG_CODE_PAULI) From 78cf4457cf421271e5d3b8670d9123b7afecfa3a Mon Sep 17 00:00:00 2001 From: daehiff Date: Wed, 31 May 2023 22:51:38 +0200 Subject: [PATCH 27/30] added latex representation --- pauliopt/pauli/pauli_polynomial.py | 114 +++++++++++++++++++++++++++++ test.py | 21 ++++++ tests/test_pauli_representation.py | 100 +++++++++++++++++++++++-- 3 files changed, 230 insertions(+), 5 deletions(-) create mode 100644 test.py diff --git a/pauliopt/pauli/pauli_polynomial.py b/pauliopt/pauli/pauli_polynomial.py index f5e9a638..71b1cdec 100644 --- a/pauliopt/pauli/pauli_polynomial.py +++ b/pauliopt/pauli/pauli_polynomial.py @@ -6,6 +6,75 @@ from pauliopt.pauli.utils import X, Y, Z, I from pauliopt.utils import SVGBuilder +LATEX_HEADER = """ +\documentclass[preview]{standalone} + +\\usepackage{tikz} +\\usetikzlibrary{zx-calculus} +\\usetikzlibrary{quantikz} +\\usepackage{graphicx} + +\\tikzset{ +diagonal fill/.style 2 args={fill=#2, path picture={ +\\fill[#1, sharp corners] (path picture bounding box.south west) -| + (path picture bounding box.north east) -- cycle;}}, +reversed diagonal fill/.style 2 args={fill=#2, path picture={ +\\fill[#1, sharp corners] (path picture bounding box.north west) |- + (path picture bounding box.south east) -- cycle;}} +} + +\\tikzset{ +diagonal fill/.style 2 args={fill=#2, path picture={ +\\fill[#1, sharp corners] (path picture bounding box.south west) -| + (path picture bounding box.north east) -- cycle;}} +} + +\\tikzset{ +pauliY/.style={ +zxAllNodes, +zxSpiders, +inner sep=0mm, +minimum size=2mm, +shape=rectangle, +%fill=colorZxX +diagonal fill={colorZxX}{colorZxZ} +} +} + +\\tikzset{ +pauliX/.style={ +zxAllNodes, +zxSpiders, +inner sep=0mm, +minimum size=2mm, +shape=rectangle, +fill=colorZxX +} +} + +\\tikzset{ +pauliZ/.style={ +zxAllNodes, +zxSpiders, +inner sep=0mm, +minimum size=2mm, +shape=rectangle, +fill=colorZxZ +} +} + +\\tikzset{ +pauliPhase/.style={ +zxAllNodes, +zxSpiders, +inner sep=0.5mm, +minimum size=2mm, +shape=rectangle, +fill=white +} +} +""" + class PauliPolynomial: def __init__(self, num_qubits): @@ -167,3 +236,48 @@ def _repr_svg_(self): See https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html """ return self.to_svg(svg_code_only=True) + + def to_latex(self, file_name=None): + out_str = LATEX_HEADER + out_str += "\\begin{document}\n" + out_str += "\\begin{ZX}\n" + + angle_line = "\zxNone{} \t\t&" + + angle_pad_max = max( + [len(str(gadget.angle.repr_latex)) for gadget in self.pauli_gadgets]) + lines = {q: "\\zxNone{} \\rar \t&" for q in range(self.num_qubits)} + for gadget in self.pauli_gadgets: + assert isinstance(gadget, PauliGadget) + pad_ = ''.join([' ' for _ in range(self.num_qubits + 26)]) + pad_angle = "".join([' ' for _ in range(angle_pad_max - + len(str(gadget.angle.repr_latex)))]) + angle_line += f" \\zxNone{{}} {pad_}&" \ + f" |[pauliPhase]| {gadget.angle.repr_latex} {pad_angle}&" \ + f" \\zxNone{{}} &" + paulis = gadget.paulis + for q in range(self.num_qubits): + us = ''.join(['u' for _ in range(q)]) + + pad_angle = "".join([' ' for _ in range(angle_pad_max)]) + if paulis[q] != I: + pad_ = ''.join([' ' for _ in range(self.num_qubits - q)]) + lines[q] += f" |[pauli{paulis[q].value}]| " \ + f"\\ar[ruu{us}, bend right] \\rar {pad_}&" \ + f" \\zxNone{{}} \\rar {pad_angle} &" \ + f" \\zxNone{{}} \\rar &" + else: + pad_ = ''.join([' ' for _ in range(self.num_qubits + 22)]) + lines[q] += f" \\zxNone{{}} \\rar {pad_}& " \ + f"\\zxNone{{}} \\rar {pad_angle} & " \ + f"\\zxNone{{}} \\rar &" + out_str += angle_line + "\\\\ \n" + out_str += "\\\\ \n" + for q in range(self.num_qubits): + out_str += lines[q] + "\\\\ \n" + out_str += "\\end{ZX} \n" + out_str += "\\end{document}\n" + if file_name is not None: + with open(f"{file_name}.tex", "w") as f: + f.write(out_str) + return out_str diff --git a/test.py b/test.py new file mode 100644 index 00000000..0f662baf --- /dev/null +++ b/test.py @@ -0,0 +1,21 @@ +import unittest + +from pauliopt.pauli.pauli_gadget import PPhase +from pauliopt.pauli.pauli_polynomial import PauliPolynomial +from pauliopt.pauli.utils import * +from pauliopt.utils import Angle, pi + + + +def main(): + pp = PauliPolynomial(5) + + pp >>= PPhase(Angle(pi)) @ [I, I, X, Z, Y] + pp >>= PPhase(Angle(pi)) @ [I, I, X, Z, Y] + pp >>= PPhase(Angle(pi/2)) @ [I, I, X, Z, Y] + + print(pp.to_latex()) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/tests/test_pauli_representation.py b/tests/test_pauli_representation.py index 11701b29..4c39bbcf 100644 --- a/tests/test_pauli_representation.py +++ b/tests/test_pauli_representation.py @@ -9,6 +9,87 @@ SVG_CODE_PAULI = '\n\n\n\n\n\n\n\n\n\n\n\nπ\n\n\n\n\n\n\n\n\n\nπ/2\n\n\n\n\n\n\n\n\n\nπ/256\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nπ/8\n\n\n\n\n\n\n\n\n\nπ/4\n\n\n\n\n\n\n\n\n\nπ/2\n\n\n\n\n\n' +LATEX_CODE_PAULI = """ +\documentclass[preview]{standalone} + +\\usepackage{tikz} +\\usetikzlibrary{zx-calculus} +\\usetikzlibrary{quantikz} +\\usepackage{graphicx} + +\\tikzset{ +diagonal fill/.style 2 args={fill=#2, path picture={ +\\fill[#1, sharp corners] (path picture bounding box.south west) -| + (path picture bounding box.north east) -- cycle;}}, +reversed diagonal fill/.style 2 args={fill=#2, path picture={ +\\fill[#1, sharp corners] (path picture bounding box.north west) |- + (path picture bounding box.south east) -- cycle;}} +} + +\\tikzset{ +diagonal fill/.style 2 args={fill=#2, path picture={ +\\fill[#1, sharp corners] (path picture bounding box.south west) -| + (path picture bounding box.north east) -- cycle;}} +} + +\\tikzset{ +pauliY/.style={ +zxAllNodes, +zxSpiders, +inner sep=0mm, +minimum size=2mm, +shape=rectangle, +%fill=colorZxX +diagonal fill={colorZxX}{colorZxZ} +} +} + +\\tikzset{ +pauliX/.style={ +zxAllNodes, +zxSpiders, +inner sep=0mm, +minimum size=2mm, +shape=rectangle, +fill=colorZxX +} +} + +\\tikzset{ +pauliZ/.style={ +zxAllNodes, +zxSpiders, +inner sep=0mm, +minimum size=2mm, +shape=rectangle, +fill=colorZxZ +} +} + +\\tikzset{ +pauliPhase/.style={ +zxAllNodes, +zxSpiders, +inner sep=0.5mm, +minimum size=2mm, +shape=rectangle, +fill=white +} +} +\\begin{document} +\\begin{ZX} +\\zxNone{} & \\zxNone{} & |[pauliPhase]| \\pi & \\zxNone{} & \\zxNone{} & |[pauliPhase]| \\pi & \\zxNone{} & \\zxNone{} & |[pauliPhase]| \\frac{\\pi}{2} & \\zxNone{} &\\\\ +\\\\ +\\zxNone{} \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar &\\\\ +\\zxNone{} \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar &\\\\ +\\zxNone{} \\rar & |[pauliX]| \\ar[ruuuu, bend right] \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar & |[pauliX]| \\ar[ruuuu, bend right] \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar & |[pauliX]| \\ar[ruuuu, bend right] \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar &\\\\ +\\zxNone{} \\rar & |[pauliZ]| \\ar[ruuuuu, bend right] \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar & |[pauliZ]| \\ar[ruuuuu, bend right] \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar & |[pauliZ]| \\ar[ruuuuu, bend right] \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar &\\\\ +\\zxNone{} \\rar & |[pauliY]| \\ar[ruuuuuu, bend right] \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar & |[pauliY]| \\ar[ruuuuuu, bend right] \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar & |[pauliY]| \\ar[ruuuuuu, bend right] \\rar & \\zxNone{} \\rar & \\zxNone{} \\rar &\\\\ +\\end{ZX} +\\end{document} +""" + + class TestPauliConversion(unittest.TestCase): def test_circuit_construction(self): @@ -34,10 +115,19 @@ def test_circuit_visualisation_svg(self): pp = PauliPolynomial(5) pp >>= PPhase(Angle(pi)) @ [I, I, X, Z, Y] - pp >>= PPhase(Angle(pi/2)) @ [X, X, I, I, Y] - pp >>= PPhase(Angle(pi/256)) @ [X, I, I, Z, Y] - pp >>= PPhase(Angle(pi/8)) @ [X, X, X, Z, Y] - pp >>= PPhase(Angle(pi/4)) @ [X, Z, I, I, Y] - pp >>= PPhase(Angle(pi/2)) @ [X, I, I, Y, Y] + pp >>= PPhase(Angle(pi / 2)) @ [X, X, I, I, Y] + pp >>= PPhase(Angle(pi / 256)) @ [X, I, I, Z, Y] + pp >>= PPhase(Angle(pi / 8)) @ [X, X, X, Z, Y] + pp >>= PPhase(Angle(pi / 4)) @ [X, Z, I, I, Y] + pp >>= PPhase(Angle(pi / 2)) @ [X, I, I, Y, Y] self.assertEqual(pp.to_svg(svg_code_only=True), SVG_CODE_PAULI) + + def test_circuit_visualization_latex(self): + pp = PauliPolynomial(5) + + pp >>= PPhase(Angle(pi)) @ [I, I, X, Z, Y] + pp >>= PPhase(Angle(pi)) @ [I, I, X, Z, Y] + pp >>= PPhase(Angle(pi / 2)) @ [I, I, X, Z, Y] + + self.assertEqual(pp.to_latex(), LATEX_CODE_PAULI) From be45825640d1a659ff601ddf2296a95cf4dfd668 Mon Sep 17 00:00:00 2001 From: daehiff Date: Wed, 31 May 2023 22:57:28 +0200 Subject: [PATCH 28/30] added test tex to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 0752b089..a199ff94 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,6 @@ dmypy.json .vscode .DS_Store + +# specific test case .tex filde +./tests/test.tex \ No newline at end of file From 09eb71d3eaf24b7275249636ce150fece361bb1a Mon Sep 17 00:00:00 2001 From: daehiff Date: Wed, 31 May 2023 22:58:34 +0200 Subject: [PATCH 29/30] added tests for tex export --- .gitignore | 2 -- pauliopt/pauli/pauli_polynomial.py | 3 +-- tests/test_pauli_representation.py | 11 +++++++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index a199ff94..a76cfa98 100644 --- a/.gitignore +++ b/.gitignore @@ -132,5 +132,3 @@ dmypy.json .vscode .DS_Store -# specific test case .tex filde -./tests/test.tex \ No newline at end of file diff --git a/pauliopt/pauli/pauli_polynomial.py b/pauliopt/pauli/pauli_polynomial.py index 71b1cdec..d1ed96ac 100644 --- a/pauliopt/pauli/pauli_polynomial.py +++ b/pauliopt/pauli/pauli_polynomial.py @@ -6,8 +6,7 @@ from pauliopt.pauli.utils import X, Y, Z, I from pauliopt.utils import SVGBuilder -LATEX_HEADER = """ -\documentclass[preview]{standalone} +LATEX_HEADER = """\documentclass[preview]{standalone} \\usepackage{tikz} \\usetikzlibrary{zx-calculus} diff --git a/tests/test_pauli_representation.py b/tests/test_pauli_representation.py index 4c39bbcf..6bbcf48f 100644 --- a/tests/test_pauli_representation.py +++ b/tests/test_pauli_representation.py @@ -4,13 +4,13 @@ from pauliopt.pauli.pauli_polynomial import PauliPolynomial from pauliopt.pauli.utils import * from pauliopt.utils import Angle, pi +import os _PAULI_REPR = "(π/2) @ { I, X, Y, Z }\n(π/4) @ { X, X, Y, X }" SVG_CODE_PAULI = '\n\n\n\n\n\n\n\n\n\n\n\nπ\n\n\n\n\n\n\n\n\n\nπ/2\n\n\n\n\n\n\n\n\n\nπ/256\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nπ/8\n\n\n\n\n\n\n\n\n\nπ/4\n\n\n\n\n\n\n\n\n\nπ/2\n\n\n\n\n\n' -LATEX_CODE_PAULI = """ -\documentclass[preview]{standalone} +LATEX_CODE_PAULI = """\documentclass[preview]{standalone} \\usepackage{tikz} \\usetikzlibrary{zx-calculus} @@ -131,3 +131,10 @@ def test_circuit_visualization_latex(self): pp >>= PPhase(Angle(pi / 2)) @ [I, I, X, Z, Y] self.assertEqual(pp.to_latex(), LATEX_CODE_PAULI) + + pp.to_latex("test") + self.assertTrue(os.path.isfile("./test.tex")) + with open("./test.tex", "r") as f: + content = f.read() + self.assertEqual(LATEX_CODE_PAULI, content) + os.remove("./test.tex") \ No newline at end of file From 3d96b05227f3d89f6973a5c1c7fc5cab0f8184c0 Mon Sep 17 00:00:00 2001 From: daehiff Date: Wed, 31 May 2023 23:04:52 +0200 Subject: [PATCH 30/30] added viz --- tests/test_pauli_representation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pauli_representation.py b/tests/test_pauli_representation.py index 6bbcf48f..25cb3cc1 100644 --- a/tests/test_pauli_representation.py +++ b/tests/test_pauli_representation.py @@ -132,7 +132,7 @@ def test_circuit_visualization_latex(self): self.assertEqual(pp.to_latex(), LATEX_CODE_PAULI) - pp.to_latex("test") + pp.to_latex(file_name="test") self.assertTrue(os.path.isfile("./test.tex")) with open("./test.tex", "r") as f: content = f.read()