diff --git a/cqlib/circuits/circuit.py b/cqlib/circuits/circuit.py index 4d430637465b43faddded575fb318417088206f1..d47fae16798cfc63f3ccc44b5229e8825c57a944 100644 --- a/cqlib/circuits/circuit.py +++ b/cqlib/circuits/circuit.py @@ -243,17 +243,6 @@ class Circuit: instruction (Instruction): The instruction to be appended. qubits (Qubits): The qubit(s) to which the instruction is applied. """ - if isinstance(instruction, CustomGate): - if qubits is None: - qubits = [i for i in range(0, instruction.num_qubits)] - else: - if len(qubits) != len(set(qubits)): - raise ValueError("qubits cannot be repeated") - if instruction.num_qubits != len(qubits): - raise ValueError( - f"The provided qubits list has {len(qubits)} elements, " - f"but {instruction.num_qubits} elements are required by the other circuit." - ) instruction, qubits = self._prepare_instruction(instruction, qubits) self._circuit_data.append(InstructionData(instruction=instruction, qubits=qubits)) @@ -302,6 +291,8 @@ class Circuit: if not instruction.params: return params = [] + if isinstance(instruction, CustomGate): + self._parameters.update(self._initialize_parameters(instruction.params)) for param in instruction.params: if isinstance(param, str): param = Parameter(param) @@ -773,7 +764,7 @@ class Circuit: """ Generates a qcis string of all instructions in the circuit. """ - return self._export_circuit_str(self._circuit_data, True, self._parameters) + return self._export_circuit_str(self.decompose().circuit_data, True, self._parameters) def as_str(self, qcis_compliant: bool = False): """ @@ -1038,32 +1029,24 @@ class Circuit: return draw_mpl(self, qubit_order=qubit_order, **kwargs) raise VisualizationError(f"Unsupported category: {category}") - def clone(self): - """ - Create a deep copy of the circuit. - - Returns: - Circuit: A new circuit object with the same qubits, parameters, and instructions. - """ - import copy - - qubits = [Qubit(qubit.index) for qubit in self.qubits] - new_circuit = Circuit(qubits=qubits, parameters=copy.deepcopy(self._parameters)) - instruction_sequence = [i for i in self.instruction_sequence] - new_circuit._instruction_sequence = instruction_sequence - return new_circuit - - def compose(self, other: "Circuit", qubits: list = None, inplace: bool = False): + def compose( + self, + other: Circuit, + qubits: list[Qubit] | list[int] = None, + inplace: bool = False + ) -> Circuit: """ - Combine two quantum circuits. + Combine two quantum circuit. Args: other (Circuit): The other circuit to be combined. - qubits (list): The positions where the qubit gates from the other circuit are placed in the current circuit. - inplace (bool): If ``True``, modify the object. Otherwise, return a new composed circuit. + qubits (list): The positions where the qubit gates from + the other circuit are placed in the current circuit. + inplace (bool): If ``True``, modify the object. + Otherwise, return a new composed circuit. Returns: - Circuit: The composed circuit (if `inplace=False`). + Circuit: The composed circuit Raises: ValueError: If the input qubits parameter is in an incorrect format or quantity @@ -1076,103 +1059,77 @@ class Circuit: >>> abc = circuit.compose(circuit1) >>> print(abc.qcis) """ + target = self if inplace else self.copy() + + for parm in other.parameters: + if parm not in target.parameters: + target.add_parameter(parm) + + if qubits is None: + qubits = other.qubits + elif len(qubits) != len(other.qubits): + raise ValueError("Number of qubits must match") + qubits_mapping = dict(zip(other.qubits, qubits)) + for instruction_data in other.instruction_sequence: + target.append( + instruction=instruction_data.instruction, + qubits=[qubits_mapping[q] for q in instruction_data.qubits] + ) + return target - target_circuit = self if inplace else self.clone() - - if qubits: - if len(qubits) != len(set(qubits)): - raise ValueError("qubits cannot be repeated") - if len(other.qubits) != len(qubits): - raise ValueError( - f"The provided qubits list has {len(qubits)} elements, " - f"but {len(other.qubits)} elements are required by the other circuit." - ) - - for instruction_data in other.instruction_sequence: - mapped_qubits = [ - Qubit(qubits[q.index]) for q in instruction_data.qubits - ] - target_circuit.append(instruction_data.instruction, qubits=mapped_qubits) - else: - for instruction_data in other.instruction_sequence: - target_circuit.append(instruction_data.instruction, instruction_data.qubits) - - if not inplace: - return target_circuit - - def to_gate(self, name: str = None, qubits: list = None) -> CustomGate: + def to_gate(self, name: str | None = None) -> CustomGate: """ - Assemble the wiring into a custom gate. + Convert the circuit to a CustomGate. Args: - name (str): Custom gate name. If not provided, a name is auto-generated. - qubits (list): Customize the location of the gate contained in the gate. + name (str): Custom gate name. If not provided, a name is auto-generated. Returns: - CustomGate: The assembled custom gate. + CustomGate: The assembled custom gate. Raises: - ValueError: If the entered gate name is not of type str. + ValueError: If the entered gate name is not of type str. Example: - >>> circuit = Circuit(4) - >>> circuit.h(0) - >>> circuit.measure(0) - >>> circuit1 = Circuit(3) - >>> circuit1.xy2p(2, 1.0) - >>> gate = circuit1.to_gate(name="abc") - >>> circuit.append(gate) - >>> print(circuit.qcis) - """ - from copy import deepcopy - from datetime import datetime - - if name is None: - current_time = datetime.now() - time_str = current_time.strftime("%H%M%S%f")[:-3] - name = f'circuit_{time_str}' - elif not isinstance(name, str): - raise ValueError("The circuit name should be a string") - - contain_gate = [] - gates_index = [] - - for instruction_data in self.instruction_sequence: - instruction_qubits = [Qubit(qubit.index) for qubit in instruction_data.qubits] - if qubits: - if instruction_qubits[0].index >= len(qubits): - raise ValueError("Invalid qubit mapping index.") - instruction_qubits[0] = Qubit(qubits[instruction_qubits[0].index]) - - contain_gate.append(instruction_data.instruction) - gates_index.append([qubit.index for qubit in instruction_qubits]) - - return CustomGate( + >>> circuit = Circuit(4) + >>> circuit.h(0) + >>> circuit.measure(0) + >>> circuit1 = Circuit(3) + >>> circuit1.xy2p(2, 2.0) + >>> gate = circuit1.to_gate(name="abc") + >>> circuit.append(gate,[1,2,3]) + >>> print(circuit.qcis) + """ + gate = CustomGate( name=name, num_qubits=self.num_qubits, - params=[], - contain_gate=contain_gate, - gates_index=gates_index, + params=self.parameters ) + gate.circuit = self.copy() + return gate - def decompose(self): + def decompose(self, inplace: bool = False) -> Circuit: """ Decomposing Custom Gates in the Circuit. - This function identifies custom gates in the circuit, decomposes them into their - constituent gates, and replaces the custom gate with the resulting instructions. - """ - for instruction_data in self.instruction_sequence: - if isinstance(instruction_data.instruction, CustomGate): - insert_index = self.instruction_sequence.index(instruction_data) - 1 - decompose_instruction = [] - for i in range(0, len(instruction_data.instruction.gates)): - instruction, qubits = self._prepare_instruction(instruction_data.instruction.gates[i], - instruction_data.instruction.gates_index[i]) - - map_qubits = [] - for qubit in qubits: - map_qubits.append(instruction_data.qubits[qubit.index]) - decompose_instruction.append(InstructionData(instruction, qubits=map_qubits)) - - self.instruction_sequence[insert_index + 1:insert_index + 1] = decompose_instruction - self.instruction_sequence.remove(instruction_data) + This function identifies custom gates in the circuit, decompose them into their + constituent gates. + + Args: + inplace (bool): If ``True``, modify the object. + Otherwise, return a new decompose circuit. + + Returns: + Circuit: The decompose circuit + """ + target_circuit = self if inplace else Circuit(qubits=self.qubits, + parameters=self.parameters) + data = [] + for item in self._circuit_data: + inst = item.instruction + qubits = item.qubits + if isinstance(inst, CustomGate): + data.extend(inst.to_qcis(qubits)) + else: + data.append(InstructionData(inst, qubits)) + setattr(target_circuit, '_circuit_data', data) + return target_circuit diff --git a/cqlib/circuits/gates/gate.py b/cqlib/circuits/gates/gate.py index c276560d22e1e2c47cfa43ffb56eed2fe03eb975..ee0efed6391b213d0f52858bf2b6df4342659cb6 100644 --- a/cqlib/circuits/gates/gate.py +++ b/cqlib/circuits/gates/gate.py @@ -13,7 +13,9 @@ """Quantum Gate""" from typing import List, Union, Optional + from cqlib.circuits.qubit import Qubit +from cqlib.circuits.instruction_data import InstructionData from cqlib.circuits.instruction import Instruction from cqlib.circuits.parameter import Parameter @@ -120,16 +122,20 @@ class ControlledGate(Gate): return qubits -class CustomGate(Instruction): +class CustomGate(Gate): + """ + Custom quantum gate + """ + + instance_count = 0 + is_supported_by_qcis = False def __init__( self, - name: str, num_qubits: int, - params: List[Union[Parameter, float, complex]], - label: Optional[str] = None, - contain_gate: List[Instruction] = None, - gates_index: List[List[int]] = None + params: list[Parameter | float], + name: str | None = None, + label: str | None = None ): """ Create a custom gate object. @@ -139,13 +145,49 @@ class CustomGate(Instruction): num_qubits (int): The number of qubits the gate acts on. params (list[Parameter | float | complex]): A list of parameters for the gate. label (str | None, optional): An optional label for the gate. Defaults to None. - contain_gate(list[Instruction]): Contained gate or controlledGate - gates_index(list[list[int]]): Specifies the coordinate position of each gate """ + if name is None: + name = f'Gate-{CustomGate.instance_count}' + CustomGate.instance_count += 1 + super().__init__(name=name, num_qubits=num_qubits, params=params, label=label) + self._circuit = None + + @property + def circuit(self) -> 'Circuit': + """ + return circuit data + """ + return self._circuit - super().__init__(name, num_qubits, params, label) - self.gates = contain_gate - self.gates_index = gates_index + @circuit.setter + def circuit(self, value: 'Circuit'): + """ + set circuit data + """ + self._circuit = value + + def to_qcis(self, qubits: list[Qubit]) -> list[InstructionData]: + """ + Decompose the custom gate into basic gates + + Args: + qubits(list): mapping qubits + + Returns: + Circuit: The decomposed custom gate content + """ + data: list[InstructionData] = [] + if len(qubits) != self.num_qubits: + raise ValueError(f'The number of qubits in the circuit ({self.num_qubits}) ' + f'does not match the number of qubits in the gate ({len(qubits)}).') + qubit_mapping = dict(zip(self._circuit.qubits, qubits)) + for item in self._circuit.instruction_sequence: + qs = [qubit_mapping[q] for q in item.qubits] + if isinstance(item.instruction, CustomGate): + data.extend(item.instruction.to_qcis(qs)) + else: + data.append(InstructionData(item.instruction, qs)) + return data def __array__(self, dtype=None): """ diff --git a/cqlib/simulator/simple_simulator.py b/cqlib/simulator/simple_simulator.py index 66813e5cb29817d4f3d1e69c946f8cbf0d7cc6d4..7f37f4dd9400b13b0bd6a7ea76c82514e3237f47 100644 --- a/cqlib/simulator/simple_simulator.py +++ b/cqlib/simulator/simple_simulator.py @@ -60,7 +60,7 @@ class SimpleSimulator: if device is None: device = "cuda" if torch.cuda.is_available() else "cpu" - self.circuit = circuit + self.circuit = circuit.decompose() self.backend = TorchBackend(device=torch.device(device), dtype=dtype) self._runner = SimpleRunner(nq=len(self.circuit.qubits), backend=self.backend) self.nq = self._runner.nq diff --git a/cqlib/simulator/statevector_simulator.py b/cqlib/simulator/statevector_simulator.py index 97539c368eedf61d11fc38d7bf972e404a60a88b..2bb0dbbd8d4f9a3cd6fb4d11eb049ebc3aed38a1 100644 --- a/cqlib/simulator/statevector_simulator.py +++ b/cqlib/simulator/statevector_simulator.py @@ -106,7 +106,8 @@ class StatevectorSimulator: """ if omp_threads < 0: raise ValueError('') - self.circuit = circuit + + self.circuit = circuit.decompose() self.is_fusion = is_fusion self.fusion_max_qubit = fusion_max_qubit self.omp_threads = omp_threads diff --git a/cqlib/visualization/circuit/mpl.py b/cqlib/visualization/circuit/mpl.py index 1f35aab18e7752fa3363023d9cc16d7b42723ef4..1a719d9718349f17ebe0e4a41f7432bea0d4d8b2 100644 --- a/cqlib/visualization/circuit/mpl.py +++ b/cqlib/visualization/circuit/mpl.py @@ -467,7 +467,7 @@ def draw_mpl( Figure: Ready-to-display circuit visualization """ return MatplotlibDrawer( - circuit, + circuit.decompose(), title=title, filename=filename, figure_styles=figure_styles, diff --git a/cqlib/visualization/circuit/text.py b/cqlib/visualization/circuit/text.py index 9c91afa48f18ee6eac7a30d534644c2bb6c19a20..5d24811ca2414e7ba31a4094afaf5cf2406bc17e 100644 --- a/cqlib/visualization/circuit/text.py +++ b/cqlib/visualization/circuit/text.py @@ -293,4 +293,4 @@ def draw_text( Returns: str: Ready-to-print circuit diagram """ - return TextDrawer(circuit, qubit_order, line_width).drawer() + return TextDrawer(circuit.decompose(), qubit_order, line_width).drawer() diff --git a/tests/circuit/test_circuit.py b/tests/circuit/test_circuit.py index 551f97703fe2e96ea1fa7dbee9cfe72074ffcc53..d9b4f0f6f4e5603df3d734103f0f6419436b3019 100644 --- a/tests/circuit/test_circuit.py +++ b/tests/circuit/test_circuit.py @@ -356,14 +356,55 @@ def test_compose_circuit(): circuit1.x(0) circuit1.y(2) circuit1.cz(0, 2) - circuit1.xy2p(2, 1.0) + circuit1.xy2p(2, 1.1) circuit.compose(circuit1, qubits=[2, 1, 3], inplace=True) - assert circuit.qcis == "H Q0\nH Q1\nCZ Q0 Q1\nH Q1\nM Q0\nM Q1\nX Q2\nY Q3\nCZ Q2 Q3\nXY2P Q3 1.0" + assert (circuit.qcis == + "H Q0\nH Q1\nCZ Q0 Q1\nH Q1\nM Q0\nM Q1\nX Q2\nY Q3\nCZ Q2 Q3\nXY2P Q3 1.1") -def test_circuit_to_gate(): - """test_circuit_to_gate""" - circuit = Circuit(4) +def test_to_gate_compose(): + """test_circuit_to_gate_compose""" + c1 = Circuit(2) + c1.h(0) + c1.h(1) + c1_gate = c1.to_gate(name="abc") + c2 = Circuit(3) + c2.x(0) + c2.x(1) + c2.x(2) + c2.append(c1_gate, qubits=[2, 1]) + print(f'\nc2:\n{c2.qcis}') + + c3 = Circuit(4) + c3.y(0) + c3.y(1) + c3.compose(c2, qubits=[0, 1, 2], inplace=True) + assert str(c3) == 'Y Q0\nY Q1\nX Q0\nX Q1\nX Q2\nabc Q2 Q1' + c3.decompose(inplace=True) + assert c3.qcis == 'Y Q0\nY Q1\nX Q0\nX Q1\nX Q2\nH Q2\nH Q1' + + +def test_compose_circuit_parameters(): + """test_compose_circuit_prams""" + theta = Parameter('theta') + circuit = Circuit(6, parameters=[theta]) + circuit.h(0) + circuit.rx(0, theta) + circuit.measure(0) + circuit.measure(1) + theta1 = Parameter('theta1') + circuit1 = Circuit(3, parameters=[theta1]) + circuit1.x(0) + circuit1.y(2) + circuit1.cz(0, 2) + circuit1.xy2p(2, theta1) + circuit.compose(circuit1, qubits=[2, 1, 3], inplace=True) + assert circuit.qcis == "H Q0\nRX Q0 theta\nM Q0\nM Q1\nX Q2\nY Q3\nCZ Q2 Q3\nXY2P Q3 theta1" + + +def test_circuit_decompose(): + """test circuit decompose""" + circuit = Circuit(6) circuit.h(0) circuit.h(1) circuit.cz(0, 1) @@ -375,15 +416,35 @@ def test_circuit_to_gate(): circuit1.x(0) circuit1.y(2) circuit1.cz(0, 2) - circuit1.xy2p(2, 1.0) + circuit1.xy2p(2, 1.2) gate = circuit1.to_gate(name="abc") circuit.append(gate, qubits=[2, 1, 0]) - assert circuit.qcis == 'H Q0\nH Q1\nCZ Q0 Q1\nH Q1\nM Q0\nM Q1\nabc Q2 Q1 Q0' + decompose_c = circuit.decompose() + assert (decompose_c.qcis == + "H Q0\nH Q1\nCZ Q0 Q1\nH Q1\nM Q0\nM Q1\nX Q2\nY Q0\nCZ Q2 Q0\nXY2P Q0 1.2") -def test_circuit_decompose(): - """test_circuit_decompose""" + +def test_circuit_decompose_inplace(): + """test_circuit_decompose_inplace""" + circuit = Circuit(6) + circuit.h(0) + circuit.cz(0, 1) + + circuit1 = Circuit(3) + circuit1.x(0) + circuit1.y(2) + + gate = circuit1.to_gate(name="gate") + circuit.append(gate, qubits=[2, 1, 0]) + decompose_c = circuit.decompose() + assert decompose_c.qcis == "H Q0\nCZ Q0 Q1\nX Q2\nY Q0" + assert str(circuit) == "H Q0\nCZ Q0 Q1\ngate Q2 Q1 Q0" + + +def test_circuit_to_gate(): + """test_circuit_to_gate""" circuit = Circuit(4) circuit.h(0) circuit.h(1) @@ -394,34 +455,83 @@ def test_circuit_decompose(): circuit1 = Circuit(3) circuit1.x(0) - circuit1.y(2) circuit1.cz(0, 2) - circuit1.xy2p(2, 1.2) + circuit1.xy2p(2, 1.0) - gate = circuit1.to_gate(name="abc") - circuit.append(gate, qubits=[2, 1, 0]) - circuit.decompose() - assert circuit.qcis == "H Q0\nH Q1\nCZ Q0 Q1\nH Q1\nM Q0\nM Q1\nX Q2\nY Q0\nCZ Q2 Q0\nXY2P Q0 1.2" + gate = circuit1.to_gate() + circuit.append(gate, [2, 1, 0]) + assert str(circuit) == 'H Q0\nH Q1\nCZ Q0 Q1\nH Q1\nM Q0\nM Q1\nGate-0 Q2 Q1 Q0' -def test_to_gate_compose(): - """test_circuit_to_gate_compose""" - c1 = Circuit(2) +def test_circuit_to_gate1(): + """test_circuit_to_gate1""" + c1 = Circuit([7, 0]) c1.h(0) - c1.h(1) - c1_gate = c1.to_gate(name="abc") + c1.cx(0, 7) + print(f"c1:{c1}\n") + + c1_gate = c1.to_gate(name="c1") + c2 = Circuit([1, 9, 10]) + c2.append(c1_gate, [10, 9]) + c2.ccx(9, 10, 9) + c2_d = c2.decompose() + assert str(c2_d) == "H Q9\nCX Q9 Q10\nCCX Q9 Q10 Q9" + + c2_gate = c2.to_gate("c2") + c3 = Circuit([0, 10, 20, 30]) + c3.x(0) + c3.append(c2_gate, qubits=[10, 20, 30]) + print(str(c3.decompose(inplace=True))) + assert str(c3) == "X Q0\nH Q20\nCX Q20 Q30\nCCX Q20 Q30 Q20" + + +def test_circuit_to_gate_param(): + """test_circuit_to_gate_param""" + theta = Parameter('theta') + circuit = Circuit(3, parameters=[theta]) + circuit.h(1) + circuit.rx(0, theta) - c2 = Circuit(3) - c2.x(0) - c2.x(1) - c2.x(2) - c2.append(c1_gate, qubits=[2, 1]) - print(f'\nc2:\n{c2.qcis}') + phi = Parameter('phi') + circuit1 = Circuit(4, parameters=[phi]) + circuit1.x(0) + circuit1.ry(1, phi + 1) - c3 = Circuit(4) - c3.y(0) - c3.y(1) - c3.compose(c2, qubits=[0, 1, 2], inplace=True) - assert c3.qcis == 'Y Q0\nY Q1\nX Q0\nX Q1\nX Q2\nabc Q2 Q1' - c3.decompose() - assert c3.qcis == 'Y Q0\nY Q1\nX Q0\nX Q1\nX Q2\nH Q2\nH Q1' + theta1 = Parameter('theta1') + phi1 = Parameter('phi1') + + gate = circuit.to_gate() + circuit1.append(gate, qubits=[0, 1, 2]) + print(circuit1) + gate1 = circuit1.to_gate("circuit1_gate") + + circuit2 = Circuit(4, parameters=[theta1, phi1]) + circuit2.xy2p(0, theta1) + circuit2.xy2p(1, theta1) + circuit2.append(gate1, qubits=[0, 1, 2, 3]) + circuit2.assign_parameters(theta=1.0, phi=2.0, theta1=3.0, inplace=True) + assert str(circuit2) == "XY2P Q0 3\nXY2P Q1 3\ncircuit1_gate Q0 Q1 Q2 Q3 2 1" + assert circuit2.qcis == "XY2P Q0 3\nXY2P Q1 3\nX Q0\nRY Q1 3\nH Q1\nRX Q0 1" + + +def test_gate_reuse(): + """ + test custom gate reuse + """ + c1 = Circuit([0, 7]) + c1.h(0) + c1.cx(0, 7) + c1_gate = c1.to_gate() + c2 = Circuit([1, 9, 10]) + c2.h(9) + c2.append(c1_gate, [9, 10]) + c2.append(c1_gate, [1, 9]) + c2_gate = c2.to_gate() + d_c2 = c2.decompose() + assert str(d_c2) == "H Q9\nH Q9\nCX Q9 Q10\nH Q1\nCX Q1 Q9" + + c3 = Circuit(10) + c3.append(c1_gate, [1, 2]) + c3.append(c2_gate, [2, 3, 4]) + d_c3 = c3.decompose() + assert str(d_c3) == "H Q1\nCX Q1 Q2\nH Q3\nH Q3\nCX Q3 Q4\nH Q2\nCX Q2 Q3"