diff --git a/cqlib/circuits/circuit.py b/cqlib/circuits/circuit.py index 4a26a526c0d1182a58ce3a272ec948853ffba452..2b02212ccf0c56b2447f52f2d72d3dbdf86a50b7 100644 --- a/cqlib/circuits/circuit.py +++ b/cqlib/circuits/circuit.py @@ -21,6 +21,8 @@ from copy import deepcopy from math import pi from typing import Union, Sequence, Optional +import numpy as np + from cqlib.circuits import gates from cqlib.circuits.barrier import Barrier from cqlib.circuits.instruction import Instruction @@ -650,10 +652,9 @@ class Circuit: if qubit not in measured_qubits: self.append(Measure(), qubit) - # pylint: disable=too-many-branches def assign_parameters( self, - values: dict[str | Parameter, float | int] = None, + values: dict[str | Parameter, float | int] | Sequence[float | int] = None, inplace: bool = False, **kwargs ): @@ -691,40 +692,24 @@ class Circuit: if values is None: values = {} + _t = type(values) + if isinstance(values, (Sequence, np.ndarray)) or \ + (hasattr(_t, "__module__") and _t.__module__ == 'torch' + and _t.__name__ == 'Tensor'): + # list like type, list/tuple/np.ndarray/torch.Tensor + if len(values) != len(target.parameters_value): + raise ValueError(f"Length of values {len(values)} does not match " + f"the number of parameters {len(self._parameters)}.") + values = dict(zip(self._parameters, 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 isinstance(value, str): + param = Parameter(param) + if param not in self._parameters: + raise KeyError(f"Parameter {param} not found.") - if isinstance(param, Parameter): - if param not in self._parameters: - raise KeyError(f"Parameter {param} not found.") - elif isinstance(param, str): - if not param.isidentifier(): - raise ValueError(f"Invalid parameter identifier: {param}") - params = [p for p in self._parameters if p == param] - if not params: - raise ValueError(f"Parameter {param} not found in circuit.") - param = params[0] - else: - raise TypeError(f"Parameter identifier must be a string or Parameter," - f" got {type(param).__name__}.") - self._parameters[param] = value - - for item in target.instruction_sequence: - instruction = item.instruction - if instruction.params: - ps = [] - for p in instruction.params: - if isinstance(p, Parameter): - p = p.value(self._parameters) - if p.is_Number: - p = float(p) - elif p.is_symbol: - p = Parameter(p) - - ps.append(p) - instruction.params = ps + # pylint: disable=protected-access + target._parameters[param] = value return target @property @@ -788,7 +773,7 @@ class Circuit: value = param if isinstance(value, (float, int)): # QCIS param value must be [-pi, pi) - if qcis_compliant: + if qcis_compliant and (value >= pi or value < -pi): value = (value + pi) % (2 * pi) - pi value = str(value).rstrip('0').rstrip('.') line.append(value) diff --git a/cqlib/simulator/simple_sim/gates.py b/cqlib/simulator/simple_sim/gates.py index 64c158a35c348f728bcdf627eb05bffe9d66d6a3..9d377fe65dce7a3f960f52689c8177e9eb14c808 100644 --- a/cqlib/simulator/simple_sim/gates.py +++ b/cqlib/simulator/simple_sim/gates.py @@ -25,13 +25,17 @@ from cqlib.circuits.gates import X, Y, Z from .torch_backend import TorchBackend +x_mat = np.asarray(X()) +y_mat = np.asarray(Y()) +z_mat = np.asarray(Z()) + 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())) + x = backend.as_tensor(x_mat) theta = backend.as_tensor(theta) num = backend.numel(theta) if num != 1: @@ -46,7 +50,7 @@ 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())) + y = backend.as_tensor(y_mat) theta = backend.as_tensor(theta) num = backend.numel(theta) if num != 1: @@ -61,7 +65,7 @@ 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())) + z = backend.as_tensor(z_mat) theta = backend.as_tensor(theta) num = backend.numel(theta) if num != 1: @@ -71,6 +75,165 @@ def rz_mat(theta, backend: TorchBackend): return backend.cos(theta / 2.0) * i - backend.i() * backend.sin(theta / 2.0) * z +def xy_mat(theta, backend: TorchBackend): + """ + Generate the matrix representation of the XY gate using hstack/vstack. + + Args: + theta: Rotation angle (in radians) + backend: Backend support (e.g., TorchBackend) + + Returns: + Tensor representation of the XY gate + """ + theta = backend.as_tensor(theta) + num = backend.numel(theta) + if num != 1: + raise ValueError( + f"XY gate requires exactly 1 parameter, got {num}." + ) + + # Compute exp(iθ) and exp(-iθ) + exp_i_theta = backend.exp(backend.i() * theta) + exp_neg_i_theta = backend.exp(-backend.i() * theta) + + matrix = -backend.i() * backend.vstack([ + backend.hstack([backend.zeros(1), exp_neg_i_theta]), + backend.hstack([exp_i_theta, backend.zeros(1)]) + ]) + + return matrix + + +def xy2p_mat(theta, backend: TorchBackend): + """ + Generate the matrix representation of the XY2P gate. + + Args: + theta: Rotation angle (in radians) + backend: Backend support (e.g., TorchBackend) + + Returns: + torch.Tensor: Matrix representation of the XY2P gate + """ + # Convert theta to tensor and validate + theta = backend.as_tensor(theta) + if backend.numel(theta) != 1: + raise ValueError("XY2P gate requires exactly 1 parameter.") + + # Compute required components + i = backend.i() # Imaginary unit 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 + + +def xy2m_mat(theta, backend: TorchBackend): + """ + Generate the matrix representation of the XY2M gate. + + Args: + theta: Rotation angle (in radians) + backend: Backend support (e.g., TorchBackend) + + Returns: + torch.Tensor: Matrix representation of the XY2M gate + """ + # Convert theta to tensor and validate + theta = backend.as_tensor(theta) + if backend.numel(theta) != 1: + raise ValueError("XY2P gate requires exactly 1 parameter.") + + # Compute required components + i = backend.i() # Imaginary unit 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) # 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]) + + # Combine rows and apply normalization + matrix = sqrt2_inv * backend.vstack([row1, row2]) + + return matrix + + +def rxy_mat(phi, theta, backend: TorchBackend): + """ + Generate the matrix representation of RXY gate + """ + i = backend.eye(2) + x = backend.as_tensor(x_mat) + y = backend.as_tensor(y_mat) + phi = backend.as_tensor(phi) + theta = backend.as_tensor(theta) + num_theta = backend.numel(theta) + num_phi = backend.numel(phi) + if num_theta != 1 or num_phi != 1: + raise ValueError( + f"The number of parameters for `RXY` gate must be 2 (theta and phi)" + f" but got {num_theta} and {num_phi}." + ) + cos_term = backend.cos(theta / 2.0) * i + sin_term = (-backend.i() * backend.sin(theta / 2.0) * + (backend.cos(phi) * x + backend.sin(phi) * y)) + return cos_term + sin_term + + +def u_mat(theta, phi, lam, backend: TorchBackend): + """ + Generate the matrix representation of the U gate. + + Args: + theta: Rotation angle (in radians) + phi: First phase angle (in radians) + lam: Second phase angle (in radians) + backend: The backend used for tensor operations (e.g., TorchBackend) + + Returns: + Tensor representation of the U gate + """ + # Convert inputs to tensors and validate + theta = backend.as_tensor(theta) + phi = backend.as_tensor(phi) + lam = backend.as_tensor(lam) + + if (backend.numel(theta) != 1 or + backend.numel(phi) != 1 or + backend.numel(lam) != 1): + raise ValueError("U gate requires exactly 1 value for each parameter.") + + # Compute trigonometric terms + half_theta = theta / 2 + cos = backend.cos(half_theta) + sin = backend.sin(half_theta) + i = backend.i() + + # Build matrix using hstack/vstack + row1 = backend.hstack([cos, -backend.exp(i * lam) * sin]) + row2 = backend.hstack([backend.exp(i * phi) * sin, backend.exp(i * (phi + lam)) * cos]) + matrix = backend.vstack([row1, row2]) + + return matrix + + def crx_mat(theta, backend: TorchBackend): """ Generate the matrix representation of controlled-RX gate. diff --git a/cqlib/simulator/simple_sim/simple_runner.py b/cqlib/simulator/simple_sim/simple_runner.py index 4c240ef1a87153475def4c4d86dffa92ef0bc118..2f19ac42a2994c97f68bb4e1eb832991c7df572f 100644 --- a/cqlib/simulator/simple_sim/simple_runner.py +++ b/cqlib/simulator/simple_sim/simple_runner.py @@ -102,6 +102,24 @@ class SimpleRunner: """Apply rotation around Z-axis gate to qubit i.""" return self.apply_single_qubit_gate(i, mat) + def XY(self, i: int, mat: np.ndarray): + """Apply rotation around Z-axis gate to qubit i.""" + return self.apply_single_qubit_gate(i, mat) + + def XY2P(self, i: int, mat: np.ndarray): + """Apply rotation around the X-axis by pi/2, + modulated by a phase theta around the Y-axis.""" + return self.apply_single_qubit_gate(i, mat) + + def XY2M(self, i: int, mat: np.ndarray): + """Apply rotation around the X-axis by -pi/2, + modulated by a phase theta around the Y-axis.""" + return self.apply_single_qubit_gate(i, mat) + + def RXY(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) @@ -169,6 +187,10 @@ 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): + """Apply SWAP gate.""" + return self.apply_three_qubit_gate(i, j, k, mat) + def apply_two_qubit_gate(self, i: int, j: int, mat: np.ndarray): """Apply arbitrary two-qubit gate to specified qubits. @@ -198,6 +220,40 @@ class SimpleRunner: ) return self + def apply_three_qubit_gate(self, i: int, j: int, k: int, mat: np.ndarray): + """Apply arbitrary three-qubit gate to specified qubits. + + Args: + i: First control qubit index + j: Second control qubit index + k: Target qubit index + mat: 8x8 unitary matrix representing the gate (e.g., CCX/Toffoli) + + Returns: + self for method chaining + """ + # Reorder qubits so that i, j, k are the first three dimensions + subscripts = list(range(self.nq)) + subscripts.remove(i) + subscripts.remove(j) + subscripts.remove(k) + subscripts = [i, j, k] + subscripts # Bring i, j, k to the front + + # Reshape state into (8, -1) to apply the 8x8 gate + self._state = self.backend.reshape( + self.backend.permute(self._state, subscripts), (8, -1)) + + # Apply the gate (mat is 8x8) + self._state = self.backend.matmul(mat, self._state) + + # Reshape back and restore original qubit order + self._state = self.backend.reshape(self._state, self._shape) + self._state = self.backend.permute( + self._state, + self._inv_subscripts(subscripts) # Inverse permutation + ) + return self + def state(self): """Get the current state vector as a flattened tensor. diff --git a/cqlib/simulator/simple_simulator.py b/cqlib/simulator/simple_simulator.py index e2c11749e7b1be8b89005cd43535d44fb073d66f..0f59b4f2e6a975eeaf4f10569d27e8863ea441eb 100644 --- a/cqlib/simulator/simple_simulator.py +++ b/cqlib/simulator/simple_simulator.py @@ -66,39 +66,62 @@ class SimpleSimulator: self._probs = None self._measure = None - def statevector(self) -> dict[str, complex]: + def statevector(self, dict_format=True) -> dict[str, complex] | list[complex]: """Get the final statevector of the quantum circuit. + Args: + dict_format: If True, returns results as dictionary mapping bitstrings to amplitudes + If False, returns raw state vector as list. + Returns: - dict: Mapping from bitstrings to complex amplitudes + dict: {bitstring: complex_amplitude} mapping (e.g. {'00': 0.707+0j, '01': 0+0j}) + list: Raw state vector array [complex_amplitude_0, complex_amplitude_1, ...] """ if self._state is None: self._run() self._state = self._runner.state() + 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(self._state)} + def probs(self, dict_format=True) -> dict[str, float] | list[float]: + """ + Calculate measurement probabilities for all basis states. - def probs(self) -> dict[str, float]: - """Calculate measurement probabilities for all basis states. + Args: + dict_format: If True, returns results as dictionary mapping bitstrings to probabilities + If False, returns probability array Returns: - dict: Mapping from bitstrings to probabilities + dict: {bitstring: probability} mapping (e.g. {'00': 0.5, '01': 0.25, ...}) + list: Probability array [p_00, p_01, p_10, p_11, ...] """ 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)} + if dict_format: + return {np.binary_repr(i, width=self.nq): val for i, val in enumerate(self._probs)} + return self._probs - def measure(self) -> dict[str, float]: - """Get measurement probabilities for measured qubits. + def measure(self, dict_format=True) -> dict[str, float] | list[float]: + """ + Get measurement probabilities for measured qubits. + + Args: + dict_format: If True, returns results as dictionary + If False, returns raw probability array Returns: - dict: Mapping from bitstrings to probabilities + dict: {bitstring: probability} for measured qubits only + list: Marginal probability array for measured qubits """ 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)} + if dict_format: + return {np.binary_repr(i, width=len(self._mq)): val + for i, val in enumerate(self._measure)} + return self._measure def sample( self, @@ -106,7 +129,8 @@ class SimpleSimulator: is_sorted: bool = True, dict_format: bool = True, ): - """Sample measurement outcomes from the quantum state. + """ + Sample measurement outcomes from the quantum state. Args: shots: Number of samples to take @@ -114,10 +138,12 @@ class SimpleSimulator: dict_format: Whether to return as dict (else string) Returns: - dict/str: Measurement results as counts or concatenated strings + 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) + # pylint: disable=too-many-branches def _run(self): """Internal method to execute the quantum circuit.""" for item in self.circuit.circuit_data: @@ -134,12 +160,22 @@ class SimpleSimulator: 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 == 'XY': + ins_mat = simple_gates.xy_mat(ps[0], self.backend) + elif instr.name == 'XY2P': + ins_mat = simple_gates.xy2p_mat(ps[0], self.backend) + elif instr.name == 'XY2M': + ins_mat = simple_gates.xy2m_mat(ps[0], self.backend) + elif instr.name == 'RXY': + ins_mat = simple_gates.rxy_mat(ps[0], ps[1], 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) + elif instr.name == 'U': + ins_mat = simple_gates.u_mat(ps[0], ps[1], ps[2], self.backend) else: raise ValueError("Unknown gate.") getattr(self._runner, instr.name)( diff --git a/tests/circuit/test_circuit.py b/tests/circuit/test_circuit.py index eb910e901502ad0a66b26c41847a130ac374b062..b2efc194a80bf09cdc86e1131c515244acd3474c 100644 --- a/tests/circuit/test_circuit.py +++ b/tests/circuit/test_circuit.py @@ -176,9 +176,9 @@ def test_circuit_param(): circuit.rx(1, phi + 1) circuit.measure_all() assert circuit.qcis == """H Q1\nRX Q0 phi\nRX Q1 phi + 1\nM Q0\nM Q1""" - circuit.assign_parameters(phi=1.2) + circuit.assign_parameters(phi=1.2, inplace=True) assert circuit.qcis == """H Q1\nRX Q0 1.2\nRX Q1 2.2\nM Q0\nM Q1""" - circuit.assign_parameters(phi=2.2) + circuit = circuit.assign_parameters(phi=2.2) assert circuit.as_str() == """H Q1\nRX Q0 2.2\nRX Q1 3.2\nM Q0\nM Q1""" assert circuit.qcis == f"""H Q1\nRX Q0 2.2\nRX Q1 {3.2 - 2 * pi}\nM Q0\nM Q1""" @@ -221,9 +221,9 @@ def test_circuit_params(): circuit.rx(1, theta) circuit.measure_all() assert circuit.qcis == """H Q1\nRX Q0 phi\nRX Q1 theta\nM Q0\nM Q1""" - circuit.assign_parameters(phi=1.2) + circuit.assign_parameters(phi=1.2, inplace=True) assert circuit.qcis == """H Q1\nRX Q0 1.2\nRX Q1 theta\nM Q0\nM Q1""" - circuit.assign_parameters(theta=2.2) + circuit.assign_parameters(theta=2.2, inplace=True) assert circuit.qcis == """H Q1\nRX Q0 1.2\nRX Q1 2.2\nM Q0\nM Q1""" @@ -241,12 +241,12 @@ def test_assign_parameters(): c2 = circuit.assign_parameters(phi=1.2) c_data = c2.instruction_sequence assert id(c2) != id(circuit) - assert str(c_data[1]) == 'RX Q0 1.2' + assert str(c_data[1]) == 'RX Q0 phi' assert str(c_data[2]) == 'RY Q1 theta' c3 = circuit.assign_parameters(theta=2.2) - assert str(c3.instruction_sequence[1]) == 'RX Q0 1.2' - assert str(c3.instruction_sequence[2]) == 'RY Q1 2.2' - assert c3.qcis == """H Q1\nRX Q0 1.2\nRY Q1 2.2\nM Q0\nM Q1\nM Q2""" + assert str(c3.instruction_sequence[1]) == 'RX Q0 phi' + assert str(c3.instruction_sequence[2]) == 'RY Q1 theta' + assert c3.qcis == "H Q1\nRX Q0 phi\nRY Q1 2.2\nM Q0\nM Q1\nM Q2" def test_load_circuit(): diff --git a/tests/circuit/test_parameter.py b/tests/circuit/test_parameter.py index f92d9ca8316d87ff9f256db5e6706deeab314d2f..580a2a27d3019cd1a217c3cc57e261c1a0751cd8 100644 --- a/tests/circuit/test_parameter.py +++ b/tests/circuit/test_parameter.py @@ -15,9 +15,12 @@ """ Test Parameter """ +from math import pi +import numpy as np from sympy import Symbol +from cqlib import Circuit from cqlib.circuits import Parameter @@ -125,12 +128,40 @@ def test_base_params(): assert str(p1) in ('phi', 'theta') -def test_value(): +def test_inplace(): + """Tests the value of a parameter.""" + phi = Parameter('phi') + c = Circuit(2, parameters=[phi]) + c.rx(0, phi) + c1 = c.assign_parameters(phi=10, inplace=False) + assert c.parameters_value[phi] is None + assert c.qcis == "RX Q0 phi" + assert c1.parameters_value[phi] == 10 + assert c1.qcis == f"RX Q0 {(10 + pi) % (2 * pi) - pi}" + + c1 = c.assign_parameters(phi=10, inplace=True) + assert c.parameters_value[phi] == 10 + assert c.qcis == f"RX Q0 {(10 + pi) % (2 * pi) - pi}" + assert c1.parameters_value[phi] == 10 + assert c1.qcis == f"RX Q0 {(10 + pi) % (2 * pi) - pi}" + + +def test_list_like(): """Tests the value of a parameter.""" phi = Parameter('phi') theta = Parameter('theta') - a = phi / theta - phi ** theta - assert isinstance(a, Parameter) - assert a.value({phi: 1, theta: 2}).evalf() == -0.5 - assert a.value({phi: 100, theta: 1}) == 0 - assert str(a.value({phi: 1, theta: 0})) == '-1 + 1/theta' + gamma = Parameter('gamma') + c = Circuit(2, parameters=[phi, theta, gamma]) + c.rx(0, phi) + c.ry(1, theta) + c.rz(0, gamma) + c1 = c.assign_parameters([1, 2, 3]) + + assert c1.parameters_value[phi] == 1 + assert c1.parameters_value[theta] == 2 + assert c1.parameters_value[gamma] == 3 + assert c.qcis == "RX Q0 phi\nRY Q1 theta\nRZ Q0 gamma" + assert c1.qcis == "RX Q0 1\nRY Q1 2\nRZ Q0 3" + + c2 = c.assign_parameters(np.asarray([1, 2, -1])) + assert c2.qcis == "RX Q0 1\nRY Q1 2\nRZ Q0 -1"