diff --git a/cqlib/circuits/circuit.py b/cqlib/circuits/circuit.py index 248f5438fe296867703dcffabed56645f32eed73..bdc47a14c797508c84658e191e70ffa36d1cb5b1 100644 --- a/cqlib/circuits/circuit.py +++ b/cqlib/circuits/circuit.py @@ -109,6 +109,7 @@ class Circuit: """Return number of qubits.""" return len(self._qubits) + @property def circuit_data(self): """Circuit data""" return self._circuit_data @@ -695,8 +696,8 @@ class Circuit: values = {} values.update(kwargs) for param, value in values.items(): - if not isinstance(value, (float, int)): - raise ValueError("Parameter value must be a float or int.") + # if not isinstance(value, (float, int)): + # raise ValueError("Parameter value must be a float or int.") if isinstance(param, Parameter): if param not in self._parameters: diff --git a/cqlib/simulator/__init__.py b/cqlib/simulator/__init__.py index 92543e4c10955434e50f7c6e3ce03381c8f1ab56..34ad3f581034e2e147bb8a231fe510f5a3893cd1 100644 --- a/cqlib/simulator/__init__.py +++ b/cqlib/simulator/__init__.py @@ -16,5 +16,6 @@ Simulator module use classical computer to simulate quantum computers' behavior. """ from .statevector_simulator import StatevectorSimulator +from .simple_simulator import SimpleSimulator -__all__ = ['StatevectorSimulator'] +__all__ = ['StatevectorSimulator', 'SimpleSimulator'] diff --git a/cqlib/simulator/mergy.py b/cqlib/simulator/mergy.py index 6775babc394b00f19ade20bb250721dcce66e183..22ce38749311dbb4279c3eeb437b206c9f4d8c80 100644 --- a/cqlib/simulator/mergy.py +++ b/cqlib/simulator/mergy.py @@ -21,10 +21,6 @@ from collections.abc import Sequence import networkx as nx import numpy as np -x_mat = np.array([[0, 1], [1, 0]], dtype=np.complex128) -y_mat = np.array([[0, -1j], [1j, 0]], dtype=np.complex128) -z_mat = np.array([[1, 0], [0, -1]], dtype=np.complex128) - class Gate: """ diff --git a/cqlib/simulator/simple_sim/__init__.py b/cqlib/simulator/simple_sim/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1d21bda9382d90413c1893bf9eae5809cf13e64c --- /dev/null +++ b/cqlib/simulator/simple_sim/__init__.py @@ -0,0 +1,24 @@ +# 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. + +""" +Backend module for simulator. +""" + +from .simple_runner import SimpleRunner +from .torch_backend import TorchBackend + +__all__ = [ + "SimpleRunner", + "TorchBackend", +] diff --git a/cqlib/simulator/simple_sim/gates.py b/cqlib/simulator/simple_sim/gates.py new file mode 100644 index 0000000000000000000000000000000000000000..64c158a35c348f728bcdf627eb05bffe9d66d6a3 --- /dev/null +++ b/cqlib/simulator/simple_sim/gates.py @@ -0,0 +1,109 @@ +# 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. + +""" +Quantum gate matrix generators for simulation. + +This module provides matrix representations of common single-qubit rotation gates +(RX, RY, RZ) and their controlled versions (CRX, CRY, CRZ) using a specified +computational backend (TorchBackend). +""" + +import numpy as np + +from cqlib.circuits.gates import X, Y, Z + +from .torch_backend import TorchBackend + + +def rx_mat(theta, backend: TorchBackend): + """ + Generate the matrix representation of RX gate (rotation around X-axis). + """ + i = backend.eye(2) + x = backend.as_tensor(np.asarray(X())) + theta = backend.as_tensor(theta) + num = backend.numel(theta) + if num != 1: + raise ValueError( + f"The number of parameters for `RX` gate can only be 1 but got `{num}`." + ) + return backend.cos(theta / 2.0) * i - backend.i() * backend.sin(theta / 2.0) * x + + +def ry_mat(theta, backend: TorchBackend): + """ + Generate the matrix representation of RY gate (rotation around Y-axis). + """ + i = backend.eye(2) + y = backend.as_tensor(np.asarray(Y())) + theta = backend.as_tensor(theta) + num = backend.numel(theta) + if num != 1: + raise ValueError( + f"The number of parameters for `RY` gate can only be 1 but got `{num}`." + ) + return backend.cos(theta / 2.0) * i - backend.i() * backend.sin(theta / 2.0) * y + + +def rz_mat(theta, backend: TorchBackend): + """ + Generate the matrix representation of RZ gate (rotation around Z-axis). + """ + i = backend.eye(2) + z = backend.as_tensor(np.asarray(Z())) + theta = backend.as_tensor(theta) + num = backend.numel(theta) + if num != 1: + raise ValueError( + f"The number of parameters for `RZ` gate can only be 1 but got `{num}`." + ) + return backend.cos(theta / 2.0) * i - backend.i() * backend.sin(theta / 2.0) * z + + +def crx_mat(theta, backend: TorchBackend): + """ + Generate the matrix representation of controlled-RX gate. + """ + rx = rx_mat(theta, backend) + return _cr_mat(backend, rx) + + +def cry_mat(theta, backend: TorchBackend): + """ + Generate the matrix representation of controlled-RY gate. + """ + ry = ry_mat(theta, backend) + return _cr_mat(backend, ry) + + +def crz_mat(theta, backend: TorchBackend): + """ + Generate the matrix representation of controlled-RZ gate. + """ + rz = rz_mat(theta, backend) + return _cr_mat(backend, rz) + + +def _cr_mat(backend: TorchBackend, mat): + """ + Internal function: Construct matrix for controlled rotation gates. + """ + i_2x2 = backend.eye(2) + zero_2x2 = backend.zeros((2, 2)) + + crz = backend.vstack([ + backend.hstack([i_2x2, zero_2x2]), + backend.hstack([zero_2x2, mat]) + ]) + return crz diff --git a/cqlib/simulator/simple_sim/simple_runner.py b/cqlib/simulator/simple_sim/simple_runner.py new file mode 100644 index 0000000000000000000000000000000000000000..4c240ef1a87153475def4c4d86dffa92ef0bc118 --- /dev/null +++ b/cqlib/simulator/simple_sim/simple_runner.py @@ -0,0 +1,276 @@ +# 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. + +""" +Quantum Circuit Simulator Runner +""" + +# pylint: disable=invalid-name +from collections import Counter + +import numpy as np + +from .torch_backend import TorchBackend + + +# pylint: disable=too-many-public-methods +class SimpleRunner: + """ + A simple quantum circuit simulator using tensor operations. + """ + + def __init__(self, nq: int, backend: TorchBackend) -> None: + """Initialize the quantum circuit simulator. + + Args: + nq: Number of qubits in the circuit + backend: Computational backend for tensor operations + """ + self.backend = backend + self.nq = nq + self._state = backend.zeros(2 ** nq) + self._state[0] = 1.0 + 0j + self._shape = [2 for _ in range(self.nq)] + self._state = self.backend.reshape(self._state, self._shape) + + def _inv_subscripts(self, subscripts: list[int]) -> list[int]: + """ + Compute inverse permutation of qubit indices. + + Args: + subscripts: List of permuted indices + + Returns: + List of inverse permutation indices + """ + subscripts = np.asarray(subscripts) + inv = np.empty_like(subscripts) + inv[subscripts] = np.arange(subscripts.shape[0]) + return inv.tolist() + + def H(self, i: int, mat: np.ndarray): + """Apply Hadamard gate to qubit i.""" + return self.apply_single_qubit_gate(i, mat) + + def X(self, i: int, mat: np.ndarray): + """Apply Pauli-X gate to qubit i.""" + return self.apply_single_qubit_gate(i, mat) + + def Y(self, i: int, mat: np.ndarray): + """Apply Pauli-Y gate to qubit i.""" + return self.apply_single_qubit_gate(i, mat) + + def Z(self, i: int, mat: np.ndarray): + """Apply Pauli-Z gate to qubit i.""" + return self.apply_single_qubit_gate(i, mat) + + def S(self, i: int, mat: np.ndarray): + """Apply S gate to qubit i.""" + return self.apply_single_qubit_gate(i, mat) + + def T(self, i: int, mat: np.ndarray): + """Apply T gate to qubit i.""" + return self.apply_single_qubit_gate(i, mat) + + def SD(self, i: int, mat: np.ndarray): + """Apply s dg gate to qubit i.""" + return self.apply_single_qubit_gate(i, mat) + + def TD(self, i: int, mat: np.ndarray): + """Apply T dg gate to qubit i.""" + return self.apply_single_qubit_gate(i, mat) + + def RX(self, i: int, mat: np.ndarray): + """Apply rotation around X-axis gate to qubit i.""" + return self.apply_single_qubit_gate(i, mat) + + def RY(self, i: int, mat: np.ndarray): + """Apply rotation around Y-axis gate to qubit i.""" + return self.apply_single_qubit_gate(i, mat) + + def RZ(self, i: int, mat: np.ndarray): + """Apply rotation around Z-axis gate to qubit i.""" + return self.apply_single_qubit_gate(i, mat) + + def X2M(self, i: int, mat: np.ndarray): + """Apply X/2 (π/2 rotation around X-axis) gate to qubit i.""" + return self.apply_single_qubit_gate(i, mat) + + def X2P(self, i: int, mat: np.ndarray): + """Apply inverse X/2 (-π/2 rotation around X-axis) gate to qubit i.""" + return self.apply_single_qubit_gate(i, mat) + + def Y2M(self, i: int, mat: np.ndarray): + """Apply Y/2 (π/2 rotation around Y-axis) gate to qubit i.""" + return self.apply_single_qubit_gate(i, mat) + + def Y2P(self, i: int, mat: np.ndarray): + """Apply inverse Y/2 (-π/2 rotation around Y-axis) 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. + + Args: + i: Target qubit index + mat: 2x2 unitary matrix representing the gate + + Returns: + self for method chaining + """ + subscripts = list(range(self.nq)) + subscripts.remove(i) + subscripts = [i] + subscripts + self._state = self.backend.reshape( + self.backend.permute(self._state, subscripts), (2, -1) + ) + self._state = self.backend.matmul( + self.backend.reshape(mat, (2, 2)), + self._state + ) + self._state = self.backend.permute( + self.backend.reshape(self._state, self._shape), + self._inv_subscripts(subscripts), + ) + return self + + def CX(self, i: int, j: int, mat: np.ndarray): + """Apply controlled-X (CNOT) gate with control i and target j.""" + return self.apply_two_qubit_gate(i, j, mat) + + def CY(self, i: int, j: int, mat: np.ndarray): + """Apply controlled-Y gate with control i and target j.""" + return self.apply_two_qubit_gate(i, j, mat) + + def CZ(self, i: int, j: int, mat: np.ndarray): + """Apply controlled-Z gate with control i and target j.""" + return self.apply_two_qubit_gate(i, j, mat) + + def CRX(self, i: int, j: int, mat: np.ndarray): + """Apply controlled rotation around X-axis gate.""" + return self.apply_two_qubit_gate(i, j, mat) + + def CRY(self, i: int, j: int, mat: np.ndarray): + """Apply controlled rotation around Y-axis gate.""" + return self.apply_two_qubit_gate(i, j, mat) + + def CRZ(self, i: int, j: int, mat: np.ndarray): + """Apply controlled rotation around Z-axis gate.""" + 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. + + Args: + i: Control qubit index + j: Target qubit index + mat: 4x4 unitary matrix representing the gate + + Returns: + self for method chaining + """ + subscripts = list(range(self.nq)) + subscripts.remove(i) + subscripts.remove(j) + subscripts = [i, j] + subscripts + + self._state = self.backend.reshape( + self.backend.permute(self._state, subscripts), (4, -1) + ) + self._state = self.backend.matmul( + self.backend.reshape(mat, (4, 4)), self._state + ) + + self._state = self.backend.permute( + self.backend.reshape(self._state, self._shape), + self._inv_subscripts(subscripts), + ) + return self + + def state(self): + """Get the current state vector as a flattened tensor. + + Returns: + torch.Tensor: Flattened state vector + """ + return self.backend.ravel(self._state) + + def probs(self): + """Compute measurement probabilities for all basis states. + + Returns: + torch.Tensor: Probability distribution over computational basis + """ + state = self.state() + return self.backend.real(self.backend.conj(state) * state) + + def measure(self, mq: list[int]): + """Compute measurement probabilities for specified qubits. + + Args: + mq: List of qubit indices to measure + + Returns: + torch.Tensor: Marginal probability distribution + + Raises: + ValueError: If specified qubits are invalid + """ + mq = list(mq) + idx = list(range(self.nq)) + try: + for i in mq: + idx.remove(i) + except ValueError as e: + raise ValueError("Make sure qubits to be measured are correct.") from e + idx = mq + idx + + probs = self.probs().reshape([2 for _ in range(self.nq)]) + + return self.backend.real( + self.backend.sum( + self.backend.reshape( + self.backend.permute(probs, idx), [2 ** len(mq), -1] + ), + axis=1, + ) + ) + + def sample( + self, + mq: list[int], + shots: int = 100, + is_sorted: bool = True, + dict_format: bool = True, + ): + """Sample measurement outcomes from the quantum state. + + Args: + mq: List of qubit indices to measure + shots: Number of samples to take + is_sorted: Whether to sort results by bitstring + dict_format: Whether to return as dict (else string) + + Returns: + dict/str: Measurement results as counts or concatenated strings + """ + 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 diff --git a/cqlib/simulator/simple_sim/torch_backend.py b/cqlib/simulator/simple_sim/torch_backend.py new file mode 100644 index 0000000000000000000000000000000000000000..85d02c1ccaec75d635d8e856cc2717fc7a8b08f9 --- /dev/null +++ b/cqlib/simulator/simple_sim/torch_backend.py @@ -0,0 +1,207 @@ +# 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. + +""" +PyTorch Backend for Quantum Circuit Simulation +""" +try: + import torch +except ImportError: + pass + +# pylint: disable=too-many-public-methods +class TorchBackend: + """ + PyTorch Backend + """ + def __init__(self, dtype: type | None = None, device="cpu") -> None: + """Initialize the PyTorch backend. + + Args: + dtype: Data type for tensors (default: torch.complex128) + device: Device for computation ('cpu' or 'cuda') + """ + if dtype is None: + dtype = torch.complex128 + self.dtype = dtype + if not device: + device = 'cuda' if torch.cuda.is_available() else 'cpu' + if isinstance(device, str): + device = torch.device(device) + self.device = device + + def as_tensor(self, tensor): + """Convert input to PyTorch tensor with specified dtype and device. + + Args: + tensor: Input data to convert + + Returns: + torch.Tensor: Converted tensor + """ + return torch.as_tensor(tensor, dtype=self.dtype, device=self.device) + + def zeros(self, shape): + """Create tensor filled with zeros. + + Args: + shape: Shape of the output tensor + + Returns: + torch.Tensor: Zero-filled tensor + """ + return torch.zeros(shape, dtype=self.dtype, device=self.device) + + def reshape(self, tensor, shape): + """Reshape tensor to specified shape. + + Args: + tensor: Input tensor + shape: Target shape + + Returns: + torch.Tensor: Reshaped tensor + """ + tensor = self.as_tensor(tensor) + return torch.reshape(tensor, shape) + + def matmul(self, tensor1, tensor2): + """Matrix multiplication of two tensors. + + Args: + tensor1: First input tensor + tensor2: Second input tensor + + Returns: + torch.Tensor: Result of matrix multiplication + """ + tensor1 = self.as_tensor(tensor1) + tensor2 = self.as_tensor(tensor2) + return torch.matmul(tensor1, tensor2) + + def vstack(self, tensors): + """Stack tensors vertically (row-wise). + + Args: + tensors: Sequence of tensors to stack + + Returns: + torch.Tensor: Vertically stacked tensor + """ + tensors = [self.as_tensor(tensor) for tensor in tensors] + return torch.vstack(tensors) + + def hstack(self, tensors): + """ Stack tensors horizontally (column-wise). """ + tensors = [self.as_tensor(tensor) for tensor in tensors] + return torch.hstack(tensors) + + def ravel(self, tensor): + """ Flatten a tensor into 1-dimensional array.""" + tensor = self.as_tensor(tensor) + return torch.ravel(tensor) + + def conj(self, tensor): + """ Compute the complex conjugate of a tensor. """ + tensor = self.as_tensor(tensor) + return torch.conj(tensor) + + def real(self, tensor): + """ Get the real part of a complex tensor. """ + tensor = self.as_tensor(tensor) + return torch.real(tensor) + + def sin(self, tensor): + """ Compute sine of tensor elements. """ + tensor = self.as_tensor(tensor) + return torch.sin(tensor) + + def cos(self, tensor): + """ Compute cosine of tensor elements. """ + tensor = self.as_tensor(tensor) + return torch.cos(tensor) + + def exp(self, tensor): + """ Compute exponential of tensor elements. """ + tensor = self.as_tensor(tensor) + return torch.exp(tensor) + + def sqrt(self, tensor): + """ Compute square root of tensor elements. """ + tensor = self.as_tensor(tensor) + return torch.sqrt(tensor) + + def add(self, tensor, other): + """ Add two tensors element-wise. """ + tensor = self.as_tensor(tensor) + other = self.as_tensor(other) + return torch.add(tensor, other) + + def mul(self, tensor, other): + """ Multiply two tensors element-wise. """ + tensor = self.as_tensor(tensor) + other = self.as_tensor(other) + return torch.mul(tensor, other) + + def i(self): + """ Get the imaginary unit (1j) as a tensor. """ + return torch.tensor(1j, dtype=self.dtype, device=self.device) + + def permute(self, tensor, axes=None): + """ Permute tensor dimensions according to given axes. """ + tensor = self.as_tensor(tensor) + return torch.permute(tensor, axes) + + def sum(self, tensor, axis=None): + """ Sum tensor elements over given axis. """ + tensor = self.as_tensor(tensor) + return torch.sum(tensor, axis) + + def to_numpy(self, tensor): + """ Convert tensor to NumPy array. """ + tensor = self.as_tensor(tensor) + return tensor.detach().cpu().numpy() + + def einsum(self, subscripts: str, *tensors): + """ Einstein summation convention. """ + tensors = [self.as_tensor(tensor) for tensor in tensors] + return torch.einsum(subscripts, *tensors) + + def kron(self, tensor1, tensor2): + """ Compute Kronecker product of two tensors. """ + tensor1 = self.as_tensor(tensor1) + tensor2 = self.as_tensor(tensor2) + return torch.kron(tensor1, tensor2) + + def eye(self, n: int): + """Create identity matrix of size N×N. + + Args: + n: Size of the identity matrix + + Returns: + torch.Tensor: Identity matrix + """ + return torch.eye(n, dtype=self.dtype) + + def numel(self, tensor) -> int: + """Get total number of elements in tensor. + + Args: + tensor: Input tensor + + Returns: + int: Total number of elements + """ + tensor = self.as_tensor(tensor) + return tensor.numel() diff --git a/cqlib/simulator/simple_simulator.py b/cqlib/simulator/simple_simulator.py new file mode 100644 index 0000000000000000000000000000000000000000..376220cac2a4515ec218c6d36b4c50605f8b2ff4 --- /dev/null +++ b/cqlib/simulator/simple_simulator.py @@ -0,0 +1,198 @@ +# 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. + +""" +Quantum Circuit Simulator Implementation +""" + +import numpy as np +from sympy import lambdify + +from cqlib import Parameter +from cqlib.circuits.circuit import Circuit + +from .simple_sim import TorchBackend, SimpleRunner, gates as simple_gates + + +# pylint: disable=too-many-instance-attributes +class SimpleSimulator: + """PyTorch-based quantum circuit simulator. + + This class provides a high-performance quantum circuit simulator using PyTorch + as the computational backend. It supports both statevector simulation and + measurement sampling. + """ + + def __init__( + self, + circuit: Circuit, + device: str | int | None = None + ): + """Initialize the quantum circuit simulator. + + Args: + circuit: Quantum circuit to simulate + device: Computation device ('cpu' or 'cuda') + + Raises: + ImportError: If PyTorch is not installed + """ + try: + # pylint: disable=import-outside-toplevel + import torch + except ImportError as e: + raise ImportError("PyTorch is not installed. `pip install torch` " + "Please install it to use the SimpleSimulator. ") from e + if device is None: + device = "cuda" if torch.cuda.is_available() else "cpu" + + self.circuit = circuit + self.backend = TorchBackend(device=torch.device(device)) + self._runner = SimpleRunner(nq=len(self.circuit.qubits), backend=self.backend) + self.nq = self._runner.nq + self._mq = [] + self._state = None + self._probs = None + self._measure = None + + def statevector(self) -> dict[str, complex]: + """Get the final statevector of the quantum circuit. + + Returns: + dict: Mapping from bitstrings to complex amplitudes + """ + if self._state is None: + self._run() + self._state = self._runner.state() + + return {np.binary_repr(i, width=self.nq): val for i, val in enumerate(self._state)} + + def probs(self) -> dict[str, float]: + """Calculate measurement probabilities for all basis states. + + Returns: + dict: Mapping from bitstrings to probabilities + """ + if self._probs is None: + self.statevector() + self._probs = self._runner.probs() + return {np.binary_repr(i, width=self.nq): val for i, val in enumerate(self._probs)} + + def measure(self) -> dict[str, float]: + """Get measurement probabilities for measured qubits. + + Returns: + dict: Mapping from bitstrings to probabilities + """ + if self._measure is None: + self.probs() + self._measure = self._runner.measure(self._mq) + return {np.binary_repr(i, width=len(self._mq)): val for i, val in enumerate(self._measure)} + + def sample( + self, + shots: int = 100, + is_sorted: bool = True, + dict_format: bool = True, + ): + """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) + + Returns: + dict/str: Measurement results as counts or concatenated strings + """ + return self._runner.sample(self._mq, shots, is_sorted, dict_format) + + def _run(self): + """Internal method to execute the quantum circuit.""" + for item in self.circuit.circuit_data: + instr = item.instruction + if instr.name == 'M': + self._mq.append(item.qubits[0].index) + if instr.name in ['I', 'B', 'M']: + continue + if instr.params: + ps = self.params_value_with_gradient(instr.params, self.circuit.parameters_value) + if instr.name == 'RX': + ins_mat = simple_gates.rx_mat(ps[0], self.backend) + elif instr.name == 'RY': + ins_mat = simple_gates.ry_mat(ps[0], self.backend) + elif instr.name == 'RZ': + ins_mat = simple_gates.rz_mat(ps[0], self.backend) + elif instr.name == 'CRX': + ins_mat = simple_gates.crx_mat(ps[0], self.backend) + elif instr.name == 'CRY': + ins_mat = simple_gates.cry_mat(ps[0], self.backend) + elif instr.name == 'CRZ': + ins_mat = simple_gates.crz_mat(ps[0], self.backend) + else: + raise ValueError("Unknown gate.") + getattr(self._runner, instr.name)( + *[q.index for q in item.qubits], + mat=ins_mat + ) + else: + getattr(self._runner, instr.name)( + *[q.index for q in item.qubits], + mat=np.asarray(instr) + ) + + def params_value_with_gradient( + self, + params: list[Parameter | float | complex | int], + parameters_value: dict[Parameter, float | int] + ): + """Evaluate parameter expressions with gradient support. + + Args: + params: List of parameters/expressions to evaluate + parameters_value: Mapping of parameter values + + Returns: + list: Evaluated parameter values + """ + values = [] + for p in params: + if isinstance(p, Parameter): + expr = p.symbol + base_params = [param for param in expr.free_symbols + if Parameter(str(param)) in parameters_value] + param_dict = {str(sym): parameters_value[Parameter(str(sym))] + for sym in base_params} + func = lambdify(base_params, expr, modules=self._torch_wrapper()) + tensor_args = [param_dict[str(sym)] for sym in base_params] + value = func(*tensor_args) + tensor_val = self.backend.as_tensor(value) + else: + tensor_val = self.backend.as_tensor(p) + values.append(tensor_val) + return values + + def _torch_wrapper(self): + """Create function mapping from sympy to PyTorch operations. + + Returns: + dict: Mapping of sympy functions to PyTorch operations + """ + return { + 'sin': self.backend.sin, + 'cos': self.backend.cos, + 'exp': self.backend.exp, + 'sqrt': self.backend.sqrt, + 'add': self.backend.add, + 'mul': self.backend.mul + } diff --git a/pyproject.toml b/pyproject.toml index 4973da940f459f604891119e823584383ebdf3e7..a8809c9d65a9f80d32988b8e9d89d0d2acec7074 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,11 +29,16 @@ keywords = [ "quantum", "sdk", ] -dynamic = ["version", "dependencies",] +dynamic = ["version", "dependencies", ] + +[project.optional-dependencies] +simple_sim = [ + "torch>=2.0.0" +] [tool.setuptools.dynamic] version = { file = "cqlib/VERSION.txt" } -dependencies = {file = "requirements.txt" } +dependencies = { file = "requirements.txt" } [project.urls] Homepage = "https://qc.zdxlz.com/" @@ -61,12 +66,12 @@ environment = "MACOSX_DEPLOYMENT_TARGET=12" repair-wheel-command = "delocate-wheel -w {dest_dir} -v {wheel} && pipx run abi3audit --strict --report {wheel}" [tool.cibuildwheel.windows] -environment = { CC="gcc", CXX="g++" } +environment = { CC = "gcc", CXX = "g++" } repair-wheel-command = [ "pip install delvewheel", 'delvewheel repair -w {dest_dir} {wheel}', -# windows 上不使用 abi3,而是每个 Python 版本打包一个 wheel 文件 -# "pipx run abi3audit --strict --report {wheel}" + # windows 上不使用 abi3,而是每个 Python 版本打包一个 wheel 文件 + # "pipx run abi3audit --strict --report {wheel}" ] diff --git a/tests/circuit/test_dag.py b/tests/circuit/test_dag.py index fda2af3048cf0e1c195a3e802ef8ec15c1a2d6a4..ee72374aa84f097f15ca34dc4031ee1496299dd1 100644 --- a/tests/circuit/test_dag.py +++ b/tests/circuit/test_dag.py @@ -74,7 +74,7 @@ class TestDAGToCircuit: assert len(circuit.qubits) == 2 assert set(circuit.qubits) == {Qubit(0), Qubit(1)} - assert len(circuit.circuit_data()) == 3 + assert len(circuit.circuit_data) == 3 assert circuit.as_str() == 'H Q1\nH Q0\nCX Q1 Q0' def test_cycle_error(self):