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