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
@@ -0,0 +1,121 @@
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 __future__ import annotations
15
+
16
+ from qilisdk.analog.hamiltonian import Hamiltonian
17
+ from qilisdk.analog.schedule import Schedule
18
+ from qilisdk.core.variables import Number, Parameter, Term
19
+ from qilisdk.yaml import yaml
20
+
21
+
22
+ @yaml.register_class
23
+ class LinearSchedule(Schedule):
24
+ """
25
+ Schedule implementation that linearly interpolates coefficients between defined time steps.
26
+
27
+ Example:
28
+ .. code-block:: python
29
+
30
+ from qilisdk.analog.hamiltonian import Hamiltonian, Z
31
+ from qilisdk.analog.linear_schedule import LinearSchedule
32
+
33
+ h = 2 * Z(0)
34
+ schedule = LinearSchedule(T=4.0, dt=1.0, hamiltonians={"hz": h})
35
+ schedule.add_schedule_step(0, {"hz": 0.0})
36
+ schedule.add_schedule_step(4, {"hz": 1.0})
37
+ assert schedule.get_coefficient(2.0, "hz") == 0.5
38
+ """
39
+
40
+ def get_coefficient_expression(self, time_step: float, hamiltonian_key: str) -> Number | Term | Parameter:
41
+ """
42
+ Return the symbolic coefficient for a Hamiltonian at an arbitrary time step.
43
+
44
+ Args:
45
+ time_step (float): The time at which to evaluate the coefficient.
46
+ hamiltonian_key (str): Label of the Hamiltonian inside the schedule.
47
+
48
+ Returns:
49
+ Number | Term: The (possibly symbolic) coefficient associated with ``hamiltonian_key``.
50
+
51
+ Raises:
52
+ ValueError: If something unexpected happens during coefficient retrieval.
53
+ """
54
+ t = time_step / self.dt
55
+ t_idx = int(t)
56
+
57
+ if t_idx in self._schedule and hamiltonian_key in self._schedule[t_idx]:
58
+ return self._schedule[t_idx][hamiltonian_key]
59
+
60
+ # search backwards
61
+ prev_idx, prev_expr = None, None
62
+ for i in range(t_idx, -1, -1):
63
+ if i in self._schedule and hamiltonian_key in self._schedule[i]:
64
+ prev_idx = i
65
+ prev_expr = self._schedule[i][hamiltonian_key]
66
+ break
67
+
68
+ # search forwards
69
+ next_idx, next_expr = None, None
70
+ for i in range(t_idx + 1, int(self.T / self.dt) + 1):
71
+ if i in self._schedule and hamiltonian_key in self._schedule[i]:
72
+ next_idx = i
73
+ next_expr = self._schedule[i][hamiltonian_key]
74
+ break
75
+
76
+ # cases
77
+ if prev_expr is None and next_expr is None:
78
+ return 0
79
+ if prev_expr is None and next_expr is not None:
80
+ return next_expr
81
+ if next_expr is None and prev_expr is not None:
82
+ return prev_expr
83
+
84
+ # linear interpolation (keeps expressions if they are Terms/Parameters)
85
+ if next_idx is None or prev_idx is None or prev_expr is None or next_expr is None:
86
+ raise ValueError("Something unexpected happened while retrieving the coefficient.")
87
+ alpha: float = (t - prev_idx) / (next_idx - prev_idx)
88
+ e1 = next_expr * alpha
89
+ e2 = prev_expr * (1 - alpha)
90
+ return e1 + e2
91
+
92
+ def get_coefficient(self, time_step: float, hamiltonian_key: str) -> Number:
93
+ """
94
+ Return the numeric coefficient for a Hamiltonian at ``time_step``.
95
+
96
+ Args:
97
+ time_step (float): Time at which to evaluate the coefficient.
98
+ hamiltonian_key (str): Label of the Hamiltonian.
99
+
100
+ Returns:
101
+ Number: Evaluated coefficient value.
102
+ """
103
+ time_step = float(time_step)
104
+ val = self.get_coefficient_expression(time_step=time_step, hamiltonian_key=hamiltonian_key)
105
+ return val.evaluate({}) if isinstance(val, Term) else (val.evaluate() if isinstance(val, Parameter) else val)
106
+
107
+ def __getitem__(self, time_step: int) -> Hamiltonian:
108
+ """
109
+ Retrieve the interpolated Hamiltonian at the specified discrete time step.
110
+
111
+ Args:
112
+ time_step (int): Discrete index to evaluate (converted internally to ``time_step * dt``).
113
+
114
+ Returns:
115
+ Hamiltonian: Hamiltonian with coefficients interpolated at the requested time step.
116
+ """
117
+ ham = Hamiltonian()
118
+ for ham_label in self._hamiltonians:
119
+ coeff = self.get_coefficient(time_step * self.dt, ham_label)
120
+ ham += coeff * self._hamiltonians[ham_label]
121
+ return ham.get_static_hamiltonian()
@@ -14,62 +14,81 @@
14
14
  from __future__ import annotations
15
15
 
16
16
  from typing import Callable
17
- from warnings import warn
17
+
18
+ from loguru import logger
18
19
 
19
20
  from qilisdk.analog.hamiltonian import Hamiltonian
21
+ from qilisdk.core.parameterizable import Parameterizable
22
+ from qilisdk.core.variables import BaseVariable, Number, Parameter, Term
23
+ from qilisdk.utils.visualization import ScheduleStyle
20
24
  from qilisdk.yaml import yaml
21
25
 
22
26
 
23
27
  @yaml.register_class
24
- class Schedule:
28
+ class Schedule(Parameterizable):
25
29
  """
26
- Represents a time-dependent schedule for Hamiltonian coefficients in an annealing process.
30
+ Builds a set of time-dependent coefficients applied to a collection of Hamiltonians.
27
31
 
28
32
  A Schedule defines the evolution of a system by associating time steps with a set
29
33
  of Hamiltonian coefficients. It maintains a dictionary of Hamiltonian objects and a
30
34
  corresponding schedule that specifies the coefficients (weights) for each Hamiltonian
31
35
  at discrete time steps.
32
36
 
33
- Attributes:
34
- _T (float): The total annealing time.
35
- _dt (float): The time step duration. Total time must be divisible by dt.
36
- _hamiltonians (dict[str, Hamiltonian]): A mapping of labels to Hamiltonian objects.
37
- _schedule (dict[int, dict[str, float]]): A mapping of time steps to coefficient dictionaries.
38
- _nqubits (int): The maximum number of qubits among the Hamiltonians.
39
- iter_time_step (int): Internal counter for iteration over time steps.
37
+ Example:
38
+ .. code-block:: python
39
+
40
+ import numpy as np
41
+ from qilisdk.analog import Schedule, X, Z
42
+
43
+ T, dt = 10, 1
44
+ steps = np.linspace(0, T, int(T / dt))
45
+
46
+ h1 = X(0) + X(1) + X(2)
47
+ h2 = -Z(0) - Z(1) - 2 * Z(2) + 3 * Z(0) * Z(1)
48
+
49
+ schedule = Schedule(
50
+ T=T,
51
+ dt=dt,
52
+ hamiltonians={"driver": h1, "problem": h2},
53
+ schedule={i: {"driver": 1 - t / T, "problem": t / T} for i, t in enumerate(steps)},
54
+ )
55
+ schedule.draw()
40
56
  """
41
57
 
42
58
  def __init__(
43
59
  self,
44
60
  T: float,
45
- dt: float,
61
+ dt: float = 1,
46
62
  hamiltonians: dict[str, Hamiltonian] | None = None,
47
- schedule: dict[int, dict[str, float]] | None = None,
63
+ schedule: dict[int, dict[str, float | Term | Parameter]] | None = None,
48
64
  ) -> None:
49
65
  """
50
- Initialize a Schedule object.
51
-
52
66
  Args:
53
- T (float): The total annealing time.
54
- dt (float): The time step for the annealing process. Note that T needs to be divisible by dt.
55
- hamiltonians (dict[str, Hamiltonian], optional): A dictionary mapping labels to Hamiltonian objects.
56
- Defaults to an empty dictionary if None.
57
- schedule (dict[int, dict[str, float]], optional): A dictionary where keys are time step indices (integers)
58
- and values are dictionaries mapping Hamiltonian labels to their coefficients at that time step.
59
- Defaults to {0: {}} if None.
67
+ T (float): Total annealing time (in nanoseconds).
68
+ dt (float, optional): Discretization step of the time grid. Defaults to 1.
69
+ hamiltonians (dict[str, Hamiltonian] | None, optional): Mapping from labels to Hamiltonian instances that
70
+ define the building blocks of the schedule. Defaults to None, which creates an empty mapping.
71
+ schedule (dict[int, dict[str, float | Term | Parameter]] | None, optional): Predefined coefficients for
72
+ specific time steps. Each inner dictionary maps Hamiltonian labels to numerical or symbolic
73
+ coefficients. Defaults to {0: {}} if None.
60
74
 
61
75
  Raises:
62
76
  ValueError: If the provided schedule references Hamiltonians that have not been defined.
63
77
  """
64
-
78
+ if dt <= 0:
79
+ raise ValueError("dt must be greater than zero.")
65
80
  self._hamiltonians: dict[str, Hamiltonian] = hamiltonians if hamiltonians is not None else {}
66
- self._schedule: dict[int, dict[str, float]] = schedule if schedule is not None else {0: {}}
81
+ self._schedule: dict[int, dict[str, float | Term | Parameter]] = schedule if schedule is not None else {0: {}}
82
+ self._parameters: dict[str, Parameter] = {}
67
83
  self._T = T
68
84
  self._dt = dt
69
85
  self.iter_time_step = 0
70
86
  self._nqubits = 0
87
+
71
88
  for hamiltonian in self._hamiltonians.values():
72
89
  self._nqubits = max(self._nqubits, hamiltonian.nqubits)
90
+ for l, param in hamiltonian.parameters.items():
91
+ self._parameters[param.label] = param
73
92
 
74
93
  if 0 not in self._schedule:
75
94
  self._schedule[0] = dict.fromkeys(self._hamiltonians, 0.0)
@@ -83,58 +102,133 @@ class Schedule:
83
102
  raise ValueError(
84
103
  "All hamiltonians defined in the schedule need to be declared in the hamiltonians dictionary."
85
104
  )
105
+ for coeff in time_step.values():
106
+ if isinstance(coeff, Term):
107
+ for v in coeff.variables():
108
+ if not isinstance(v, Parameter):
109
+ raise ValueError(
110
+ f"The schedule can only contain Parameters, but a generic variable was provided ({time_step})"
111
+ )
112
+ self._parameters[v.label] = v
113
+ elif isinstance(coeff, BaseVariable):
114
+ if not isinstance(coeff, Parameter):
115
+ raise ValueError(
116
+ f"The schedule can only contain Parameters, but a generic variable was provided ({time_step})"
117
+ )
118
+ self._parameters[coeff.label] = coeff
86
119
 
87
120
  @property
88
121
  def hamiltonians(self) -> dict[str, Hamiltonian]:
89
122
  """
90
- Get the dictionary of Hamiltonians known to this schedule.
123
+ Return the Hamiltonians managed by the schedule.
91
124
 
92
125
  Returns:
93
- dict[str, Hamiltonian]: A mapping of Hamiltonian labels to Hamiltonian objects.
126
+ dict[str, Hamiltonian]: Mapping of labels to Hamiltonian instances.
94
127
  """
95
128
  return self._hamiltonians
96
129
 
97
130
  @property
98
- def schedule(self) -> dict[int, dict[str, float]]:
131
+ def schedule(self) -> dict[int, dict[str, Number]]:
99
132
  """
100
- Get the full schedule of Hamiltonian coefficients.
101
-
102
- The schedule is returned as a dictionary sorted by time step.
133
+ Return the evaluated schedule of Hamiltonian coefficients.
103
134
 
104
135
  Returns:
105
- dict[int, dict[str, float]]: The mapping of time steps to coefficient dictionaries.
136
+ dict[int, dict[str, Number]]: Mapping from time indices to evaluated coefficients.
106
137
  """
107
- return dict(sorted(self._schedule.items()))
138
+ out_dict = {}
139
+ for k, v in self._schedule.items():
140
+ out_dict[k] = {
141
+ ham: (
142
+ coeff
143
+ if isinstance(coeff, Number)
144
+ else (coeff.evaluate() if isinstance(coeff, Parameter) else coeff.evaluate({}))
145
+ )
146
+ for ham, coeff in v.items()
147
+ }
148
+ return dict(sorted(out_dict.items()))
108
149
 
109
150
  @property
110
151
  def T(self) -> float:
111
- """
112
- Get the total annealing time.
113
-
114
- Returns:
115
- float: The total time T.
116
- """
152
+ """Total annealing time of the schedule."""
117
153
  return self._T
118
154
 
119
155
  @property
120
156
  def dt(self) -> float:
121
- """
122
- Get the time step duration.
123
-
124
- Returns:
125
- float: The duration of each time step.
126
- """
157
+ """Duration of a single time step in the annealing grid."""
127
158
  return self._dt
128
159
 
129
160
  @property
130
161
  def nqubits(self) -> int:
162
+ """Maximum number of qubits affected by Hamiltonians contained in the schedule."""
163
+ return self._nqubits
164
+
165
+ @property
166
+ def nparameters(self) -> int:
167
+ """Number of symbolic parameters introduced by the Hamiltonians or coefficients."""
168
+ return len(self._parameters)
169
+
170
+ def get_parameter_values(self) -> list[float]:
171
+ """Return the current values associated with the schedule parameters."""
172
+ return [param.value for param in self._parameters.values()]
173
+
174
+ def get_parameter_names(self) -> list[str]:
175
+ """Return the ordered list of parameter labels managed by the schedule."""
176
+ return list(self._parameters.keys())
177
+
178
+ def get_parameters(self) -> dict[str, float]:
179
+ """Return a mapping from parameter labels to their current numerical values."""
180
+ return {label: param.value for label, param in self._parameters.items()}
181
+
182
+ def set_parameter_values(self, values: list[float]) -> None:
131
183
  """
132
- Get the maximum number of qubits among all Hamiltonians in the schedule.
184
+ Update the numerical values of all parameters referenced by the schedule.
133
185
 
134
- Returns:
135
- int: The number of qubits.
186
+ Args:
187
+ values (list[float]): New parameter values ordered according to ``get_parameter_names()``.
188
+
189
+ Raises:
190
+ ValueError: If the number of provided values does not match ``nparameters``.
136
191
  """
137
- return self._nqubits
192
+ if len(values) != self.nparameters:
193
+ raise ValueError(f"Provided {len(values)} but Schedule has {self.nparameters} parameters.")
194
+ for i, parameter in enumerate(self._parameters.values()):
195
+ parameter.set_value(values[i])
196
+
197
+ def set_parameters(self, parameter_dict: dict[str, int | float]) -> None:
198
+ """
199
+ Update a subset of parameters by label.
200
+
201
+ Args:
202
+ parameter_dict (dict[str, float]): Mapping from parameter labels to new values.
203
+
204
+ Raises:
205
+ ValueError: If an unknown parameter label is provided.
206
+ """
207
+ for label, param in parameter_dict.items():
208
+ if label not in self._parameters:
209
+ raise ValueError(f"Parameter {label} is not defined in this Schedule.")
210
+ self._parameters[label].set_value(param)
211
+
212
+ def get_parameter_bounds(self) -> dict[str, tuple[float, float]]:
213
+ """Return the bounds registered for each schedule parameter."""
214
+ return {k: v.bounds for k, v in self._parameters.items()}
215
+
216
+ def set_parameter_bounds(self, ranges: dict[str, tuple[float, float]]) -> None:
217
+ """
218
+ Update the bounds of existing parameters.
219
+
220
+ Args:
221
+ ranges (dict[str, tuple[float, float]]): Mapping from label to ``(lower, upper)`` bounds.
222
+
223
+ Raises:
224
+ ValueError: If an unknown parameter label is provided.
225
+ """
226
+ for label, bound in ranges.items():
227
+ if label not in self._parameters:
228
+ raise ValueError(
229
+ f"The provided parameter label {label} is not defined in the list of parameters in this object."
230
+ )
231
+ self._parameters[label].set_bounds(bound[0], bound[1])
138
232
 
139
233
  def add_hamiltonian(
140
234
  self, label: str, hamiltonian: Hamiltonian, schedule: Callable | None = None, **kwargs: dict
@@ -151,50 +245,78 @@ class Schedule:
151
245
  schedule (Callable, optional): A function that returns the coefficient of the Hamiltonian at time t.
152
246
  It should accept time (and any additional keyword arguments) and return a float.
153
247
  **kwargs (dict): Additional keyword arguments to pass to the schedule function.
248
+
249
+ Raises:
250
+ ValueError: if the parameterized schedule contains generic variables instead of only Parameters.
154
251
  """
155
- if label not in self._hamiltonians:
156
- self._hamiltonians[label] = hamiltonian
157
- self._schedule[0][label] = 0
158
- self._nqubits = max(self._nqubits, hamiltonian.nqubits)
159
- else:
160
- warn(
161
- (
162
- f"label {label} is already assigned to a hamiltonian, "
163
- + "ignoring new hamiltonian and updating schedule of existing hamiltonian."
164
- ),
165
- RuntimeWarning,
252
+ if label in self._hamiltonians:
253
+ logger.warning(
254
+ (f"label {label} is already assigned to a hamiltonian, " + "updating schedule of existing hamiltonian.")
166
255
  )
256
+ self._hamiltonians[label] = hamiltonian
257
+ self._schedule[0][label] = 0
258
+ self._nqubits = max(self._nqubits, hamiltonian.nqubits)
259
+ for _, param in hamiltonian.parameters.items():
260
+ self._parameters[param.label] = param
167
261
 
168
262
  if schedule is not None:
169
- for t in range(int(self.T / self.dt) + 1):
170
- self.update_hamiltonian_coefficient_at_time_step(t, label, schedule(t, **kwargs))
171
-
172
- def add_schedule_step(self, time_step: int, hamiltonian_coefficient_list: dict[str, float]) -> None:
263
+ for t in range(int(self.T / self.dt)):
264
+ time_step = schedule(float(t), **kwargs)
265
+ if isinstance(time_step, Term):
266
+ for v in time_step.variables():
267
+ if not isinstance(v, Parameter):
268
+ raise ValueError(
269
+ f"The schedule can only contain Parameters, but a generic variable was provided ({time_step})"
270
+ )
271
+ self._parameters[v.label] = v
272
+ elif isinstance(time_step, BaseVariable):
273
+ if not isinstance(time_step, Parameter):
274
+ raise ValueError(
275
+ f"The schedule can only contain Parameters, but a generic variable was provided ({time_step})"
276
+ )
277
+ self._parameters[time_step.label] = time_step
278
+ self.update_hamiltonian_coefficient_at_time_step(t, label, time_step)
279
+
280
+ def add_schedule_step(
281
+ self, time_step: int, hamiltonian_coefficient_list: dict[str, float | Term | Parameter]
282
+ ) -> None:
173
283
  """
174
284
  Add or update a schedule step with specified Hamiltonian coefficients.
175
285
 
176
286
  Args:
177
287
  time_step (int): The time step index at which the Hamiltonian coefficients are updated.
178
288
  The actual time is computed as dt * time_step.
179
- hamiltonian_coefficient_list (dict[str, float]): A dictionary mapping Hamiltonian labels to their
180
- coefficient values at this time step.
289
+ hamiltonian_coefficient_list (dict[str, float | Term | Parameter]): Mapping from Hamiltonian labels to coefficients
290
+ (numeric or symbolic) at this time step.
181
291
  If a Hamiltonian is not included in the dictionary, it is assumed its coefficient remains unchanged.
182
292
 
183
293
  Raises:
184
294
  ValueError: If hamiltonian_coefficient_list references a Hamiltonian that is not defined in the schedule.
185
295
  """
186
296
  if time_step in self._schedule:
187
- warn(
297
+ logger.warning(
188
298
  f"time step {time_step} is already defined in the schedule, the values are going to be overwritten.",
189
- RuntimeWarning,
190
299
  )
191
- for key in hamiltonian_coefficient_list:
300
+ for key, coeff in hamiltonian_coefficient_list.items():
192
301
  if key not in self._hamiltonians:
193
302
  raise ValueError(f"trying to reference a hamiltonian {key} that is not defined in this schedule.")
303
+ if isinstance(coeff, Term):
304
+ for v in coeff.variables():
305
+ if not isinstance(v, Parameter):
306
+ raise ValueError(
307
+ f"The schedule can only contain Parameters, but a generic variable was provided ({time_step})"
308
+ )
309
+ self._parameters[v.label] = v
310
+ if isinstance(coeff, BaseVariable):
311
+ if not isinstance(coeff, Parameter):
312
+ raise ValueError(
313
+ f"The schedule can only contain Parameters, but a generic variable was provided ({time_step})"
314
+ )
315
+ self._parameters[coeff.label] = coeff
194
316
  self._schedule[time_step] = hamiltonian_coefficient_list
195
317
 
196
318
  def update_hamiltonian_coefficient_at_time_step(
197
- self, time_step: int, hamiltonian_label: str, new_coefficient: float
319
+ self, time_step: int, hamiltonian_label: str, new_coefficient: float | Term | Parameter
198
320
  ) -> None:
199
321
  """
200
322
  Update the coefficient value of a specific Hamiltonian at a given time step.
@@ -202,7 +324,7 @@ class Schedule:
202
324
  Args:
203
325
  time_step (int): The time step (as an integer multiple of dt) at which to update the coefficient.
204
326
  hamiltonian_label (str): The label of the Hamiltonian to update.
205
- new_coefficient (float): The new coefficient value.
327
+ new_coefficient (float | Term | Parameter): The new coefficient value or symbolic expression.
206
328
 
207
329
  Raises:
208
330
  ValueError: If the specified time step exceeds the total annealing time.
@@ -214,6 +336,20 @@ class Schedule:
214
336
  self._schedule[time_step] = {}
215
337
  self._schedule[time_step][hamiltonian_label] = new_coefficient
216
338
 
339
+ if isinstance(new_coefficient, Term):
340
+ for v in new_coefficient.variables():
341
+ if not isinstance(v, Parameter):
342
+ raise ValueError(
343
+ f"The schedule can only contain Parameters, but a generic variable was provided ({time_step})"
344
+ )
345
+ self._parameters[v.label] = v
346
+ if isinstance(new_coefficient, BaseVariable):
347
+ if not isinstance(new_coefficient, Parameter):
348
+ raise ValueError(
349
+ f"The schedule can only contain Parameters, but a generic variable was provided ({time_step})"
350
+ )
351
+ self._parameters[new_coefficient.label] = new_coefficient
352
+
217
353
  def __getitem__(self, time_step: int) -> Hamiltonian:
218
354
  """
219
355
  Retrieve the effective Hamiltonian at a given time step.
@@ -222,10 +358,10 @@ class Schedule:
222
358
  using the latest defined coefficients at or before the given time step.
223
359
 
224
360
  Args:
225
- time_step (int): The time step index for which to retrieve the Hamiltonian.
361
+ time_step (int): Time step index for which to retrieve the Hamiltonian (``time_step * dt`` in units).
226
362
 
227
363
  Returns:
228
- Hamiltonian: The effective Hamiltonian at the specified time step.
364
+ Hamiltonian: The effective Hamiltonian at the specified time step with coefficients evaluated to numbers.
229
365
  """
230
366
  ham = Hamiltonian()
231
367
  read_labels = []
@@ -235,12 +371,24 @@ class Schedule:
235
371
  time_step -= 1
236
372
  if time_step in self._schedule:
237
373
  for ham_label in self._schedule[time_step]:
238
- ham += self._schedule[time_step][ham_label] * self._hamiltonians[ham_label]
374
+ aux = self._schedule[time_step][ham_label]
375
+ coeff = (
376
+ aux.evaluate({})
377
+ if isinstance(aux, Term)
378
+ else (aux.evaluate() if isinstance(aux, Parameter) else aux)
379
+ )
380
+ ham += coeff * self._hamiltonians[ham_label]
239
381
  read_labels.append(ham_label)
240
382
  break
241
383
  else:
242
384
  for ham_label in self._schedule[time_step]:
243
- ham += self._schedule[time_step][ham_label] * self._hamiltonians[ham_label]
385
+ aux = self._schedule[time_step][ham_label]
386
+ coeff = (
387
+ aux.evaluate({})
388
+ if isinstance(aux, Term)
389
+ else (aux.evaluate() if isinstance(aux, Parameter) else aux)
390
+ )
391
+ ham += coeff * self._hamiltonians[ham_label]
244
392
  read_labels.append(ham_label)
245
393
  if len(read_labels) < len(self._hamiltonians):
246
394
  all_labels = self._hamiltonians.keys()
@@ -250,11 +398,17 @@ class Schedule:
250
398
  while current_time > 0:
251
399
  current_time -= 1
252
400
  if current_time in self._schedule and label in self._schedule[current_time]:
253
- ham += self._schedule[current_time][label] * self._hamiltonians[label]
401
+ aux = self._schedule[current_time][label]
402
+ coeff = (
403
+ aux.evaluate({})
404
+ if isinstance(aux, Term)
405
+ else (aux.evaluate() if isinstance(aux, Parameter) else aux)
406
+ )
407
+ ham += coeff * self._hamiltonians[label]
254
408
  break
255
- return ham
409
+ return ham.get_static_hamiltonian()
256
410
 
257
- def get_coefficient(self, time_step: float, hamiltonian_key: str) -> float:
411
+ def get_coefficient(self, time_step: float, hamiltonian_key: str) -> Number:
258
412
  """
259
413
  Retrieve the coefficient of a specified Hamiltonian at a given time.
260
414
 
@@ -262,16 +416,36 @@ class Schedule:
262
416
  coefficient for the given Hamiltonian.
263
417
 
264
418
  Args:
265
- time_step (float): The time (in the same units as T) at which to query the coefficient.
419
+ time_step (float): The time (in the same units as ``T``) at which to query the coefficient.
266
420
  hamiltonian_key (str): The label of the Hamiltonian.
267
421
 
268
422
  Returns:
269
- float: The coefficient of the Hamiltonian at the specified time, or 0 if not defined.
423
+ Number: The coefficient of the Hamiltonian at the specified time, or 0 if not defined.
424
+ """
425
+ time_step = float(time_step)
426
+ val = self.get_coefficient_expression(time_step=time_step, hamiltonian_key=hamiltonian_key)
427
+ return val.evaluate({}) if isinstance(val, Term) else (val.evaluate() if isinstance(val, Parameter) else val)
428
+
429
+ def get_coefficient_expression(self, time_step: float, hamiltonian_key: str) -> Number | Term | Parameter:
430
+ """
431
+ Retrieve the expression of a specified Hamiltonian at a given time. If any parameters are
432
+ present in the expression they will be printed in the expression.
433
+
434
+ This function searches backwards in time (by multiples of dt) until it finds a defined
435
+ coefficient for the given Hamiltonian.
436
+
437
+ Args:
438
+ time_step (float): The time (in the same units as ``T``) at which to query the coefficient.
439
+ hamiltonian_key (str): The label of the Hamiltonian.
440
+
441
+ Returns:
442
+ Number | Term: The coefficient expression of the Hamiltonian at the specified time, or 0 if not defined.
270
443
  """
271
444
  time_idx = int(time_step / self.dt)
272
445
  while time_idx >= 0:
273
446
  if time_idx in self._schedule and hamiltonian_key in self._schedule[time_idx]:
274
- return self._schedule[time_idx][hamiltonian_key]
447
+ val = self._schedule[time_idx][hamiltonian_key]
448
+ return val
275
449
  time_idx -= 1
276
450
  return 0
277
451
 
@@ -309,3 +483,25 @@ class Schedule:
309
483
  self.iter_time_step += 1
310
484
  return result
311
485
  raise StopIteration
486
+
487
+ def draw(self, style: ScheduleStyle | None = None, filepath: str | None = None) -> None:
488
+ """Render a plot of the schedule using matplotlib and optionally save it to a file.
489
+
490
+ The schedule is rendered using the provided style configuration. If ``filepath`` is
491
+ given, the resulting figure is saved to disk (the output format is inferred
492
+ from the file extension, e.g. ``.png``, ``.pdf``, ``.svg``).
493
+
494
+ Args:
495
+ style (ScheduleStyle, optional): Customization options for the plot appearance.
496
+ Defaults to ScheduleStyle().
497
+ filepath (str | None, optional): If provided, saves the plot to the specified file path.
498
+ """
499
+ from qilisdk.utils.visualization.schedule_renderers import MatplotlibScheduleRenderer # noqa: PLC0415
500
+
501
+ style = style or ScheduleStyle()
502
+ renderer = MatplotlibScheduleRenderer(self, style=style)
503
+ renderer.plot()
504
+ if filepath:
505
+ renderer.save(filepath)
506
+ else:
507
+ renderer.show()
@@ -21,12 +21,17 @@ OPTIONAL_FEATURES: list[OptionalFeature] = [
21
21
  OptionalFeature(
22
22
  name="cuda",
23
23
  dependencies=["cuda-quantum-cu12"],
24
- symbols=[Symbol(path="qilisdk.extras.cuda.cuda_backend", name="CudaBackend")],
24
+ symbols=[
25
+ Symbol(path="qilisdk.backends.cuda_backend", name="CudaBackend"),
26
+ Symbol(path="qilisdk.backends.cuda_backend", name="CudaSamplingMethod"),
27
+ ],
25
28
  ),
26
29
  OptionalFeature(
27
- name="qaas",
28
- dependencies=["httpx", "keyring"],
29
- symbols=[Symbol(path="qilisdk.extras.qaas.qaas_backend", name="QaaSBackend")],
30
+ name="qutip",
31
+ dependencies=["qutip", "qutip-qip", "matplotlib"],
32
+ symbols=[
33
+ Symbol(path="qilisdk.backends.qutip_backend", name="QutipBackend"),
34
+ ],
30
35
  ),
31
36
  ]
32
37
 
@@ -12,5 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ from .cuda_backend import CudaBackend, CudaSamplingMethod
16
+ from .qutip_backend import QutipBackend
15
17
 
16
- class Model: ...
18
+ __all__ = ["CudaBackend", "CudaSamplingMethod", "QutipBackend"]