diff --git a/quafu/__init__.py b/quafu/__init__.py index a7d67369..3745276f 100644 --- a/quafu/__init__.py +++ b/quafu/__init__.py @@ -1,4 +1,5 @@ from .circuits.quantum_circuit import QuantumCircuit +from .algorithms.hamiltonian import Hamiltonian from .circuits.quantum_register import QuantumRegister, Qubit from .results.results import ExecResult, SimuResult from .simulators.simulator import simulate @@ -9,6 +10,7 @@ "QuantumCircuit", "QuantumRegister", "Qubit", + "Hamiltonian", "ExecResult", "Task", "User", diff --git a/quafu/algorithms/__init__.py b/quafu/algorithms/__init__.py index 2a33ee56..7d49cc76 100644 --- a/quafu/algorithms/__init__.py +++ b/quafu/algorithms/__init__.py @@ -5,4 +5,5 @@ from .hamiltonian import Hamiltonian from .templates.amplitude import AmplitudeEmbedding from .templates.angle import AngleEmbedding +from .templates.amplitude import AmplitudeEmbedding from .templates.basic_entangle import BasicEntangleLayers diff --git a/quafu/algorithms/ansatz.py b/quafu/algorithms/ansatz.py index 41e64f84..da844f96 100644 --- a/quafu/algorithms/ansatz.py +++ b/quafu/algorithms/ansatz.py @@ -21,7 +21,6 @@ from .hamiltonian import Hamiltonian from .interface_provider import InterfaceProvider -from .templates import AngleEmbedding class Ansatz(QuantumCircuit, ABC): @@ -44,10 +43,11 @@ def _build(self): class QAOAAnsatz(Ansatz): """QAOA Ansatz""" - def __init__(self, hamiltonian: Hamiltonian, num_layers: int = 1): + def __init__(self, hamiltonian: Hamiltonian, num_qubits: int, num_layers: int = 1): """Instantiate a QAOAAnsatz""" - self._pauli_list = hamiltonian.pauli_list - self._coeffs = hamiltonian.coeffs + # self._pauli_list = hamiltonian.pauli_list + # self._coeffs = hamiltonian.coeffs + self._h = hamiltonian self._num_layers = num_layers self._evol = ProductFormula() @@ -56,7 +56,6 @@ def __init__(self, hamiltonian: Hamiltonian, num_layers: int = 1): self._gamma = np.zeros(num_layers) # Build circuit structure - num_qubits = len(self._pauli_list[0]) super().__init__(num_qubits) @property @@ -68,7 +67,7 @@ def parameters(self): """Return complete parameters of the circuit""" paras_list = [] for layer in range(self._num_layers): - for _ in range(len(self._pauli_list)): + for _ in range(len(self._h.paulis)): paras_list.append(self._gamma[layer]) for _ in range(self.num): paras_list.append(self._beta[layer]) @@ -86,8 +85,8 @@ def _build(self): for layer in range(self._num_layers): # Add H_D layer - for pauli_str in self._pauli_list: - gate_list = self._evol.evol(pauli_str, self._gamma[layer]) + for pauli in self._h.paulis: + gate_list = self._evol.evol(pauli, self._gamma[layer]) for g in gate_list: self.add_ins(g) @@ -156,10 +155,6 @@ def __init__(self, num_qubits: int, layers: List[Any], interface="torch"): self._weights = np.empty((1, 1)) super().__init__(num_qubits) - def __call__(self, features): - """Compute outputs of QNN given input features""" - return self._transformer.execute(self, features) - def _build(self): """Essentially initialize weights using transformer""" for layer in self._layers: diff --git a/quafu/algorithms/estimator.py b/quafu/algorithms/estimator.py index e94ee5c0..eaa5d207 100644 --- a/quafu/algorithms/estimator.py +++ b/quafu/algorithms/estimator.py @@ -13,24 +13,17 @@ # limitations under the License. """Pre-build wrapper to calculate expectation value""" from typing import List, Optional - -import numpy as np -from quafu.algorithms.hamiltonian import Hamiltonian -from quafu.simulators.simulator import simulate -from quafu.tasks.tasks import Task - -from quafu import QuantumCircuit +from ..circuits.quantum_circuit import QuantumCircuit +from ..tasks.tasks import Task +from .hamiltonian import Hamiltonian +from ..simulators.simulator import simulate def execute_circuit(circ: QuantumCircuit, observables: Hamiltonian): """Execute circuit on quafu simulator""" - sim_state = simulate(circ, output="state_vector").get_statevector() - - expectation = np.matmul( - np.matmul(sim_state.conj().T, observables.get_matrix()), sim_state - ).real - - return expectation + sim_res = simulate(circ, output="state_vector") + expectations = sim_res.expect_paulis(observables) + return sum(expectations) class Estimator: @@ -91,11 +84,6 @@ def run(self, observables: Hamiltonian, params: List[float]): Returns: Expectation value """ - if observables.num_qubits != self._circ.num: - raise ValueError( - "The number of qubits in the observables does not match the circuit" - ) - if params is not None: self._circ.update_params(params) diff --git a/quafu/algorithms/gradients/vjp.py b/quafu/algorithms/gradients/vjp.py index aa314e11..1096ff8a 100644 --- a/quafu/algorithms/gradients/vjp.py +++ b/quafu/algorithms/gradients/vjp.py @@ -24,9 +24,8 @@ def _generate_expval_z(num_qubits: int): obs_list = [] - base_pauli = "I" * num_qubits for i in range(num_qubits): - pauli = base_pauli[:i] + "Z" + base_pauli[i + 1 :] + pauli = "Z" + str(i) obs_list.append(Hamiltonian.from_pauli_list([(pauli, 1)])) return obs_list diff --git a/quafu/algorithms/hamiltonian.py b/quafu/algorithms/hamiltonian.py index a4e1402b..7ea31783 100644 --- a/quafu/algorithms/hamiltonian.py +++ b/quafu/algorithms/hamiltonian.py @@ -11,97 +11,189 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Quafu Hamiltonian class""" -from __future__ import annotations +from typing import Iterable +import scipy.sparse as sp +import numpy as np -from collections.abc import Iterable +from quafu.exceptions.quafu_error import QuafuError -import numpy as np -from quafu.elements.matrices import IdMatrix, XMatrix, YMatrix, ZMatrix -from quafu.exceptions import QuafuError +IMat = sp.coo_matrix(np.array([[1.0, 0.0], [0.0, 1.0]], dtype=complex)) -PAULI_MAT = {"I": IdMatrix, "X": XMatrix, "Y": YMatrix, "Z": ZMatrix} +XMat = sp.coo_matrix(np.array([[0.0, 1.0], [1.0, 0.0]], dtype=complex)) +YMat = sp.coo_matrix(np.array([[0.0, -1.0j], [1.0j, 0.0]], dtype=complex)) -class Hamiltonian: - """TODO""" +ZMat = sp.coo_matrix(np.array([[1.0, 0.0], [0.0, -1.0]], dtype=complex)) - def __init__(self, pauli_str_list: list[str], coeffs: np.ndarray) -> None: - """ - Args: - pauli_str_list: List of Pauli strs, e.g., ['IIIZZ', "IIZIZ", ...] - coeffs: List of efficients - """ - self._pauli_str_list = pauli_str_list - self._coeffs = coeffs +PauliMats = {"X": XMat, "Y": YMat, "Z": ZMat, "I": IMat} + + +class PauliOp: + def __init__(self, paulis: str, coeff: complex = 1.0): + paulist = paulis.split(" ") + self.paulistr = "" + self.pos = [] + for p in paulist: + assert p[0] in "XYZ" + self.paulistr += p[0] + self.pos.append(int(p[1:])) + self.coeff = coeff + + def __repr__(self): + repstr = "" + if self.coeff != 1.0: + repstr = str(self.coeff) + "*" + for i in range(len(self.pos)): + repstr += self.paulistr[i] + repstr += str(self.pos[i]) + repstr += "*" + return repstr[:-1] + + def __str__(self): + return self.__repr__() + + def __mul__(self, obj): + pass + + def __rmul__(self, obj): + pass + + def commutator(self, obj): + pass - @property - def num_qubits(self): - """Get the number of qubits of the system""" - return len(self.pauli_list[0]) + def get_matrix(self, qnum, big_endian=False): + if qnum - 1 < max(self.pos): + raise ValueError("The support of the paulis exceed the total qubit number") - @property - def pauli_list(self): - """Get pauli string list""" - return self._pauli_str_list + pos = np.array(self.pos) + if not big_endian: + pos = qnum - 1 - pos + inds = np.argsort(pos) + iq = 0 + ip = 0 + mat = 1.0 + while iq < qnum: + if ip < len(pos): + if iq == pos[inds[ip]]: + opstr = self.paulistr[inds[ip]] + mat = sp.kron(mat, PauliMats[opstr]) + iq += 1 + ip += 1 + else: + mat = sp.kron(mat, PauliMats["I"]) + iq += 1 + else: + mat = sp.kron(mat, PauliMats["I"]) + iq += 1 - @property - def coeffs(self): - """Get coefficients of each pauli string""" - return self._coeffs + return self.coeff * mat + + +class Hamiltonian: + def __init__(self, paulis: list[PauliOp]): + self.paulis = paulis @staticmethod - def from_pauli_list(pauli_list: Iterable[tuple[str, complex]]) -> Hamiltonian: - """ + def from_pauli_list(pauli_list: Iterable[tuple[str, complex]]): + """Generate Hamiltonian from Pauli string list + Args: - pauli: The supported format of pauli list is [(, )], - e.g., [('IIIZZ', 1), ("IIZIZ", 1), ...)], 0th qubit is farthest right + pauli_list: e.g., [("Z0 Z1", 1), ("Z0 Z2", 1), ...] """ - pauli_list = list(pauli_list) size = len(pauli_list) if size == 0: raise QuafuError("Pauli list cannot be empty.") - coeffs = np.zeros(size, dtype=complex) + pauli_op_list = [PauliOp(p[0], coeff=p[1]) for p in pauli_list] + + return Hamiltonian(pauli_op_list) - pauli_str_list = [] - for i, (pauli_str, coef) in enumerate(pauli_list): - pauli_str_list.append(pauli_str) - coeffs[i] = coef + def __repr__(self): + return "+".join([str(pauli) for pauli in self.paulis]) - return Hamiltonian(pauli_str_list, coeffs) + def __str__(self): + return self.__repr__() + + def get_matrix(self, qnum, big_endian=False): + mat = 0.0 + for pauli in self.paulis: + mat += pauli.get_matrix(qnum, big_endian) + + return mat # TODO(zhaoyilun): delete this in the future def to_legacy_quafu_pauli_list(self): """Transform to legacy quafu pauli list format, this is a temperal function and should be deleted later""" res = [] - for pauli_str in self._pauli_str_list: - for i, pauli in enumerate(pauli_str[::-1]): - if pauli in ["X", "Y", "Z"]: - res.append([pauli, [i]]) + for pauli_str in self.paulis: + for i, pos in enumerate(pauli_str.pos): + res.append([pauli_str.paulistr[i], [pos]]) return res - def _get_pauli_mat(self, pauli_str: str): - """Calculate the matrix of a pauli string""" - mat = None - for pauli in pauli_str[::-1]: - mat = PAULI_MAT[pauli] if mat is None else np.kron(PAULI_MAT[pauli], mat) - return mat - - def matrix_generator(self): - """Generating matrix for each Pauli str""" - for i, pauli_str in enumerate(self._pauli_str_list): - yield self._coeffs[i] * self._get_pauli_mat(pauli_str) - - def get_matrix(self): - """Generate matrix of Hamiltonian""" - dim = 2**self.num_qubits - matrix = np.zeros((dim, dim), dtype=complex) - for mat in self.matrix_generator(): - matrix += mat - return matrix +def intersec(a, b): + inter = [] + aind = [] + bind = [] + for i in range(len(a)): + for j in range(len(b)): + if a[i] == b[j]: + inter.append(a[i]) + aind.append(i) + bind.append(j) + + return inter, aind, bind + + +def diff(a, b): + diff = [] + aind = [] + for i in range(len(a)): + if a[i] not in b: + diff.append(a[i]) + aind.append(i) + + return diff, aind + + +def merge_paulis(obslist): + measure_basis = [] + targ_basis = [] + for obs in obslist: + if len(measure_basis) == 0: + measure_basis.append(obs) + targ_basis.append(len(measure_basis) - 1) + else: + added = 0 + for mi in range(len(measure_basis)): + measure_base = measure_basis[mi] + interset, intobsi, intbasei = intersec(obs.pos, measure_base.pos) + diffset, diffobsi = diff(obs.pos, measure_base.pos) + if not len(interset) == 0: + if all( + np.array(list(obs.paulistr))[intobsi] + == np.array(list(measure_base.paulistr))[intbasei] + ): + measure_base.paulistr += "".join( + np.array(list(obs.paulistr))[diffobsi] + ) + measure_base.pos.extend(diffset) + targ_basis.append(mi) + added = 1 + break + else: + measure_base.paulistr += obs.paulistr + measure_base.pos.extend(obs.pos) + targ_basis.append(mi) + added = 1 + break + + if not added: + measure_basis.append(obs) + targ_basis.append(len(measure_basis) - 1) + + return measure_basis, targ_basis diff --git a/quafu/algorithms/templates/__init__.py b/quafu/algorithms/templates/__init__.py index 0e1b8490..e69de29b 100644 --- a/quafu/algorithms/templates/__init__.py +++ b/quafu/algorithms/templates/__init__.py @@ -1,16 +0,0 @@ -# (C) Copyright 2023 Beijing Academy of Quantum Information Sciences -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from .angle import AngleEmbedding -from .basic_entangle import BasicEntangleLayers diff --git a/quafu/algorithms/templates/amplitude.py b/quafu/algorithms/templates/amplitude.py index e36b550f..58fee471 100644 --- a/quafu/algorithms/templates/amplitude.py +++ b/quafu/algorithms/templates/amplitude.py @@ -11,10 +11,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + """Amplitude Embedding by a decomposition into gates""" -import numpy as np -import quafu.elements.element_gates as qeg from quafu.circuits import QuantumCircuit +import quafu.elements.element_gates as qeg +import numpy as np class AmplitudeEmbedding: diff --git a/quafu/circuits/quantum_circuit.py b/quafu/circuits/quantum_circuit.py index 253a1d83..684e3cb0 100644 --- a/quafu/circuits/quantum_circuit.py +++ b/quafu/circuits/quantum_circuit.py @@ -12,16 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +import copy from contextlib import contextmanager -from typing import Any, List +from typing import Any, Iterable, List, Optional import numpy as np import quafu.elements.element_gates as qeg -from quafu.elements import Measure, Reset from quafu.elements.classical_element import Cif from quafu.elements.instruction import Instruction +from quafu.elements import Measure, Reset +from quafu.elements.parameters import ParameterType from quafu.elements.pulses import QuantumPulse - +from quafu.elements.oracle import OracleGate from ..elements import ( Barrier, ControlledGate, @@ -36,12 +38,12 @@ from .quantum_register import QuantumRegister -class QuantumCircuit(object): +class QuantumCircuit: """ Representation of quantum circuit. """ - def __init__(self, qnum: int, cnum: int = None, *args, **kwargs): + def __init__(self, qnum: int, cnum: Optional[int] = None, *args, **kwargs): """ Initialize a QuantumCircuit object @@ -72,6 +74,11 @@ def parameterized_gates(self): def num(self): return sum([len(qreg) for qreg in self.qregs]) + @num.setter + def num(self, num: int): + self.qregs = [QuantumRegister(num)] if num > 0 else [] + self.cregs = [ClassicalRegister(num)] if num > 0 else [] + @property def cbits_num(self): return sum([len(creg) for creg in self.cregs]) @@ -104,6 +111,15 @@ def gates(self): def gates(self, gates: list): self._gates = gates + def __lshift__(self, operation: Instruction): + max_pos = max(operation.pos) if isinstance(operation.pos, Iterable) else operation.pos + if max_pos >= self.num: + raise CircuitError("Operation act on qubit that not allocated") + self.add_ins(operation) + if isinstance(operation, OracleGate): + self._has_wrap = True + return self + # TODO(qtzhuang): add_gates is just a temporary call function to add gate from gate_list def add_gates(self, gates: list): for gate in gates: @@ -118,6 +134,15 @@ def add_gate(self, gate: QuantumGate): raise CircuitError(f"Gate position out of range: {gate.pos}") self.gates.append(gate) + def add_pulse(self, pulse: QuantumPulse, pos: int = None) -> "QuantumCircuit": + """ + Add quantum gate from pulse. + """ + if pos is not None: + pulse.set_pos(pos) + self.add_ins(pulse) + return self + def add_ins(self, ins: Instruction): """ Add instruction to circuit, with NO checking yet. @@ -390,6 +415,53 @@ def wrap_to_gate(self, name: str): customized = customize_gate(name, gate_structure, self.num) return customized + def _reallocate(self, num, qbits: List[int]): + """Remap the qubits and operations to new positions. """ + assert self.num == len(qbits) + if max(qbits) > num: + raise CircuitError("Bad allocation") + + self.num = num + qbits_map = dict(zip(range(len(qbits)), qbits)) + operations = self.instructions + for op in operations: + for i in range(len(op.pos)): + op.pos[i] = qbits_map[op.pos[i]] + + if isinstance(op, ControlledGate): + for i in range(len(op.ctrls)): + op.ctrls[i] = qbits_map[op.ctrls[i]] + + for i in range(len(op.targs)): + op.targs[i] = qbits_map[op.targs[i]] + + def add_controls(self, ctrlnum, ctrls: List[int] = None, targs: List[int] = None) -> "QuantumCircuit": + """Append control- qubits to the circuit. + + Several last qubits appended as ctrl-qubits by default. If ctrls and targs are provided, + reallocated in order of ctrls + targs. + """ + num = self.num + ctrlnum + qc = QuantumCircuit(num) + if ctrls is None and targs is None: + ctrls = list(range(self.num, self.num + ctrlnum)) + targs = list(range(self.num)) + do_reallocate = False + else: + if not (ctrls is not None and targs is not None): + raise ValueError("Args ctrls and targs must provide simultaneously.") + assert len(targs) == self.num + assert len(ctrls) == ctrlnum + do_reallocate = True + + for op in self.gates: + qc << op.ctrl_by(ctrls) + + if do_reallocate: + qc._reallocate(num, ctrls + targs) + return qc + + # # # # # # # # # # # # # # helper functions # # # # # # # # # # # # # # def id(self, pos: int) -> "QuantumCircuit": """ Identity gate. @@ -546,7 +618,7 @@ def sw(self, pos: int) -> "QuantumCircuit": self.add_ins(qeg.SWGate(pos)) return self - def rx(self, pos: int, para: float) -> "QuantumCircuit": + def rx(self, pos: int, para: ParameterType) -> "QuantumCircuit": """ Single qubit rotation Rx gate. @@ -557,7 +629,7 @@ def rx(self, pos: int, para: float) -> "QuantumCircuit": self.add_ins(qeg.RXGate(pos, para)) return self - def ry(self, pos: int, para: float) -> "QuantumCircuit": + def ry(self, pos: int, para: ParameterType) -> "QuantumCircuit": """ Single qubit rotation Ry gate. @@ -568,7 +640,7 @@ def ry(self, pos: int, para: float) -> "QuantumCircuit": self.add_ins(qeg.RYGate(pos, para)) return self - def rz(self, pos: int, para: float) -> "QuantumCircuit": + def rz(self, pos: int, para: ParameterType) -> "QuantumCircuit": """ Single qubit rotation Rz gate. @@ -579,7 +651,7 @@ def rz(self, pos: int, para: float) -> "QuantumCircuit": self.add_ins(qeg.RZGate(pos, para)) return self - def p(self, pos: int, para: float) -> "QuantumCircuit": + def p(self, pos: int, para: ParameterType) -> "QuantumCircuit": """ Phase gate @@ -652,7 +724,7 @@ def ct(self, ctrl: int, tar: int) -> "QuantumCircuit": self.add_ins(qeg.CTGate(ctrl, tar)) return self - def cp(self, ctrl: int, tar: int, para: float) -> "QuantumCircuit": + def cp(self, ctrl: int, tar: int, para: ParameterType) -> "QuantumCircuit": """ Control-P gate. @@ -930,12 +1002,3 @@ def cif(self, cbits: List[int], condition: int): return else: instructions.append(self.instructions[i]) - - def add_pulse(self, pulse: QuantumPulse, pos: int = None) -> "QuantumCircuit": - """ - Add quantum gate from pulse. - """ - if pos is not None: - pulse.set_pos(pos) - self.add_ins(pulse) - return self diff --git a/quafu/elements/__init__.py b/quafu/elements/__init__.py index 7e5a2b67..b6b391d4 100644 --- a/quafu/elements/__init__.py +++ b/quafu/elements/__init__.py @@ -1,5 +1,6 @@ -from .instruction import Instruction, Barrier, Measure, Reset -from .pulses import Delay, XYResonance, QuantumPulse -from .quantum_gate import QuantumGate, ControlledGate, MultiQubitGate, SingleQubitGate from .classical_element import Cif +from .instruction import Barrier, Instruction, Measure, Reset +from .pulses import Delay, QuantumPulse, XYResonance +from .quantum_gate import ControlledGate, MultiQubitGate, QuantumGate, SingleQubitGate from .unitary import UnitaryDecomposer +from .utils import extract_float, reorder_matrix diff --git a/quafu/elements/element_gates/__init__.py b/quafu/elements/element_gates/__init__.py index 4b0d9d31..222b84fd 100644 --- a/quafu/elements/element_gates/__init__.py +++ b/quafu/elements/element_gates/__init__.py @@ -1,4 +1,6 @@ -from .element_gates import * +from .element_gates import XGate, YGate, ZGate, IdGate, WGate, HGate, SGate, SdgGate, TGate, TdgGate, SXGate, SXdgGate, SYGate, SYdgGate, SWGate, SWdgGate, RXGate, RYGate, RZGate, RXXGate, RYYGate, RZZGate, SwapGate, ISwapGate, CXGate, CYGate, CZGate, CSGate, CTGate, CPGate, ToffoliGate, FredkinGate, MCXGate, MCYGate, MCZGate +from .element_gates import PhaseGate + __all__ = [ "XGate", @@ -20,6 +22,7 @@ "RXGate", "RYGate", "RZGate", + "PhaseGate" "RXXGate", "RYYGate", "RZZGate", @@ -35,5 +38,5 @@ "FredkinGate", "MCXGate", "MCYGate", - "MCZGate" + "MCZGate", ] diff --git a/quafu/elements/element_gates/clifford.py b/quafu/elements/element_gates/clifford.py index 0ca22c5b..c8fa0b9f 100644 --- a/quafu/elements/element_gates/clifford.py +++ b/quafu/elements/element_gates/clifford.py @@ -1,9 +1,5 @@ from .element_gates import HGate, SGate, CXGate -""" -TODO: add documentation about Clifford gates. -""" - __all__ = ['HGate', 'SGate', 'CXGate'] diff --git a/quafu/elements/element_gates/rotation.py b/quafu/elements/element_gates/rotation.py new file mode 100644 index 00000000..54d83671 --- /dev/null +++ b/quafu/elements/element_gates/rotation.py @@ -0,0 +1,118 @@ +from typing import Dict + +from quafu.elements.parameters import ParameterType +from quafu.elements.matrices import ( + pmatrix, + rx_mat, + rxx_mat, + ry_mat, + ryy_mat, + rz_mat, + rzz_mat, +) + +from ..quantum_gate import ParametricGate, QuantumGate, SingleQubitGate + +__all__ = ["RXGate", "RYGate", "RZGate", "RXXGate", "RYYGate", "RZZGate", "PhaseGate"] + + +@QuantumGate.register("rx") +class RXGate(ParametricGate, SingleQubitGate): + name = "RX" + + def __init__(self, pos: int, paras: ParameterType = 0.0): + ParametricGate.__init__(self, pos, paras=paras) + + @property + def matrix(self): + return rx_mat(self.paras) + + +@QuantumGate.register("ry") +class RYGate(ParametricGate, SingleQubitGate): + name = "RY" + + def __init__(self, pos: int, paras: ParameterType = 0.0): + ParametricGate.__init__(self, pos, paras=paras) + + @property + def matrix(self): + return ry_mat(self.paras) + + +@QuantumGate.register("rz") +class RZGate(ParametricGate, SingleQubitGate): + name = "RZ" + + def __init__(self, pos: int, paras: ParameterType = 0.0): + ParametricGate.__init__(self, pos, paras=paras) + + @property + def matrix(self): + return rz_mat(self.paras) + + +@QuantumGate.register("rxx") +class RXXGate(ParametricGate): + name = "RXX" + + def __init__(self, q1: int, q2: int, paras: ParameterType = 0.0): + ParametricGate.__init__(self, [q1, q2], paras=paras) + + @property + def matrix(self): + return rxx_mat(self.paras) + + @property + def named_pos(self) -> Dict: + return {"pos": self.pos} + + +@QuantumGate.register("ryy") +class RYYGate(ParametricGate): + name = "RYY" + + def __init__(self, q1: int, q2: int, paras: ParameterType = 0.0): + ParametricGate.__init__(self, [q1, q2], paras=paras) + + @property + def matrix(self): + return ryy_mat(self.paras) + + @property + def named_pos(self) -> Dict: + return {"pos": self.pos} + + +@QuantumGate.register("rzz") +class RZZGate(ParametricGate): + name = "RZZ" + + def __init__(self, q1: int, q2: int, paras: ParameterType = 0.0): + ParametricGate.__init__(self, [q1, q2], paras=paras) + + @property + def matrix(self): + return rzz_mat(self.paras) + + @property + def named_pos(self) -> Dict: + return {"pos": self.pos} + + +@SingleQubitGate.register(name="p") +class PhaseGate(SingleQubitGate): + """Ally of rz gate, but with a different name and global phase.""" + + name = "P" + + def __init__(self, pos: int, paras: ParameterType = 0.0): + super().__init__(pos, paras=paras) + + @property + def matrix(self): + return pmatrix(self.paras) + + @property + def named_paras(self) -> Dict: + return {"phase": self.paras} diff --git a/quafu/elements/instruction.py b/quafu/elements/instruction.py index 58ae2f31..7994fcb2 100644 --- a/quafu/elements/instruction.py +++ b/quafu/elements/instruction.py @@ -13,12 +13,13 @@ # limitations under the License. from abc import ABC, abstractmethod -from typing import Dict, List, Union +from typing import Dict, List, Optional, Union -__all__ = ["Instruction", "Barrier", "Measure", "PosType", "ParaType", "Reset"] +from .parameters import ParameterType + +__all__ = ["Instruction", "Barrier", "Measure", "PosType", "Reset"] PosType = Union[int, List[int]] -ParaType = Union[float, int, List] class Instruction(ABC): @@ -32,9 +33,16 @@ class Instruction(ABC): ins_classes = {} - def __init__(self, pos: PosType, paras: ParaType = None, *args, **kwargs): + def __init__( + self, + pos: PosType, + paras: Optional[Union[ParameterType, List[ParameterType]]] = None, + *args, + **kwargs, + ): self.pos = pos self.paras = paras + self._symbol = None @property @abstractmethod diff --git a/quafu/elements/matrices/mat_lib.py b/quafu/elements/matrices/mat_lib.py index 6be737df..57b29d87 100644 --- a/quafu/elements/matrices/mat_lib.py +++ b/quafu/elements/matrices/mat_lib.py @@ -1,17 +1,31 @@ import numpy as np IdMatrix = np.eye(2, dtype=complex) -XMatrix = np.array([[0.0, 1.0], [1.0, 0.0]], dtype=complex) -YMatrix = np.array([[0.0, -1.0j], [1.0j, 0.0]], dtype=complex) -ZMatrix = np.array([[1.0, 0.0], [0.0, -1.0]], dtype=complex) -SMatrix = np.array([[1.0, 0.0], [0.0, 1.0j]], dtype=complex) -SXMatrix = np.array([[1.0, 1.0j], [1.0j, 1.0]], dtype=complex) / np.sqrt(2) -SYMatrix = np.array([[1.0, -1.0], [1.0, 1.0]], dtype=complex) / np.sqrt(2) -TMatrix = np.array([[1.0, 0.0], [0.0, np.exp(1.0j * np.pi / 4)]], dtype=complex) +XMatrix = np.array( + [[0.0, 1.0], + [1.0, 0.0]], dtype=complex) +YMatrix = np.array( + [[0.0, -1.0j], + [1.0j, 0.0]], dtype=complex) +ZMatrix = np.array( + [[1.0, 0.0], + [0.0, -1.0]], dtype=complex) +SMatrix = np.array( + [[1.0, 0.0], + [0.0, 1.0j]], dtype=complex) +SXMatrix = np.array( + [[1.0, 1.0j], + [1.0j, 1.0]], dtype=complex) / np.sqrt(2) +SYMatrix = np.array( + [[1.0, -1.0], + [1.0, 1.0]], dtype=complex) / np.sqrt(2) +TMatrix = np.array( + [[1.0, 0.0], + [0.0, np.exp(1.0j * np.pi / 4)]], dtype=complex) WMatrix = (XMatrix + YMatrix) / np.sqrt(2) SWMatrix = np.array( - [[0.5 + 0.5j, -np.sqrt(0.5) * 1j], [np.sqrt(0.5), 0.5 + 0.5j]], dtype=complex -) + [[0.5 + 0.5j, -np.sqrt(0.5) * 1j], + [np.sqrt(0.5), 0.5 + 0.5j]], dtype=complex) HMatrix = (XMatrix + ZMatrix) / np.sqrt(2) SwapMatrix = np.array( [ @@ -86,31 +100,6 @@ ) -def u2matrix(_phi=0.0, _lambda=0.0): - """OpenQASM 3.0 specification""" - return np.array( - [ - [1.0, np.exp(-1.0j * _lambda)], - [np.exp(1.0j * _phi), np.exp((_phi + _lambda) * 1.0j)], - ], - dtype=complex, - ) - - -def u3matrix(_theta=0.0, _phi=0.0, _lambda=0.0): - """OpenQASM 3.0 specification""" - return np.array( - [ - [np.cos(0.5 * _theta), -np.exp(_lambda * 1.0j) * np.sin(0.5 * _theta)], - [ - np.exp(_phi * 1.0j) * np.sin(0.5 * _theta), - np.exp((_phi + _lambda) * 1.0j) * np.cos(0.5 * _theta), - ], - ], - dtype=complex, - ) - - def rx_mat(theta): return np.array( [ @@ -171,19 +160,37 @@ def rzz_mat(theta): ) -# def su2_matrix(gamma: float, beta: float, delta: float): -# """ -# SU = Rz(beta)Ry(gamma)Rz(delta). -# -# Symbol convention is the same as in the textbook -# of Chuang and Nielsen. -# """ -# s, c = np.sin(gamma / 2), np.cos(gamma / 2) -# alpha1, alpha2 = (delta + beta) / 2, (delta - beta) / 2 -# su2_mat = np.array([[np.exp(-1.j * alpha1) * c, -np.exp(-1.j * alpha2) * s], -# [np.exp(1.j * alpha2) * s, np.exp(1.j * alpha1) * c]]) -# return su2_mat -# -# -# def u2_matrix(alpha: float, gamma: float, beta: float, delta: float): -# return np.exp(1.j * alpha) * su2_matrix(gamma, beta, delta) +def u2matrix(_phi=0.0, _lambda=0.0): + """OpenQASM 3.0 specification""" + return np.array( + [ + [1.0, np.exp(-1.0j * _lambda)], + [np.exp(1.0j * _phi), np.exp((_phi + _lambda) * 1.0j)], + ], + dtype=complex, + ) + + +def u3matrix(_theta=0.0, _phi=0.0, _lambda=0.0): + """OpenQASM 3.0 specification""" + return np.array( + [ + [np.cos(0.5 * _theta), -np.exp(_lambda * 1.0j) * np.sin(0.5 * _theta)], + [ + np.exp(_phi * 1.0j) * np.sin(0.5 * _theta), + np.exp((_phi + _lambda) * 1.0j) * np.cos(0.5 * _theta), + ], + ], + dtype=complex, + ) + + +mat_dict = {'id': IdMatrix, 'x': XMatrix, 'y': YMatrix, 'z': ZMatrix, + 'h': HMatrix, 'w': WMatrix, + 's': SMatrix, 't': TMatrix, + 'cx': CXMatrix, 'cnot': CXMatrix, 'cy': CYMatrix, 'cz': CZMatrix, + 'swap': SwapMatrix, 'iswap': ISwapMatrix, + 'rx': rx_mat, 'ry': ry_mat, 'rz': rz_mat, 'p': pmatrix, + 'rxx': rxx_mat, 'ryy': ryy_mat, 'rzz': rzz_mat, + 'u2': u2matrix, 'u3': u3matrix + } diff --git a/quafu/elements/oracle.py b/quafu/elements/oracle.py index 794eb3df..070bf0e0 100644 --- a/quafu/elements/oracle.py +++ b/quafu/elements/oracle.py @@ -16,7 +16,7 @@ from abc import ABCMeta from typing import Dict, Iterable, List -from quafu.elements import Instruction, QuantumGate +from quafu.elements import Instruction, QuantumGate, ControlledGate class OracleGateMeta(ABCMeta): @@ -41,6 +41,12 @@ def __init__(cls, name, bases, attrs): class OracleGate(QuantumGate): # TODO: Can it be related to OracleGateMeta explicitly? """ OracleGate is a gate that can be customized by users. + + Attributes: + name: name of the gate + gate_structure: structure of the gate + qubit_num: number of qubits the gate acts on + insides: instances of instructions inside the gate from gate_structure """ name = None @@ -107,10 +113,29 @@ def map_pos(pos): self.insides.append(gate_) +class ControlledOracle(OracleGate): + def __init__(self, ctrls: List, targs: List, paras=None, label: str = None): + super().__init__(pos=ctrls + targs, paras=paras, label=label) + self.ctrls = ctrls + self.targs = targs + self.__check_insides__() + + def __check_insides__(self): + """Make sure that every gate inside is a controlled gate, and the control qubits correct.""" + for gate in self.insides: + if not isinstance(gate, ControlledGate): + raise ValueError(f"ControlledOracle: {gate} is not a controlled gate.") + if not set(self.ctrls) in gate.ctrls: + raise ValueError( + f"ControlledOracle: {gate} control qubits {gate.ctrls} does not match {self.ctrls}." + ) + + def customize_gate( cls_name: str, gate_structure: List[Instruction], qubit_num: int, + controlled: bool = False, ): """ Helper function to create customized gate class @@ -119,6 +144,7 @@ def customize_gate( cls_name: name of the gate class gate_structure: a list of instruction INSTANCES qubit_num: number of qubits of the gate (TODO: extract from gate_structure?) + controlled: whether the gate is a ctrl-U Returns: customized gate class @@ -131,11 +157,13 @@ def customize_gate( attrs = { "cls_name": cls_name, - "gate_structure": gate_structure, + "gate_structure": gate_structure, # TODO: translate "qubit_num": qubit_num, } - - customized_cls = OracleGateMeta(cls_name, (OracleGate,), attrs) + if controlled: + customized_cls = type(cls_name, (ControlledOracle,), attrs) + else: + customized_cls = OracleGateMeta(cls_name, (OracleGate,), attrs) assert issubclass(customized_cls, OracleGate) QuantumGate.register_gate(customized_cls, cls_name) return customized_cls diff --git a/quafu/elements/parameters.py b/quafu/elements/parameters.py new file mode 100644 index 00000000..335be6d8 --- /dev/null +++ b/quafu/elements/parameters.py @@ -0,0 +1,260 @@ +import copy +from typing import Union + +import _operator +import autograd.numpy as anp +from autograd import grad + + +class ParameterExpression: + def __init__(self, pivot: "Parameter", value=0.0, operands=[], funcs=[], latex=""): + self.pivot = pivot + self.value = value + self.operands = operands + self.funcs = funcs + self.latex = latex # TODO:Generate latex source code when apply operation + + @property + def _variables(self): + vars = {self.pivot: 0} + vi = 1 + for o in self.operands: + if isinstance(o, Parameter): + if o not in vars.keys(): + vars[o] = vi + vi += 1 + elif isinstance(o, ParameterExpression): + for v in o._variables: + if v not in vars.keys(): + vars[v] = vi + vi += 1 + return vars + + def _undo(self, step): + for _ in range(step): + if len(self.operands) > 0: + self.operands.pop() + self.funcs.pop() + else: + return self.pivot + return self + + @property + def _func(self): + vars = self._variables + + def __func(x): + z = x[0] + for i in range(len(self.funcs)): + f = self.funcs[i] + op = self.operands[i] + if op is None: + z = f(z) + elif isinstance(op, float) or isinstance(op, int): + z = f(z, op) + elif isinstance(op, Parameter): + z = f(z, x[vars[op]]) + elif isinstance(op, ParameterExpression): + opvars = op._variables.keys() + varind = [vars[ov] for ov in opvars] + z = f(z, op._func(x[varind])) + else: + raise NotImplementedError + return z + + return __func + + def __repr__(self): + return str(self.get_value()) + + def grad(self, input=anp.array([])): + g = grad(self._func) + if len(input) == 0: + input = anp.array([v.value for v in self._variables]) + gd = g(input) + return gd + + def get_value(self): + input = anp.array([v.value for v in self._variables]) + value = self._func(input) + self.value = value + return value + + def __add__(self, r): + operands = [opr for opr in self.operands] + funcs = copy.deepcopy(self.funcs) + operands.append(r) + funcs.append(_operator.add) + v = 0.0 + if isinstance(r, Parameter) or isinstance(r, ParameterExpression): + v = self.value + r.value + elif isinstance(r, float) or isinstance(r, int): + v = self.value + r + else: + raise NotImplementedError + + return ParameterExpression(self.pivot, v, operands, funcs) + + def __mul__(self, r): + operands = [opr for opr in self.operands] + funcs = copy.deepcopy(self.funcs) + operands.append(r) + funcs.append(_operator.mul) + v = 0.0 + if isinstance(r, Parameter) or isinstance(r, ParameterExpression): + v = self.value * r.value + elif isinstance(r, float) or isinstance(r, int): + v = self.value * r + else: + raise NotImplementedError + + return ParameterExpression(self.pivot, v, operands, funcs) + + def __rmul__(self, r): + return self * r + + def __neg__(self): + return -1.0 * self + + def __sub__(self, r): + operands = [opr for opr in self.operands] + funcs = copy.deepcopy(self.funcs) + operands.append(r) + funcs.append(_operator.sub) + v = 0.0 + if isinstance(r, Parameter) or isinstance(r, ParameterExpression): + v = self.value - r.value + elif isinstance(r, float) or isinstance(r, int): + v = self.value - r + else: + raise NotImplementedError + + return ParameterExpression(self.pivot, v, operands, funcs) + + def __rsub__(self, r): + return r - self + + def __truediv__(self, r): + operands = [opr for opr in self.operands] + funcs = copy.deepcopy(self.funcs) + operands.append(r) + funcs.append(_operator.truediv) + v = 0.0 + if isinstance(r, Parameter) or isinstance(r, ParameterExpression): + v = self.value / r.value + elif isinstance(r, float) or isinstance(r, int): + v = self.value / r + else: + raise NotImplementedError + + return ParameterExpression(self.pivot, v, operands, funcs) + + def __rtruediv__(self, r): + return r / self + + def __pow__(self, n): + operands = [opr for opr in self.operands] + funcs = copy.deepcopy(self.funcs) + operands.append(n) + funcs.append(_operator.truediv) + v = 0.0 + if isinstance(n, float) or isinstance(n, int): + v = self**n + else: + raise NotImplementedError + return ParameterExpression(self.pivot, v, operands, funcs) + + def sin(self): + operands = [opr for opr in self.operands] + funcs = copy.deepcopy(self.funcs) + operands.append(None) + funcs.append(anp.sin) + v = anp.sin(self.value) + return ParameterExpression(self.pivot, v, operands, funcs) + + def cos(self): + operands = [opr for opr in self.operands] + funcs = copy.deepcopy(self.funcs) + operands.append(None) + funcs.append(anp.cos) + v = anp.cos(self.value) + return ParameterExpression(self.pivot, v, operands, funcs) + + def tan(self): + operands = [opr for opr in self.operands] + funcs = copy.deepcopy(self.funcs) + operands.append(None) + funcs.append(anp.tan) + v = anp.tan(self.value) + return ParameterExpression(self.pivot, v, operands, funcs) + + def arcsin(self): + operands = [opr for opr in self.operands] + funcs = copy.deepcopy(self.funcs) + operands.append(None) + funcs.append(anp.arcsin) + v = anp.arcsin(self.value) + return ParameterExpression(self.pivot, v, operands, funcs) + + def arccos(self): + operands = [opr for opr in self.operands] + funcs = copy.deepcopy(self.funcs) + operands.append(None) + funcs.append(anp.arccos) + v = anp.arccos(self.value) + return ParameterExpression(self.pivot, v, operands, funcs) + + def arctan(self): + operands = [opr for opr in self.operands] + funcs = copy.deepcopy(self.funcs) + operands.append(None) + funcs.append(anp.arctan) + v = anp.arctan(self.value) + return ParameterExpression(self.pivot, v, operands, funcs) + + def exp(self): + operands = [opr for opr in self.operands] + funcs = copy.deepcopy(self.funcs) + operands.append(None) + funcs.append(anp.exp) + v = anp.exp(self.value) + return ParameterExpression(self.pivot, v, operands, funcs) + + def log(self): + operands = [opr for opr in self.operands] + funcs = copy.deepcopy(self.funcs) + operands.append(None) + funcs.append(anp.log) + v = anp.log(self.value) + return ParameterExpression(self.pivot, v, operands, funcs) + + +class Parameter(ParameterExpression): + def __init__(self, name, value: float = 0.0): + self.name = name + self.value = float(value) + self.operands = [] + self.funcs = [] + self.latex = self.name + + @property + def pivot(self): + return self + + def grad(self): + return anp.array([1.0]) + + def get_value(self): + return self.value + + def __hash__(self): + return hash((self.name)) + + def __eq__(self, other): + return (self.name) == (other.name) + + def __repr__(self): + return f"{self.name}({self.value})" + + +ParameterType = Union[float, Parameter, ParameterExpression] diff --git a/quafu/elements/quantum_gate.py b/quafu/elements/quantum_gate.py index d4ba8e45..a2482cad 100644 --- a/quafu/elements/quantum_gate.py +++ b/quafu/elements/quantum_gate.py @@ -1,13 +1,60 @@ +# (C) Copyright 2023 Beijing Academy of Quantum Information Sciences +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import copy from abc import ABC, abstractmethod -from typing import List, Union, Iterable, Dict, Callable +from typing import Callable, Dict, Iterable, List, Optional, Union import numpy as np - +from numpy import ndarray from quafu.elements.matrices.mat_utils import reorder_matrix -from .instruction import Instruction, PosType - -__all__ = ['QuantumGate', 'FixedGate', 'ParametricGate', 'SingleQubitGate', 'MultiQubitGate', 'ControlledGate'] +from .instruction import Instruction, PosType +from .parameters import ParameterType +from .utils import extract_float + +__all__ = [ + "QuantumGate", + "FixedGate", + "ParametricGate", + "SingleQubitGate", + "MultiQubitGate", + "ControlledGate", +] + +HERMITIAN = [ + "id", + "x", + "y", + "z", + "h", + "w", + "cx", + "cz", + "cy", + "cnot", + "swap", + "mcx", + "mxy", + "mcz", +] +ROTATION = ["rx", "ry", "rz", "p", "rxx", "ryy", "rzz", "cp"] +paired = {k: k + "dg" for k in ["sx", "sy", "s", "t", "sw"]} +PAIRED = {**paired, **{v: k for k, v in paired.items()}} + +MatrixType = Union[np.ndarray, Callable] class QuantumGate(Instruction, ABC): @@ -19,50 +66,47 @@ class QuantumGate(Instruction, ABC): paras: Parameters of this gate. Properties: + symbol: Text symbolic representation of this gate. matrix: Matrix representation of this gate. + + Functions: + register_gate: Register a new gate class. + to_qasm: Convert this gate to QASM format. + update_paras: Update the parameters of this gate. + """ + gate_classes = {} - def __init__(self, - pos: PosType, - paras: Union[float, List[float]] = None, - matrix: Union[np.ndarray, Callable] = None, - ): + def __init__( + self, + pos: PosType, + paras: Optional[Union[ParameterType, List[ParameterType]]] = None, + matrix: Optional[Union[ndarray, Callable]] = None, + ): super().__init__(pos, paras) - self._matrix = matrix self._symbol = None + self._matrix = matrix - @property - def symbol(self): - if self._symbol is not None: - return self._symbol - if self.paras is not None: - if isinstance(self.paras, Iterable): - symbol = "%s(" % self.name + ",".join(["%.3f" % para for para in self.paras]) + ")" - else: - symbol = "%s(%.3f)" % (self.name, self.paras) + def __str__(self): + # only when the gate is a known(named) gate, the matrix is not shown + if self.name.lower() in self.gate_classes: + properties_names = ["pos", "paras"] else: - symbol = "%s" % self.name - return symbol - - @symbol.setter - def symbol(self, symbol): - self._symbol = symbol - - def update_params(self, paras: Union[float, List[float]]): - """Update parameters of this gate""" - if paras is None: - return - self.paras = paras + properties_names = ["pos", "paras", "matrix"] + properties_values = [getattr(self, x) for x in properties_names] + return "%s:\n%s" % ( + self.__class__.__name__, + "\n".join( + [ + f"{x} = {repr(properties_values[i])}" + for i, x in enumerate(properties_names) + ] + ), + ) - @property - @abstractmethod - def matrix(self): - if self._matrix is not None: - return self._matrix - else: - raise NotImplementedError("Matrix is not implemented for %s" % self.__class__.__name__ + - ", this should never happen.") + def __repr__(self): + return f"{self.__class__.__name__}" @classmethod def register_gate(cls, subclass, name: str = None): @@ -83,26 +127,51 @@ def register_gate(cls, subclass, name: str = None): @classmethod def register(cls, name: str = None): """Decorator for register_gate.""" + def wrapper(subclass): cls.register_gate(subclass, name) return subclass return wrapper - def __str__(self): - # only when the gate is a known(named) gate, the matrix is not shown - if self.name.lower() in self.gate_classes: - properties_names = ['pos', 'paras'] + @property + def _paras(self): + return extract_float(self.paras) + + @property + def symbol(self) -> str: + """Symbol used in text-drawing.""" + if self._symbol is not None: + return self._symbol + + # TODO: Use latex repr for Parameter + if self.paras is not None: + symbol = ( + "%s(" % self.name + + ",".join(["%.3f" % para for para in self._paras]) + + ")" + ) + return symbol else: - properties_names = ['pos', 'paras', 'matrix'] - properties_values = [getattr(self, x) for x in properties_names] - return "%s:\n%s" % (self.__class__.__name__, '\n'.join( - [f"{x} = {repr(properties_values[i])}" for i, x in enumerate(properties_names)])) + return "%s" % self.name - def __repr__(self): - return f"{self.__class__.__name__}" + @symbol.setter + def symbol(self, symbol: str): + self._symbol = symbol + + @property + @abstractmethod + def matrix(self): + if self._matrix is not None: + return self._matrix + else: + raise NotImplementedError( + "Matrix is not implemented for %s" % self.__class__.__name__ + + ", this should never happen." + ) - def to_qasm(self): + def to_qasm(self) -> str: + """OPENQASM 2.0""" # TODO: support register naming qstr = "%s" % self.name.lower() @@ -119,14 +188,130 @@ def to_qasm(self): return qstr + def update_params(self, paras: Union[ParameterType, List[ParameterType]]): + """Update parameters of this gate""" + if paras is None: + return + self.paras = paras + + # # # # # # # # # # # # algebraic operations # # # # # # # # # # # # + def power(self, n) -> "QuantumGate": + """Return another gate equivalent to n-times operation of the present gate.""" + name = self.name.lower() + name = 'sz' if name == 's' else name + + order4 = ["sx", "sy", "s", 'sw'] + order4 += [_+'dg' for _ in order4] + order8 = ["t", "tdg"] + + if name in HERMITIAN: + if n % 2 == 0: + return self.gate_classes["id"](self.pos) + else: + return copy.deepcopy(self) + elif name in order4: # ["sx", "sy", "s", "t", "sw"] + if n % 4 == 0: + return self.gate_classes["id"](self.pos) + elif n % 4 == 1: + return copy.deepcopy(self) + elif n % 4 == 2: + _square_name = 'z' if name == 's' else name[1:] + return self.gate_classes[_square_name](self.pos) + elif n % 4 == 3: + _conj_name = PAIRED[name] + return self.gate_classes[_conj_name](self.pos) + elif name in order8: # ["t", "tdg"] + # note: here we transform a fixed gate into a parametric gate + # which might cause error in future + theta = np.pi / 4 + if name.endswith("dg"): + theta = -theta + return self.gate_classes["rz"](self.pos, theta * n) + elif name in ROTATION: + return self.gate_classes[name](self.pos, self.paras * n) + else: + from .oracle import OracleGate + + if not isinstance(self, OracleGate): + raise NotImplementedError( + f"Power is not implemented for {self.__class__.__name__}" + ) + else: + gate = copy.deepcopy(self) + gate.gate_structure = [gate.power(n) for gate in self.gate_structure] + + def dagger(self) -> "QuantumGate": + """Return the hermitian conjugate gate with same the position.""" + name = self.name + if name in HERMITIAN: # Hermitian gate + return copy.deepcopy(self) + if name in ROTATION: # rotation gate + return self.gate_classes[name](self.pos, -self.paras) + elif name in PAIRED: # pairwise-occurrence gate + _conj_name = PAIRED[name] + return self.gate_classes[_conj_name](self.pos) + else: + from .oracle import OracleGate + + if not isinstance(self, OracleGate): + raise NotImplementedError( + f"Power is not implemented for {self.__class__.__name__}" + ) + else: + gate = copy.deepcopy(self) + gate.gate_structure = [gate.dagger() for gate in self.gate_structure] + raise NotImplementedError + + def ctrl_by(self, ctrls: Union[int, List[int]]) -> "QuantumGate": + """Return a controlled gate with present gate as the controlled target.""" + ctrls = [ctrls] if not isinstance(ctrls, list) else ctrls + pos = [self.pos] if not isinstance(self.pos, list) else self.pos + name = self.name.lower() + + if isinstance(self, ControlledGate): + """ + [m1]control-([m2]control-U) = [m1+m2]control-U + """ + ctrls = list(set(self.ctrls) | set(ctrls)) + elif set(ctrls) & set(pos): + raise ValueError("Control qubits should not be overlap with target qubits.") + + if len(ctrls) == 1 and len(pos) == 1: # ctrl-single-qubit gate + cname = "c" + name + if cname not in self.gate_classes: + raise NotImplementedError( + f"ctrl-by is not implemented for {self.__class__.__name__}" + ) + else: + cop = self.gate_classes[cname](ctrls[0], pos[0]) + elif name in ["mcx", "mcy", "mcz"]: + cname = name + cop = self.gate_classes[cname](ctrls, self.pos) + elif name in ["x", "y", "z"]: + cname = "mc" + self.name.lower() + cop = self.gate_classes[cname](ctrls, self.pos) + else: + from .oracle import OracleGate + + if not isinstance(self, OracleGate): + raise NotImplementedError( + f"ctrl-by is not implemented for {self.__class__.__name__}" + ) + else: + cop = copy.deepcopy(self) + cop.insides = [gate.dagger() for gate in self.gate_structure] + + return cop + # Gate types below are statically implemented to support type identification # and provide shared attributes. However, single/multi qubit may be # inferred from ``pos``, while para/fixed type may be inferred by ``paras``. # Therefore, these types may be (partly) deprecated in the future. + class SingleQubitGate(QuantumGate, ABC): - def __init__(self, pos: int, paras: float = None): + def __init__(self, pos: int, paras: Optional[ParameterType] = None): QuantumGate.__init__(self, pos=pos, paras=paras) def get_targ_matrix(self): @@ -134,17 +319,15 @@ def get_targ_matrix(self): @property def named_pos(self) -> Dict: - return {'pos': self.pos} + return {"pos": self.pos} class MultiQubitGate(QuantumGate, ABC): - def __init__(self, pos: List, paras: float = None): + def __init__(self, pos: List, paras: Optional[ParameterType] = None): QuantumGate.__init__(self, pos, paras) def get_targ_matrix(self, reverse_order=False): - """ - - """ + """ """ targ_matrix = self.matrix if reverse_order and (len(self.pos) > 1): @@ -158,18 +341,18 @@ def get_targ_matrix(self, reverse_order=False): class ParametricGate(QuantumGate, ABC): - def __init__(self, pos: PosType, paras: Union[float, List[float]]): + def __init__(self, pos: PosType, paras: Union[ParameterType, List[ParameterType]]): if paras is None: raise ValueError("`paras` can not be None for ParametricGate") super().__init__(pos, paras) @property def named_paras(self) -> Dict: - return {'paras': self.paras} + return {"paras": self.paras} @property def named_pos(self) -> Dict: - return {'pos': self.pos} + return {"pos": self.pos} class FixedGate(QuantumGate, ABC): @@ -181,16 +364,25 @@ def named_paras(self) -> Dict: return {} -class ControlledGate(MultiQubitGate, ABC): - """ Controlled gate class, where the matrix act non-trivially on target qubits""" +class ControlledGate(MultiQubitGate): + """Controlled gate class, where the matrix act non-trivially on target qubits""" - def __init__(self, targe_name, ctrls: List[int], targs: List[int], paras, tar_matrix): + def __init__( + self, + targ_name: str, + ctrls: PosType, + targs: PosType, + paras: Optional[Union[ParameterType, List[ParameterType]]] = None, + tar_matrix: MatrixType = None, + ): MultiQubitGate.__init__(self, ctrls + targs, paras) self.ctrls = ctrls self.targs = targs - self.targ_name = targe_name + self.targ_name = targ_name self._targ_matrix = tar_matrix + self.__build_matrix__() + def __build_matrix__(self): # set matrix # TODO: change matrix according to control-type 0/1 c_n, t_n, n = self.ct_nums @@ -201,17 +393,16 @@ def __init__(self, targe_name, ctrls: List[int], targs: List[int], paras, tar_ma self._matrix[ctrl_dim:, ctrl_dim:] = self.targ_matrix self._matrix = reorder_matrix(self._matrix, self.pos) + @property + def name(self) -> str: + return "c" + self.targ_name + @property def symbol(self): - name = self.targ_name - if self.paras is not None: - if isinstance(self.paras, Iterable): - symbol = "%s(" % name + ",".join(["%.3f" % para for para in self.paras]) + ")" - else: - symbol = "%s(%.3f)" % (name, self.paras) + if self._symbol is not None: + return self._symbol else: - symbol = "%s" % name - return symbol + return self.targ_name @symbol.setter def symbol(self, symbol): @@ -249,4 +440,100 @@ def get_targ_matrix(self, reverse_order=False): @property def named_pos(self) -> Dict: - return {'ctrls': self.ctrls, 'targs': self.targs} + return {"ctrls": self.ctrls, "targs": self.targs} + + @property + def named_paras(self) -> Dict: + return {"paras": self.paras} + + @classmethod + def from_target(cls, targ: QuantumGate, ctrls: PosType): + return cls(targ.name, ctrls, targ.pos, targ.paras, targ.matrix) + +# TODO(ChenWei): update OracleGate so that compatible with CtrlGate +# class CircuitWrapper(QuantumGate): +# def __init__(self, name: str, circ, qbits=[]): +# self.name = name +# self.pos = list(range(circ.num)) +# self.circuit = copy.deepcopy(circ) +# +# # TODO:Handle wrapper paras +# # if hasattr(circ, "paras"): +# # self._paras = circ.paras +# # else: +# # self._paras = [] +# # for op in self.circuit.operations: +# # self._paras.extend(op.paras) +# +# if qbits: +# self._reallocate(qbits) +# +# # @property +# # def paras(self): +# # return self._paras +# +# # @paras.setter +# # def paras(self, __paras): +# # self._paras = __paras +# # self.circuit.paras = __paras +# +# def _reallocate(self, qbits): +# num = max(self.circuit.num - 1, max(qbits)) + 1 +# self.pos = qbits +# self.circuit._reallocate(num, qbits) +# +# @property +# def symbol(self): +# return "%s" % self.name +# +# def add_controls(self, ctrls: List[int] = []) -> QuantumGate: +# return ControlCircuitWrapper("MC" + self.name, self, ctrls) +# +# def power(self, n: int): +# self.name += "^%d" % n +# self.circuit = self.circuit.power(n) +# return self +# +# def dagger(self): +# self.name += "^†" +# self.circuit = self.circuit.dagger() +# return self +# +# def to_qasm(self): +# qasm = "" +# for operation in self.circuit.operations: +# qasm += operation.to_qasm() + ";\n" +# return qasm +# +# +# class ControlCircuitWrapper(CircuitWrapper): +# def __init__(self, name: str, circwrp: CircuitWrapper, ctrls: List[int]): +# self.name = name +# self.ctrls = ctrls +# self.targs = circwrp.pos +# self.circuit = circwrp.circuit.add_controls(len(ctrls), ctrls, self.targs) +# self.pos = list(range(self.circuit.num)) +# self._targ_name = circwrp.name +# +# @property +# def symbol(self): +# return "%s" % self._targ_name +# +# # def power(self, n: int): +# # self._targ_name += "^%d" % n +# # return super().power(n) +# # +# # def dagger(self): +# # self.name += "^†" +# # return super().dagger() +# +# def _reallocate(self, qbits): +# num = max(self.circuit.num - 1, max(qbits)) + 1 +# self.pos = qbits +# self.circuit._reallocate(num, qbits) +# qbits_map = dict(zip(range(len(qbits)), qbits)) +# for i in range(len(self.ctrls)): +# self.ctrls[i] = qbits_map[self.ctrls[i]] +# +# for i in range(len(self.targs)): +# self.targs[i] = qbits_map[self.targs[i]] diff --git a/quafu/elements/utils.py b/quafu/elements/utils.py new file mode 100644 index 00000000..528b26dc --- /dev/null +++ b/quafu/elements/utils.py @@ -0,0 +1,42 @@ +# (C) Copyright 2023 Beijing Academy of Quantum Information Sciences +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from typing import Iterable, List, Union + +import numpy as np + +from .parameters import Parameter, ParameterExpression + + +def reorder_matrix(matrix: np.ndarray, pos: List): + """Reorder the input sorted matrix to the pos order""" + qnum = len(pos) + dim = 2**qnum + inds = np.argsort(pos) + inds = np.concatenate([inds, inds + qnum]) + tensorm = np.reshape(matrix, [2] * 2 * qnum) + return np.transpose(tensorm, inds).reshape([dim, dim]) + + +def extract_float(paras): + if not isinstance(paras, Iterable): + paras = [paras] + paras_f = [] + for para in paras: + if isinstance(para, float) or isinstance(para, int): + paras_f.append(para) + elif isinstance(para, Parameter) or isinstance(para, ParameterExpression): + paras_f.append(para.get_value()) + return paras_f diff --git a/quafu/results/results.py b/quafu/results/results.py index d913d55a..fc497127 100644 --- a/quafu/results/results.py +++ b/quafu/results/results.py @@ -2,7 +2,7 @@ from collections import OrderedDict import matplotlib.pyplot as plt - +from ..algorithms.hamiltonian import Hamiltonian from ..utils.basis import * @@ -161,6 +161,12 @@ def calculate_obs(self, pos): res_reduced = dict(zip(basis, probs)) return measure_obs(pos, res_reduced) + def expect_paulis(self, hamiltonian: Hamiltonian): + """Calculate expectation value given a Hamiltonian""" + from quafu.simulators.qfvm import expect_statevec + + return expect_statevec(self.state_vector, hamiltonian.paulis) + def intersec(a, b): inter = [] diff --git a/quafu/simulators/simulator.py b/quafu/simulators/simulator.py index 609b2822..32d386b6 100644 --- a/quafu/simulators/simulator.py +++ b/quafu/simulators/simulator.py @@ -128,14 +128,14 @@ def simulate( rho = permutebits(rho, values) return SimuResult(rho, output, count_dict) - elif output == "probabilities": + if output == "probabilities": if simulator in ["qfvm_circ", "qfvm_qasm"]: psi = permutebits(psi, range(num)[::-1]) probabilities = ptrace(psi, measures) probabilities = permutebits(probabilities, values) return SimuResult(probabilities, output, count_dict) - elif output == "state_vector": + if output == "state_vector": return SimuResult(psi, output, count_dict) elif output == "count_dict": diff --git a/quafu/synthesis/evolution.py b/quafu/synthesis/evolution.py index 76d07fca..900ac2ce 100644 --- a/quafu/synthesis/evolution.py +++ b/quafu/synthesis/evolution.py @@ -16,35 +16,31 @@ from abc import ABC, abstractmethod import numpy as np +from quafu.algorithms.hamiltonian import PauliOp import quafu.elements.element_gates as qeg -def single_qubit_evol(pauli: str, time: float): +def single_qubit_evol(pauli: PauliOp, time: float): """ Args: pauli: Pauli string (little endian convention) time: Evolution time """ - reversed_pauli = pauli[::-1] gates = [] - for i, pauli_i in enumerate(reversed_pauli): - if pauli_i == "I": - continue - elif pauli_i == "X": - gates.append(qeg.RXGate(i, 2 * time)) - return gates - elif pauli_i == "Y": - gates.append(qeg.RYGate(i, 2 * time)) - return gates - elif pauli_i == "Z": - gates.append(qeg.RZGate(i, 2 * time)) - return gates - else: - raise NotImplementedError("Pauli string not yet supported") + if pauli.paulistr == "X": + gates.append(qeg.RXGate(pauli.pos[0], 2 * time)) + return gates + if pauli.paulistr == "Y": + gates.append(qeg.RYGate(pauli.pos[0], 2 * time)) + return gates + if pauli.paulistr == "Z": + gates.append(qeg.RZGate(pauli.pos[0], 2 * time)) + return gates + raise NotImplementedError("Unsupported Pauli string, should be in [X, Y, Z]") -def two_qubit_evol(pauli: str, time: float, cx_structure: str = "chain"): +def two_qubit_evol(pauli: PauliOp, time: float, cx_structure: str = "chain"): """ Args: pauli: Pauli string (little endian convention) @@ -52,25 +48,22 @@ def two_qubit_evol(pauli: str, time: float, cx_structure: str = "chain"): cx_structure: Determine the structure of CX gates, can be either "chain" for next-neighbor connections or "fountain" to connect directly to the top qubit. """ - reversed_pauli = pauli[::-1] - qubits = [i for i in range(len(reversed_pauli)) if reversed_pauli[i] != "I"] - labels = np.array([reversed_pauli[i] for i in qubits]) gates = [] - if all(labels == "X"): - gates.append(qeg.RXXGate(qubits[0], qubits[1], 2 * time)) - elif all(labels == "Y"): - gates.append(qeg.RYYGate(qubits[0], qubits[1], 2 * time)) - elif all(labels == "Z"): - gates.append(qeg.RZZGate(qubits[0], qubits[1], 2 * time)) + if pauli.paulistr == "XX": + gates.append(qeg.RXXGate(pauli.pos[0], pauli.pos[1], 2 * time)) + elif pauli.paulistr == "YY": + gates.append(qeg.RYYGate(pauli.pos[0], pauli.pos[1], 2 * time)) + elif pauli.paulistr == "ZZ": + gates.append(qeg.RZZGate(pauli.pos[0], pauli.pos[1], 2 * time)) else: return multi_qubit_evol(pauli, time, cx_structure) return gates -def multi_qubit_evol(pauli: str, time: float, cx_structure: str = "chain"): +def multi_qubit_evol(pauli: PauliOp, time: float, cx_structure: str = "chain"): # determine whether the Pauli string consists of Pauli operators - if not all(pauli_char in "XYZI" for pauli_char in pauli): + if not all(pauli_char in "XYZI" for pauli_char in pauli.paulistr): raise NotImplementedError("Pauli string not yet supported") gates = [] # get diagonalizing clifford gate list @@ -86,10 +79,8 @@ def multi_qubit_evol(pauli: str, time: float, cx_structure: str = "chain"): target = None # Note that all phases are removed from the pauli label and are only in the coefficients. # That's because the operators we evolved have all been translated to a SparsePauliOp. - for i, pauli_i in enumerate(pauli[::-1]): - if pauli_i != "I": - target = i - break + sorted_pos = sorted(pauli.pos) + target = sorted_pos[0] # build the evolution as: diagonalization, reduction, 1q evolution, followed by inverses gates.extend(cliff) @@ -101,7 +92,7 @@ def multi_qubit_evol(pauli: str, time: float, cx_structure: str = "chain"): return gates -def diagonalizing_clifford(pauli: str): +def diagonalizing_clifford(pauli: PauliOp): """Get the clifford gate list to diagonalize the Pauli operator. Args: @@ -110,22 +101,21 @@ def diagonalizing_clifford(pauli: str): Returns: A gate list for clifford. """ - reversed_pauli = pauli[::-1] gates = [] gates_inverse = [] - for i, pauli_i in enumerate(reversed_pauli): - if pauli_i == "Y": - gates.append(qeg.SdgGate(i)) - gates_inverse.append(qeg.SGate(i)) - if pauli_i in ["X", "Y"]: - gates.append(qeg.HGate(i)) - gates_inverse.append(qeg.HGate(i)) + for i, pos in enumerate(pauli.pos): + if pauli.paulistr[i] == "Y": + gates.append(qeg.SdgGate(pos)) + gates_inverse.append(qeg.SGate(pos)) + if pauli.paulistr[i] in ["X", "Y"]: + gates.append(qeg.HGate(pos)) + gates_inverse.append(qeg.HGate(pos)) gates_inverse = gates_inverse[::-1] return gates, gates_inverse -def cnot_chain(pauli: str): +def cnot_chain(pauli: PauliOp): """CX chain. For example, for the Pauli with the label 'XYZIX'. @@ -152,23 +142,21 @@ def cnot_chain(pauli: str): control, target = None, None # iterate over the Pauli's and add CNOTs - for i, pauli_i in enumerate(pauli): - i = len(pauli) - i - 1 - if pauli_i != "I": - if control is None: - control = i - else: - target = i + for pos in sorted(pauli.pos): + if control is None: + control = pos + else: + target = pos if control is not None and target is not None: gates.append(qeg.CXGate(control, target)) - control = i + control = pos target = None return gates, gates[::-1] -def cnot_fountain(pauli: str): +def cnot_fountain(pauli: PauliOp): """CX chain in the fountain shape. For example, for the Pauli with the label 'XYZIX'. @@ -193,12 +181,12 @@ def cnot_fountain(pauli: str): gates = [] control, target = None, None - for i, pauli_i in enumerate(pauli[::-1]): - if pauli_i != "I": - if target is None: - target = i - else: - control = i + + for pos in sorted(pauli.pos): + if target is None: + target = pos + else: + control = pos if control is not None and target is not None: gates.append(qeg.CXGate(control, target)) @@ -227,8 +215,8 @@ def evol(self, pauli: str, time: float): class ProductFormula(BaseEvolution): """Product formula for decomposition of operator exponentials""" - def evol(self, pauli: str, time: float): - num_non_id = len([label for label in pauli if label != "I"]) + def evol(self, pauli: PauliOp, time: float): + num_non_id = len(pauli.paulistr) if num_non_id == 0: pass diff --git a/requirements.txt b/requirements.txt index 72c69e3a..4441adcf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ scikit-build>=0.16.1 scipy>=1.8.1 setuptools>=58.0.4 sparse>=0.13.0 +autograd>+1.6.2 \ No newline at end of file diff --git a/setup.py b/setup.py index 1f8e3573..6e8a42f7 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,7 @@ "pybind11>=2.10.3", "graphviz>=0.14.2", "ply~=3.11", + "autograd>=1.6.2", ] setup( diff --git a/tests/quafu/algorithms/amplitude_test.py b/tests/quafu/algorithms/amplitude_test.py index e194e531..5019ed39 100644 --- a/tests/quafu/algorithms/amplitude_test.py +++ b/tests/quafu/algorithms/amplitude_test.py @@ -11,10 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import numpy as np +from quafu.circuits import QuantumCircuit import quafu.elements.element_gates as qeg from quafu.algorithms import AmplitudeEmbedding -from quafu.circuits import QuantumCircuit +import numpy as np class TestAmplitudeEmbedding: diff --git a/tests/quafu/algorithms/ansatz_test.py b/tests/quafu/algorithms/ansatz_test.py index 8413322c..2ec408e8 100644 --- a/tests/quafu/algorithms/ansatz_test.py +++ b/tests/quafu/algorithms/ansatz_test.py @@ -15,14 +15,16 @@ import numpy as np from quafu.algorithms.ansatz import AlterLayeredAnsatz, QAOAAnsatz, QuantumNeuralNetwork -from quafu.algorithms.hamiltonian import Hamiltonian +from quafu.algorithms.hamiltonian import Hamiltonian, PauliOp class TestQAOACircuit: - TEST_HAM = Hamiltonian(["IIZZ", "ZZII", "IZZI", "ZIIZ"], np.array([1, 1, 1, 1])) + TEST_HAM = Hamiltonian( + [PauliOp("Z0 Z1"), PauliOp("Z2 Z3"), PauliOp("Z1 Z2"), PauliOp("Z0 Z3")] + ) def test_build(self): - qaoa = QAOAAnsatz(self.TEST_HAM) + qaoa = QAOAAnsatz(self.TEST_HAM, 4) print("\n ::: testing ::: \n") qaoa.draw_circuit() diff --git a/tests/quafu/algorithms/estimator_test.py b/tests/quafu/algorithms/estimator_test.py index 0386fba4..9003bae4 100644 --- a/tests/quafu/algorithms/estimator_test.py +++ b/tests/quafu/algorithms/estimator_test.py @@ -19,7 +19,8 @@ import numpy as np import pytest from quafu.algorithms.estimator import Estimator -from quafu.algorithms.hamiltonian import Hamiltonian +from quafu.algorithms.hamiltonian import Hamiltonian, PauliOp + from quafu.circuits.quantum_circuit import QuantumCircuit from quafu.tasks.tasks import Task @@ -53,7 +54,7 @@ def build_circuit(self): measures = list(range(5)) circ.measure(measures) test_ising = Hamiltonian( - ["IIIZZ", "ZZIII", "IZZII", "ZIIIZ"], np.array([1, 1, 1, 1]) + [PauliOp("Z0 Z1"), PauliOp("Z3 Z4"), PauliOp("Z2 Z3"), PauliOp("Z0 Z4")] ) return circ, test_ising diff --git a/tests/quafu/algorithms/gradient_test.py b/tests/quafu/algorithms/gradient_test.py index 40c31d97..429f25e0 100644 --- a/tests/quafu/algorithms/gradient_test.py +++ b/tests/quafu/algorithms/gradient_test.py @@ -26,7 +26,7 @@ class TestParamShift: sys.platform == "darwin", reason="Avoid error on MacOS arm arch." ) def test_call(self): - ham = Hamiltonian.from_pauli_list([("ZZ", 1), ("XI", 1)]) + ham = Hamiltonian.from_pauli_list([("Z0 Z1", 1), ("X1", 1)]) circ = QuantumCircuit(2) # circ.h(0) # circ.h(1) diff --git a/tests/quafu/algorithms/hamiltonian_test.py b/tests/quafu/algorithms/hamiltonian_test.py index 09faa3ef..f67e40dc 100644 --- a/tests/quafu/algorithms/hamiltonian_test.py +++ b/tests/quafu/algorithms/hamiltonian_test.py @@ -13,7 +13,7 @@ # limitations under the License. import numpy as np -from quafu.algorithms.hamiltonian import Hamiltonian +from quafu.algorithms.hamiltonian import Hamiltonian, PauliOp M_0 = np.array( [ @@ -273,20 +273,20 @@ class TestHamiltonian: def test_init(self): - h = Hamiltonian(["IIIZZ", "IIZIZ"], np.array([1, 1])) + h = Hamiltonian([PauliOp("Z0 Z1", 1), PauliOp("Z1 Z2", 1)]) h = Hamiltonian.from_pauli_list( - [("IIIZZ", 1), ("IIZIZ", 1), ("IZIIZ", 1), ("ZIIIZ", 1)] + [("Z0 Z1", 1), ("Z0 Z2", 1), ("Z0 Z3", 1), ("Z0 Z4", 1)] ) def test_to_matrix(self): - h = Hamiltonian.from_pauli_list([("IZZ", 1), ("ZIZ", 1), ("ZZI", 1)]) - m = h.get_matrix() + h = Hamiltonian.from_pauli_list([("Z0 Z1", 1), ("Z0 Z2", 1), ("Z1 Z2", 1)]) + m = h.get_matrix(3).toarray() assert np.array_equal(m, M_0) - h = Hamiltonian.from_pauli_list([("IZX", 1), ("ZIX", 1), ("ZXI", 1)]) - m = h.get_matrix() + h = Hamiltonian.from_pauli_list([("X0 Z1", 1), ("X0 Z2", 1), ("X1 Z2", 1)]) + m = h.get_matrix(3).toarray() assert np.array_equal(m, M_1) - h = Hamiltonian.from_pauli_list([("YZI", 1), ("ZIY", 1), ("ZXI", 1)]) - m = h.get_matrix() + h = Hamiltonian.from_pauli_list([("Z1 Y2", 1), ("Y0 Z2", 1), ("X1 Z2", 1)]) + m = h.get_matrix(3).toarray() assert np.array_equal(m, M_2) diff --git a/tests/quafu/algorithms/integration_test.py b/tests/quafu/algorithms/integration_test.py index b50eb930..f40f5db8 100644 --- a/tests/quafu/algorithms/integration_test.py +++ b/tests/quafu/algorithms/integration_test.py @@ -48,6 +48,62 @@ def check_solution(self, probs: List[float], correct_answers: List[str]): assert eval_answers == sorted(correct_answers) + def test_ansatz_construction(self): + num_layers = 2 + print("The test for ansatz.") + + # test the zero qubit evolution + # hamiltonian__ = Hamiltonian.from_pauli_list( + # [("IIIII", 1), ("IIIII", 1), ("IIIII", 1), ("IIIII", 1)] + # ) + # ansatz__ = QAOAAnsatz(hamiltonian__, num_layers=num_layers) + # ansatz__.draw_circuit() + + def one_qubits_evolution(pauli): + h = Hamiltonian.from_pauli_list( + [ + (f"{pauli}0", 1), + (f"{pauli}1", 1), + (f"{pauli}3", 1), + (f"{pauli}4", 1), + ] + ) + ansatz = QAOAAnsatz(h, 5, num_layers=num_layers) + + for p in "XYZ": + one_qubits_evolution(p) + + # test the two qubits evolution + def two_qubits_evolution(pauli0, pauli1): + h = Hamiltonian.from_pauli_list( + [ + (f"{pauli0}0 {pauli1}1", 1), + (f"{pauli0}0 {pauli1}2", 1), + (f"{pauli0}0 {pauli1}3", 1), + (f"{pauli0}0 {pauli1}4", 1), + ] + ) + ansatz = QAOAAnsatz(h, 5, num_layers=num_layers) + + two_q_paulis = ["XX", "XY", "XZ", "YX", "YY", "YZ", "ZX", "ZY", "ZZ"] + + for two_q_pauli in two_q_paulis: + two_qubits_evolution(two_q_pauli[0], two_q_pauli[1]) + + # test the multiple qubits evolution + hamiltonian_multi = Hamiltonian.from_pauli_list( + [ + ("X0 Z2 Y3 X4", 1), + ("X0 Z1 Y3 X4", 1), + ("X0 Z1 Y2 X4", 1), + ("X0 Z1 Y2 X3", 1), + ] + ) + ansatz_multi = QAOAAnsatz(hamiltonian_multi, 5, num_layers=num_layers) + ansatz_multi.draw_circuit() + # ansatz_multi.plot_circuit(title='MULTI QUBITS') + # plt.show() + @pytest.mark.skipif( sys.platform == "darwin", reason="Avoid error on MacOS arm arch." ) @@ -65,92 +121,20 @@ def test_run(self): print("The test for ansatz.") # test the zero qubit evolution - hamiltonian__ = Hamiltonian.from_pauli_list( - [("IIIII", 1), ("IIIII", 1), ("IIIII", 1), ("IIIII", 1)] - ) - ansatz__ = QAOAAnsatz(hamiltonian__, num_layers=num_layers) - ansatz__.draw_circuit() - - # test the single qubit evolution - hamiltonian_x = Hamiltonian.from_pauli_list( - [("IIIIX", 1), ("IIIXI", 1), ("IXIII", 1), ("XIIII", 1)] - ) - ansatz_x = QAOAAnsatz(hamiltonian_x, num_layers=num_layers) - ansatz_x.draw_circuit() - hamiltonian_y = Hamiltonian.from_pauli_list( - [("IIIIY", 1), ("IIIYI", 1), ("IYIII", 1), ("YIIII", 1)] - ) - ansatz_y = QAOAAnsatz(hamiltonian_y, num_layers=num_layers) - ansatz_y.draw_circuit() - hamiltonian_z = Hamiltonian.from_pauli_list( - [("IIIIZ", 1), ("IIIZI", 1), ("IZIII", 1), ("ZIIII", 1)] - ) - ansatz_z = QAOAAnsatz(hamiltonian_z, num_layers=num_layers) - ansatz_z.draw_circuit() - - # test the two qubits evolution - hamiltonian_xx = Hamiltonian.from_pauli_list( - [("IIIXX", 1), ("IIXIX", 1), ("IXIIX", 1), ("XIIIX", 1)] - ) - ansatz_xx = QAOAAnsatz(hamiltonian_xx, num_layers=num_layers) - ansatz_xx.draw_circuit() - hamiltonian_xy = Hamiltonian.from_pauli_list( - [("IIIYX", 1), ("IIYIX", 1), ("IYIIX", 1), ("YIIIX", 1)] - ) - ansatz_xy = QAOAAnsatz(hamiltonian_xy, num_layers=num_layers) - ansatz_xy.draw_circuit() - hamiltonian_xz = Hamiltonian.from_pauli_list( - [("IIIXZ", 1), ("IIZIX", 1), ("IZIIX", 1), ("ZIIIX", 1)] - ) - ansatz_xz = QAOAAnsatz(hamiltonian_xz, num_layers=num_layers) - ansatz_xz.draw_circuit() - hamiltonian_yx = Hamiltonian.from_pauli_list( - [("IIIXY", 1), ("IIXIY", 1), ("IXIIY", 1), ("XIIIY", 1)] - ) - ansatz_yx = QAOAAnsatz(hamiltonian_yx, num_layers=num_layers) - ansatz_yx.draw_circuit() - hamiltonian_yy = Hamiltonian.from_pauli_list( - [("IIIYY", 1), ("IIYIY", 1), ("IYIIY", 1), ("YIIIY", 1)] - ) - ansatz_yy = QAOAAnsatz(hamiltonian_yy, num_layers=num_layers) - ansatz_yy.draw_circuit() - hamiltonian_yz = Hamiltonian.from_pauli_list( - [("IIIZY", 1), ("IIZIY", 1), ("IZIIY", 1), ("ZIIIY", 1)] - ) - ansatz_yz = QAOAAnsatz(hamiltonian_yz, num_layers=num_layers) - ansatz_yz.draw_circuit() - hamiltonian_zx = Hamiltonian.from_pauli_list( - [("IIIXZ", 1), ("IIXIZ", 1), ("IXIIZ", 1), ("XIIIZ", 1)] - ) - ansatz_zx = QAOAAnsatz(hamiltonian_zx, num_layers=num_layers) - ansatz_zx.draw_circuit() - hamiltonian_zy = Hamiltonian.from_pauli_list( - [("IIIYZ", 1), ("IIYIZ", 1), ("IYIIZ", 1), ("YIIIZ", 1)] - ) - ansatz_zy = QAOAAnsatz(hamiltonian_zy, num_layers=num_layers) - ansatz_zy.draw_circuit() - hamiltonian_zz = Hamiltonian.from_pauli_list( - [("IIIZZ", 1), ("IIZIZ", 1), ("IZIIZ", 1), ("ZIIIZ", 1)] - ) - ansatz_zz = QAOAAnsatz(hamiltonian_zz, num_layers=num_layers) - ansatz_zz.draw_circuit() - - # test the multiple qubits evolution - hamiltonian_multi = Hamiltonian.from_pauli_list( - [("XYZIX", 1), ("XYIZX", 1), ("XIYZX", 1), ("IXYZX", 1)] - ) - ansatz_multi = QAOAAnsatz(hamiltonian_multi, num_layers=num_layers) - ansatz_multi.draw_circuit() - # ansatz_multi.plot_circuit(title='MULTI QUBITS') - # plt.show() + # hamiltonian__ = Hamiltonian.from_pauli_list( + # [("IIIII", 1), ("IIIII", 1), ("IIIII", 1), ("IIIII", 1)] + # ) + # ansatz__ = QAOAAnsatz(hamiltonian__, num_layers=num_layers) + # ansatz__.draw_circuit() hamiltonian = Hamiltonian.from_pauli_list( - [("IIIZZ", 1), ("IIZIZ", 1), ("IZIIZ", 1), ("ZIIIZ", 1)] + [("Z0 Z1", 1), ("Z0 Z2", 1), ("Z0 Z3", 1), ("Z0 Z4", 1)] ) + ref_mat = np.load("tests/quafu/algorithms/data/qaoa_hamiltonian.npy") # ref_mat = np.load("data/qaoa_hamiltonian.npy") - assert np.array_equal(ref_mat, hamiltonian.get_matrix()) - ansatz = QAOAAnsatz(hamiltonian, num_layers=num_layers) + assert np.array_equal(ref_mat, hamiltonian.get_matrix(5).toarray()) + ansatz = QAOAAnsatz(hamiltonian, 5, num_layers=num_layers) ansatz.draw_circuit() def cost_func(params, ham, estimator: Estimator): @@ -180,10 +164,10 @@ def test_run(self): num_layers = 4 num_qubits = 2 hamlitonian = Hamiltonian.from_pauli_list( - [("YZ", 0.3980), ("ZI", -0.3980), ("ZZ", -0.0113), ("XX", 0.1810)] + [("Z0 Y1", 0.3980), ("Z1", -0.3980), ("Z0 Z1", -0.0113), ("X0 X1", 0.1810)] ) ref_mat = np.load("tests/quafu/algorithms/data/vqe_hamiltonian.npy") - assert np.array_equal(ref_mat, hamlitonian.get_matrix()) + assert np.array_equal(ref_mat, hamlitonian.get_matrix(2).toarray()) ansatz = AlterLayeredAnsatz(num_qubits, num_layers) def cost_func(params, ham, estimator: Estimator): diff --git a/tests/quafu/algorithms/qnn_test.py b/tests/quafu/algorithms/qnn_test.py index 7210585b..ac14de2c 100644 --- a/tests/quafu/algorithms/qnn_test.py +++ b/tests/quafu/algorithms/qnn_test.py @@ -42,18 +42,6 @@ def forward(self, features): return out -class ModelQuantumNeuralNetworkNative(torch.nn.Module): - """Test execution of qnn()""" - - def __init__(self, qnn: QuantumNeuralNetwork): - super().__init__() - self.qnn = qnn - - def forward(self, features): - out = self.qnn(features) - return out - - class TestLayers: circ = QuantumCircuit(2) circ.x(0) @@ -61,19 +49,6 @@ class TestLayers: circ.ry(1, 0.5) circ.ry(0, 0.1) - def _model_grad(self, model, batch_size): - """Test one forward pass and gradient calculation of a model""" - - # TODO(zhaoyilun): Make out dimension configurable - features = torch.randn( - batch_size, 3, requires_grad=True, dtype=torch.double - ) # batch_size=4, num_params=3 - outputs = model(features) - targets = torch.randn(batch_size, 2, dtype=torch.double) - criterion = torch.nn.MSELoss() - loss = criterion(outputs, targets) - loss.backward() - def test_compute_vjp(self): params_input = np.random.randn(4, 3) jac = jacobian(self.circ, params_input) @@ -102,11 +77,12 @@ def test_torch_layer_qnn(self): entangle_layer = BasicEntangleLayers(weights, 2) qnn = QuantumNeuralNetwork(2, [entangle_layer]) batch_size = 1 - - # Legacy invokation style model = ModelQuantumNeuralNetwork(qnn) - self._model_grad(model, batch_size) - - # New invokation style - model = ModelQuantumNeuralNetworkNative(qnn) - self._model_grad(model, batch_size) + features = torch.randn( + batch_size, 3, requires_grad=True, dtype=torch.double + ) # batch_size=4, num_params=3 + outputs = model(features) + targets = torch.randn(batch_size, 2, dtype=torch.double) + criterion = torch.nn.MSELoss() + loss = criterion(outputs, targets) + loss.backward() diff --git a/tests/quafu/circuits/building_circuit_test.py b/tests/quafu/circuits/building_circuit_test.py new file mode 100644 index 00000000..47ea5e39 --- /dev/null +++ b/tests/quafu/circuits/building_circuit_test.py @@ -0,0 +1,23 @@ +import quafu.elements.element_gates as qeg +from quafu.circuits import QuantumCircuit + + +class TestBuildingCircuit: + def test_add(self): + q = QuantumCircuit(3) + q << (qeg.XGate(1)) + q << (qeg.CXGate(0, 2)) + q << (qeg.RXGate(0, 0.1)) + q << (qeg.XGate(2)).ctrl_by([0]) + q << qeg.RZZGate(0, 2, 0.26) + try: + nq = q.add_controls(2) + except NotImplementedError: + nq = q + nq.draw_circuit() + nq.to_openqasm() + + +if __name__ == '__main__': + test = TestBuildingCircuit() + test.test_add() diff --git a/tests/quafu/circuits/quantum_circuit_test.py b/tests/quafu/circuits/quantum_circuit_test.py index 5b7037f9..8a2a04b2 100644 --- a/tests/quafu/circuits/quantum_circuit_test.py +++ b/tests/quafu/circuits/quantum_circuit_test.py @@ -1,7 +1,7 @@ -import math - from quafu.circuits import QuantumCircuit from quafu.elements.element_gates import RXGate +from quafu.elements.parameters import Parameter +import math class TestQuantumCircuit: @@ -30,3 +30,14 @@ def test_update_parameters(self): assert math.isclose(g.paras, 0.2) c.update_params([None]) assert math.isclose(g.paras, 0.2) + + def test_instantiated_params(self): + """Create Parameter objects""" + pq = QuantumCircuit(4) + theta = [Parameter("theta_%d" %(i), i+1) for i in range(4)] + + for i in range(4): + pq.rx(i, theta[i]) + + pq.ry(2, theta[0]*theta[1]-3.*theta[0]) + pq.draw_circuit()