Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert qnumbers to lazy operators in to_numeric #191

Merged
merged 7 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "QuantumCumulants"
uuid = "35bcea6d-e19f-57db-af74-8011de6c7255"
version = "0.2.25"
version = "0.2.26"

[deps]
Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa"
Expand Down
18 changes: 10 additions & 8 deletions docs/Project.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
[deps]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316"
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7"
SymbolicUtils = "d1185830-fcd6-423d-90d6-eec64667417b"
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316"
QuantumOptics = "6e0679c1-51ea-5a7c-ac74-d61b76210b0c"
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
StochasticDiffEq = "789caeaf-c7a9-5a7d-9973-96adeb23e2a0"
QuantumOpticsBase = "4f57444f-1401-5e15-980d-4471b28d5678"
SteadyStateDiffEq = "9672c7b4-1e72-59bd-8a11-6ac3964bc41f"
StochasticDiffEq = "789caeaf-c7a9-5a7d-9973-96adeb23e2a0"
SymbolicUtils = "d1185830-fcd6-423d-90d6-eec64667417b"
Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7"

[compat]
Documenter = "1"
Expand All @@ -18,8 +19,9 @@ MacroTools = "0.5"
ModelingToolkit = "8"
OrdinaryDiffEq = "6"
Plots = "1"
QuantumOptics = "0.8, 1"
QuantumOptics = "1"
QuantumOpticsBase = "0.4.21"
SteadyStateDiffEq = "1"
StochasticDiffEq = "6"
Symbolics = "5"
SymbolicUtils = "1"
Symbolics = "5"
125 changes: 121 additions & 4 deletions docs/src/implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,124 @@ nothing # hide
Now, the state of the system at each time-step is stored in `sol.u`. To access one specific solution, you can simply type e.g. `sol[average(a)]` to obtain the time evolution of the expectation value ``\langle a \rangle``.


### Calculating the initial state

When trying to solve a system of equations numerically, it can sometimes become tricky to find the correct initial state.
In the above, we simply did `u0 = zeros(ComplexF64, length(me))`, since that was a viable initial state.
However, things become more involved when you have, say, a superposition of two coherent states in a harmonic oscillator as starting point,

```math
|\psi_0\rangle = \frac{1}{\sqrt{2}} \left( |\alpha\rangle + |\beta\rangle \right),
```

where $\alpha, \beta \in \mathbb{C}$ are the respective complex amplitudes.
While computing the first-order expectation values such as `\langle \psi_0 | a |\psi_0\rangle` is still simple enough, things become more tricky in higher orders and when mixing in another Hilbert space (e.g. an atom in a cavity).
Since the system of equations can become quite large, this may result in quite some manual effort when trying to calculate all initial values.
And we hate manual effort.

These expectations values are, however, only difficult to calculate symbolically, yet are easy enough to compute numerically.
QuantumCumulants therefore offers a convenient integration to [QuantumOpticsBase.jl](https://github.com/qojulia/QuantumOpticsBase.jl), which allows you to quickly calculate the initial expectation values of a system of equations from a given numerical initial state.
The function is called [`initial_values`](@ref).
For example, we could use it in the above example to compute a coherent initial states

```@example meanfield
using QuantumOpticsBase
b = FockBasis(10)
alpha = 0.3 + 0.4im
psi_0 = coherentstate(b, alpha)
u0 = initial_values(me, psi_0)
nothing # hide
```

Note that you can also compute initial values for mixed states.
You simply have to use a density operator in the function call.

```@example meanfield
u0 = initial_values(me, dm(psi_0))
nothing # hide
```

#### Mapping levels for `NLevelSpace`

The conversion to a numeric representation between [`FockSpace`](@ref) and `FockBasis` is always uniquely defined.
However, there is some freedom of choice when it comes to [`NLevelSpace`](@ref) and the equivalent of `NLevelBasis`, specifically when using symbolic levels.
While it is clear that a symbolic [`Transition`](@ref) operator should map to a numeric `transition`, the choice of which level represents maps to which basis state in the `NLevelBasis` is not fixed.

When using numeric level representations, the [`initial_values`](@ref) and [`to_numeric`](@ref) methods default to using the same numbered basis state:

```@example levelmap
using QuantumCumulants, QuantumOpticsBase
h = NLevelSpace(:TwoLevelAtom, (1, 2))
b = NLevelBasis(2)
s = Transition(h, :s, 1, 2)
@assert to_numeric(s, b) == transition(b, 1, 2)
nothing # hide
```

The order here can be overridden using the `level_map` keyword.
When using symbolic levels, the `level_map` keyword is required.

```@example levelmap2
using QuantumCumulants, QuantumOpticsBase
h = NLevelSpace(:TwoLevelAtom, (:g, :e))
b = NLevelBasis(2)
s = Transition(h, :s, :g, :e)
level_map = Dict(:g => 1, :e => 2)
@assert to_numeric(s, b; level_map=level_map) == transition(b, 1, 2)
nothing # hide
```

#### Numeric averages and conversion

While the examples so far were relatively simple and would have been easy to calculate by hand, things quickly become more difficult whenever product spaces and higher-order products are involved.

Behind the scenes, [`initial_values`](@ref) just uses the [`numeric_average`](@ref) method in order to compute the numeric expectation value for the given operators and states.
This method in turn calls into the numeric conversion [`to_numeric`](@ref) and then uses `QuantumOpticsBase.expect` on the result in order to calculate the respective expectation values for the given state and operators numerically.
Should you need to compute numerical averages from a symbolic one for a given numerical state you can also call [`numeric_average`](@ref) directly.

```@example tonumeric
using QuantumCumulants, QuantumOpticsBase
hfock = FockSpace(:cavity)
hnlevel = NLevelSpace(:ThreeLevelAtom, (:a, :b, :c))
h = hfock ⊗ hnlevel
a = Destroy(h, :a)
s = Transition(h, :s, :a, :c)
levelmap = Dict(
:a => 3,
:b => 2,
:c => 1,
)

bfock = FockBasis(10)
bnlevel = NLevelBasis(3)
psi = coherentstate(bfock, 0.3) ⊗ (nlevelstate(bnlevel, 1) + nlevelstate(bnlevel, 3)) / sqrt(2)

avg = average(a' * s)
avg_num = numeric_average(avg, psi; level_map=levelmap)
nothing # hide
```

Similarly, you can also just obtain the numerical representation of an operator by directly calling [`to_numeric`](@ref) and a given basis.

```@example tonumeric
b = bfock ⊗ bnlevel
a_num = to_numeric(a, b)
nothing # hide
```

Note that [`to_numeric`](@ref) returns a `SparseOperator` for single operators, but a `LazyTensor` operator whenever a product space is involved.
Lazy evaluation of tensor products is incredibly useful here, as symbolically easy to treat systems can become quite large numerically.

When a large number of Hilbert spaces is involved, it can even become tricky to store a single `Ket`.
In order to overcome this limitation, QuantumOpticsBase also offers lazy evaluation of state products, allowing you to compute expectation values and initial states for very large product states.

```@example tonumeric
psi_lazy = LazyKet(b, (coherentstate(bfock, 0.3), (nlevelstate(bnlevel, 1) + nlevelstate(bnlevel, 3)) / sqrt(2)),)
avg_num_lazy = numeric_average(avg, psi_lazy; level_map=levelmap)
@assert isapprox(avg_num, avg_num_lazy)
```


## [The *q*-number interface](@id interface)

While there are currently only two different Hilbert spaces and two different types of fundamental operators implemented, their implementations are somewhat generic. This means that one can implement custom operator types along with some commutation relations for rewriting. The requirements for that are:
Expand Down Expand Up @@ -287,10 +405,9 @@ Now, for methods we simply need:

```@example custom-operators
QuantumCumulants.ismergeable(::Position,::Momentum) = true
Base.:*(x::Position,p::Momentum) = im + p*x
for T in (:Position, :Momentum)
@eval Base.isequal(a::$T, b::$T) = isequal(a.hilbert, b.hilbert) && isequal(a.name, b.name) && isequal(a.aon, b.aon)
end
Base.:*(x::Position, p::Momentum) = im + p*x
Base.isequal(a::Position, b::Position) = isequal(a.hilbert, b.hilbert) && isequal(a.name, b.name) && isequal(a.aon, b.aon)
Base.isequal(a::Momentum, b::Momentum) = isequal(a.hilbert, b.hilbert) && isequal(a.name, b.name) && isequal(a.aon, b.aon)
```

The `Base.isequal` methods do not compare metadata fields. Note that if your subtypes of
Expand Down
4 changes: 2 additions & 2 deletions src/index_utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function to_numeric(op::NumberedOperator,b::QuantumOpticsBase.CompositeBasis; ra
end
end
start = 0
if !=(h,nothing) #this is fine here since there are assertions above
if h !== nothing #this is fine here since there are assertions above
aon_ = acts_on(op)
for i = 1:(aon_ - 1)
start = start + ranges[i]
Expand All @@ -64,7 +64,7 @@ function to_numeric(op::NumberedOperator,b::QuantumOpticsBase.CompositeBasis; ra
aon = op.numb + start
end
op_ = _to_numeric(op.op,b.bases[aon];kwargs...)
return QuantumOpticsBase.embed(b,aon,op_)
return QuantumOpticsBase.LazyTensor(b,[aon],(op_,))
end
#function that returns the conjugate of an average, but also preserving the correct ordering
function _inconj(v::Average)
Expand Down
31 changes: 29 additions & 2 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -375,20 +375,47 @@
check_basis_match(op.hilbert, b; kwargs...)
aon = acts_on(op)
op_num = _to_numeric(op, b.bases[aon]; kwargs...)
return QuantumOpticsBase.embed(b, aon, op_num)
return QuantumOpticsBase.LazyTensor(b, aon, op_num)
end

# Symbolic expressions
function to_numeric(op::QTerm, b::QuantumOpticsBase.Basis; kwargs...)
f = SymbolicUtils.operation(op)
return _to_numeric_term(f, op, b; kwargs...)
end

function _to_numeric_term(f::Function, op, b; kwargs...)
args = SymbolicUtils.arguments(op)
return f((to_numeric(arg, b; kwargs...) for arg in args)...)
end

function _to_numeric_term(::typeof(*), op::QTerm, b::QuantumOpticsBase.Basis; kwargs...)
args = SymbolicUtils.arguments(op)
factor = 1
args_num = Any[]
for arg in args
if arg isa Number
factor *= arg
else
push!(args_num, to_numeric(arg, b; kwargs...))
end
end

if length(args_num) == 0
return factor * one(b)

Check warning on line 405 in src/utils.jl

View check run for this annotation

Codecov / codecov/patch

src/utils.jl#L405

Added line #L405 was not covered by tests
end

return *(factor, args_num...)
end

function to_numeric(x::Number, b::QuantumOpticsBase.Basis; kwargs...)
op = one(b)*x
op = _lazy_one(b)*x
return op
end
_lazy_one(b::QuantumOpticsBase.Basis) = one(b)
function _lazy_one(b::QuantumOpticsBase.CompositeBasis)
LazyTensor(b, [1:length(b.bases);], Tuple(one(b_) for b_ in b.bases))
end


"""
Expand Down
12 changes: 5 additions & 7 deletions test/test_index_basic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -245,16 +245,14 @@ j1 = Index(h_,:j1,N_f,hfock)
ai(i) = IndexedOperator(Destroy(h_,:a),i)
σi(i,j,k) = IndexedOperator(Transition(h_,:σ,i,j),k)

@test to_numeric(ai(1),b_;ranges=ranges) isa Operator
@test to_numeric(ai(1),b_;ranges=ranges) == QuantumOpticsBase.embed(b_,5,destroy(bfock))
@test to_numeric(ai(2),b_;ranges=ranges) == QuantumOpticsBase.embed(b_,6,destroy(bfock))
@test to_numeric(σi(1,2,4),b_;ranges=ranges) isa Operator
@test to_numeric(σi(1,2,4),b_;ranges=ranges) == QuantumOpticsBase.embed(b_,4,transition(bnlevel,1,2))
@test to_numeric(ai(1),b_;ranges=ranges) == LazyTensor(b_, [5], (destroy(bfock),))
@test to_numeric(ai(2),b_;ranges=ranges) == LazyTensor(b_, [6], (destroy(bfock),))
@test to_numeric(σi(1,2,4),b_;ranges=ranges) == LazyTensor(b_, [4], (transition(bnlevel,1,2),))
@test_throws MethodError to_numeric(σi(1,2,5),b_;ranges=ranges)

ai2(i) = IndexedOperator(Destroy(hfock,:a),i)
@test to_numeric(ai2(1),b_2;ranges=[2]) isa Operator
@test to_numeric(ai2(2),b_2;ranges=[2]) isa Operator
@test to_numeric(ai2(1),b_2;ranges=[2]) isa LazyTensor
@test to_numeric(ai2(2),b_2;ranges=[2]) isa LazyTensor
@test_throws BoundsError to_numeric(ai2(3),b_2;ranges=[2])

# Indices and only one HilbertSpace
Expand Down
Loading
Loading