Pulse Forms¶
We saw in the quick start section that we can use pulse_form() to re-parameterise the control amplitudes. Alternatively, we can subclass QuantumSystem. If we subclass QuantumSystem and override _pre_processing() we should also create a .pyi stub file file to document the new interfaces for:
_eager_processing()_traceable_eager_processing()
That is we should update the arguments in the docstrings to correspond to those of the new _pre_processing() function.
Below is an example subclass followed the the stub file:
# examples/subclass.py
import numpy as np
import tensorflow as tf
from functools import reduce
from qugrad import QuantumSystem, HilbertSpace
def kron(*args) -> np.ndarray:
"""
Kronecker product of multiple matrices.
Parameters
----------
args: list[NDArray]
The matrices to take the Kronecker product of
Returns
-------
NDArray
The Kronecker product of the matrices
"""
return reduce(np.kron, args)
class ExampleSubclass(QuantumSystem):
"""
An example subclass of QuantumSystem.
"""
def __init__(self,
qubits: int,
use_graph: bool = True):
"""
Initialises the ExampleSubclass.
Parameters
----------
qubits: int
The number of qubits in the system
use_graph: bool
Whether to use TensorFlow graphs during computation.
"""
self.qubits = qubits
X = np.array([[0, 1], # Pauli-X
[1, 0]])
Z = np.array([[1, 0], # Pauli-Z
[0, -1]])
H0 = kron(*[Z]*qubits)
Xn = lambda n: kron(np.identity(2**(n-1)),
X,
np.identity(2**(qubits-n)))
Hs = [Xn(n) for n in range(1, qubits+1)]
super().__init__(H0,
Hs,
HilbertSpace(np.arange(2**qubits)),
use_graph)
def _pre_processing(self,
frequencies: np.ndarray,
amplitudes: np.ndarray,
T: float
) -> tuple:
"""
When calling any evolution method (listed in the See also section)
`_pre_processing()` is executed on the arguements before the control
amplitudes are modulated by the frequencies (during
`_envolope_processing()`) and then finally the modulated control
amplitudes are used by the evolution method.
Parameters
----------
frequencies: NDArray[Shape[`qubits`], float]
The frequencies of the to drive X on each of the qubits
amplitudes: NDArray[Shape[`qubits`], complex]
The amplitude of to drive X on each of the qubits
T: float
The time to evolve the system for
Returns
-------
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]]
A tuple of
1. The control amplitude envolopes
2. The initial state
3. The integrator time step
4. The frequencies to modulate the control amplitude envolopes with
5. A list of the number of channels for each control Hamiltonian
Warning
-------
The number of channels for each control Hamiltonian must be stored
as a ``list`` and not an ``NDArray`` or a
`TensorFlow <https://www.tensorflow.org>`__ tensor.
See Also
--------
* `propagate()`
* `propagate_collection()`
* `propagate_all()`
* `evolved_expectation_value()`
* `evolved_expectation_value_all()`
* `get_driving_pulses()`
* `gradient()`
"""
amplitudes = tf.cast(amplitudes, tf.complex128)
amplitudes = tf.reshape(amplitudes, (1, self.qubits))
n_time_steps = tf.cast(tf.math.ceil(T)*100, tf.int32)
ctrl_amp = tf.broadcast_to(amplitudes, tf.stack([n_time_steps, self.qubits], axis=0))
initial_state = self.hilbert_space.basis_vector(0)
dt = T/tf.cast(n_time_steps, tf.float64)
frequencies = tf.reshape(tf.cast(frequencies, dtype=tf.complex128), (self.qubits,))
number_channels = [1]*self.qubits
return super()._pre_processing(ctrl_amp, initial_state, dt, frequencies, number_channels)
The corresponding stub file:
# examples/subclass.pyi
import numpy as np
from qugrad import QuantumSystem
from typing import Callable
def kron(*args) -> np.ndarray:
"""
Kronecker product of multiple matrices.
Parameters
----------
args: list[NDArray]
The matrices to take the Kronecker product of
Returns
-------
NDArray
The Kronecker product of the matrices
"""
...
class ExampleSubclass(QuantumSystem):
"""
An example subclass of QuantumSystem.
"""
_processing: Callable[..., tuple]
"""
Executes `_pre_processing()` followed by
`_envolope_processing()` eagerly (i.e. without using a TensorFlow
graph). Nonetheless, `_eager_processing()` is still auto
differentiable.
Parameters
----------
frequencies: NDArray[Shape[`qubits`], float]
The frequencies of the to drive X on each of the qubits
amplitudes: NDArray[Shape[`qubits`], complex]
The amplitude of to drive X on each of the qubits
T: float
The time to evolve the system for
Returns
-------
tuple[tf.Tensor[Shape[n_time_steps, `n_ctrl`], complex], tf.Tensor[Shape[`state_shape`], complex], tf.Tensor[Shape[], float]]
A tuple of:
1. Control amplitudes
2. Initial state
3. Integrator time step
"""
def __init__(self,
qubits: int,
use_graph: bool = True):
"""
Initialises the ExampleSubclass.
Parameters
----------
qubits: int
The number of qubits in the system
use_graph: bool
Whether to use TensorFlow graphs during computation.
"""
...
def _pre_processing(self,
frequencies: np.ndarray,
amplitudes: np.ndarray,
T: float
) -> tuple:
"""
When calling any evolution method (listed in the See also section)
`_pre_processing()` is executed on the arguements before the control
amplitudes are modulated by the frequencies (during
`_envolope_processing()`) and then finally the modulated control
amplitudes are used by the evolution method.
Parameters
----------
frequencies: NDArray[Shape[`qubits`], float]
The frequencies of the to drive X on each of the qubits
amplitudes: NDArray[Shape[`qubits`], complex]
The amplitude of to drive X on each of the qubits
T: float
The time to evolve the system for
Returns
-------
tuple[tf.Tensor[Shape[n_time_steps, total_n_channels], tf.complex128], tf.Tensor[Shape[`state_shape`], tf.complex128], float, tf.Tensor[Shape[n_time_steps, total_n_channels], tf.complex128], list[int]]
A tuple of
1. The control amplitude envolopes
2. The initial state
3. The integrator time step
4. The frequencies to modulate the control amplitude envolopes with
5. A list of the number of channels for each control Hamiltonian
Warning
-------
The number of channels for each control Hamiltonian must be stored
as a ``list`` and not an ``NDArray`` or a
`TensorFlow <https://www.tensorflow.org>`__ tensor.
See Also
--------
* `propagate()`
* `propagate_collection()`
* `propagate_all()`
* `evolved_expectation_value()`
* `evolved_expectation_value_all()`
* `get_driving_pulses()`
* `gradient()`
"""
...
def propagate(self,
ctrl_amp : np.ndarray[complex],
initial_state : np.ndarray[complex],
dt: float,
frequencies: np.ndarray[complex],
number_channels: list[int]
) -> np.ndarray[complex]:
"""
Evolves a state vector under the time-dependent Hamiltonian defined by
the control amplitudes using `propagate()` from
`PySTE <https://PySTE.readthedocs.io>`__.
Parameters
----------
frequencies: NDArray[Shape[`qubits`], float]
The frequencies of the to drive X on each of the qubits
amplitudes: NDArray[Shape[`qubits`], complex]
The amplitude of to drive X on each of the qubits
T: float
The time to evolve the system for
Warning
-------
Keyword arguments are not supported.
Returns
-------
NDArray[Shape[`state_shape`], complex]
The final state
See Also
--------
* `propagate_collection()`
* `propagate_all()`
"""
...
def propagate_collection(self,
ctrl_amp : np.ndarray[complex],
initial_states : np.ndarray[complex],
dt: float,
frequencies: np.ndarray[complex],
number_channels: list[int]
) -> np.ndarray[complex]:
"""
Evolves a collection of state vectors under the time-dependent
Hamiltonian defined by the control amplitudes using
`propagate_collection()` from `PySTE <https://PySTE.readthedocs.io>`__.
Parameters
----------
frequencies: NDArray[Shape[`qubits`], float]
The frequencies of the to drive X on each of the qubits
amplitudes: NDArray[Shape[`qubits`], complex]
The amplitude of to drive X on each of the qubits
T: float
The time to evolve the system for
Warning
-------
Keyword arguments are not supported.
Returns
-------
NDArray[Shape[n_states, `state_shape`], complex]
The final state
See Also
--------
* `propagate()`
* `propagate_all()`
"""
...
def propagate_all(self,
ctrl_amp : np.ndarray[complex],
initial_states : np.ndarray[complex],
dt: float,
frequencies: np.ndarray[complex],
number_channels: list[int]
) -> np.ndarray[complex]:
"""
Evolves a state vector under the time-dependent Hamiltonian defined by
the control amplitudes using `propagate_all()` from
`PySTE <https://PySTE.readthedocs.io>`__ and returns the state at each
time-step.
Parameters
----------
frequencies: NDArray[Shape[`qubits`], float]
The frequencies of the to drive X on each of the qubits
amplitudes: NDArray[Shape[`qubits`], complex]
The amplitude of to drive X on each of the qubits
T: float
The time to evolve the system for
Warning
-------
Keyword arguments are not supported.
Returns
-------
NDArray[Shape[n_time_steps+1, `state_shape`], complex]
The state at each integrator time step (including the initial
state).
See Also
--------
* `propagate()`
* `propagate_collection()`
"""
...
def evolved_expectation_value(self,
ctrl_amp : np.ndarray[complex],
initial_state : np.ndarray[complex],
dt: float,
frequencies: np.ndarray[complex],
number_channels: list[int],
observable : np.ndarray[complex]
) -> complex:
"""
Evolves a state vector under the time-dependent Hamiltonian defined by
the control amplitudes and computes the expectation value of a specified
observable with respect to the final state using
`evolved_expectation_value()` from
`PySTE <https://PySTE.readthedocs.io>`__.
Parameters
----------
frequencies: NDArray[Shape[`qubits`], float]
The frequencies of the to drive X on each of the qubits
amplitudes: NDArray[Shape[`qubits`], complex]
The amplitude of to drive X on each of the qubits
T: float
The time to evolve the system for
observable : NDArray[Shape[`dim`, `dim`], complex]
The observable to take the expectation value of.
Warning
-------
Keyword arguments are not supported.
Returns
-------
complex
The expectation value.
See Also
--------
* `evolved_expectation_value_all()`
* `gradient()`
"""
...
def evolved_expectation_value_all(self,
ctrl_amp : np.ndarray[complex],
initial_state : np.ndarray[complex],
dt: float,
frequencies: np.ndarray[complex],
number_channels: list[int],
observable : np.ndarray[complex]
) -> np.ndarray[complex]:
"""
Evolves a state vector under the time-dependent Hamiltonian defined by
the control amplitudes and computes the expectation value of a specified
observable with respect to the state at each time-step using
`evolved_expectation_value_all()` from
`PySTE <https://PySTE.readthedocs.io>`__.
Parameters
----------
frequencies: NDArray[Shape[`qubits`], float]
The frequencies of the to drive X on each of the qubits
amplitudes: NDArray[Shape[`qubits`], complex]
The amplitude of to drive X on each of the qubits
T: float
The time to evolve the system for
observable : NDArray[Shape[`dim`, `dim`], complex]
The observable to take the expectation value of.
Warning
-------
Keyword arguments are not supported.
Returns
-------
NDArray[Shape[n_time_steps+1], complex]
The state at each integrator time step (including the initial
state).
See Also
--------
* `evolved_expectation_value()`
* `gradient()`
"""
...
def get_driving_pulses(self,
ctrl_amp : np.ndarray[complex],
initial_states : np.ndarray[complex],
dt: float,
frequencies: np.ndarray[complex],
number_channels: list[int]
) -> tuple[np.ndarray[complex], np.ndarray[complex], float]:
"""
When calling any evolution method (listed in the See also section`)
`get_driving_pulses()` is executed on the arguements before the
evolution method.
Parameters
----------
frequencies: NDArray[Shape[`qubits`], float]
The frequencies of the to drive X on each of the qubits
amplitudes: NDArray[Shape[`qubits`], complex]
The amplitude of to drive X on each of the qubits
T: float
The time to evolve the system for
Warning
-------
Keyword arguments are not supported.
Returns
-------
tuple[NDArray[Shape[n_time_steps, `n_ctrl`], complex], NDArray[Shape[`state_shape`], complex], float]
A tuple of:
1. Control amplitudes
2. Initial state
3. Integrator time step
.. _get_driving_pulses_see_also:
See Also
--------
* `propagate()`
* `propagate_collection()`
* `propagate_all()`
* `evolved_expectation_value()`
* `evolved_expectation_value_all()`
* `gradient()`
"""
...
def _eager_processing(self,
ctrl_amp : np.ndarray[complex],
initial_states : np.ndarray[complex],
dt: float,
frequencies: np.ndarray[complex],
number_channels: list[int]
) -> tuple:
"""
Executes `_pre_processing()` followed by
`_envolope_processing()` eagerly (i.e. without using a
`TensorFlow <https://www.tensorflow.org>`__ graph). Nonetheless,
`_eager_processing()` is still auto differentiable.
Parameters
----------
frequencies: NDArray[Shape[`qubits`], float]
The frequencies of the to drive X on each of the qubits
amplitudes: NDArray[Shape[`qubits`], complex]
The amplitude of to drive X on each of the qubits
T: float
The time to evolve the system for
Warning
-------
Keyword arguments are not supported.
Returns
-------
tuple[tf.Tensor[Shape[n_time_steps, `n_ctrl`], complex], tf.Tensor[Shape[`state_shape`], complex], tf.Tensor[Shape[], float]]
A tuple of:
1. Control amplitudes
2. Initial state
3. Integrator time step
"""
...
def _traceable_eager_processing(self,
ctrl_amp : np.ndarray[complex],
initial_states : np.ndarray[complex],
dt: float,
frequencies: np.ndarray[complex],
number_channels: list[int]
) -> tuple:
"""
A function that will be traced by
`TensorFlow <https://www.tensorflow.org>`__ to produce a graph of
`_pre_processing()` followed by `_envolope_processing()`.
Parameters
----------
frequencies: NDArray[Shape[`qubits`], float]
The frequencies of the to drive X on each of the qubits
amplitudes: NDArray[Shape[`qubits`], complex]
The amplitude of to drive X on each of the qubits
T: float
The time to evolve the system for
Warning
-------
Keyword arguments are not supported.
Returns
-------
tuple[tf.Tensor[Shape[n_time_steps, `n_ctrl`], complex], tf.Tensor[Shape[`state_shape`], complex], tf.Tensor[Shape[], float]]
A tuple of:
1. Control amplitudes
2. Initial state
3. Integrator time step
"""
...
def gradient(self,
ctrl_amp : np.ndarray[complex],
initial_state : np.ndarray[complex],
dt: float,
frequencies: np.ndarray[complex],
number_channels: list[int],
observable : np.ndarray[complex]
) -> tuple[float, np.ndarray[float]]:
"""
Evolves a state vector under the time-dependent Hamiltonian defined by
the control amplitudes and computes the expectation value of a specified
observable with respect to the final state and then computes the
gradient of the final state with respect to the first argument
(`args[0]`) using `switching_function()` from
`PySTE <https://PySTE.readthedocs.io>`__.
Parameters
----------
frequencies: NDArray[Shape[`qubits`], float]
The frequencies of the to drive X on each of the qubits
amplitudes: NDArray[Shape[`qubits`], complex]
The amplitude of to drive X on each of the qubits
T: float
The time to evolve the system for
observable : NDArray[Shape[`dim`, `dim`], complex]
The observable to take the expectation value of.
Warning
-------
Keyword arguments are not supported.
Returns
-------
tuple[complex, NDArray[Shape[n_parameters], float]]
A tuple of the expectation value and the gradient.
See Also
--------
* `evolved_expectation_value()`
* `evolved_expectation_value_all()`
"""
...