From ac779bb74b3acc920c6435c5617fc252f6181ca0 Mon Sep 17 00:00:00 2001 From: Gao Jianjian Date: Wed, 2 Apr 2025 11:40:07 +0800 Subject: [PATCH 1/4] feat(cqlib/circuits/parameter): add support for negation operation in Parameter Implemented the `__neg__` method in the Parameter class to allow negation operations, enabling more flexible arithmetic manipulations of parameters. --- cqlib/circuits/parameter.py | 3 +++ tests/circuit/test_parameter.py | 1 + 2 files changed, 4 insertions(+) diff --git a/cqlib/circuits/parameter.py b/cqlib/circuits/parameter.py index 5924d7f..14a415b 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/tests/circuit/test_parameter.py b/tests/circuit/test_parameter.py index 580a2a2..a0b405f 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(): -- Gitee From aec133f47fa1d49c91c07d15f2b286ab98099210 Mon Sep 17 00:00:00 2001 From: Gao Jianjian Date: Wed, 2 Apr 2025 17:41:11 +0800 Subject: [PATCH 2/4] refactor(simple_sim/gates): simplify XY2P and XY2M gate matrix construction Removed intermediate computation steps for clearer and more concise matrix construction in XY2P and XY2M gates. Combined operations to reduce redundancy and improved readability by directly constructing matrix rows with necessary elements. --- cqlib/simulator/simple_sim/gates.py | 30 +++++++++++++---------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/cqlib/simulator/simple_sim/gates.py b/cqlib/simulator/simple_sim/gates.py index 9d377fe..8353b9a 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): -- Gitee From 9e10e7e76c9bbb53466d57daf2251f4bf50be707 Mon Sep 17 00:00:00 2001 From: Gao Jianjian Date: Wed, 2 Apr 2025 17:44:50 +0800 Subject: [PATCH 3/4] feat(simple_simulator): add reverse qubit order functionality and CCX gate Implement reverse_qubit_order method in SimpleSimulator to reorder state vectors, probabilities, and measurement outcomes. Add support for the CCX gate in SimpleRunner. Refactor sample method to remove unused parameters and improve output formatting. --- cqlib/simulator/simple_sim/simple_runner.py | 23 ++++---- cqlib/simulator/simple_simulator.py | 64 +++++++++++++++------ 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/cqlib/simulator/simple_sim/simple_runner.py b/cqlib/simulator/simple_sim/simple_runner.py index 2f19ac4..03763bd 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 0f59b4f..66813e5 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] -- Gitee From ed44e23d20e5f0bea1a3d02d5931b63dbc9eaf56 Mon Sep 17 00:00:00 2001 From: Gao Jianjian Date: Wed, 2 Apr 2025 17:48:20 +0800 Subject: [PATCH 4/4] test(simulator): add comprehensive tests for SimpleSimulator Added multiple test cases to validate the functionality of SimpleSimulator against StatevectorSimulator. Test cases include basic gates, controlled gates, rotation gates, parameterized circuits, and gradient calculations using PyTorch. Ensured all statevectors, probabilities, and measurement outcomes match between the two simulators. --- tests/simulator/test_simple_sim.py | 147 +++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 tests/simulator/test_simple_sim.py diff --git a/tests/simulator/test_simple_sim.py b/tests/simulator/test_simple_sim.py new file mode 100644 index 0000000..94dc65d --- /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])) -- Gitee