Expectation Values
The propagate
routine allows the storage of data for every point of the time grid. This is done by passing it a storage
object created with QuantumPropagators.Storage.init_storage
, or simply storage=true
in order to create the appropriate storage automatically.
By default, the storage
will be used to store the propagated states at each point in time. However, more generally, what goes into storage
can be customized via the observables
parameter of propagate
. This allows to obtain arbitrary expectation values from the propagation. In short, the following options are available:
- Pass
observables
as a tuple of functions that each take the propagated stateΨ
and return some expectation value. - For time-dependent expectation values, the functions may take positional parameters
tlist
andi
in addition toΨ
, that is,(Ψ, tlist, i) -> value
, for an expectation value at timetlist[i]
. - Pass
observables
as a tuple of matrices for which to calculate expectation values via the three-argumentdot
function. This assumes the propagated state to be a vector. - For maximum flexibility, pass
observables
as a tuple of arbitrary objects for which a custommap_observable
method has been defined, or passobservables
as a single object for which a custommap_observables
method has been defined.
Details
After each propagation step, with a propagated state at time slot i
,
data = QuantumPropagators.Storage.map_observables(observables, tlist, i, state)
generatesdata
from the propagated stateQuantumPropagators.Storage.write_to_storage!(storage, i, data)
places thatdata
intostorage
for time sloti
After propagate
returns, the QuantumPropagators.Storage.get_from_storage!
routine can be used to extract data from any time slot. This interface hides the internal memory organization of storage
, which is set up by init_storage
based on the type of state
and the given observables
. This system can be extended with multiple dispatch, allowing to optimize the storage
for custom data types. Obviously, init_storage
, map_observables
, write_to_storage!
, and get_from_storage!
must all be consistent.
The default implementation of these routine uses either a standard Vector or a Matrix as storage
.
Roughly speaking, when storing states, if the state of some arbitrary type, the storage will be a Vector where the i'th entry points to a copy of the propagated state at the i'th time slot. If the state is a Vector, the storage will be a Matrix containing the state for the i'th time slot in the i'th column.
When a tuple of observables
is passed to propagate
, if map_observables
returns data of the same type for each observable, storage
will be a Matrix containing the values from the different observables for the i'th time slot in the i'th column. This would be typical for the storage of expectation values, e.g. with
observables=(state->dot(state, Ô₁, state), state->dot(state, Ô₂, state))
where Ô₁
, Ô₂
are two Hermitian operators. In this case, storage
would be a 2 × nt
Float64
array. Calling get_from_storage!(data, storage, i)
would be equivalent to copyto!(data, storage[:,i])
and extract the i'th column of storage
, i.e. the vector [⟨Ô₁⟩, ⟨Ô₂A]⟩]
at time slot i
. Alternatively, storage[1,:]
would return the values of ⟨Ô₁⟩
over time. This would be useful for plotting, and illustrates the benefits of using a Matrix as storage
.
Usually, the observables
should be functions acting on the state
, but map_observables
can be extended to other types of observables as well. For example, the situation were state
is a vector and the observables
are matrices is also supported; if Ô₁
, Ô₂
are matrices,
observables=(Ô₁, Ô₂)
would have the same result of storing the expectation values of those two operators.
The observables
are not required to yield real values: the term "observable" is used very loosely here. We could directly calculate, e.g., the complex amplitude α of a coherent state in quantum optics, or the number of levels with non-zero population (as an integer).
It is possible to have time-dependent "observables", for example, to store "lab-frame" states from a propagation in a (time-dependent) rotating frame. This is supported by default by passing a function with the three arguments state
, tlist
, i
as an observable, where state
is defined to be at time tlist[i]
.
If there are multiple observables that return data of different types, by default storage
will be a Vector that contains tuples with the result for each observable. For example, with
observables=(state->dot(state, Ô₁, state), state->count_poplevels(state))
where count_poplevels
is a function that counts the number of levels with non-zero population, the resulting storage
would be a Vector{Tuple{Float64, Int64}}
.
If there is a single observable that yields a vector, that vector is stored in the i'th column of a storage
matrix. This is in fact what happens when storing the propagated states (observables=(Ψ->copy(Ψ), )
) if Ψ
is a Vector, but there are other use cases, such as calculating the population in all levels in one go, with observables=(Ψ -> abs.(Ψ).^2, )
.
If there is a single variable that yields a non-vector object, storage
will be a Vector where the i'th entry points to the object. This is in fact what happens by default when storing states that are e.g. instances of QuantumOptics.Ket
. In such a case, it might be advisable to add new methods for QuantumPropagators.Storage.init_storage
and QuantumPropagators.Storage.write_to_storage!
that implement a more efficient in-place storage.