diff --git a/cqlib/circuits/parameter.py b/cqlib/circuits/parameter.py index 5924d7fab5857a86144fcb53b08cc79785de7992..14a415b3f71006843f62f4d46f41d694689a2cac 100644 --- a/cqlib/circuits/parameter.py +++ b/cqlib/circuits/parameter.py @@ -99,6 +99,9 @@ class Parameter: def __rmul__(self, other: Union[int, float, Parameter]) -> Parameter: return self._apply_operation(other, Mul) + def __neg__(self) -> Parameter: + return self._apply_operation(0, lambda a, b: b - a) + def __truediv__(self, other: Union[int, float, Parameter]) -> Parameter: return self._apply_operation(other, lambda a, b: a / b) diff --git a/cqlib/simulator/simple_sim/gates.py b/cqlib/simulator/simple_sim/gates.py index 9d377fe65dce7a3f960f52689c8177e9eb14c808..8353b9a22ebd03b554e21d160133041fd9e8515b 100644 --- a/cqlib/simulator/simple_sim/gates.py +++ b/cqlib/simulator/simple_sim/gates.py @@ -121,23 +121,18 @@ def xy2p_mat(theta, backend: TorchBackend): if backend.numel(theta) != 1: raise ValueError("XY2P gate requires exactly 1 parameter.") - # Compute required components - i = backend.i() # Imaginary unit 1j + i = backend.i() # 1j + neg_i = backend.mul(-1, i) # -1j exp_i_theta = backend.exp(i * theta) exp_neg_i_theta = backend.exp(-i * theta) sqrt2_inv = 1 / backend.sqrt(backend.as_tensor(2.0)) # 1/sqrt(2) - # Create matrix elements one = backend.eye(1) - # Construct matrix rows - row1 = backend.hstack([one, -i * exp_neg_i_theta]) - row2 = backend.hstack([-i * exp_i_theta, one]) - - # Combine rows and apply normalization - matrix = sqrt2_inv * backend.vstack([row1, row2]) - - return matrix + return backend.mul(sqrt2_inv, backend.vstack([ + backend.hstack([one, backend.mul(neg_i, exp_neg_i_theta).reshape(1, 1)]), + backend.hstack([backend.mul(neg_i, exp_i_theta).reshape(1, 1), one]) + ])) def xy2m_mat(theta, backend: TorchBackend): @@ -165,14 +160,15 @@ def xy2m_mat(theta, backend: TorchBackend): # Create matrix elements one = backend.eye(1) # Create [[1]] and then reshape if needed - # Construct matrix rows - row1 = backend.hstack([one, i * exp_neg_i_theta]) - row2 = backend.hstack([i * exp_i_theta, one]) + # Create complex elements as 2D tensors + top_right = backend.mul(i, exp_neg_i_theta).reshape(1, 1) # [[i*exp(-iθ)]] + bottom_left = backend.mul(i, exp_i_theta).reshape(1, 1) - # Combine rows and apply normalization - matrix = sqrt2_inv * backend.vstack([row1, row2]) + # Construct matrix rows + row1 = backend.hstack([one, top_right]) + row2 = backend.hstack([bottom_left, one]) - return matrix + return backend.mul(sqrt2_inv, backend.vstack([row1, row2])) def rxy_mat(phi, theta, backend: TorchBackend): diff --git a/cqlib/simulator/simple_sim/simple_runner.py b/cqlib/simulator/simple_sim/simple_runner.py index 2f19ac42a2994c97f68bb4e1eb832991c7df572f..03763bdb130421080c31661daa78c87a1621e2c2 100644 --- a/cqlib/simulator/simple_sim/simple_runner.py +++ b/cqlib/simulator/simple_sim/simple_runner.py @@ -16,8 +16,6 @@ Quantum Circuit Simulator Runner """ # pylint: disable=invalid-name -from collections import Counter - import numpy as np from .torch_backend import TorchBackend @@ -136,6 +134,10 @@ class SimpleRunner: """Apply inverse Y/2 (-π/2 rotation around Y-axis) gate to qubit i.""" return self.apply_single_qubit_gate(i, mat) + def U(self, i: int, mat: np.ndarray): + """Apply U gate to qubit i.""" + return self.apply_single_qubit_gate(i, mat) + def apply_single_qubit_gate(self, i: int, mat: np.ndarray): """ Apply arbitrary single-qubit gate to specified qubit. @@ -187,9 +189,9 @@ class SimpleRunner: """Apply controlled rotation around Z-axis gate.""" return self.apply_two_qubit_gate(i, j, mat) - def SWAP(self, i: int, j: int, k: int, mat: np.ndarray): + def SWAP(self, i: int, j: int, mat: np.ndarray): """Apply SWAP gate.""" - return self.apply_three_qubit_gate(i, j, k, mat) + return self.apply_two_qubit_gate(i, j, mat) def apply_two_qubit_gate(self, i: int, j: int, mat: np.ndarray): """Apply arbitrary two-qubit gate to specified qubits. @@ -220,6 +222,10 @@ class SimpleRunner: ) return self + def CCX(self, i: int, j: int, k: int, mat: np.ndarray): + """Apply CCX gate.""" + return self.apply_three_qubit_gate(i, j, k, mat) + def apply_three_qubit_gate(self, i: int, j: int, k: int, mat: np.ndarray): """Apply arbitrary three-qubit gate to specified qubits. @@ -307,8 +313,6 @@ class SimpleRunner: self, mq: list[int], shots: int = 100, - is_sorted: bool = True, - dict_format: bool = True, ): """Sample measurement outcomes from the quantum state. @@ -324,9 +328,4 @@ class SimpleRunner: p = np.real(self.backend.to_numpy(self.measure(mq))) p_norm = p / np.sum(p) r = np.random.choice(2 ** len(mq), shots, p=p_norm) - if not dict_format: - return "".join([np.binary_repr(v, width=len(mq)) for v in r]) - result = {np.binary_repr(k, width=len(mq)): v for k, v in Counter(r).items()} - if is_sorted: - result = dict(sorted(result.items())) - return result + return r diff --git a/cqlib/simulator/simple_simulator.py b/cqlib/simulator/simple_simulator.py index 0f59b4f2e6a975eeaf4f10569d27e8863ea441eb..66813e5cb29817d4f3d1e69c946f8cbf0d7cc6d4 100644 --- a/cqlib/simulator/simple_simulator.py +++ b/cqlib/simulator/simple_simulator.py @@ -14,6 +14,7 @@ """ Quantum Circuit Simulator Implementation """ +from collections import Counter import numpy as np from sympy import lambdify @@ -23,6 +24,11 @@ from cqlib.circuits.circuit import Circuit from .simple_sim import TorchBackend, SimpleRunner, gates as simple_gates +try: + import torch +except ImportError as e: + pass + # pylint: disable=too-many-instance-attributes class SimpleSimulator: @@ -48,12 +54,9 @@ class SimpleSimulator: Raises: ImportError: If PyTorch is not installed """ - try: - # pylint: disable=import-outside-toplevel - import torch - except ImportError as e: + if torch is None: raise ImportError("PyTorch is not installed. `pip install torch` " - "Please install it to use the SimpleSimulator. ") from e + "Please install it to use the SimpleSimulator. ") if device is None: device = "cuda" if torch.cuda.is_available() else "cpu" @@ -80,9 +83,10 @@ class SimpleSimulator: if self._state is None: self._run() self._state = self._runner.state() + state = self.reverse_qubit_order(self._state, self.nq) if dict_format: - return {np.binary_repr(i, width=self.nq): val for i, val in enumerate(self._state)} - return self._state + return {np.binary_repr(i, width=self.nq): val for i, val in enumerate(state)} + return state def probs(self, dict_format=True) -> dict[str, float] | list[float]: """ @@ -99,9 +103,10 @@ class SimpleSimulator: if self._probs is None: self.statevector() self._probs = self._runner.probs() + probs = self.reverse_qubit_order(self._probs, self.nq) if dict_format: - return {np.binary_repr(i, width=self.nq): val for i, val in enumerate(self._probs)} - return self._probs + return {np.binary_repr(i, width=self.nq): val for i, val in enumerate(probs)} + return probs def measure(self, dict_format=True) -> dict[str, float] | list[float]: """ @@ -118,30 +123,35 @@ class SimpleSimulator: if self._measure is None: self.probs() self._measure = self._runner.measure(self._mq) + measure = self.reverse_qubit_order(self._measure, len(self._mq)) + if dict_format: - return {np.binary_repr(i, width=len(self._mq)): val - for i, val in enumerate(self._measure)} - return self._measure + return {np.binary_repr(i, width=len(self._mq)): val for i, val in enumerate(measure)} + return measure def sample( self, shots: int = 100, - is_sorted: bool = True, dict_format: bool = True, - ): + ) -> dict[str, int] | list[str]: """ Sample measurement outcomes from the quantum state. Args: shots: Number of samples to take - is_sorted: Whether to sort results by bitstring - dict_format: Whether to return as dict (else string) + dict_format: Whether to return as dict Returns: dict: Measurement counts {bitstring: count} (e.g. {'00': 57, '01': 43}) str: Concatenated measurement results (e.g. '0001100101...') """ - return self._runner.sample(self._mq, shots, is_sorted, dict_format) + if self._state is None: + self.statevector() + samples = self._runner.sample(self._mq, shots) + if dict_format: + return {np.binary_repr(int(k), width=len(self._mq))[::-1]: v + for k, v in Counter(samples).items()} + return [np.binary_repr(v, width=len(self._mq))[::-1] for v in samples] # pylint: disable=too-many-branches def _run(self): @@ -233,3 +243,23 @@ class SimpleSimulator: 'add': self.backend.add, 'mul': self.backend.mul } + + @staticmethod + def reverse_qubit_order(list_value, num_qubits): + """ + Reverse the order of qubits in a list. + + Args: + list_value: List of values to reverse + num_qubits: Number of qubits + + Returns: + list: Reversed list of values + """ + indices = torch.arange(2 ** num_qubits) + reversed_indices = torch.zeros_like(indices) + + for i in range(num_qubits): + reversed_indices |= ((indices >> i) & 1) << (num_qubits - 1 - i) + + return list_value[reversed_indices] diff --git a/tests/circuit/test_parameter.py b/tests/circuit/test_parameter.py index 580a2a27d3019cd1a217c3cc57e261c1a0751cd8..a0b405fc08af1f4b92ee18e5d4193cfe9b5f16a0 100644 --- a/tests/circuit/test_parameter.py +++ b/tests/circuit/test_parameter.py @@ -30,6 +30,7 @@ def test_symbol(): theta = Parameter('theta') assert phi.symbol == Symbol('phi') assert theta.symbol == Symbol('theta') + assert str(-theta) == '-theta' def test_add(): diff --git a/tests/simulator/test_simple_sim.py b/tests/simulator/test_simple_sim.py new file mode 100644 index 0000000000000000000000000000000000000000..94dc65d7aa920efd6ab1bc93098ed5fae0fe24ac --- /dev/null +++ b/tests/simulator/test_simple_sim.py @@ -0,0 +1,147 @@ +# This code is part of cqlib. +# +# (C) Copyright China Telecom Quantum Group, QuantumCTek Co., Ltd., +# Center for Excellence in Quantum Information and Quantum Physics 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" Test the simple simulator. """ +from math import pi + +from cqlib import Circuit, Parameter +from cqlib.simulator import SimpleSimulator, StatevectorSimulator + +from .test_statevector_sim import all_close + + +# pylint: disable=missing-function-docstring + +def run_test(circuit: Circuit): + sim = SimpleSimulator(circuit) + sim2 = StatevectorSimulator(circuit) + all_close(sim.statevector(), sim2.statevector()) + all_close(sim.probs(), sim2.probs()) + all_close(sim.measure(), sim2.measure()) + + +def test_base(): + circuit = Circuit(2) + circuit.h(0) + circuit.cx(0, 1) + circuit.measure_all() + run_test(circuit) + + circuit = Circuit(2) + circuit.x(0) + circuit.measure_all() + run_test(circuit) + + circuit = Circuit(3) + circuit.h(1) + circuit.cx(1, 0) + circuit.measure(0) + circuit.measure(1) + run_test(circuit) + + circuit = Circuit(5) + circuit.h(1) + circuit.cx(1, 0) + circuit.cx(1, 2) + circuit.rx(1, pi / 4) + circuit.crx(0, 2, pi / 3) + circuit.ccx(0, 1, 2) + circuit.ry(3, pi) + circuit.crx(4, 1, pi / 2) + circuit.swap(4, 1) + circuit.swap(3, 2) + circuit.s(2) + circuit.ccx(3, 4, 1) + circuit.measure(0) + circuit.measure(1) + circuit.measure(4) + circuit.measure(3) + run_test(circuit) + + +def test_r(): + circuit = Circuit(3) + circuit.h(0) + circuit.rx(0, pi / 3) + circuit.ry(1, pi / 4) + circuit.rz(2, pi / 5) + circuit.rxy(1, pi / 3, pi / 2) + circuit.measure_all() + + run_test(circuit) + + +def test_c(): + circuit = Circuit(3) + circuit.h(0) + circuit.cx(0, 1) + circuit.crx(0, 1, pi / 3) + circuit.cy(0, 2) + circuit.cry(1, 2, pi / 4) + circuit.cz(0, 2) + circuit.crz(2, 0, pi / 5) + circuit.ccx(2, 0, 1) + circuit.measure_all() + + run_test(circuit) + + +def test_xy(): + circuit = Circuit(3) + circuit.h(0) + circuit.x2p(0) + circuit.x2m(1) + circuit.y2p(0) + circuit.y2m(1) + circuit.xy(2, pi / 3) + circuit.xy2p(2, - pi / 8) + circuit.xy2m(0, pi / 9) + circuit.measure_all() + + run_test(circuit) + + +def test_normal(): + circuit = Circuit(3) + circuit.h(0) + circuit.s(1) + circuit.sd(2) + circuit.t(0) + circuit.td(1) + circuit.u(2, 1, 2, 3) + circuit.i(0, 10) + circuit.measure_all() + run_test(circuit) + + +def test_grad(): + try: + # pylint: disable=import-outside-toplevel + import torch + except ImportError: + return + + params = [Parameter(f'p{i}') for i in range(2)] + circuit = Circuit(3, parameters=params) + circuit.h(0) + circuit.ry(1, params[0] + 2 * params[1]) + circuit.rx(2, params[1]) + circuit.measure_all() + + x = torch.tensor([pi / 2, 0.5], requires_grad=True) + c1 = circuit.assign_parameters(x, inplace=False) + + simple_sim = SimpleSimulator(c1) + s = simple_sim.measure(dict_format=False) + s[0].backward() + assert torch.allclose(x.grad, torch.tensor([-0.12680773437023163, -0.2631158232688904]))