Developer API Reference¶
The documentation presented for qugrad.QuantumSystem and its child
classes is demonstrates their usage by specifying method arguments. The
underlying methods actually accept *args to allow developers to extend the
functionality of QuGrad. To aid in extending QuGrad’s functionality we include
source code for qugrad.QuantumSystem and its child classes below. The
documentation in this source code is focused on extending the functionality
oposed to utilising it.
1"""
2Defines classes for quantum systems
3"""
4
5from typing import Callable, Optional, Union, Any
6
7import numpy as np
8import tensorflow as tf
9
10from py_ste import get_unitary_evolver
11from py_ste.evolvers import UnitaryEvolver
12
13from .._hilbert_space import HilbertSpace
14from ..pulses import compose_unpack
15
16def generate_channel_couplings(number_channels: list[int]) -> np.ndarray[bool]:
17 """Generates a boolean array indicating which channels couple to which
18 drives.
19
20 Parameters
21 ----------
22 number_chanels : list[int]
23 A list with the nth entry indicating the number of channels that
24 correspond the nth drive.
25
26 Returns
27 -------
28 NDArray[Shape[``len(number_channels)``, total_number_channels], bool]
29 An entry is `True` if the channel corresponds to the drive.
30 """
31 number_channels: np.ndarray[int] = np.array(number_channels)
32 if len(number_channels) == 0:
33 return np.empty((0, 0), dtype=bool)
34 number_channels = number_channels.cumsum()
35 channel_couplings: np.ndarray[bool] = np.zeros((len(number_channels),
36 number_channels[-1]),
37 dtype=bool)
38 c_old = 0
39 for i, c in enumerate(number_channels):
40 channel_couplings[i, c_old: (c_old := c)] = True
41 return channel_couplings
42
43# Defining classes
44class ExpValCustom:
45 """
46 A class implimenting :meth:`QuantumSystem.evolved_expectation_value()` with
47 a `TensorFlow <https://www.tensorflow.org>`__ gradient.
48 """
49
50 system: "QuantumSystem"
51 "The system in which to take the expectation value"
52
53 initial_state: np.ndarray[complex]
54 "The initial state for the integrator"
55
56 dt: float
57 "The integrator time step"
58
59 observable: np.ndarray[complex]
60 "The observable to take the expectation value of"
61
62 switching_function: Any
63 """
64 The value of the switching function multiplied by :attr:`dt` evaluated at
65 each integration timestep
66 """
67
68 def __init__(self,
69 system: "QuantumSystem",
70 initial_state: np.ndarray[complex],
71 dt: float,
72 observable: np.ndarray[complex]):
73 """
74 Initialises the class with the `system` in which to take the expectation
75 value and the `observable` in the `system` to take the expectation value
76 of. Additionally, the initial state before evolution and
77 the integrator time step are specified.
78
79 Parameters
80 ----------
81 system : QuantumSystem
82 The system in which to take the expectation value
83 initial_state : NDArray[Shape[``system.state_shape``], complex]
84 The initial state for the integrator
85 dt : float
86 The integrator time step
87 observable : NDArray[Shape[``system.dim``, ``system.dim``], complex])
88 The observable to take the expectation value of
89 """
90 self.system = system
91 self.initial_state = initial_state
92 self.dt = dt
93 self.observable = observable
94 @tf.custom_gradient
95 def run(self, ctrl_amp):
96 """
97 Computes the expectation value of the :attr:`observable` in the
98 :attr:`system` with respect to the :attr:`initial_state` evolved under
99 the Hamiltonian generated by the specified control amplitudes.
100
101 Parameters
102 ----------
103 ctrl_amp : tf.Tensor[Shape[n_time_steps, ``system.n_ctrl``], tf.complex128]
104 $\left(a_{ij}\right)$ The control amplitudes at each time step
105 expressed as an $N\times\textrm{length}$ matrix where the element
106 $a_{ij}$ corresponds to the control amplitude of the $j$th control
107 Hamiltonian at the $i$th time step.
108
109 Returns
110 -------
111 tf.Tensor[Shape[], tf.complex128]
112 The expectation value
113
114 Note
115 ----
116 This function is differentiable using TensorFlow's ``tf.GradientTape``.
117
118 Note
119 ----
120 This function uses
121 :meth:`~py_ste.evolvers.DenseUnitaryEvolver.switching_function()`
122 from `PySTE <https://PySTE.readthedocs.io>`__.
123 """
124 E, self.switching_function = self.system.evolver.switching_function(ctrl_amp.numpy().copy(), self.initial_state, self.dt, self.observable)
125 self.switching_function = tf.constant(self.switching_function*self.dt, dtype=tf.complex128)
126 def grad(upstream): return self.switching_function
127 return tf.math.real(tf.constant(E, dtype=tf.complex128)), grad # @tf.custom_gradient removes the second returned variable
128
129class GateInfidelityCustom():
130 """
131 A class implimenting :meth:`QuantumSystem.evolved_gate_infidelity()` with
132 a `TensorFlow <https://www.tensorflow.org>`__ gradient.
133 """
134
135 system: "QuantumSystem"
136 "The system in which to calculate the gate infidelity"
137
138 dt: float
139 "The integrator time step"
140
141 target: np.ndarray[complex]
142 "The target gate to calculate the gate infidelity with respect to."
143
144 switching_function: Any
145 """
146 The value of the switching function multiplied by :attr:`dt` evaluated at
147 each integration timestep
148 """
149
150 def __init__(self,
151 system: "QuantumSystem",
152 dt: float,
153 target: np.ndarray[complex]):
154 """
155 Initialises the class with the `system` in which to calculate the
156 gate infidelity and the `target` gate calculate the gate infidelity
157 from. Additionally, the integrator time step is specified.
158
159 Parameters
160 ----------
161 system : QuantumSystem
162 The system in which to calculate the gate infidelity
163 dt : float
164 The integrator time step
165 target : NDArray[Shape[``system.dim``, ``system.dim``], complex])
166 The target gate to calculate the gate infidelity with respect to
167 """
168 self.system = system
169 self.dt = dt
170 self.target = target
171 @tf.custom_gradient
172 def run(self, ctrl_amp):
173 """
174 Computes the gate infidelity from :attr:`target` of the evolution due to
175 the :attr:`system` Hamiltonian generated by the specified control
176 amplitudes.
177
178 Parameters
179 ----------
180 ctrl_amp : tf.Tensor[Shape[n_time_steps, ``system.n_ctrl``], tf.complex128]
181 $\left(a_{ij}\right)$ The control amplitudes at each time step
182 expressed as an $N\times\textrm{length}$ matrix where the element
183 $a_{ij}$ corresponds to the control amplitude of the $j$th control
184 Hamiltonian at the $i$th time step.
185
186 Returns
187 -------
188 tf.Tensor[Shape[], tf.complex128]
189 The gate infidelity
190
191 Note
192 ----
193 This function is differentiable using TensorFlow's ``tf.GradientTape``.
194
195 Note
196 ----
197 This function uses
198 :meth:`~py_ste.evolvers.DenseUnitaryEvolver.gate_switching_function()`
199 from `PySTE <https://PySTE.readthedocs.io>`__.
200 """
201 infidelity, self.switching_function = self.system.evolver.gate_switching_function(ctrl_amp.numpy().copy(), self.dt, self.target)
202 self.switching_function = tf.constant(self.switching_function*self.dt, dtype=tf.complex128)
203 def grad(upstream): return self.switching_function
204 return tf.constant(infidelity, dtype=tf.float64), grad # @tf.custom_gradient removes the second returned variable
205
206class QuantumSystem:
207 """
208 A class storing the properties of a quantum system.
209 """
210 _evolver: Optional[UnitaryEvolver] = None
211 "The integrator used for time evolutions of the system."
212
213 _hilbert_space: HilbertSpace
214 "The Hilbert space of the system"
215
216 _H0: np.ndarray
217 "The systems drift Hamiltonian as a :attr:`dim` x :attr:`dim` matrix."
218
219 _Hs: np.ndarray
220 """
221 An array of the system's control Hamiltonians with shape
222 (:attr:`n_ctrl`, :attr:`dim`, :attr:`dim`).
223 """
224
225 _graph_processing: Callable[..., tuple]
226 "A Tensorflow graph of :attr:`_processing()`"
227
228 _processing: Callable[..., tuple]
229 """
230 Executes :meth:`_pre_processing()` followed by
231 :meth:`_envolope_processing()` eagerly (i.e. without using a TensorFlow
232 graph). Nonetheless, :meth:`_eager_processing()` is still auto
233 differentiable.
234
235 Parameters
236 ----------
237 *args
238 The placeholder parameters. See ``_systems.pyi`` for actual parameters.
239 Each child class that implements a new :meth:`_pre_processing()` should
240 implement a ``.pyi`` file to document the parameters for this function:
241 the same parameters as passed to :meth:`_pre_processing()`.
242
243 Returns
244 -------
245 tuple[tf.Tensor[Shape[n_time_steps, :attr:`n_ctrl`], complex], tf.Tensor[Shape[:attr:`state_shape`], complex], tf.Tensor[Shape[], float]]
246 A tuple of:
247 1. Control amplitudes
248 2. Initial state
249 3. Integrator time step
250 """
251
252 _using_graph: bool
253 """
254 Whether to use TensorFlow graphs during computation. Using a TensorFlow
255 graph will increase the speed of computation. However, you have to be
256 careful that function parameters have not been baked into the graph leading
257 to unexpected behaviour.
258 """
259
260 def __init__(self,
261 H0: np.ndarray[complex],
262 Hs: np.ndarray[complex],
263 hilbert_space: HilbertSpace,
264 use_graph: bool = True):
265 """
266 Initialises a new :class:`QuantumSystem`.
267
268 Parameters
269 ----------
270 H0 : NDArray[Shape[:attr:`dim`, :attr:`dim`], complex]
271 The systems drift Hamiltonian
272 Hs : NDArray[Shape[:attr:`n_ctrl`, :attr:`dim`, :attr:`dim`], complex] | NDArray[Shape[:attr:`n_ctrl` * :attr:`dim`, :attr:`dim`], complex]
273 The systems control Hamiltonians either as an array of control
274 Hamiltonians or the control Hamiltonians stacked along the first
275 axis.
276 hilbert_space : HilbertSpace
277 The Hilbert space of the system
278 use_graph : bool
279 Whether to use `TensorFlow <https://www.tensorflow.org>`__ graphs
280 during computation, by default ``True``
281 """
282 self._hilbert_space: HilbertSpace = hilbert_space
283 self._H0 = np.array(H0)
284 self._H0.flags.writeable = False
285 Hs = np.array(Hs)
286 self._Hs = Hs if Hs.ndim == 2 else Hs.reshape(-1, self.dim)
287 self._Hs.flags.writeable = False
288 self._graph_processing = tf.function(self._traceable_eager_processing, autograph=False)
289 self.using_graph = use_graph
290
291 def __del__(self):
292 # to force clear up of tracing
293 del self._graph_processing
294 del self._processing
295 @property
296 def using_graph(self) -> bool:
297 """
298 Whether to use `TensorFlow <https://www.tensorflow.org>`__ graphs during
299 computation. Using a `TensorFlow <https://www.tensorflow.org>`__ graph
300 will increase the speed of computation. However, you have to be careful
301 that function parameters have not been baked into the graph leading to
302 unexpected behaviour.
303 """
304 return self._using_graph
305 @using_graph.setter
306 def using_graph(self, value: bool):
307 self._using_graph = value
308 self._processing = self._graph_processing if value else self._eager_processing
309 @property
310 def hilbert_space(self) -> HilbertSpace:
311 "The Hilbert space of the system"
312 return self._hilbert_space
313 @property
314 def H0(self) -> np.ndarray[complex]:
315 """
316 The systems drift Hamiltonian as a :attr:`dim` x :attr:`dim` matrix.
317
318 See Also
319 --------
320 :attr:`Hs`
321 """
322 return self._H0
323 @property
324 def Hs(self) -> np.ndarray[complex]:
325 """
326 An array of the system's control Hamiltonians with shape
327 (:attr:`n_ctrl`, :attr:`dim`, :attr:`dim`).
328
329 See Also
330 --------
331 :attr:`H0`
332 """
333 return self._Hs.reshape((-1, self.dim, self.dim))
334 @property
335 def dim(self) -> int:
336 """
337 The dimension of states in the quantum system.
338
339 See Also
340 --------
341 :attr:`state_shape`
342 """
343 return self._hilbert_space.dim
344 @property
345 def state_shape(self) -> tuple[int]:
346 """
347 The shape of the states in the system.
348
349 See Also
350 --------
351 :attr:`dim`
352 """
353 return (self.dim,)
354 @property
355 def n_ctrl(self) -> int:
356 """
357 The number of control Hamiltonians.
358 """
359 return len(self.Hs)
360 @property
361 def evolver(self) -> UnitaryEvolver:
362 """
363 The integrator used for time evolutions of the system.
364
365 Note
366 ----
367 The `evolver` can take a while to initialise and so is not initialised
368 until `evolver` is is first used or when :meth:`initialise_evolver()` is
369 called. Using `evolver` before calling :meth:`initialise_evolver()`
370 initialises the `evolver` with the default parameters of
371 :meth:`initialise_evolver()`.
372 """
373 if self._evolver is None:
374 self.initialise_evolver()
375 return self._evolver
376 def initialise_evolver(self,
377 sparse: bool = False,
378 force_dynamic: bool = False):
379 """
380 Initialises :attr:`evolver` with an evolver from
381 `PySTE <https://PySTE.readthedocs.io>`__.
382 `PySTE <https://PySTE.readthedocs.io>`__ is Python
383 wrapper around the C++ header-only library
384 `Suzuki-Trotter-Evolver <https://Suzuki-Trotter-Evolver.readthedocs.io>`__:
385 a fast Schrödinger solver utilising the first-order Suzuki-Trotter
386 expansion.
387
388 Warning
389 -------
390 This can take a very long time to execute, especially for large Hilbert
391 space dimensions. If you plan to evolve the same quantum system many
392 times we recommended pickling the :attr:`evolver`.
393
394 Parameters
395 ----------
396 sparse : bool
397 Whether to use sparse or dense matrices during integration.
398 To make a decision on whether sparse or dense matrices are likely to
399 lead to faster integration you can consult the benchmarks at
400 https://PySTE.readthedocs.io/en/latest/benchmarks.
401 force_dynamic : bool
402 Whether to force `PySTE <https://PySTE.readthedocs.io>`__ to use a
403 dynamic evolver.
404
405 Note
406 ----
407 `PySTE <https://PySTE.readthedocs.io>`__ has precompiled evolvers
408 for specific Hilbert space dimensions and numbers of control
409 Hamiltonians. When these cannot be found
410 `PySTE <https://PySTE.readthedocs.io>`__ uses less efficient
411 evolvers with the Hilbert space dimension and the number of controls
412 determined dynamically at runtime.
413 """
414 self._evolver = get_unitary_evolver(self.H0, self._Hs, sparse, force_dynamic)
415 def _H(self, ctrl_amp: np.ndarray[float]) -> np.ndarray[complex]:
416 """
417 Computes the system Hamiltonian for the specified control amplitudes.
418
419 Parameters
420 ----------
421 ctrl_amp : NDArray[Shape[s := Any_Shape, :attr:`n_ctrl`], float]
422 The control amplitudes (stored in the last axis). The prior axes
423 allow for multiple sets of control amplitudes to be passed and the
424 Hamiltonian for each computed.
425
426 Returns
427 -------
428 NDArray[Shape[s, :attr:`dim`, :attr:`dim`], complex]
429 The system's Hamiltonian (stored in the last two axes).
430 """
431 return self._H0 + np.einsum("...i,ijk->...jk", ctrl_amp, self.Hs)
432 def H(self,
433 ctrl_amp: Union[np.ndarray[float], np.ndarray[Callable[[float], np.ndarray[float]]], Callable[[float], np.ndarray[float]]]
434 ) -> Union[np.ndarray[complex], Callable[[float], np.ndarray[complex]]]:
435 """
436 Computes the system Hamiltonian for the specified control amplitudes.
437
438 Parameters
439 ----------
440 ctrl_amp : NDArray[Shape[s := Any_Shape, :attr:`n_ctrl`], float | Callable[[float], np.ndarray[float]]] | Callable[[float], NDArray[Shape[:attr:`n_ctrl`], float]
441 The control amplitudes (stored in the last axis). The prior axes
442 allow for multiple sets of control amplitudes to be passed and the
443 Hamiltonian for each computed. The control amplitudes can be passed
444 as ``np.ndarray[float]`` to compute the system Hamiltonian for a
445 specific value of the control ampltiudes. Alternatively,
446 time-dependent control amplitudes can be passed.
447 ``np.ndarray[Callable[[float], np.ndarray[float]]]`` can be passed
448 where each element is a function of time. Alternatively, a function
449 of time that returns the control amplitudes can be passed as
450 ``Callable[[float], NDArray[Shape[:attr:`n_ctrl`], float]``. These
451 will generate a time-dependent Hamiltonian: a function that takes a
452 single parameter (time) and returns the Hamiltonian at this time.
453
454 Returns
455 -------
456 NDArray[Shape[s, :attr:`dim`, :attr:`dim`], complex] | NDArray[Shape[s], Callable[[float], np.ndarray[complex]]]]
457 Either the systems Hamiltonian stored in the last two axes (if
458 specific control amplitudes were passed) or a collection of
459 time-dependent Hamiltonians (if time-dependent controls were
460 passed).
461 """
462 if callable(ctrl_amp):
463 return lambda t: self._H(ctrl_amp(t))
464 ctrl_amp = np.array(ctrl_amp)
465 if ctrl_amp.dtype == object:
466 return lambda t: self._H([a(t) for a in ctrl_amp])
467 return self._H(ctrl_amp)
468 def _pre_processing(self, *args):
469 """
470 When calling any evolution method (listed in the
471 :ref:`See also section <pre_processing_see_also>`)
472 :meth:`_pre_processing()` is executed on the arguments before the
473 control amplitudes are modulated by the frequencies (during
474 :meth:`_envolope_processing()`) and then finally the modulated control
475 amplitudes are used by the evolution method.
476
477 :meth:`_pre_processing()` should be overridden to produce desired pulse
478 shapes. You can either override :meth:`_pre_processing()` directly by
479 creating a child class, or you can use :meth:`pulse_form()`.
480
481 For :meth:`gradient()` to function correctly :meth:`_pre_processing()`
482 should be written in `TensorFlow <https://www.tensorflow.org>`__.
483
484 Parameters
485 ----------
486 *args
487 The placeholder parameters. See ``_systems.pyi`` for actual
488 parameters. Each child class that implements a new
489 :meth:`_pre_processing()` should implement a ``.pyi`` file to
490 document the parameters for this function: the same parameters as
491 passed to :meth:`_pre_processing()`.
492
493 Returns
494 -------
495 tuple[tf.Tensor[Shape[n_time_steps, total_n_channels], tf.complex128], tf.Tensor[Shape[:attr:`state_shape`], tf.complex128], float, tf.Tensor[Shape[n_time_steps, total_n_channels], tf.complex128], list[int]]
496 A tuple of
497 1. The control amplitude envolopes
498 2. The initial state
499 3. The integrator time step
500 4. The frequencies to modulate the control amplitude envolopes with
501 5. A list of the number of channels for each control Hamiltonian
502
503 Warning
504 -------
505 The number of channels for each control Hamiltonian must be stored
506 as a ``list`` and not an ``NDArray`` or a
507 `TensorFlow <https://www.tensorflow.org>`__ tensor.
508
509
510 .. _pre_processing_see_also:
511
512 See Also
513 --------
514 * :meth:`propagate()`
515 * :meth:`propagate_collection()`
516 * :meth:`propagate_all()`
517 * :meth:`get_evolution()`
518 * :meth:`evolved_expectation_value()`
519 * :meth:`evolved_expectation_value_all()`
520 * :meth:`evolved_inner_product()`
521 * :meth:`evolved_inner_product_all()`
522 * :meth:`evolved_gate_infidelity()`
523 * :meth:`get_driving_pulses()`
524 * :meth:`gradient()`
525 * :meth:`gate_gradient()`
526 """
527 return args
528 def _envolope_processing(self,
529 ctrl_amp,
530 dt: float,
531 frequencies,
532 number_channels: list[int]
533 ) -> tuple:
534 """
535 When calling any evolution method (listed in the
536 :ref:`See also section <envolope_processing_see_also>` section)
537 :meth:`_pre_processing()` is executed on the arguements before the
538 control amplitudes are modulated by the frequencies during
539 :meth:`_envolope_processing()` and then finally the modulated control
540 amplitudes are used by the evolution method.
541
542 Parameters
543 ----------
544 ctrl_amp : tf.Tensor[Shape[n_time_steps, total_n_channels], tf.complex128]
545 The envolope control amplitudes
546 dt : float
547 The itegration time step
548 frequencies : tf.Tensor[Shape[n_time_steps, total_n_channels], tf.complex128]
549 The frequencies to modulate the control amplitudes with
550 number_channels : list[int]
551 The number of channels associated with each control Hamiltonian
552
553 Warning
554 -------
555 This must be a ``list`` and not an ``NDArray`` or a
556 `TensorFlow <https://www.tensorflow.org>`__ tensor.
557
558 Returns
559 -------
560 tf.Tensor[Shape[n_time_steps, :attr:`n_ctrl`], tf.complex128]
561 The modulated control amplitudes
562
563
564 .. _envolope_processing_see_also:
565
566 See Also
567 --------
568 * :meth:`propagate()`
569 * :meth:`propagate_collection()`
570 * :meth:`propagate_all()`
571 * :meth:`get_evolution()`
572 * :meth:`evolved_expectation_value()`
573 * :meth:`evolved_expectation_value_all()`
574 * :meth:`evolved_inner_product()`
575 * :meth:`evolved_inner_product_all()`
576 * :meth:`evolved_gate_infidelity()`
577 * :meth:`get_driving_pulses()`
578 * :meth:`gradient()`
579 * :meth:`gate_gradient()`
580 """
581 dt = tf.cast(dt, dtype=tf.complex128)
582 frequencies = tf.cast(frequencies, dtype=tf.complex128)
583 ctrl_amp = tf.cast(ctrl_amp, dtype=tf.complex128)
584 channel_couplings = tf.constant(generate_channel_couplings(number_channels), dtype=tf.complex128)
585 x = tf.exp(tf.einsum("i,j->ij", dt*tf.cast(tf.range(tf.shape(ctrl_amp)[0]), dtype=tf.complex128), -1j*frequencies))
586 ctrl_amp = tf.cast(tf.math.real(tf.einsum("tc,tc,dc->td", x, ctrl_amp, channel_couplings)), dtype=tf.complex128)
587 return ctrl_amp
588 def propagate(self, *args) -> np.ndarray[complex]:
589 """
590 Evolves a state vector under the time-dependent Hamiltonian defined by
591 the control amplitudes using
592 :meth:`~py_ste.evolvers.DenseUnitaryEvolver.propagate()`
593 from `PySTE <https://PySTE.readthedocs.io>`__.
594
595 Parameters
596 ----------
597 *args
598 The placeholder parameters. See ``_systems.pyi`` for actual
599 parameters. Each child class that implements a new
600 :meth:`_pre_processing()` should implement a ``.pyi`` file to
601 document the parameters for this function: the same parameters as
602 passed to :meth:`_pre_processing()`.
603
604 Returns
605 -------
606 NDArray[Shape[:attr:`state_shape`], complex]
607 The final state
608
609 See Also
610 --------
611 * :meth:`propagate_collection()`
612 * :meth:`propagate_all()`
613 * :meth:`get_evolution()`
614 """
615 ctrl_amp, initial_state, dt = self.get_driving_pulses(*args)
616 return self.evolver.propagate(ctrl_amp, initial_state, dt)
617 def propagate_collection(self, *args) -> np.ndarray[complex]:
618 """
619 Evolves a collection of state vectors under the time-dependent
620 Hamiltonian defined by the control amplitudes using
621 :meth:`~py_ste.evolvers.DenseUnitaryEvolver.propagate_collection()`
622 from `PySTE <https://PySTE.readthedocs.io>`__.
623
624 Parameters
625 ----------
626 *args
627 The placeholder parameters. See ``_systems.pyi`` for actual
628 parameters. Each child class that implements a new
629 :meth:`_pre_processing()` should implement a ``.pyi`` file to
630 document the parameters for this function: the same parameters as
631 passed to :meth:`_pre_processing()`.
632
633 Returns
634 -------
635 NDArray[Shape[n_states, :attr:`state_shape`], complex]
636 The final state
637
638 See Also
639 --------
640 * :meth:`propagate()`
641 * :meth:`propagate_all()`
642 * :meth:`get_evolution()`
643 """
644 ctrl_amp, initial_state, dt = self.get_driving_pulses(*args)
645 return self.evolver.propagate_collection(ctrl_amp, initial_state, dt)
646 def propagate_all(self, *args) -> np.ndarray[complex]:
647 """
648 Evolves a state vector under the time-dependent Hamiltonian defined by
649 the control amplitudes using
650 :meth:`~py_ste.evolvers.DenseUnitaryEvolver.propagate_all()`
651 from `PySTE <https://PySTE.readthedocs.io>`__ and returns the state at
652 each time-step.
653
654 Parameters
655 ----------
656 *args
657 The placeholder parameters. See ``_systems.pyi`` for actual
658 parameters. Each child class that implements a new
659 :meth:`_pre_processing()` should implement a ``.pyi`` file to
660 document the parameters for this function: the same parameters as
661 passed to :meth:`_pre_processing()`.
662
663 Returns
664 -------
665 NDArray[Shape[:attr:`state_shape`, n_time_steps+1], complex]
666 The state at each integrator time step (including the initial
667 state).
668
669 See Also
670 --------
671 * :meth:`propagate()`
672 * :meth:`propagate_collection()`
673 * :meth:`get_evolution()`
674 """
675 ctrl_amp, initial_state, dt = self.get_driving_pulses(*args)
676 return self.evolver.propagate_all(ctrl_amp, initial_state, dt)
677 def evolved_expectation_value(self, *args) -> complex:
678 """
679 Evolves a state vector under the time-dependent Hamiltonian defined by
680 the control amplitudes and computes the expectation value of a specified
681 observable with respect to the final state using
682 :meth:`~py_ste.evolvers.DenseUnitaryEvolver.evolved_expectation_value()`
683 from `PySTE <https://PySTE.readthedocs.io>`__.
684
685 Parameters
686 ----------
687 ``*args[:-1]``
688 The placeholder parameters. See ``_systems.pyi`` for actual
689 parameters. Each child class that implements a new
690 :meth:`_pre_processing()` should implement a ``.pyi`` file to
691 document the parameters for this function: the same parameters as
692 passed to :meth:`_pre_processing()`.
693 ``args[-1]`` : NDArray[Shape[:attr:`dim`, :attr:`dim`], complex]
694 The observable to take the expectation value of.
695
696
697 Returns
698 -------
699 complex
700 The expectation value.
701
702 See Also
703 --------
704 * :meth:`evolved_expectation_value_all()`
705 * :meth:`gradient()`
706 """
707 observable: np.ndarray[complex] = args[-1]
708 ctrl_amp, initial_state, dt = self.get_driving_pulses(*args[:-1])
709 return self.evolver.evolved_expectation_value(ctrl_amp,
710 initial_state,
711 dt,
712 observable)
713 def evolved_expectation_value_all(self, *args) -> np.ndarray[complex]:
714 """
715 Evolves a state vector under the time-dependent Hamiltonian defined by
716 the control amplitudes and computes the expectation value of a specified
717 observable with respect to the state at each time-step using
718 :meth:`~py_ste.evolvers.DenseUnitaryEvolver.evolved_expectation_value_all()`
719 from `PySTE <https://PySTE.readthedocs.io>`__.
720
721 Parameters
722 ----------
723 ``*args[:-1]``
724 The placeholder parameters. See ``_systems.pyi`` for actual
725 parameters. Each child class that implements a new
726 :meth:`_pre_processing()` should implement a ``.pyi`` file to
727 document the parameters for this function: the same parameters as
728 passed to :meth:`_pre_processing()`.
729 ``args[-1]`` : NDArray[Shape[:attr:`dim`, :attr:`dim`], complex]
730 The observable to take the expectation value of.
731
732 Returns
733 -------
734 NDArray[Shape[n_time_steps+1], complex]
735 The expectation value of the state at each integrator time step
736 (including the initial state) with respect to the observable.
737
738 See Also
739 --------
740 * :meth:`evolved_expectation_value()`
741 * :meth:`gradient()`
742 """
743 observable: np.ndarray[complex] = args[-1]
744 ctrl_amp, initial_state, dt = self.get_driving_pulses(*args[:-1])
745 return self.evolver.evolved_expectation_value_all(ctrl_amp,
746 initial_state,
747 dt,
748 observable)
749 def evolved_inner_product(self, *args) -> complex:
750 """
751 Evolves a state vector under the time-dependent Hamiltonian defined by
752 the control amplitudes and computes the inner product of the final state
753 vector with a fixed vector using
754 :meth:`~py_ste.evolvers.DenseUnitaryEvolver.evolved_inner_product()`
755 from `PySTE <https://PySTE.readthedocs.io>`__.
756
757 Parameters
758 ----------
759 ``*args[:-1]``
760 The placeholder parameters. See ``_systems.pyi`` for actual
761 parameters. Each child class that implements a new
762 :meth:`_pre_processing()` should implement a ``.pyi`` file to
763 document the parameters for this function: the same parameters as
764 passed to :meth:`_pre_processing()`.
765 ``args[-1]`` : NDArray[Shape[:attr:`dim`], complex]
766 The fixed vector to calculate the inner product with.
767
768 Returns
769 -------
770 complex
771 The inner product of the evolved state vector with the fixed vector.
772
773 See Also
774 --------
775 * :meth:`evolved_inner_product_all()`
776 """
777 fixed_vector: np.ndarray[complex] = args[-1]
778 ctrl_amp, initial_state, dt = self.get_driving_pulses(*args[:-1])
779 return self.evolver.evolved_inner_product(ctrl_amp,
780 initial_state,
781 dt,
782 fixed_vector)
783 def evolved_inner_product_all(self, *args) -> complex:
784 """
785 Evolves a state vector under the time-dependent Hamiltonian defined by
786 the control amplitudes and computes the innper product of state at each
787 time-step with a fixed vector using
788 :meth:`~py_ste.evolvers.DenseUnitaryEvolver.evolved_inner_product_all()`
789 from `PySTE <https://PySTE.readthedocs.io>`__.
790
791 Parameters
792 ----------
793 ``*args[:-1]``
794 The placeholder parameters. See ``_systems.pyi`` for actual
795 parameters. Each child class that implements a new
796 :meth:`_pre_processing()` should implement a ``.pyi`` file to
797 document the parameters for this function: the same parameters as
798 passed to :meth:`_pre_processing()`.
799 ``args[-1]`` : NDArray[Shape[:attr:`dim`], complex]
800 The fixed vector to calculate the inner product with.
801
802 Returns
803 -------
804 NDArray[Shape[n_time_steps+1], complex]
805 The inner product of state at each integrator time step (including
806 the initial state) with the fixed vector.
807
808 See Also
809 --------
810 * :meth:`evolved_inner_product()`
811 """
812 fixed_vector: np.ndarray[complex] = args[-1]
813 ctrl_amp, initial_state, dt = self.get_driving_pulses(*args[:-1])
814 return self.evolver.evolved_inner_product_all(ctrl_amp,
815 initial_state,
816 dt,
817 fixed_vector)
818 def get_evolution(self, *args) -> np.ndarray[complex]:
819 """
820 Computes the unitary evolution of the system under the time-dependent
821 Hamiltonian defined by the control amplitudes using
822 :meth:`~py_ste.evolvers.DenseUnitaryEvolver.get_evolution()`
823 from `PySTE <https://PySTE.readthedocs.io>`__.
824
825 Parameters
826 ----------
827 *args
828 The placeholder parameters. See ``_systems.pyi`` for actual
829 parameters. Each child class that implements a new
830 :meth:`_pre_processing()` should implement a ``.pyi`` file to
831 document the parameters for this function: the same parameters as
832 passed to :meth:`_pre_processing()`.
833
834 Returns
835 -------
836 NDArray[Shape[:attr:`dim`, :attr:`dim`], complex]
837 The unitary corresponding to the evolution of the system.
838
839 See Also
840 --------
841 * :meth:`propagate()`
842 * :meth:`propagate_collection()`
843 * :meth:`propagate_all()`
844 """
845 ctrl_amp, _, dt = self.get_driving_pulses(*args)
846 return self.evolver.get_evolution(ctrl_amp, dt)
847 def evolved_gate_infidelity(self, *args) -> float:
848 """
849 Evolves the system under the time-dependent Hamiltonian defined by
850 the control amplitudes and computes the gate infidelity to the target
851 gate using
852 :meth:`~py_ste.evolvers.DenseUnitaryEvolver.evolved_gate_infidelity()`
853 from `PySTE <https://PySTE.readthedocs.io>`__.
854
855 Parameters
856 ----------
857 ``*args[:-1]``
858 The placeholder parameters. See ``_systems.pyi`` for actual
859 parameters. Each child class that implements a new
860 :meth:`_pre_processing()` should implement a ``.pyi`` file to
861 document the parameters for this function: the same parameters as
862 passed to :meth:`_pre_processing()`.
863 ``args[-1]`` : NDArray[Shape[:attr:`dim`, :attr:`dim`], complex]
864 The target gate to calculate the gate infidelity with respect to.
865
866 Returns
867 -------
868 float
869 The gate infidelity
870
871 See Also
872 --------
873 * :meth:`gate_gradient()`
874 """
875 target: np.ndarray[complex] = args[-1]
876 ctrl_amp, _, dt = self.get_driving_pulses(*args[:-1])
877 return self.evolver.evolved_gate_infidelity(ctrl_amp, dt, target)
878 def get_driving_pulses(self, *args) -> tuple[np.ndarray[complex], np.ndarray[complex], float]:
879 """
880 When calling any evolution method (listed in the
881 :ref:`See also section <get_driving_pulses_see_also>`) :meth:`get_driving_pulses()`
882 is executed on the arguements before the evolution method.
883
884 Parameters
885 ----------
886 *args
887 The placeholder parameters. See ``_systems.pyi`` for actual
888 parameters. Each child class that implements a new
889 :meth:`_pre_processing()` should implement a ``.pyi`` file to
890 document the parameters for this function: the same parameters as
891 passed to :meth:`_pre_processing()`.
892
893 Returns
894 -------
895 tuple[NDArray[Shape[n_time_steps, :attr:`n_ctrl`], complex], NDArray[Shape[:attr:`state_shape`], complex], float]
896 A tuple of:
897 1. Control amplitudes
898 2. Initial state
899 3. Integrator time step
900
901
902 .. _get_driving_pulses_see_also:
903
904 See Also
905 --------
906 * :meth:`propagate()`
907 * :meth:`propagate_collection()`
908 * :meth:`propagate_all()`
909 * :meth:`get_evolution()`
910 * :meth:`evolved_expectation_value()`
911 * :meth:`evolved_expectation_value_all()`
912 * :meth:`evolved_inner_product()`
913 * :meth:`evolved_inner_product_all()`
914 * :meth:`evolved_gate_infidelity()`
915 * :meth:`gradient()`
916 * :meth:`gate_gradient()`
917 """
918 ctrl_amp, initial_state, dt = self._processing(*args)
919 try: ctrl_amp: np.ndarray[complex] = ctrl_amp.numpy()
920 except: pass
921 try: initial_state: np.ndarray[complex] = initial_state.numpy().flatten()
922 except: pass
923 try: dt = dt.numpy()
924 except: pass
925 dt = float(dt.real)
926 return ctrl_amp, initial_state, dt
927 def _eager_processing(self, *args) -> tuple:
928 """
929 Executes :meth:`_pre_processing()` followed by
930 :meth:`_envolope_processing()` eagerly (i.e. without using a
931 `TensorFlow <https://www.tensorflow.org>`__ graph). Nonetheless,
932 :meth:`_eager_processing()` is still auto differentiable.
933
934 Parameters
935 ----------
936 *args
937 The placeholder parameters. See ``_systems.pyi`` for actual
938 parameters. Each child class that implements a new
939 :meth:`_pre_processing()` should implement a ``.pyi`` file to
940 document the parameters for this function: the same parameters as
941 passed to :meth:`_pre_processing()`.
942
943 Returns
944 -------
945 tuple[tf.Tensor[Shape[n_time_steps, :attr:`n_ctrl`], complex], tf.Tensor[Shape[:attr:`state_shape`], complex], tf.Tensor[Shape[], float]]
946 A tuple of:
947 1. Control amplitudes
948 2. Initial state
949 3. Integrator time step
950 """
951 ctrl_amp, initial_state, dt, frequencies, number_channels = self._pre_processing(*args)
952 return self._envolope_processing(ctrl_amp, dt, frequencies, list(number_channels)), initial_state, dt
953 def _traceable_eager_processing(self, *args) -> tuple:
954 """
955 A function that will be traced by
956 `TensorFlow <https://www.tensorflow.org>`__ to produce a graph of
957 :meth:`_pre_processing()` followed by :meth:`_envolope_processing()`.
958
959 Parameters
960 ----------
961 *args
962 The placeholder parameters. See ``_systems.pyi`` for actual
963 parameters. Each child class that implements a new
964 :meth:`_pre_processing()` should implement a ``.pyi`` file to
965 document the parameters for this function: the same parameters as
966 passed to :meth:`_pre_processing()`.
967
968 Returns
969 -------
970 tuple[tf.Tensor[Shape[n_time_steps, :attr:`n_ctrl`], complex], tf.Tensor[Shape[:attr:`state_shape`], complex], tf.Tensor[Shape[], float]]
971 A tuple of:
972 1. Control amplitudes
973 2. Initial state
974 3. Integrator time step
975 """
976 print("Tracing control amplitude graph.")
977 return self._eager_processing(*args)
978 def gradient(self, *args) -> tuple[float, np.ndarray[float]]:
979 """
980 Evolves a state vector under the time-dependent Hamiltonian defined by
981 the control amplitudes and computes the expectation value of a specified
982 observable with respect to the final state and then computes the
983 gradient of the final state with respect to the first argument
984 (``args[0]``) using
985 :meth:`~py_ste.evolvers.DenseUnitaryEvolver.switching_function()`
986 from `PySTE <https://PySTE.readthedocs.io>`__.
987
988 Parameters
989 ----------
990 ``*args[:-1]``
991 The placeholder parameters. See ``_systems.pyi`` for actual
992 parameters. Each child class that implements a new
993 :meth:`_pre_processing()` should implement a ``.pyi`` file to
994 document the parameters for this function: the same parameters as
995 passed to :meth:`_pre_processing()`.
996 ``args[-1]`` : NDArray[Shape[:attr:`dim`, :attr:`dim`], complex]
997 The observable to take the expectation value of.
998
999 Returns
1000 -------
1001 tuple[complex, NDArray[Shape[n_parameters], float]]
1002 A tuple of the expectation value and the gradient.
1003
1004 See Also
1005 --------
1006 * :meth:`evolved_expectation_value()`
1007 * :meth:`evolved_expectation_value_all()`
1008 * :meth:`gate_gradient()`
1009 """
1010 cost: np.ndarray[complex] = args[-1]
1011 args = list(args[:-1])
1012 args[0] = tf.constant(args[0], dtype=tf.float64)
1013 with tf.GradientTape(persistent=False,
1014 watch_accessed_variables=False
1015 ) as tape:
1016 tape.watch(args[0])
1017 ctrl_amp, initial_state, dt = self._processing(*args)
1018 try: initial_state: np.ndarray[complex] = initial_state.numpy().flatten()
1019 except: pass
1020 try: dt = dt.numpy()
1021 except: pass
1022 dt = float(dt.real)
1023 E = ExpValCustom(self, initial_state, dt, cost).run(ctrl_amp)
1024 grad = tape.gradient(E, args[0])
1025 del tape
1026
1027 grad = tf.convert_to_tensor(grad)
1028 grad: np.ndarray[float] = grad.numpy()
1029 grad = grad.real
1030 return E.numpy(), grad
1031 def gate_gradient(self, *args) -> tuple[float, np.ndarray[float]]:
1032 """
1033 Evolves the system under the time-dependent Hamiltonian defined by
1034 the control amplitudes and computes the gate infidelity to the target
1035 gate and then computes the gradient with respect to the first argument
1036 (``args[0]``) using
1037 :meth:`~py_ste.evolvers.DenseUnitaryEvolver.gate_switching_function()`
1038 from `PySTE <https://PySTE.readthedocs.io>`__.
1039
1040 Parameters
1041 ----------
1042 ``*args[:-1]``
1043 The placeholder parameters. See ``_systems.pyi`` for actual
1044 parameters. Each child class that implements a new
1045 :meth:`_pre_processing()` should implement a ``.pyi`` file to
1046 document the parameters for this function: the same parameters as
1047 passed to :meth:`_pre_processing()`.
1048 ``args[-1]`` : NDArray[Shape[:attr:`dim`, :attr:`dim`], complex]
1049 The target gate to calculate the gate infidelity with respect to.
1050
1051 Returns
1052 -------
1053 tuple[complex, NDArray[Shape[n_parameters], float]]
1054 A tuple of the gate infidelity value and the gradient.
1055
1056 See Also
1057 --------
1058 * :meth:`evolved_gate_infidelity()`
1059 * :meth:`gradient()`
1060 """
1061 target: np.ndarray[complex] = args[-1]
1062 args = list(args[:-1])
1063 args[0] = tf.constant(args[0], dtype=tf.float64)
1064 with tf.GradientTape(persistent=False,
1065 watch_accessed_variables=False
1066 ) as tape:
1067 tape.watch(args[0])
1068 ctrl_amp, _, dt = self._processing(*args)
1069 try: dt: float = dt.numpy()
1070 except: pass
1071 dt = float(dt.real)
1072 infidelity = GateInfidelityCustom(self, dt, target).run(ctrl_amp)
1073 grad = tape.gradient(infidelity, args[0])
1074 del tape
1075
1076 grad = tf.convert_to_tensor(grad)
1077 grad: np.ndarray[float] = grad.numpy()
1078 grad = grad.real
1079 return infidelity.numpy(), grad
1080 def pulse_form(self,
1081 pulse_function: Callable,
1082 append: bool = False,
1083 ) -> "PulseForm":
1084 """
1085 Initialises a new :class:`QuantumSystem` in which
1086 :meth:`_pre_processing()` corresponds to executing ``pulse_function()``
1087 and piping the output into the previous definition of
1088 :meth:`_pre_processing()`.
1089
1090 Parameters
1091 ----------
1092 pulse_function : Callable
1093 The function to compose with :meth:`_pre_processing()`.
1094 append : bool, optional
1095 Whether to prepend
1096 (``self._pre_processing(*pulse_function())``) or append
1097 (``pulse_function(*self._pre_processing())``)
1098 ``pulse_function``. By default ``False``.
1099
1100 Returns
1101 -------
1102 PulseForm
1103 The new :class:`QuantumSystem`
1104 """
1105 return PulseForm(self, pulse_function, append)
1106
1107class TransformedSystem(QuantumSystem):
1108 """
1109 A base class for representing a transformation on a :class:`qugrad.QuantumSystem`.
1110 """
1111
1112 _original_system: QuantumSystem
1113 "The system that was transformed into this system"
1114
1115 _base_system: QuantumSystem
1116 """
1117 The system before any transformations were applied. That is `_base_system`
1118 is the recursive :attr:`original_system`
1119 (``original_system.original_system.original_system....``) until
1120 :attr:`original_system` is no longer a :class:`TransformedSystem`.
1121 """
1122
1123 def __init__(self,
1124 original_system: QuantumSystem,
1125 H0: np.ndarray[complex],
1126 Hs: Union[np.ndarray[complex], np.ndarray[complex]],
1127 hilbert_space: HilbertSpace):
1128 """
1129 Performs a transformation on a :class:`qugrad.QuantumSystem`.
1130
1131 Parameters
1132 ----------
1133 original_system: QuantumSystem
1134 The system to be transformed into this system
1135 H0: NDArray[Shape[:attr:`dim`, :attr:`dim`], complex]
1136 The new drift Hamiltonian
1137 Hs: NDArray[Shape[":attr:`n_ctrl`, :attr:`dim`, :attr:`dim`"], complex] | NDArray[Shape[:attr:`n_ctrl` * :attr:`dim`, :attr:`dim`], complex]
1138 The new control Hamiltonians either as an array of control
1139 Hamiltonians or the control Hamiltonians stacked along the first
1140 axis.
1141 hilbert_space: HilbertSpace
1142 The new Hilbert space of the system
1143 """
1144 self._original_system = original_system
1145 if isinstance(original_system, TransformedSystem):
1146 self._base_system = original_system._base_system
1147 else:
1148 self._base_system = original_system
1149 super().__init__(H0, Hs, hilbert_space, self._base_system.using_graph)
1150 @property
1151 def original_system(self) -> QuantumSystem:
1152 "The system that was transformed into this system"
1153 return self._original_system
1154 @property
1155 def base_system(self) -> QuantumSystem:
1156 """
1157 The system before any transformations were applied. That is
1158 :attr:`base_system` is the recursive :attr:`original_system`
1159 (``original_system.original_system.original_system....``) until
1160 :attr:`original_system` is no longer a :class:`TransformedSystem`.
1161 """
1162 return self._base_system
1163 def _pre_processing(self, *args) -> tuple:
1164 """
1165 When calling any evolution method (listed in the
1166 :ref:`See also section <TransformedSystem_pre_processing_see_also>` section)
1167 :meth:`_pre_processing()` is executed on the arguements before the
1168 control amplitudes are modulated by the frequencies (during
1169 :meth:`_envolope_processing()`) and then finally the modulated control
1170 amplitudes are used by the evolution method.
1171
1172 This is a placeholder for ``original_system._pre_processing()``.
1173
1174 Parameters
1175 ----------
1176 *args
1177 The placeholder parameters. See ``_systems.pyi`` for actual
1178 parameters. Each child class that implements a new
1179 :meth:`_pre_processing()` should implement a ``.pyi`` file to
1180 document the parameters for this function: the same parameters as
1181 passed to :meth:`_pre_processing()`.
1182
1183 Returns
1184 -------
1185 tuple[tf.Tensor[Shape[n_time_steps, total_n_channels], tf.complex128], tf.Tensor[Shape[:attr:`state_shape`], tf.complex128], float, tf.Tensor[Shape[n_time_steps, total_n_channels], tf.complex128], list[int]]
1186 A tuple of
1187 1. The control amplitude envolopes
1188 2. The initial state
1189 3. The integrator time step
1190 4. The frequencies to modulate the control amplitude envolopes with
1191 5. A list of the number of channels for each control Hamiltonian
1192
1193 Warning
1194 -------
1195 The number of channels for each control Hamiltonian must be stored
1196 as a ``list`` and not an ``NDArray`` or a
1197 `TensorFlow <https://www.tensorflow.org>`__ tensor.
1198
1199
1200 .. _TransformedSystem_pre_processing_see_also:
1201
1202 See Also
1203 --------
1204 * :meth:`propagate()`
1205 * :meth:`propagate_collection()`
1206 * :meth:`propagate_all()`
1207 * :meth:`evolved_expectation_value()`
1208 * :meth:`evolved_expectation_value_all()`
1209 * :meth:`get_driving_pulses()`
1210 * :meth:`gradient()`
1211 """
1212 return self._original_system._pre_processing(*args)
1213 def _envolope_processing(self,
1214 ctrl_amp,
1215 dt: float,
1216 frequencies,
1217 number_channels: list[int]):
1218 """
1219 When calling any evolution method (listed in the
1220 :ref:`See also section <envolope_processing_see_also>` section) :meth:`_pre_processing()`
1221 is executed on the arguements before the control amplitudes are
1222 modulated by the frequencies during :meth:`_envolope_processing()` and
1223 then finally the modulated control amplitudes are used by the evolution
1224 method.
1225
1226 Parameters
1227 ----------
1228 ctrl_amp : tf.Tensor[Shape[n_time_steps, total_n_channels], tf.complex128]
1229 The envolope control amplitudes
1230 dt : float
1231 The itegration time step
1232 frequencies : tf.Tensor[Shape[n_time_steps, total_n_channels], tf.complex128]
1233 The frequencies to modulate the control amplitudes with
1234 number_channels : list[int]
1235 The number of channels associated with each control Hamiltonian
1236
1237 Warning
1238 -------
1239 This must be a ``list`` and not an ``NDArray`` or a
1240 `TensorFlow <https://www.tensorflow.org>`__ tensor.
1241
1242 Returns
1243 -------
1244 tf.Tensor[Shape[n_time_steps,:attr:`n_ctrl`], tf.complex128]
1245 The modulated control amplitudes
1246
1247
1248 .. _envolope_processing_see_also:
1249
1250 See Also
1251 --------
1252 * :meth:`propagate()`
1253 * :meth:`propagate_collection()`
1254 * :meth:`propagate_all()`
1255 * :meth:`evolved_expectation_value()`
1256 * :meth:`evolved_expectation_value_all()`
1257 * :meth:`get_driving_pulses()`
1258 * :meth:`gradient()`
1259 """
1260 return self._original_system._envolope_processing(ctrl_amp,
1261 dt,
1262 frequencies,
1263 number_channels)
1264
1265class PulseForm(TransformedSystem):
1266 """
1267 A transformed :class:`qugrad.QuantumSystem` in which :meth:`_pre_processing()`
1268 has been composed with another pre processing function.
1269 """
1270
1271 _pulse_function: Callable
1272 "The function composed with ``original_system._pre_processing()``"
1273
1274 _appended: bool
1275 """
1276 Whether the :attr:`pulse_function` was prepended
1277 (``original_system._pre_processing(*pulse_function())``) or appended
1278 (``pulse_function(*original_system._pre_processing())``)
1279 """
1280
1281 def __init__(self,
1282 original_system: QuantumSystem,
1283 pulse_function: Callable,
1284 append: bool = False):
1285 """
1286 Initialises a new :class:`qugrad.QuantumSystem` in which :meth:`_pre_processing()`
1287 corresponds to running ``pulse_function()`` and piping the output into
1288 ``original_system._pre_processing()``.
1289
1290 Parameters
1291 ----------
1292 original_system : QuantumSystem
1293 The system that was transformed into this system
1294 pulse_function : Callable
1295 The function to compose with :meth:`_pre_processing()`.
1296 append : bool
1297 Whether to prepend
1298 (``original_system._pre_processing(*pulse_function())``) or append
1299 (``pulse_function(*original_system._pre_processing())``)
1300 ``pulse_function``. By default ``False``.
1301 """
1302 super().__init__(original_system,
1303 original_system.H0,
1304 original_system.Hs,
1305 original_system.hilbert_space)
1306 self._evolver = original_system._evolver
1307 if append:
1308 self._pre_processing = compose_unpack(
1309 pulse_function,
1310 original_system._pre_processing
1311 )
1312 else:
1313 self._pre_processing = compose_unpack(
1314 original_system._pre_processing,
1315 pulse_function
1316 )
1317 self._pulse_function = pulse_function
1318 self._appended = append
1319 @property
1320 def pulse_function(self) -> Callable:
1321 "The function composed with ``original_system._pre_processing()``"
1322 return self._pulse_function
1323 @property
1324 def appended(self) -> bool:
1325 """
1326 Whether the :attr:`pulse_function` was prepended
1327 (``original_system._pre_processing(*pulse_function())``) or appended
1328 (``pulse_function(*original_system._pre_processing())``)
1329 """
1330 return self._appended