diff --git a/cqlib/circuits/circuit.py b/cqlib/circuits/circuit.py index eed3fe622727b2dc52e72c70ace36a1d21ace6a9..7db78c2b2912f351321fac32540d1c32bd4cbb45 100644 --- a/cqlib/circuits/circuit.py +++ b/cqlib/circuits/circuit.py @@ -18,9 +18,14 @@ from __future__ import annotations import re from collections import defaultdict from copy import deepcopy +from decimal import Decimal from math import pi +from numbers import Number from typing import Union, Sequence, Optional +import numpy as np +import sympy + from cqlib.circuits import gates from cqlib.circuits.barrier import Barrier from cqlib.circuits.instruction import Instruction @@ -651,10 +656,12 @@ 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] | Sequence[float | int] = None, inplace: bool = False, + cache_params: bool = False, **kwargs ): """ @@ -670,15 +677,35 @@ class Circuit: specified as a string, it must be a valid identifier and already exist in the circuit's parameter list. inplace (bool): create new circuit or not? + cache_params (bool): cache parameters or not? for SimpleSimulator value cache. **kwargs: Additional parameters and their values provided as keyword arguments. This is useful for directly setting values when calling the method. Example: - >>> from cqlib.circuits import Circuit - >>> circuit = Circuit(2, ['theta', 'phi']) - >>> circuit.assign_parameters({'theta': 3.14159}) - >>> circuit.assign_parameters(theta=0.5, phi=0.25) + >>> from cqlib.circuits import Circuit, Parameter + >>> import numpy + >>> params = [Parameter(f'theta_{i}') for i in range(3)] + >>> circuit = Circuit(2, parameters=params) + >>> circuit.rx(0, params[0]) + >>> circuit.ry(0, params[0] + params[1] * params[2]) + >>> circuit.qcis + 'RX Q0 theta_0\nRY Q0 theta_0 + theta_1*theta_2' + >>> c1 = circuit.assign_parameters({'theta_0': 1, 'theta_1': 2, 'theta_2': 0.2}) + >>> c1.qcis + RX Q0 1\nRY Q0 1.4 + >>> c2 = circuit.assign_parameters([0.1, 2, 0.2]) + >>> c2.qcis + RX Q0 0.1\nRY Q0 0.5 + >>> c3 = circuit.assign_parameters(theta_0=1, theta_1=0.2, theta_2=0.3) + >>> c3.qcis + 'RX Q0 1\nRY Q0 1.06' + >>> c4 = circuit.assign_parameters(theta_1=2) + >>> c4.qcis + 'RX Q0 theta_0\nRY Q0 theta_0 + 2*theta_2' + >>> c5 = circuit.assign_parameters(numpy.array([1, 2, 0.2])) + >>> c5.qcis + 'RX Q0 1\nRY Q0 1.4' This method updates the internal dictionary of parameters to reflect the new values, allowing these parameters to be used @@ -691,22 +718,39 @@ class Circuit: if values is None: values = {} - _t = type(values) if isinstance(values, ArrayLike): # Array like type, list/tuple/np.ndarray/torch.Tensor - if lv := len(values) != len(target.parameters_value): + if lv := len(values) != len(self._parameters): raise ValueError(f"Length of values {lv} does not match " - f"the number of parameters {len(target.parameters_value)}.") - values = dict(zip(target.parameters_value, values)) + f"the number of parameters {len(self._parameters)}.") + values = dict(zip(self._parameters, values)) values.update(kwargs) for param, value in values.items(): - if isinstance(value, str): + if isinstance(param, str): param = Parameter(param) if param not in target.parameters_value: raise KeyError(f"Parameter {param} not found.") # pylint: disable=protected-access target._parameters[param] = value + if cache_params and not inplace: + self._parameters[param] = value + + for item in target.circuit_data: + instruction = item.instruction + if instruction.params: + ps = [] + for p in instruction.params: + if p in target.parameters_value and target.parameters_value[p] is not None: + p = target.parameters_value[p] + elif isinstance(p, Parameter): + p = p.value(target.parameters_value) + if p.is_Number: + p = float(p) + elif p.is_symbol or isinstance(p, sympy.Basic): + p = Parameter(p) + ps.append(p) + instruction.params = ps return target @property @@ -727,6 +771,7 @@ class Circuit: params = self._parameters return self._export_circuit_str(self._circuit_data, qcis_compliant, params) + # pylint: disable=too-many-branches @classmethod def _export_circuit_str( cls, @@ -768,11 +813,13 @@ class Circuit: value = str(value) else: value = param - if isinstance(value, (float, int)): + if isinstance(value, (Number, Decimal, np.number)): # QCIS param value must be [-pi, pi) if qcis_compliant and (value >= pi or value < -pi): value = (value + pi) % (2 * pi) - pi - value = str(value).rstrip('0').rstrip('.') + value = str(value) + if '.' in value: + value = value.rstrip('0').rstrip('.') line.append(value) ops.append(' '.join(line)) return '\n'.join(ops) diff --git a/cqlib/circuits/parameter.py b/cqlib/circuits/parameter.py index 14a415b3f71006843f62f4d46f41d694689a2cac..4db2a861c90c122782b9c10ed7f0805bd71b6c98 100644 --- a/cqlib/circuits/parameter.py +++ b/cqlib/circuits/parameter.py @@ -77,7 +77,7 @@ class Parameter: Returns: Expr: The result of the symbol after substitution. """ - values = {str(param): value for param, value in params.items() if value} + values = {str(param): value for param, value in params.items() if value is not None} return self._symbol.subs(values) @@ -151,7 +151,9 @@ class Parameter: Returns: bool: True if both parameters are equal, False otherwise. """ - return str(self) == str(other) + if not isinstance(other, Parameter): + return False + return self._symbol == other._symbol def copy(self) -> Parameter: """ diff --git a/tests/circuit/test_circuit.py b/tests/circuit/test_circuit.py index b2efc194a80bf09cdc86e1131c515244acd3474c..232d24e9a7e668e898cd0edcfc693e68debb79a8 100644 --- a/tests/circuit/test_circuit.py +++ b/tests/circuit/test_circuit.py @@ -37,6 +37,18 @@ def test_basic_circuit(): assert circuit.as_str() == 'H Q0\nH Q1\nCZ Q0 Q1\nH Q1\nM Q0\nM Q1' +def test_p0(): + """Test basic circuit""" + ps = [Parameter(f'theta_{i}') for i in range(3)] + circuit = Circuit(2, parameters=ps) + circuit.h(0) + circuit.rx(1, ps[0]) + circuit.rz(1, ps[0] + 2 * ps[1] * ps[2]) + circuit.measure_all() + c1 = circuit.assign_parameters([0, 1.2, 0.1]) + assert c1.qcis == 'H Q0\nRX Q1 0\nRZ Q1 0.24\nM Q0\nM Q1' + + def test_qcis_gate(): """Test QCIS gate""" circuit = Circuit(5) @@ -176,8 +188,8 @@ 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, inplace=True) - assert circuit.qcis == """H Q1\nRX Q0 1.2\nRX Q1 2.2\nM Q0\nM Q1""" + c1 = circuit.assign_parameters(phi=1.2, inplace=False) + assert c1.qcis == """H Q1\nRX Q0 1.2\nRX Q1 2.2\nM Q0\nM Q1""" 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,10 +233,10 @@ 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, inplace=True) - assert circuit.qcis == """H Q1\nRX Q0 1.2\nRX Q1 theta\nM Q0\nM Q1""" + c1 = circuit.assign_parameters(phi=1.2, inplace=False) + assert c1.qcis == """H Q1\nRX Q0 1.2\nRX Q1 theta\nM Q0\nM Q1""" 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""" + assert circuit.qcis == """H Q1\nRX Q0 phi\nRX Q1 2.2\nM Q0\nM Q1""" def test_assign_parameters(): @@ -239,15 +251,23 @@ def test_assign_parameters(): assert circuit.qcis == 'H Q1\nRX Q0 phi\nRY Q1 theta\nM Q0\nM Q1\nM Q2' c2 = circuit.assign_parameters(phi=1.2) - c_data = c2.instruction_sequence + c_data = c2.circuit_data assert id(c2) != id(circuit) - assert str(c_data[1]) == 'RX Q0 phi' + assert str(c_data[1]) == 'RX Q0 1.2' assert str(c_data[2]) == 'RY Q1 theta' + c3 = circuit.assign_parameters(theta=2.2) - assert str(c3.instruction_sequence[1]) == 'RX Q0 phi' - assert str(c3.instruction_sequence[2]) == 'RY Q1 theta' + assert str(c3.circuit_data[1]) == 'RX Q0 phi' + assert str(c3.circuit_data[2]) == 'RY Q1 2.2' assert c3.qcis == "H Q1\nRX Q0 phi\nRY Q1 2.2\nM Q0\nM Q1\nM Q2" + params = [Parameter(f'theta_{i}') for i in range(3)] + circuit = Circuit(2, parameters=params) + circuit.rx(0, params[0]) + circuit.ry(0, params[0] + params[1] * params[2]) + c4 = circuit.assign_parameters(theta_0=1) + assert c4.qcis == 'RX Q0 1\nRY Q0 theta_1*theta_2 + 1' + def test_load_circuit(): """test load circuit""" diff --git a/tests/circuit/test_parameter.py b/tests/circuit/test_parameter.py index a0b405fc08af1f4b92ee18e5d4193cfe9b5f16a0..11aa9c43ae24e41a71dea3a18c28fc394d70e0c0 100644 --- a/tests/circuit/test_parameter.py +++ b/tests/circuit/test_parameter.py @@ -153,16 +153,19 @@ def test_list_like(): theta = Parameter('theta') gamma = Parameter('gamma') c = Circuit(2, parameters=[phi, theta, gamma]) - c.rx(0, phi) - c.ry(1, theta) + c.rx(0, phi + 1) + c.ry(1, theta + phi) 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" + # assert c.qcis == "RX Q0 phi + 1\nRY Q1 phi + theta\nRZ Q0 gamma" + assert c1.qcis == "RX Q0 2\nRY Q1 3\nRZ Q0 3" c2 = c.assign_parameters(np.asarray([1, 2, -1])) - assert c2.qcis == "RX Q0 1\nRY Q1 2\nRZ Q0 -1" + assert c2.qcis == "RX Q0 2\nRY Q1 3\nRZ Q0 -1" + + c2 = c.assign_parameters((1, 2, 0)) + assert c2.qcis == "RX Q0 2\nRY Q1 3\nRZ Q0 0"