Skip to content

Commit 2c23ba0

Browse files
committed
Merge #120 from branch storage-ownership
2 parents 7b49f1b + 9b437c3 commit 2c23ba0

9 files changed

Lines changed: 754 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ Also see the [GitHub Releases](https://github.com/JuliaQuantumControl/QuantumPro
1313

1414
## [Unreleased]
1515

16+
* Added: `QuantumPropagators.Interfaces.check_storage`, which verifies that a storage implementation fulfills the storage contract [[#119], [#120]]
17+
* Fixed: `write_to_storage!` now stores a copy of any mutable data, so that the storage owns its data. Previously, storing the state of an in-place propagation at every time step could leave all slots aliasing a single buffer [[#119], [#120]]
18+
1619
## [v0.9.0] — 2026-06-15
1720

1821
* Added: `ExponentialUtilitiesPropagator`, a propagator based on `expv` from [ExponentialUtilities.jl](https://github.com/SciML/ExponentialUtilities.jl), with Krylov-subspace caching for in-place propagation [[#97]]
@@ -233,3 +236,5 @@ Initial public release
233236
[#108]: https://github.com/JuliaQuantumControl/QuantumPropagators.jl/pull/108
234237
[#110]: https://github.com/JuliaQuantumControl/QuantumPropagators.jl/pull/110
235238
[#111]: https://github.com/JuliaQuantumControl/QuantumPropagators.jl/pull/111
239+
[#119]: https://github.com/JuliaQuantumControl/QuantumPropagators.jl/issues/119
240+
[#120]: https://github.com/JuliaQuantumControl/QuantumPropagators.jl/pull/120

docs/src/storage.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ After each propagation step, with a propagated state at time slot `i`,
2222
1. [`data = QuantumPropagators.Storage.map_observables(observables, tlist, i, state)`](@ref QuantumPropagators.Storage.map_observables) generates `data` from the propagated state
2323
2. [`QuantumPropagators.Storage.write_to_storage!(storage, i, data)`](@ref QuantumPropagators.Storage.write_to_storage!) places that `data` into `storage` for time slot `i`
2424

25-
After [`propagate`](@ref) returns, the [`QuantumPropagators.Storage.get_from_storage!`](@ref) 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`](@ref QuantumPropagators.Storage.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`](@ref QuantumPropagators.Storage.init_storage), [`map_observables`](@ref QuantumPropagators.Storage.map_observables), [`write_to_storage!`](@ref QuantumPropagators.Storage.write_to_storage!), and [`get_from_storage!`](@ref QuantumPropagators.Storage.get_from_storage!) must all be consistent.
25+
After [`propagate`](@ref) returns, the [`QuantumPropagators.Storage.get_from_storage!`](@ref) 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`](@ref QuantumPropagators.Storage.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`](@ref QuantumPropagators.Storage.init_storage), [`map_observables`](@ref QuantumPropagators.Storage.map_observables), [`write_to_storage!`](@ref QuantumPropagators.Storage.write_to_storage!), and [`get_from_storage!`](@ref QuantumPropagators.Storage.get_from_storage!) must all be consistent, see [the storage contract](@ref storage-contract).
2626

2727
The default implementation of these routine uses either a standard Vector or a Matrix as `storage`.
2828

29-
Roughly speaking, when storing states, if the state of some arbitrary type,
30-
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.
29+
Roughly speaking, when storing states, if the state is of some arbitrary type,
30+
the storage will be a Vector where the i'th entry holds 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.
3131

3232
When a tuple of `observables` is passed to [`propagate`](@ref), if [`map_observables`](@ref QuantumPropagators.Storage.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
3333

@@ -62,8 +62,21 @@ observables=(state->dot(state, Ô₁, state), state->count_poplevels(state))
6262
where `count_poplevels` is a function that counts the number of levels with
6363
non-zero population, the resulting `storage` would be a `Vector{Tuple{Float64, Int64}}`.
6464

65-
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
65+
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=(Ψ->Ψ, )`) if `Ψ` is a Vector, but there are other use cases, such as calculating the population in all levels in one go, with
6666
`observables=(Ψ -> abs.(Ψ).^2, )`.
6767

68-
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
68+
If there is a single observable that yields a non-vector object, `storage` will be a Vector where the i'th entry holds a copy of the object. This is in fact what happens by default when storing states that are e.g. instances of
6969
`QuantumOptics.Ket`. In such a case, it might be advisable to add new methods for [`QuantumPropagators.Storage.init_storage`](@ref) and [`QuantumPropagators.Storage.write_to_storage!`](@ref) that implement a more efficient in-place storage.
70+
71+
72+
## [The storage contract](@id storage-contract)
73+
74+
A `storage` object is created by [`init_storage`](@ref QuantumPropagators.Storage.init_storage), written to by [`write_to_storage!`](@ref QuantumPropagators.Storage.write_to_storage!), and read back by [`get_from_storage`](@ref QuantumPropagators.Storage.get_from_storage) or [`get_from_storage!`](@ref QuantumPropagators.Storage.get_from_storage!). Together, these four functions guarantee the following, for any `storage = init_storage(data, nt)` and any slot `i` in `1:nt`:
75+
76+
1. What you put in is what you get out. After `write_to_storage!(storage, i, data)`, `get_from_storage(storage, i)` reproduces the value that `data` had at the time of the write, and so does `get_from_storage!(buffer, storage, i)` for data that supports in-place extraction.
77+
78+
2. The storage owns its data. Modifying `data` after the write does not change what is stored, and writing to one slot does not change any other slot. This holds even when the caller passes the same object on every write, which is the normal situation when storing the states of an in-place propagation: [`prop_step!`](@ref) returns the same buffer at every time step.
79+
80+
3. The result of [`get_from_storage`](@ref QuantumPropagators.Storage.get_from_storage) is read-only. It may alias the internals of `storage`, so mutating it may corrupt the storage. Use [`get_from_storage!`](@ref QuantumPropagators.Storage.get_from_storage!) to obtain data the caller may modify.
81+
82+
Custom [`init_storage`](@ref QuantumPropagators.Storage.init_storage), [`write_to_storage!`](@ref QuantumPropagators.Storage.write_to_storage!), and [`get_from_storage!`](@ref QuantumPropagators.Storage.get_from_storage!) methods must be mutually consistent, and must uphold the guarantees above. Use [`check_storage`](@ref QuantumPropagators.Interfaces.check_storage) to verify them.

src/QuantumPropagators.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,15 @@ export init_prop, reinit_prop!, prop_step!
4444
module Interfaces
4545
export supports_inplace, supports_vector_interface, supports_matrix_interface
4646
export check_operator, check_state, check_tlist, check_amplitude
47+
export check_storage
4748
export check_control, check_generator, check_propagator
4849
export check_parameterized_function, check_parameterized
4950
include("interfaces/supports_inplace.jl")
5051
include("interfaces/supports_vector_interface.jl")
5152
include("interfaces/supports_matrix_interface.jl")
5253
include("interfaces/utils.jl")
5354
include("interfaces/state.jl")
55+
include("interfaces/storage.jl")
5456
include("interfaces/tlist.jl")
5557
include("interfaces/operator.jl")
5658
include("interfaces/amplitude.jl")

src/interfaces/state.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ verifies the following requirements:
2222
This is the *definition* of a Hilbert space, a.k.a a "complete inner product
2323
space" or more precisely a "Banach space (normed vector space) where the
2424
norm is induced by an inner product".
25-
* The [`QuantumPropagators.Interfaces.supports_inplace](@ref) method must be
25+
* The [`QuantumPropagators.Interfaces.supports_inplace`](@ref) method must be
2626
defined for `state`
2727
2828
Any `state` must support the following not-in-place operations:

0 commit comments

Comments
 (0)