qilisdk 0.1.4__py3-none-any.whl → 0.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. qilisdk/__init__.py +11 -2
  2. qilisdk/__init__.pyi +2 -3
  3. qilisdk/_logging.py +135 -0
  4. qilisdk/_optionals.py +5 -7
  5. qilisdk/analog/__init__.py +3 -18
  6. qilisdk/analog/exceptions.py +2 -4
  7. qilisdk/analog/hamiltonian.py +455 -110
  8. qilisdk/analog/linear_schedule.py +121 -0
  9. qilisdk/analog/schedule.py +275 -79
  10. qilisdk/{extras → backends}/__init__.py +9 -4
  11. qilisdk/{common/model.py → backends/__init__.pyi} +3 -1
  12. qilisdk/backends/backend.py +117 -0
  13. qilisdk/{extras/cuda → backends}/cuda_backend.py +152 -159
  14. qilisdk/backends/qutip_backend.py +473 -0
  15. qilisdk/core/__init__.py +63 -0
  16. qilisdk/{common → core}/algorithm.py +2 -1
  17. qilisdk/{extras/qaas/qaas_settings.py → core/exceptions.py} +12 -6
  18. qilisdk/core/model.py +1034 -0
  19. qilisdk/core/parameterizable.py +75 -0
  20. qilisdk/core/qtensor.py +666 -0
  21. qilisdk/{common → core}/result.py +2 -1
  22. qilisdk/core/variables.py +1969 -0
  23. qilisdk/cost_functions/__init__.py +18 -0
  24. qilisdk/cost_functions/cost_function.py +77 -0
  25. qilisdk/cost_functions/model_cost_function.py +145 -0
  26. qilisdk/cost_functions/observable_cost_function.py +109 -0
  27. qilisdk/digital/__init__.py +3 -22
  28. qilisdk/digital/ansatz.py +200 -160
  29. qilisdk/digital/circuit.py +81 -9
  30. qilisdk/digital/exceptions.py +12 -6
  31. qilisdk/digital/gates.py +229 -86
  32. qilisdk/{extras/qaas/qaas_analog_result.py → functionals/__init__.py} +14 -5
  33. qilisdk/functionals/functional.py +39 -0
  34. qilisdk/{common/backend.py → functionals/functional_result.py} +3 -1
  35. qilisdk/functionals/sampling.py +81 -0
  36. qilisdk/functionals/sampling_result.py +92 -0
  37. qilisdk/functionals/time_evolution.py +98 -0
  38. qilisdk/functionals/time_evolution_result.py +84 -0
  39. qilisdk/functionals/variational_program.py +80 -0
  40. qilisdk/functionals/variational_program_result.py +69 -0
  41. qilisdk/logging_config.yaml +16 -0
  42. qilisdk/{common → optimizers}/__init__.py +1 -1
  43. qilisdk/optimizers/optimizer.py +39 -0
  44. qilisdk/{common → optimizers}/optimizer_result.py +3 -12
  45. qilisdk/{common/optimizer.py → optimizers/scipy_optimizer.py} +10 -28
  46. qilisdk/settings.py +78 -0
  47. qilisdk/speqtrum/__init__.py +41 -0
  48. qilisdk/{extras → speqtrum}/__init__.pyi +3 -3
  49. qilisdk/speqtrum/experiments/__init__.py +25 -0
  50. qilisdk/speqtrum/experiments/experiment_functional.py +124 -0
  51. qilisdk/speqtrum/experiments/experiment_result.py +231 -0
  52. qilisdk/{extras/qaas → speqtrum}/keyring.py +8 -4
  53. qilisdk/speqtrum/speqtrum.py +587 -0
  54. qilisdk/speqtrum/speqtrum_models.py +467 -0
  55. qilisdk/utils/__init__.py +0 -14
  56. qilisdk/utils/openqasm2.py +1 -1
  57. qilisdk/utils/serialization.py +1 -1
  58. qilisdk/utils/visualization/PlusJakartaSans-SemiBold.ttf +0 -0
  59. qilisdk/utils/visualization/__init__.py +24 -0
  60. qilisdk/utils/visualization/circuit_renderers.py +781 -0
  61. qilisdk/utils/visualization/schedule_renderers.py +166 -0
  62. qilisdk/utils/visualization/style.py +154 -0
  63. qilisdk/utils/visualization/themes.py +76 -0
  64. qilisdk/yaml.py +126 -0
  65. {qilisdk-0.1.4.dist-info → qilisdk-0.1.6.dist-info}/METADATA +186 -140
  66. qilisdk-0.1.6.dist-info/RECORD +69 -0
  67. qilisdk/analog/algorithms.py +0 -111
  68. qilisdk/analog/analog_backend.py +0 -43
  69. qilisdk/analog/analog_result.py +0 -114
  70. qilisdk/analog/quantum_objects.py +0 -596
  71. qilisdk/digital/digital_algorithm.py +0 -20
  72. qilisdk/digital/digital_backend.py +0 -90
  73. qilisdk/digital/digital_result.py +0 -145
  74. qilisdk/digital/vqe.py +0 -166
  75. qilisdk/extras/cuda/__init__.py +0 -13
  76. qilisdk/extras/cuda/cuda_analog_result.py +0 -19
  77. qilisdk/extras/cuda/cuda_digital_result.py +0 -19
  78. qilisdk/extras/qaas/__init__.py +0 -13
  79. qilisdk/extras/qaas/models.py +0 -132
  80. qilisdk/extras/qaas/qaas_backend.py +0 -255
  81. qilisdk/extras/qaas/qaas_digital_result.py +0 -20
  82. qilisdk/extras/qaas/qaas_time_evolution_result.py +0 -20
  83. qilisdk/extras/qaas/qaas_vqe_result.py +0 -20
  84. qilisdk-0.1.4.dist-info/RECORD +0 -51
  85. {qilisdk-0.1.4.dist-info → qilisdk-0.1.6.dist-info}/WHEEL +0 -0
  86. {qilisdk-0.1.4.dist-info → qilisdk-0.1.6.dist-info}/licenses/LICENCE +0 -0
qilisdk/digital/ansatz.py CHANGED
@@ -1,160 +1,200 @@
1
- # Copyright 2025 Qilimanjaro Quantum Tech
2
- #
3
- # Licensed under the Apache License, Version 2.0 (the "License");
4
- # you may not use this file except in compliance with the License.
5
- # You may obtain a copy of the License at
6
- #
7
- # http://www.apache.org/licenses/LICENSE-2.0
8
- #
9
- # Unless required by applicable law or agreed to in writing, software
10
- # distributed under the License is distributed on an "AS IS" BASIS,
11
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- # See the License for the specific language governing permissions and
13
- # limitations under the License.
14
- from abc import ABC, abstractmethod
15
- from typing import Any, ClassVar, Literal, Union
16
-
17
- from qilisdk.digital.circuit import Circuit
18
- from qilisdk.digital.gates import CNOT, CZ, U1, U2, U3, M
19
- from qilisdk.yaml import yaml
20
-
21
-
22
- class Ansatz(ABC):
23
- _ONE_QUBIT_GATES: ClassVar[dict[str, type[Union[U1, U2, U3]]]] = {
24
- "U1": U1,
25
- "U2": U2,
26
- "U3": U3,
27
- }
28
- _TWO_QUBITS_GATES: ClassVar[dict[str, type[Union[CNOT, CZ]]]] = {
29
- "CZ": CZ,
30
- "CNOT": CNOT,
31
- }
32
-
33
- def __init__(self, nqubits: int, layers: int = 1) -> None:
34
- self.nqubits = nqubits
35
- self.layers = layers
36
-
37
- @property
38
- def nparameters(self) -> int:
39
- """
40
- Retrieve the total number of parameters required by all parameterized gates in the ansatz's circuit.
41
-
42
- Returns:
43
- int: The total count of parameters from all parameterized gates.
44
- """
45
- raise NotImplementedError
46
-
47
- def get_circuit(self, parameters: list[float]) -> Circuit:
48
- """Get the underlying circuit with the given list of parameters.
49
-
50
- Args:
51
- params (list[float]): the list of parameters for the unitary gates.
52
-
53
- Raises:
54
- ValueError: if the number of parameters provided are less than the parameters expected by the ansatz.
55
-
56
- Returns:
57
- Circuit: The underlying circuit with the updated parameters.
58
- """
59
- if len(parameters) != self.nparameters:
60
- raise ValueError(f"Expecting {self.nparameters} but received {len(parameters)}")
61
-
62
- return self._construct_circuit(parameters=list(parameters))
63
-
64
- @abstractmethod
65
- def _construct_circuit(self, parameters: list[float]) -> Circuit: ...
66
-
67
-
68
- @yaml.register_class
69
- class HardwareEfficientAnsatz(Ansatz):
70
- def __init__(
71
- self,
72
- n_qubits: int,
73
- layers: int = 1,
74
- connectivity: Literal["Circular", "Linear", "Full"] | list[tuple[int, int]] = "Linear",
75
- structure: Literal["grouped", "interposed"] = "grouped",
76
- one_qubit_gate: Literal["U1", "U2", "U3"] = "U1",
77
- two_qubit_gate: Literal["CZ", "CNOT"] = "CZ",
78
- ) -> None:
79
- super().__init__(n_qubits, layers)
80
-
81
- # Define chip topology
82
- if isinstance(connectivity, list):
83
- self.connectivity = connectivity
84
- elif connectivity == "Full":
85
- self.connectivity = [(i, j) for i in range(self.nqubits) for j in range(i + 1, self.nqubits)]
86
- elif connectivity == "Circular":
87
- self.connectivity = [(i, i + 1) for i in range(self.nqubits - 1)] + [(self.nqubits - 1, 0)]
88
- elif connectivity == "Linear":
89
- self.connectivity = [(i, i + 1) for i in range(self.nqubits - 1)]
90
- else:
91
- raise ValueError(f"Unrecognized connectivity type ({connectivity}).")
92
-
93
- self.gate_types: dict[str, str] = {"one_qubit_gate": one_qubit_gate, "two_qubit_gates": two_qubit_gate}
94
- self.one_qubit_gate: type[Union[U1, U2, U3]] = self._ONE_QUBIT_GATES[one_qubit_gate]
95
- self.two_qubit_gate: type[Union[CNOT, CZ]] = self._TWO_QUBITS_GATES[two_qubit_gate]
96
-
97
- if structure not in {"grouped", "interposed"}:
98
- raise ValueError(f"provided structure {structure} is not supported.")
99
- self.structure = structure
100
-
101
- self.construct_layer_handlers = {
102
- "interposed": self._construct_layer_interposed,
103
- "grouped": self._construct_layer_grouped,
104
- }
105
-
106
- def __getstate__(self) -> dict[str, Any]:
107
- state = self.__dict__.copy()
108
- # Exclude the mapping that contains bound methods (not serializable).
109
- state.pop("construct_layer_handlers", None)
110
- return state
111
-
112
- def __setstate__(self, state) -> None: # noqa: ANN001
113
- """
114
- Restore the object's state after deserialization and reinitialize any attributes that were omitted.
115
- """
116
- self.__dict__.update(state)
117
- # Reconstruct the mapping with the proper bound methods.
118
- self.construct_layer_handlers = {
119
- "interposed": self._construct_layer_interposed,
120
- "grouped": self._construct_layer_grouped,
121
- }
122
-
123
- @property
124
- def nparameters(self) -> int:
125
- """
126
- Retrieve the total number of parameters required by all parameterized gates in the ansatz's circuit.
127
-
128
- Returns:
129
- int: The total count of parameters from all parameterized gates.
130
- """
131
- return self.nqubits * (self.layers + 1) * len(self.one_qubit_gate.PARAMETER_NAMES)
132
-
133
- def _construct_circuit(self, parameters: list[float]) -> Circuit:
134
- self._circuit = Circuit(self.nqubits)
135
- # Add initial layer of unitaries
136
- for i in range(self.nqubits):
137
- self._circuit.add(self.one_qubit_gate(i, **dict(zip(self.one_qubit_gate.PARAMETER_NAMES, parameters))))
138
-
139
- construct_layer_handler = self.construct_layer_handlers[self.structure]
140
- for _ in range(self.layers):
141
- construct_layer_handler(parameters)
142
- self._circuit.add(M(*list(range(self.nqubits))))
143
-
144
- return self._circuit
145
-
146
- def _construct_layer_interposed(self, parameters: list[float]) -> None:
147
- for i in range(self.nqubits):
148
- self._circuit.add(
149
- self.one_qubit_gate(i, **{name: parameters.pop() for name in self.one_qubit_gate.PARAMETER_NAMES})
150
- )
151
- for p, j in self.connectivity:
152
- self._circuit.add(self.two_qubit_gate(p, j))
153
-
154
- def _construct_layer_grouped(self, parameters: list[float]) -> None:
155
- for i in range(self.nqubits):
156
- self._circuit.add(
157
- self.one_qubit_gate(i, **{name: parameters.pop() for name in self.one_qubit_gate.PARAMETER_NAMES})
158
- )
159
- for i, j in self.connectivity:
160
- self._circuit.add(self.two_qubit_gate(i, j))
1
+ # Copyright 2025 Qilimanjaro Quantum Tech
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ from abc import ABC
15
+ from typing import Iterator, Literal, Type
16
+
17
+ from qilisdk.digital.circuit import Circuit
18
+ from qilisdk.digital.gates import CNOT, CZ, U1, U2, U3
19
+ from qilisdk.yaml import yaml
20
+
21
+ Connectivity = Literal["circular", "linear", "full"] | list[tuple[int, int]]
22
+ Structure = Literal["grouped", "interposed"]
23
+
24
+
25
+ class Ansatz(Circuit, ABC):
26
+ """Abstract template for parameterised digital circuits."""
27
+
28
+ def __init__(self, nqubits: int) -> None:
29
+ """
30
+ Args:
31
+ nqubits (int): Number of logical qubits in the circuit.
32
+ """
33
+ super().__init__(nqubits=nqubits)
34
+
35
+
36
+ @yaml.register_class
37
+ class HardwareEfficientAnsatz(Ansatz):
38
+ """
39
+ Hardware-efficient ansatz with `(layers + 1)` single-qubit blocks and ``layers``
40
+ entangling blocks.
41
+
42
+ Example:
43
+ .. code-block:: python
44
+
45
+ from qilisdk.digital.ansatz import HardwareEfficientAnsatz
46
+ from qilisdk.digital.gates import U3, CNOT
47
+
48
+ ansatz = HardwareEfficientAnsatz(
49
+ nqubits=4,
50
+ layers=3,
51
+ connectivity="linear",
52
+ structure="grouped",
53
+ one_qubit_gate=U3,
54
+ two_qubit_gate=CNOT,
55
+ )
56
+ ansatz.draw()
57
+
58
+ Notes:
59
+ ``structure="grouped"`` applies full single-qubit layers followed by entanglers,
60
+ while ``structure="interposed"`` alternates single-qubit updates with entanglers
61
+ per qubit. No measurements are added automatically.
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ nqubits: int,
67
+ layers: int = 1,
68
+ connectivity: Connectivity = "linear",
69
+ structure: Structure = "grouped",
70
+ one_qubit_gate: Type[U1 | U2 | U3] = U1,
71
+ two_qubit_gate: Type[CZ | CNOT] = CZ,
72
+ ) -> None:
73
+ """
74
+ Args:
75
+ nqubits (int): Number of qubits in the circuit.
76
+ layers (int, optional): Number of entangling layers. Defaults to 1.
77
+ connectivity (Connectivity, optional): Topology used for two-qubit gates.
78
+ Accepts ``"linear"``, ``"circular"``, ``"full"``, or an explicit list of edges.
79
+ Defaults to ``"linear"``.
80
+ structure (Structure, optional): Layout of single- and two-qubit gates within each layer.
81
+ ``"grouped"`` applies all single-qubit gates before the entangler block; ``"interposed"``
82
+ interleaves them per qubit. Defaults to ``"grouped"``.
83
+ one_qubit_gate (Type[U1 | U2 | U3], optional): Parameterised single-qubit gate class. Defaults to :class:`U1`.
84
+ two_qubit_gate (Type[CZ | CNOT], optional): Entangling gate class. Defaults to :class:`CZ`.
85
+
86
+ Raises:
87
+ ValueError: If ``layers`` is negative or the connectivity definition is invalid.
88
+ """
89
+ super().__init__(nqubits)
90
+
91
+ if layers < 0:
92
+ raise ValueError("layers must be >= 0")
93
+
94
+ self._layers = int(layers)
95
+ self._connectivity = tuple(self._normalize_connectivity(connectivity))
96
+ self._structure: Structure = "grouped" if structure.lower() == "grouped" else "interposed"
97
+ self._one_qubit_gate: type[U1 | U2 | U3] = one_qubit_gate
98
+ self._two_qubit_gate: type[CZ | CNOT] = two_qubit_gate
99
+
100
+ self._build_circuit()
101
+
102
+ @property
103
+ def layers(self) -> int:
104
+ """Number of entangling layers."""
105
+ return self._layers
106
+
107
+ @property
108
+ def connectivity(self) -> tuple[tuple[int, int], ...]:
109
+ """Entangling edges as an immutable tuple of (control, target) pairs."""
110
+ return self._connectivity
111
+
112
+ @property
113
+ def structure(self) -> Structure:
114
+ """Declared structure ('grouped' or 'interposed')."""
115
+ return self._structure
116
+
117
+ @property
118
+ def one_qubit_gate(self) -> type[U1 | U2 | U3]:
119
+ """Single-qubit gate class used for parameterized layers (U1, U2, or U3)."""
120
+ return self._one_qubit_gate
121
+
122
+ @property
123
+ def two_qubit_gate(self) -> type[CZ | CNOT]:
124
+ """Two-qubit entangling gate class (CZ or CNOT)."""
125
+ return self._two_qubit_gate
126
+
127
+ def _normalize_connectivity(self, connectivity: Connectivity) -> list[tuple[int, int]]:
128
+ """
129
+ Returns:
130
+ list[tuple[int, int]]: a validated list of entangling edges derived from ``connectivity``.
131
+
132
+ Raises:
133
+ ValueError: If ``connectivity`` is invalid.
134
+ """
135
+ if isinstance(connectivity, list):
136
+ edges = connectivity
137
+ else:
138
+ kind = connectivity.lower()
139
+ if kind == "full":
140
+ edges = [(i, j) for i in range(self.nqubits) for j in range(i + 1, self.nqubits)]
141
+ elif kind == "circular":
142
+ edges = (
143
+ []
144
+ if self.nqubits < 2 # noqa: PLR2004
145
+ else [(i, i + 1) for i in range(self.nqubits - 1)] + [(self.nqubits - 1, 0)]
146
+ )
147
+ elif kind == "linear":
148
+ edges = [(i, i + 1) for i in range(self.nqubits - 1)]
149
+ else:
150
+ raise ValueError(f"Unrecognized connectivity: {connectivity!r}")
151
+
152
+ # basic validation
153
+ for a, b in edges:
154
+ if not (0 <= a < self.nqubits and 0 <= b < self.nqubits):
155
+ raise ValueError(f"Edge {(a, b)} out of range for {self.nqubits} qubits.")
156
+ if a == b:
157
+ raise ValueError(f"Self-edge {(a, b)} is not allowed.")
158
+ return edges
159
+
160
+ def _parameter_blocks(self) -> Iterator[dict[str, float]]:
161
+ """Yield dictionaries initialised for each parameterised single-qubit gate in build order."""
162
+ names = tuple(self.one_qubit_gate.PARAMETER_NAMES)
163
+ blocks = (self.layers + 1) * self.nqubits
164
+
165
+ zero = dict.fromkeys(names, 0.0)
166
+ for _ in range(blocks):
167
+ # fresh dict each time
168
+ yield dict(zero)
169
+
170
+ def _apply_single_qubit(self, qubit: int, parameter_iterator: Iterator[dict[str, float]]) -> None:
171
+ """Apply one parameterised single-qubit gate to ``qubit`` using the next parameter block."""
172
+ params = next(parameter_iterator)
173
+ self.add(self.one_qubit_gate(qubit, **params))
174
+
175
+ def _apply_single_qubit_block(self, parameter_iterator: Iterator[dict[str, float]]) -> None:
176
+ """Apply a parameterised gate to every qubit using successive parameter blocks."""
177
+ for qubit in range(self.nqubits):
178
+ params = next(parameter_iterator)
179
+ self.add(self.one_qubit_gate(qubit, **params))
180
+
181
+ def _apply_entanglers(self) -> None:
182
+ """Append the entangling block across all connectivity edges."""
183
+ for i, j in self.connectivity:
184
+ self.add(self.two_qubit_gate(i, j))
185
+
186
+ def _build_circuit(self) -> None:
187
+ """Populate the circuit according to the current structure and connectivity settings."""
188
+ # Parameter iterator covering all single-qubit blocks, in order
189
+ parameter_iterator = iter(self._parameter_blocks())
190
+
191
+ # For each remaining layer: U -> E
192
+ if self.structure == "grouped":
193
+ for _ in range(self.layers):
194
+ self._apply_single_qubit_block(parameter_iterator)
195
+ self._apply_entanglers()
196
+ else:
197
+ for _ in range(self.layers):
198
+ for q in range(self.nqubits):
199
+ self._apply_single_qubit(q, parameter_iterator)
200
+ self._apply_entanglers()
@@ -11,8 +11,12 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
+
14
15
  import numpy as np
15
16
 
17
+ from qilisdk.core.parameterizable import Parameterizable
18
+ from qilisdk.core.variables import Parameter, RealNumber
19
+ from qilisdk.utils.visualization import CircuitStyle
16
20
  from qilisdk.yaml import yaml
17
21
 
18
22
  from .exceptions import ParametersNotEqualError, QubitOutOfRangeError
@@ -20,7 +24,7 @@ from .gates import Gate
20
24
 
21
25
 
22
26
  @yaml.register_class
23
- class Circuit:
27
+ class Circuit(Parameterizable):
24
28
  def __init__(self, nqubits: int) -> None:
25
29
  """
26
30
  Initialize a Circuit instance with a specified number of qubits.
@@ -31,7 +35,7 @@ class Circuit:
31
35
  self._nqubits: int = nqubits
32
36
  self._gates: list[Gate] = []
33
37
  self._init_state: np.ndarray = np.zeros(nqubits)
34
- self._parameterized_gates: list[Gate] = []
38
+ self._parameters: dict[str, Parameter] = {}
35
39
 
36
40
  @property
37
41
  def nqubits(self) -> int:
@@ -51,7 +55,7 @@ class Circuit:
51
55
  Returns:
52
56
  int: The total count of parameters from all parameterized gates.
53
57
  """
54
- return len([value for gate in self._gates if gate.is_parameterized for value in gate.parameter_values])
58
+ return len(self._parameters)
55
59
 
56
60
  @property
57
61
  def gates(self) -> list[Gate]:
@@ -70,7 +74,25 @@ class Circuit:
70
74
  Returns:
71
75
  list[float]: A list of parameter values from each parameterized gate.
72
76
  """
73
- return [value for gate in self._gates if gate.is_parameterized for value in gate.parameter_values]
77
+ return [param.value for param in self._parameters.values()]
78
+
79
+ def get_parameter_names(self) -> list[str]:
80
+ """
81
+ Retrieve the parameter values from all parameterized gates in the circuit.
82
+
83
+ Returns:
84
+ list[float]: A list of parameter values from each parameterized gate.
85
+ """
86
+ return list(self._parameters.keys())
87
+
88
+ def get_parameters(self) -> dict[str, float]:
89
+ """
90
+ Retrieve the parameter names and values from all parameterized gates in the circuit.
91
+
92
+ Returns:
93
+ dict[str, float]: A dictionary of the parameters with their current values.
94
+ """
95
+ return {label: param.value for label, param in self._parameters.items()}
74
96
 
75
97
  def set_parameter_values(self, values: list[float]) -> None:
76
98
  """
@@ -84,10 +106,33 @@ class Circuit:
84
106
  """
85
107
  if len(values) != self.nparameters:
86
108
  raise ParametersNotEqualError
87
- k = 0
88
- for i, gate in enumerate(self._parameterized_gates):
89
- gate.set_parameter_values(values[i + k : i + k + gate.nparameters])
90
- k += gate.nparameters - 1
109
+ for i, parameter in enumerate(self._parameters.values()):
110
+ parameter.set_value(values[i])
111
+
112
+ def set_parameters(self, parameter_dict: dict[str, RealNumber]) -> None:
113
+ """Set the parameter values by their label. No need to provide the full list of parameters.
114
+
115
+ Args:
116
+ parameter_dict (dict[str, RealNumber]): A dictionary with the labels of the parameters to be modified and their new value.
117
+
118
+ Raises:
119
+ ValueError: if the label provided doesn't correspond to a parameter defined in this circuit.
120
+ """
121
+ for label, param in parameter_dict.items():
122
+ if label not in self._parameters:
123
+ raise ValueError(f"Parameter {label} is not defined in this circuit.")
124
+ self._parameters[label].set_value(param)
125
+
126
+ def get_parameter_bounds(self) -> dict[str, tuple[float, float]]:
127
+ return {k: v.bounds for k, v in self._parameters.items()}
128
+
129
+ def set_parameter_bounds(self, ranges: dict[str, tuple[float, float]]) -> None:
130
+ for label, bound in ranges.items():
131
+ if label not in self._parameters:
132
+ raise ValueError(
133
+ f"The provided parameter label {label} is not defined in the list of parameters in this object."
134
+ )
135
+ self._parameters[label].set_bounds(bound[0], bound[1])
91
136
 
92
137
  def add(self, gate: Gate) -> None:
93
138
  """
@@ -102,5 +147,32 @@ class Circuit:
102
147
  if any(qubit >= self.nqubits for qubit in gate.qubits):
103
148
  raise QubitOutOfRangeError
104
149
  if gate.is_parameterized:
105
- self._parameterized_gates.append(gate)
150
+ param_base_label = f"{gate.name}({','.join(map(str, gate.qubits))})"
151
+ for label, parameter in gate.parameters.items():
152
+ if label == parameter.label:
153
+ parameter_label = param_base_label + f"_{label}_{len(self._parameters)}"
154
+ else:
155
+ parameter_label = parameter.label
156
+ self._parameters[parameter_label] = gate.parameters[label]
106
157
  self._gates.append(gate)
158
+
159
+ def draw(self, style: CircuitStyle = CircuitStyle(), filepath: str | None = None) -> None:
160
+ """
161
+ Render this circuit with Matplotlib and optionally save it to a file.
162
+
163
+ The circuit is rendered using the provided style configuration. If ``filepath`` is
164
+ given, the resulting figure is saved to disk (the output format is inferred
165
+ from the file extension, e.g. ``.png``, ``.pdf``, ``.svg``).
166
+
167
+ Args:
168
+ style (CircuitStyle): Visual style configuration applied to the plot.
169
+ If not provided, the default :class:`CircuitStyle` is used.
170
+ filepath (str | None): Destination file path for the rendered figure.
171
+ If ``None``, the figure is not saved.
172
+ """
173
+ from qilisdk.utils.visualization.circuit_renderers import MatplotlibCircuitRenderer # noqa: PLC0415
174
+
175
+ renderer = MatplotlibCircuitRenderer(self, style=style)
176
+ renderer.plot()
177
+ if filepath:
178
+ renderer.save(filepath)
@@ -13,19 +13,25 @@
13
13
  # limitations under the License.
14
14
 
15
15
 
16
- class QubitOutOfRangeError(Exception): ...
16
+ class QubitOutOfRangeError(Exception):
17
+ """Raised when a qubit index is outside the defined circuit range."""
17
18
 
18
19
 
19
- class GateHasNoMatrixError(Exception): ...
20
+ class GateHasNoMatrixError(Exception):
21
+ """Raised when a gate lacks an associated matrix representation."""
20
22
 
21
23
 
22
- class GateNotParameterizedError(Exception): ...
24
+ class GateNotParameterizedError(Exception):
25
+ """Raised when attempting to set parameters on a non-parameterized gate."""
23
26
 
24
27
 
25
- class ParametersNotEqualError(Exception): ...
28
+ class ParametersNotEqualError(Exception):
29
+ """Raised when parameterized gates receive mismatched parameter values."""
26
30
 
27
31
 
28
- class InvalidParameterNameError(Exception): ...
32
+ class InvalidParameterNameError(Exception):
33
+ """Raised when an unknown parameter name is supplied to a gate."""
29
34
 
30
35
 
31
- class UnsupportedGateError(Exception): ...
36
+ class UnsupportedGateError(Exception):
37
+ """Raised when a gate is not supported by the target backend."""