Skip to content

Commit c1f882a

Browse files
committed
Prototype SciMLOperators-backed lazy operators
1 parent c470242 commit c1f882a

7 files changed

Lines changed: 440 additions & 4 deletions

File tree

Project.toml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ UnsafeArrays = "c4a57d5a-5b31-53a6-b365-19f8c011fbd6"
1919

2020
[weakdeps]
2121
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
22-
22+
SciMLOperators = "c0aeaf25-5076-4817-a8d5-81caf7dfa961"
23+
2324
[extensions]
2425
QuantumOpticsBaseMakieExt = ["Makie"]
25-
26+
QuantumOpticsBaseSciMLOperatorsExt = "SciMLOperators"
27+
2628
[compat]
2729
Adapt = "3.3, 4"
2830
FFTW = "1.2"
@@ -35,7 +37,8 @@ Makie = "0.24"
3537
QuantumInterface = "0.4"
3638
Random = "1"
3739
RecursiveArrayTools = "3.1, 4"
40+
SciMLOperators = "1"
3841
SparseArrays = "1"
3942
Strided = "1, 2"
4043
UnsafeArrays = "1"
41-
julia = "1.10"
44+
julia = "1.10"

benchmark/Project.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
name = "QuantumOpticsBaseBenchmarks"
2+
uuid = "729dd8e0-ae36-4b43-9a76-c0170d3614b7"
3+
4+
[deps]
5+
BenchmarkTools = "6e4b80f9-dd1a-5c18-9360-3c73fbc0a7c0"
6+
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
7+
QuantumOpticsBase = "4f57444f-1401-5e15-980d-4471b28d5678"
8+
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
9+
SciMLOperators = "c0aeaf25-5076-4817-a8d5-81caf7dfa961"

benchmark/sciml_lazy_operators.jl

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
using BenchmarkTools
2+
using LinearAlgebra
3+
using QuantumOpticsBase
4+
using Random
5+
using SciMLOperators
6+
7+
const SUITE = BenchmarkGroup()
8+
9+
for family in ("lazy_sum", "lazy_product", "lazy_tensor", "mixed_hamiltonian")
10+
SUITE[family] = BenchmarkGroup([family])
11+
end
12+
13+
function chain_basis(n)
14+
site = SpinBasis(1//2)
15+
return tensor(ntuple(_ -> site, n)...)
16+
end
17+
18+
function random_ket(basis, seed)
19+
rng = MersenneTwister(seed)
20+
return Ket(basis, randn(rng, ComplexF64, length(basis)))
21+
end
22+
23+
function chain_sum_case(n)
24+
site = SpinBasis(1//2)
25+
basis = tensor(ntuple(_ -> site, n)...)
26+
sx = sigmax(site)
27+
sz = sigmaz(site)
28+
terms = AbstractOperator[LazyTensor(basis, i, isodd(i) ? sx : sz) for i in 1:n]
29+
return LazySum(terms...)
30+
end
31+
32+
function product_case(n)
33+
site = SpinBasis(1//2)
34+
basis = tensor(ntuple(_ -> site, n)...)
35+
sx = sigmax(site)
36+
sz = sigmaz(site)
37+
terms = AbstractOperator[LazyTensor(basis, i, isodd(i) ? sx : sz) for i in 1:n]
38+
return LazyProduct(terms...)
39+
end
40+
41+
function two_site_product_case(n)
42+
site = SpinBasis(1//2)
43+
basis = tensor(ntuple(_ -> site, n)...)
44+
mid = max(1, n ÷ 2)
45+
return LazyProduct(LazyTensor(basis, mid, sigmax(site)), LazyTensor(basis, mid + 1, sigmaz(site)))
46+
end
47+
48+
function tensor_case(n)
49+
site = SpinBasis(1//2)
50+
basis = tensor(ntuple(_ -> site, n)...)
51+
return LazyTensor(basis, [1, n], (sigmax(site), sigmaz(site)))
52+
end
53+
54+
function mixed_hamiltonian_case(n)
55+
site = SpinBasis(1//2)
56+
basis = tensor(ntuple(_ -> site, n)...)
57+
sx = sigmax(site)
58+
sz = sigmaz(site)
59+
terms = AbstractOperator[]
60+
61+
for i in 1:(n - 1)
62+
push!(terms, LazyProduct(LazyTensor(basis, i, sx), LazyTensor(basis, i + 1, sx)))
63+
push!(terms, LazyProduct(LazyTensor(basis, i, sz), LazyTensor(basis, i + 1, sz)))
64+
end
65+
66+
for i in 1:n
67+
push!(terms, 0.3 * LazyTensor(basis, i, sz))
68+
end
69+
70+
return LazySum(terms...)
71+
end
72+
73+
function add_apply_benchmarks!(suite, name, current, psi)
74+
group = BenchmarkGroup([name])
75+
sciml_uncached = sciml_lazy_operator(current)
76+
sciml_cached = cache_sciml_lazy_operator(sciml_uncached, psi)
77+
78+
out_current = Ket(current.basis_l)
79+
out_uncached = Ket(current.basis_l)
80+
out_cached = Ket(current.basis_l)
81+
82+
group["current"] = @benchmarkable mul!($out_current, $current, $psi, 1, 0)
83+
group["sciml_uncached"] = @benchmarkable mul!($out_uncached, $sciml_uncached, $psi, 1, 0)
84+
group["sciml_cached"] = @benchmarkable mul!($out_cached, $sciml_cached, $psi, 1, 0)
85+
86+
suite[name] = group
87+
return group
88+
end
89+
90+
for n in (4, 6, 8)
91+
op = chain_sum_case(n)
92+
add_apply_benchmarks!(SUITE["lazy_sum"], "n=$n", op, random_ket(op.basis_r, n))
93+
end
94+
95+
for n in (4, 6)
96+
op = product_case(n)
97+
add_apply_benchmarks!(SUITE["lazy_product"], "n=$n", op, random_ket(op.basis_r, 20 + n))
98+
end
99+
100+
for n in (4, 6, 8)
101+
op = two_site_product_case(n)
102+
add_apply_benchmarks!(SUITE["lazy_product"], "two_site n=$n", op, random_ket(op.basis_r, 30 + n))
103+
end
104+
105+
for n in (4, 6, 8)
106+
op = tensor_case(n)
107+
add_apply_benchmarks!(SUITE["lazy_tensor"], "n=$n", op, random_ket(op.basis_r, 40 + n))
108+
end
109+
110+
for n in (4, 6)
111+
op = mixed_hamiltonian_case(n)
112+
add_apply_benchmarks!(SUITE["mixed_hamiltonian"], "n=$n", op, random_ket(op.basis_r, 60 + n))
113+
end
114+
115+
if abspath(PROGRAM_FILE) == @__FILE__
116+
results = run(SUITE; verbose=true)
117+
show(stdout, MIME("text/plain"), results)
118+
println()
119+
end
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
module QuantumOpticsBaseSciMLOperatorsExt
2+
3+
import Base: +, -, *, /, copy, eltype, size, transpose
4+
import LinearAlgebra: adjoint, mul!
5+
import SparseArrays
6+
import QuantumOpticsBase
7+
import QuantumOpticsBase: Bra, Ket
8+
import SciMLOperators
9+
10+
const QOB = QuantumOpticsBase
11+
12+
struct SciMLLazyOperator{BL,BR,O} <: QOB.LazyOperator{BL,BR}
13+
basis_l::BL
14+
basis_r::BR
15+
operator::O
16+
17+
function SciMLLazyOperator(basis_l::BL, basis_r::BR, operator::O) where {BL,BR,O}
18+
expected = (length(basis_l), length(basis_r))
19+
size(operator) == expected || throw(DimensionMismatch("SciML operator has size $(size(operator)), expected $expected from bases."))
20+
return new{BL,BR,O}(basis_l, basis_r, operator)
21+
end
22+
end
23+
24+
copy(op::SciMLLazyOperator) = SciMLLazyOperator(op.basis_l, op.basis_r, copy(op.operator))
25+
eltype(op::SciMLLazyOperator) = eltype(op.operator)
26+
size(op::SciMLLazyOperator) = (length(op.basis_l), length(op.basis_r))
27+
size(op::SciMLLazyOperator, dim::Integer) = size(op)[dim]
28+
29+
function _check_same_bases(a, b)
30+
a.basis_l == b.basis_l && a.basis_r == b.basis_r || throw(QOB.IncompatibleBases())
31+
return nothing
32+
end
33+
34+
function _check_multiplicable(a, b)
35+
a.basis_r == b.basis_l || throw(QOB.IncompatibleBases())
36+
return nothing
37+
end
38+
39+
_wrap(basis_l, basis_r, operator) = SciMLLazyOperator(basis_l, basis_r, operator)
40+
41+
function _operator_number_type(op)
42+
T = eltype(op)
43+
return T <: Number ? T : ComplexF64
44+
end
45+
46+
function _matrix_identity(::Type{T}, basis_l, basis_r) where {T}
47+
if length(basis_l) == length(basis_r)
48+
return SciMLOperators.IdentityOperator(length(basis_l))
49+
end
50+
return SciMLOperators.MatrixOperator(QOB.identityoperator(T, basis_l, basis_r).data)
51+
end
52+
53+
_sciml_operator(op::SciMLLazyOperator) = op.operator
54+
_sciml_operator(op::QOB.DataOperator) = SciMLOperators.MatrixOperator(op.data)
55+
56+
function _sciml_operator(op::QOB.LazySum)
57+
if isempty(op.operators)
58+
return SciMLOperators.NullOperator(length(op.basis_l), length(op.basis_r))
59+
end
60+
61+
result = op.factors[1] * _sciml_operator(op.operators[1])
62+
for i in 2:length(op.operators)
63+
result = result + op.factors[i] * _sciml_operator(op.operators[i])
64+
end
65+
return result
66+
end
67+
68+
function _combined_lazytensor_product(op::QOB.LazyProduct)
69+
length(op.operators) == 2 || return nothing
70+
all(operator -> operator isa QOB.LazyTensor, op.operators) || return nothing
71+
72+
result = op.operators[1]
73+
for i in 2:length(op.operators)
74+
result = result * op.operators[i]
75+
result isa QOB.LazyTensor || return nothing
76+
end
77+
return result
78+
end
79+
80+
function _sciml_operator(op::QOB.LazyProduct)
81+
combined = _combined_lazytensor_product(op)
82+
combined === nothing || return op.factor * _sciml_operator(combined)
83+
84+
result = _sciml_operator(op.operators[1])
85+
for i in 2:length(op.operators)
86+
result = result * _sciml_operator(op.operators[i])
87+
end
88+
return op.factor * result
89+
end
90+
91+
function _sciml_operator(op::QOB.LazyTensor)
92+
T = _operator_number_type(op)
93+
factors = map(1:length(op.basis_l.bases)) do index
94+
op_index = findfirst(isequal(index), op.indices)
95+
if op_index === nothing
96+
return _matrix_identity(T, op.basis_l.bases[index], op.basis_r.bases[index])
97+
end
98+
return _sciml_operator(op.operators[op_index])
99+
end
100+
101+
# QuantumOpticsBase stores tensor data as kron(right, left), which matches
102+
# SciMLOperators when the factor order is reversed.
103+
tensor_operator = SciMLOperators.TensorProductOperator(reverse(factors)...)
104+
return op.factor * tensor_operator
105+
end
106+
107+
function _sciml_operator(op::QOB.AbstractOperator)
108+
return SciMLOperators.MatrixOperator(QOB.dense(op).data)
109+
end
110+
111+
function QOB.sciml_lazy_operator(op::QOB.AbstractOperator; cache=nothing)
112+
wrapped = _wrap(op.basis_l, op.basis_r, _sciml_operator(op))
113+
cache === nothing && return wrapped
114+
return QOB.cache_sciml_lazy_operator(wrapped, cache)
115+
end
116+
117+
function QOB.sciml_lazy_operator(op::SciMLLazyOperator; cache=nothing)
118+
cache === nothing && return op
119+
return QOB.cache_sciml_lazy_operator(op, cache)
120+
end
121+
122+
function QOB.cache_sciml_lazy_operator(op::SciMLLazyOperator, state::Ket)
123+
return QOB.cache_sciml_lazy_operator(op, state.data)
124+
end
125+
126+
function QOB.cache_sciml_lazy_operator(op::SciMLLazyOperator, data::AbstractVecOrMat)
127+
return _wrap(op.basis_l, op.basis_r, SciMLOperators.cache_operator(op.operator, data))
128+
end
129+
130+
function QOB.cache_sciml_lazy_operator(op::QOB.AbstractOperator, state)
131+
return QOB.cache_sciml_lazy_operator(QOB.sciml_lazy_operator(op), state)
132+
end
133+
134+
function QOB.dense(op::SciMLLazyOperator)
135+
return QOB.DenseOperator(op.basis_l, op.basis_r, Matrix(op.operator))
136+
end
137+
138+
function SparseArrays.sparse(op::SciMLLazyOperator)
139+
return QOB.SparseOperator(op.basis_l, op.basis_r, SparseArrays.sparse(Matrix(op.operator)))
140+
end
141+
142+
function -(op::SciMLLazyOperator)
143+
return _wrap(op.basis_l, op.basis_r, -op.operator)
144+
end
145+
146+
function +(a::SciMLLazyOperator, b::SciMLLazyOperator)
147+
_check_same_bases(a, b)
148+
return _wrap(a.basis_l, a.basis_r, a.operator + b.operator)
149+
end
150+
151+
function -(a::SciMLLazyOperator, b::SciMLLazyOperator)
152+
_check_same_bases(a, b)
153+
return _wrap(a.basis_l, a.basis_r, a.operator - b.operator)
154+
end
155+
156+
function +(a::SciMLLazyOperator, b::QOB.AbstractOperator)
157+
return a + QOB.sciml_lazy_operator(b)
158+
end
159+
160+
function +(a::QOB.AbstractOperator, b::SciMLLazyOperator)
161+
return QOB.sciml_lazy_operator(a) + b
162+
end
163+
164+
function -(a::SciMLLazyOperator, b::QOB.AbstractOperator)
165+
return a - QOB.sciml_lazy_operator(b)
166+
end
167+
168+
function -(a::QOB.AbstractOperator, b::SciMLLazyOperator)
169+
return QOB.sciml_lazy_operator(a) - b
170+
end
171+
172+
function *(a::SciMLLazyOperator, b::SciMLLazyOperator)
173+
_check_multiplicable(a, b)
174+
return _wrap(a.basis_l, b.basis_r, a.operator * b.operator)
175+
end
176+
177+
function *(a::SciMLLazyOperator{B1,B2}, b::QOB.Operator{B2,B3,T}) where {B1,B2,B3,T}
178+
return a * QOB.sciml_lazy_operator(b)
179+
end
180+
181+
function *(a::QOB.Operator{B1,B2,T}, b::SciMLLazyOperator{B2,B3}) where {B1,B2,B3,T}
182+
return QOB.sciml_lazy_operator(a) * b
183+
end
184+
185+
function *(a::SciMLLazyOperator, b::QOB.AbstractOperator)
186+
return a * QOB.sciml_lazy_operator(b)
187+
end
188+
189+
function *(a::QOB.AbstractOperator, b::SciMLLazyOperator)
190+
return QOB.sciml_lazy_operator(a) * b
191+
end
192+
193+
*(a::Number, b::SciMLLazyOperator) = _wrap(b.basis_l, b.basis_r, a * b.operator)
194+
*(a::SciMLLazyOperator, b::Number) = _wrap(a.basis_l, a.basis_r, a.operator * b)
195+
/(a::SciMLLazyOperator, b::Number) = _wrap(a.basis_l, a.basis_r, a.operator / b)
196+
197+
function QOB.dagger(op::SciMLLazyOperator)
198+
return _wrap(op.basis_r, op.basis_l, adjoint(op.operator))
199+
end
200+
201+
function transpose(op::SciMLLazyOperator)
202+
return _wrap(op.basis_r, op.basis_l, transpose(op.operator))
203+
end
204+
205+
function _operator_for_mul(op, data)
206+
SciMLOperators.iscached(op) && return op
207+
return SciMLOperators.cache_operator(op, data)
208+
end
209+
210+
function mul!(result::Ket{B1}, op::SciMLLazyOperator{B1,B2}, state::Ket{B2}, alpha, beta) where {B1,B2}
211+
sciml_op = _operator_for_mul(op.operator, state.data)
212+
if isone(alpha) && iszero(beta)
213+
mul!(result.data, sciml_op, state.data)
214+
return result
215+
end
216+
mul!(result.data, sciml_op, state.data, alpha, beta)
217+
return result
218+
end
219+
220+
function mul!(result::Bra{B2}, state::Bra{B1}, op::SciMLLazyOperator{B1,B2}, alpha, beta) where {B1,B2}
221+
sciml_op = _operator_for_mul(transpose(op.operator), state.data)
222+
if isone(alpha) && iszero(beta)
223+
mul!(result.data, sciml_op, state.data)
224+
return result
225+
end
226+
mul!(result.data, sciml_op, state.data, alpha, beta)
227+
return result
228+
end
229+
230+
end # module

0 commit comments

Comments
 (0)