Ingredients#
Ingredients are functions that encode the physics of a model. QC Lab is designed to be operational with a minimal set of ingredients that describe the Hamiltonian of the system which is assumed to have the form:
where \(\hat{H}_\mathrm{q}\) is the quantum Hamiltonian, \(\hat{H}_{\mathrm{q-c}}(q)\) is the quantum-classical coupling Hamiltonian, and \(H_{\mathrm{c}}(q,p)\) is the classical Hamiltonian.
A generic ingredient has the form:
def ingredient_specifier(model, parameters, **kwargs):
# Get any keyword arguments, with default values if not provided.
kwarg1 = kwargs.get('kwarg1', default_value1)
# Compute the ingredient using attributes of the model and parameters objects.
constants = model.constants
# Return the computed ingredient.
return ingredient
where model
is a Model object (see Models) which contains all the constants of the model, parameters
is a variable object (see Variable Objects) containing time-dependent parameters of the simulation, and **kwargs
are any additional keyword arguments that are specific to that ingredient type.
Ingredients in QC lab can come in different variations, for example the quantum Hamiltonian ingredient could describe a two-level system, a nearest-neighbor lattice, or a more complex Hamiltonian. The type and variety of an ingredient is specified in its name which follows the convention <ingredient_type>_<variety>
. For example, the quantum Hamiltonian ingredient for a two-level system is named h_q_two_level
where h_q
indicates that it is a quantum Hamiltonian ingredient and two_level
indicates that it describes a two-level system. Likewise the classical Hamiltonian ingredient for a harmonic oscillator is named h_c_harmonic
where h_c
indicates that it is a classical Hamiltonian ingredient and harmonic
indicates that it describes a harmonic oscillator.
Ingredients can be included in a model by appending them to the model’s ingredients
attribute, which is a list of tuples where each tuple contains the name of the ingredient and the ingredient function itself. Because the ingredients list is read back to front, appending an ingredient is sufficient to overwrite any existing ingredient with the same name. For example, to include a custom quantum Hamiltonian ingredient in a model, one would do:
def h_q_custom(model, parameters, **kwargs):
# Custom quantum Hamiltonian implementation.
h_q = ...
return h_q
model.ingredients.append(("h_q", h_q_custom))
The minimal set of ingredients required to run a simulation are:
A quantum Hamiltonian ingredient, named
h_q
.A classical Hamiltonian ingredient, named
h_c
.A quantum-classical coupling Hamiltonian ingredient, named
h_qc
.
Additional ingredients that make the simulation more efficient or accurate are:
An initialization function for the classical coordinates, named
init_classical
.A gradient of the classical Hamiltonian with respect to the conjugate classical coordinates, named
dh_c_dzc
.A gradient of the quantum-classical coupling Hamiltonian with respect to the conjugate classical coordinates, named
dh_qc_dzc
.A hopping function for surface hopping algorithms, named
hop
.
Vectorization#
By default, QC Lab assumes that ingredients are implemented in a vectorized fashion. This means that rather than constructing the respective term for an individual trajectory, each ingredient constructs the term for all trajectories in a batch at once. Additionally, any keyword argument associated with an ingredient will have an initial trajectory dimension. For example, the quantum-classical Hamiltonian ingredient h_qc
has a keyword argument z
which is the complex classical coordinate. If the batch size is 100, then z
will have shape (100, model.constants.num_classical_coordinates)
where model.constants.num_classical_coordinates
is the number of classical coordinates in the model. The output of the h_qc
ingredient will then have shape (100, model.constants.num_quantum_states, model.constants.num_quantum_states)
where model.constants.num_quantum_states
is the number of quantum states in the model.
Rather than implementing this vectorization yourself, you can use the @vectorize_ingredient
decorator provided in the qclab.functions
module. This decorator will automatically vectorize an ingredient that is implemented for a single trajectory (i.e., without any batch dimension). For example, the following implementation of the quantum Hamiltonian ingredient for a two-level system is not vectorized:
from qclab.functions import vectorize_ingredient
@vectorize_ingredient
def h_q_two_level(model, parameters, **kwargs):
"""
Quantum Hamiltonian for a two-level system.
"""
e_0 = model.constants.two_level_00
e_1 = model.constants.two_level_11
v = model.constants.two_level_01_re + 1j * model.constants.two_level_01_im
h_q = np.zeros((2, 2), dtype=complex)
h_q[0, 0] = e_0
h_q[1, 1] = e_1
h_q[0, 1] = v
h_q[1, 0] = np.conj(v)
return h_q
The ingredient can then be included in a model as:
model.ingredients.append(("h_q", h_q_two_level))
Alternatively, vectorization can be implemented manually. For example, the following implementation of the quantum Hamiltonian ingredient for a two-level system is manually vectorized:
def h_q_two_level(model, parameters, **kwargs):
"""
Quantum Hamiltonian for a two-level system.
"""
batch_size = kwargs["batch_size"]
e_0 = model.constants.two_level_00
e_1 = model.constants.two_level_11
v = model.constants.two_level_01_re + 1j * model.constants.two_level_01_im
h_q = np.zeros((2, 2), dtype=complex)
h_q[0, 0] = e_0
h_q[1, 1] = e_1
h_q[0, 1] = v
h_q[1, 0] = np.conj(v)
# Here we use np.broadcast_to to add the batch dimension to the output.
# This is only possible because the output does not depend on any trajectory-specific
# information.
return np.broadcast_to(h_q, (batch_size, 2, 2))
Sparse Quantum-Classical Gradients#
If left unspecified, the gradient of the quantum-classical Hamiltonian will be calculated numerically using finite differences. This can be computationally expensive and introduce inaccuracies. This can be avoided by providing an analytical implementation of the gradient ingredient dh_qc_dzc
. In QC Lab, we implement this gradient in a sparse manner, meaning that we only compute the non-zero elements of the gradient matrix. This reduces the potentially large gradient tensor with shape (sim.settings.batch_size, model.constants.num_classical_coordinates, model.constants.num_quantum_states, model.constants.num_quantum_states)
to a list of non-zero elements, mels
, their indices inds
, and the shape of the full tensor shape
.
In practice, the ordering of the matrix elements can have a dramatic impact on performance due to memory access patterns. Therefore, we recommend using numpy.where
to determine the indices of the non-zero elements. For example, the following implementation of the gradient of the quantum-classical Hamiltonian for a spin-boson model computes only the non-zero elements of the gradient:
@vectorize_ingredient
def dh_qc_dzc_spinboson(model, parameters, **kwargs):
z = kwargs["z"]
A = model.constants.get("A")
diag_coupling = model.constants.diagonal_linear_coupling
out = np.zeros((A, 2, 2), dtype=complex)
for i in range(A):
out[i, 0, 0] = diag_coupling[0, i]
out[i, 1, 1] = diag_coupling[1, i]
inds = np.where(out != 0)
mels = out[inds]
shape = (len(z), A, 2, 2)
return inds, mels, shape
A dense ingredient can be automatically made sparse by invoking the @make_ingredient_sparse
decorator from the qclab.functions
module. This decorator will convert a dense ingredient into a sparse one by determining the non-zero elements, their indices, and the shape of the full tensor. For example, the following implementation of the gradient of the quantum-classical Hamiltonian for a spin-boson model is dense, but made sparse by the decorator:
from qclab.functions import make_ingredient_sparse
@make_ingredient_sparse
@vectorize_ingredient
def dh_qc_dzc_spinboson(model, parameters, **kwargs):
z = kwargs["z"]
A = model.constants.get("A")
diag_coupling = model.constants.diagonal_linear_coupling
out = np.zeros((A, 2, 2), dtype=complex)
for i in range(A):
out[i, 0, 0] = diag_coupling[0, i]
out[i, 1, 1] = diag_coupling[1, i]
return out
Of course the most efficient implementation is one that is both analytical and sparse without invoking the decorator. This is what is implemented in the ingredient ingredients.dh_qc_dzc_diagonal_linear
which is included in the Spin-Boson model by default.
Ingredients in QC Lab#
The built-in ingredients in QC Lab can be found in the qclab.ingredients
module.
Note
All ingredients assume that the model object has a minimal set of constants including num_quantum_states
(the number of quantum states) and num_classical_coordinates
(the number of classical coordinates), classical_coordinate_mass
(the mass of the classical coordinates), and classical_coordinate_weight
(the weight of the classical coordinates). These constants are discussed in Models. For brevity we exclude explicit mention of these constants in the task documentation.
This module contains ingredients for use in Model classes.
- qclab.ingredients.h_c_harmonic(model, parameters, **kwargs)#
Harmonic oscillator classical Hamiltonian function.
\(H_{\mathrm{c}} = \frac{1}{2}\sum_{n} \left( \frac{p_n^2}{m_n} + m_n \omega_n^2 q_n^2 \right)\)
Keyword Args
- zndarray
Complex classical coordinate.
Model Constants
- harmonic_frequencyndarray
Harmonic frequency of each classical coordinate.
Returns
- h_cndarray
Classical Hamiltonian.
(batch_size,)
.
- qclab.ingredients.h_c_free(model, parameters, **kwargs)#
Free particle classical Hamiltonian function.
\(H_{\mathrm{c}} = \sum_{n} \left( \frac{p_n^2}{2m_n} \right)\)
Keyword Args
- zndarray
Complex classical coordinate.
Model Constants
None
Returns
- h_cndarray
Classical Hamiltonian.
(batch_size,)
.
- qclab.ingredients.dh_c_dzc_harmonic(model, parameters, **kwargs)#
Derivative of the harmonic oscillator classical Hamiltonian function with respect to the conjugate z coordinate. This is an ingredient that calls the low-level function
dh_c_dzc_harmonic_jit
.Keyword Args
- zndarray
Complex classical coordinate.
Model Constants
- harmonic_frequencyndarray
Harmonic frequency of each classical coordinate.
Returns
- dh_c_dzcndarray
Gradient of the classical Hamiltonian with respect to the conjugate classical coordinate.
(batch_size, num_classical_coordinates)
.
- qclab.ingredients.dh_c_dzc_free(model, parameters, **kwargs)#
Derivative of the free particle classical Hamiltonian function with respect to the conjugate z coordinate.
Keyword Args
- zndarray
Complex classical coordinate.
Model Constants
None
Returns
- dh_c_dzcndarray
Gradient of the classical Hamiltonian with respect to the conjugate classical coordinate.
(batch_size, num_classical_coordinates)
.
- qclab.ingredients.h_q_two_level(model, parameters, **kwargs)#
Quantum Hamiltonian for a two-level system.
\(H_{nm} = \delta_{nm}\mathrm{two\_level\_nn}+(1-\delta_{nm})(\mathrm{two\_level\_nm\_re} + i \mathrm{two\_level\_nm\_im})\)
Keyword Args
- batch_sizeint
Number of trajectories in a batch.
Model Constants
- two_level_00float
Energy of the first level.
- two_level_11float
Energy of the second level.
- two_level_01_refloat
Real part of the off-diagonal coupling.
- two_level_01_imfloat
Imaginary part of the off-diagonal coupling.
Returns
- h_qndarray
Quantum Hamiltonian.
(batch_size, num_states, num_states)
.
- qclab.ingredients.h_q_nearest_neighbor(model, parameters, **kwargs)#
Quantum Hamiltonian for a nearest-neighbor lattice.
\(H_{nm} = -t (\delta_{n,m+1} + \delta_{n,m-1})\)
Keyword Args
- batch_sizeint
Number of trajectories in a batch.
Model Constants
- nearest_neighbor_hopping_energyfloat
Hopping energy between sites \(t\).
- nearest_neighbor_periodicbool
Whether to apply periodic boundary conditions.
Returns
- h_qndarray
Quantum Hamiltonian.
(batch_size, num_states, num_states)
.
- qclab.ingredients.h_qc_diagonal_linear(model, parameters, **kwargs)#
Diagonal linear quantum-classical Hamiltonian.
\(H_{nm} = \delta_{nm}\sum_{j} \gamma_{nj} (z_{j} + z_{j}^*)\)
Keyword Args
- zndarray
Complex classical coordinate.
Model Constants
- diagonal_linear_couplingndarray
Coupling constants \(\gamma\).
Returns
- h_qcndarray
Quantum-classical coupling Hamiltonian.
(batch_size, num_states, num_states)
.
- qclab.ingredients.dh_qc_dzc_diagonal_linear(model, parameters, **kwargs)#
Gradient of the diagonal linear quantum-classical coupling Hamiltonian in sparse format.
\([\partial_{z} H_{qc}]_{ijkl} = \delta_{kl}\gamma_{kj}\)
Keyword Args
- zndarray
Complex classical coordinate.
Model Constants
- diagonal_linear_couplingndarray
Coupling constants \(\gamma\).
Returns
- indstuple of ndarray
Indices of the non-zero elements of the gradient.
(batch_index, coordinate_index, row_index, column_index)
.- melsndarray
Values of the non-zero elements of the gradient.
- shapetuple
Shape of the full gradient array.
(batch_size, num_classical_coordinates, num_states, num_states)
.
- qclab.ingredients.hop_harmonic(model, parameters, **kwargs)#
FSSH hop function for taking the classical coordinates to represent harmonic oscillators.
Determines the shift in the classical coordinates required to conserve energy following a hop between quantum states.
If enough energy is available, the function returns the shift in the classical coordinates such that the new classical coordinate is
z + shift
and a Boolean equalingTrue
if the hop has occurred. If not enough energy is available, the shift becomes zero and the Boolean isFalse
.Solves the equation:
\[H_{\mathrm{c}}(z) + \epsilon_{\mathrm{initial}} = H_{\mathrm{c}}(z + \mathrm{shift}) + \epsilon_{\mathrm{final}}\]Keyword Args
- zndarray
Current classical coordinate.
- delta_zndarray
Rescaling direction of
z
.- eigval_difffloat
Energy difference between final and initial states.
Model Constants
- harmonic_frequencyndarray
Harmonic frequency of each classical coordinate.
Returns
- shiftndarray
Shift in the classical coordinate.
- hopbool
Whether the hop has occurred.
- qclab.ingredients.hop_free(model, parameters, **kwargs)#
FSSH hop function taking the classical coordinates to represent free particles.
Determines the shift in the classical coordinates required to conserve energy following a hop between quantum states.
If enough energy is available, the function returns the shift in the classical coordinates such that the new classical coordinate is
z + shift
and a Boolean equalingTrue
if the hop has occurred. If not enough energy is available, the shift becomes zero and the Boolean isFalse
.Solves the equation:
\[H_{\mathrm{c}}(z) + \epsilon_{\mathrm{initial}} = H_{\mathrm{c}}(z + \mathrm{shift}) + \epsilon_{\mathrm{final}}\]Keyword Args
- zndarray
Current classical coordinate.
- delta_zndarray
Rescaling direction.
- eigval_difffloat
Energy difference between final and initial states.
Model Constants
None
Returns
- shiftndarray
Shift in the classical coordinate.
- hopbool
Whether the hop has occurred.
- qclab.ingredients.init_classical_boltzmann_harmonic(model, parameters, **kwargs)#
Initialize classical coordinates according to Boltzmann statistics taking the classical coordinates to represent harmonic oscillators.
\(P(z) \propto \exp(-H_{\mathrm{c}}/k_{\mathrm{B}}T)\)
Keyword Args
- seedndarray, int
Random seeds for each trajectory.
Model Constants
- kBTfloat
Thermal quantum.
- harmonic_frequencyndarray
Harmonic frequency of each classical coordinate.
Returns
- zndarray
Complex classical coordinate.
- qclab.ingredients.init_classical_wigner_harmonic(model, parameters, **kwargs)#
Initialize classical coordinates according to the Wigner distribution of the ground state of a harmonic oscillator.
Keyword Args
- seedndarray, int
Random seeds for each trajectory.
Model Constants
- kBTfloat
Thermal quantum.
- harmonic_frequencyndarray
Harmonic frequency of each classical coordinate.
Returns
- zndarray
Complex classical coordinate.
- qclab.ingredients.init_classical_definite_position_momentum(model, parameters, **kwargs)#
Initialize classical coordinates with definite position and momentum.
Keyword Args
- seedndarray, int
Random seeds for each trajectory.
Model Constants
- init_positionndarray
Initial position of the classical coordinates.
- init_momentumndarray
Initial momentum of the classical coordinates.
Returns
- zndarray
Complex classical coordinate.
- qclab.ingredients.init_classical_wigner_coherent_state(model, parameters, **kwargs)#
Initialize classical coordinates according to the Wigner distribution of a coherent state of a harmonic oscillator.
\(\vert a\rangle = \exp(a\hat{b}^{\dagger} - a^{*}\hat{b})\vert 0\rangle\)
where \(a\) is the complex displacement parameter of the coherent state. The Wigner distribution is given by
\(P(z) \propto \exp(-2\vert(z - a)\vert^{2})\).
Keyword Args
- seedndarray, int
Random seeds for each trajectory.
Model Constants
- coherent_state_displacementndarray
Complex displacement parameter of the coherent state.
- harmonic_frequencyndarray
Harmonic frequency of each classical coordinate.
Returns
- zndarray
Complex classical coordinate.
- qclab.ingredients.rescaling_direction_random(model, parameters, **kwargs)#
Random rescaling direction function.
This function returns a random array for the rescaling direction. It is only included for documentation purposes.
Note that this is not a vectorized ingredient, as it is only called on the per-trajectory level.
Keyword Args
- z_trajndarray
Current classical coordinate in the trajectory being rescaled.
- init_state_indint
Index of the initial quantum state.
- final_state_indint
Index of the final quantum state.
Model Constants
None
Returns
- delta_zndarray
Direction in which to rescale coordinates.
- qclab.ingredients.gauge_field_force_zero(model, parameters, **kwargs)#
Gauge field force function.
This function returns a zero array for the gauge field force. It is only included for documentation purposes.
Keyword Args
- zndarray
Current classical coordinate.
- state_indint
Index of the state for which the gauge field force is calculated.
Model Constants
None
Returns
- gauge_forcendarray
Gauge field force.