Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# News

## [Unreleased]

- Add lazy QuantumOptics conversion for sums, products, and tensor products when using `QuantumOpticsRepr(lazy=true)`.

## v0.4.16 - 2026-04-01

- Add `QuantumClifford.Register` symbolic `apply!` methods to the `QuantumClifford` extension.
Expand Down
15 changes: 14 additions & 1 deletion docs/src/express.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,17 @@ Operator(dim=5x5)
0.0+0.0im 0.0+0.0im 2.0+0.0im 0.0+0.0im 0.0+0.0im
0.0+0.0im 0.0+0.0im 0.0+0.0im 3.0+0.0im 0.0+0.0im
0.0+0.0im 0.0+0.0im 0.0+0.0im 0.0+0.0im 4.0+0.0im
```
```

Operator expressions can also be converted to lazy QuantumOptics operators by
passing `QuantumOpticsRepr(lazy=true)`. In this mode, symbolic sums, products,
and tensor products are represented as `LazySum`, `LazyProduct`, and
`LazyTensor` respectively, while `dense` can still be used to materialize the
ordinary operator when needed.

```julia
julia> using QuantumOpticsBase: LazyTensor

julia> express(QuantumSymbolics.X ⊗ (QuantumSymbolics.Y + QuantumSymbolics.Z), QuantumOpticsRepr(lazy=true)) isa LazyTensor
true
```
42 changes: 41 additions & 1 deletion ext/QuantumOpticsExt/QuantumOpticsExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ using QuantumSymbolics:
NumberOp, CreateOp, DestroyOp,
FockState,
MixedState, IdentityOp,
qubit_basis
qubit_basis,
SAddOperator, SScaledOperator, SMulOperator, STensorOperator
import QuantumSymbolics: express, express_nolookup
using TermInterface
using TermInterface: isexpr, head, operation, arguments, metadata
Expand Down Expand Up @@ -43,6 +44,45 @@ const _ad₂ = create(_bf2)
const _a₂ = destroy(_bf2)
const _n₂ = number(_bf2)

_is_lazy(r::QuantumOpticsRepr) = hasproperty(r, :lazy) && r.lazy
_express_eager(x, r::QuantumOpticsRepr) = operation(x)(express.(arguments(x), (r,))...)
_lazytensor_suboperator(op::DataOperator) = op
_lazytensor_suboperator(op) = dense(op)

function express_nolookup(x::SScaledOperator, r::QuantumOpticsRepr)
x.coeff * express(x.obj, r)
end

function express_nolookup(x::SAddOperator, r::QuantumOpticsRepr)
if !_is_lazy(r)
return _express_eager(x, r)
end

terms = collect(x.dict)
factors = [coeff for (_, coeff) in terms]
operators = Tuple(express(obj, r) for (obj, _) in terms)
LazySum(factors, operators)
end

function express_nolookup(x::SMulOperator, r::QuantumOpticsRepr)
if !_is_lazy(r)
return _express_eager(x, r)
end

LazyProduct(Tuple(express(op, r) for op in arguments(x)))
end

function express_nolookup(x::STensorOperator, r::QuantumOpticsRepr)
if !_is_lazy(r)
return _express_eager(x, r)
end

operators = Tuple(_lazytensor_suboperator(express(op, r)) for op in arguments(x))
basis_l = tensor((op.basis_l for op in operators)...)
basis_r = tensor((op.basis_r for op in operators)...)
LazyTensor(basis_l, basis_r, collect(1:length(operators)), operators)
end

express_nolookup(::HGate, ::QuantumOpticsRepr) = _hadamard
express_nolookup(::XGate, ::QuantumOpticsRepr) = _x
express_nolookup(::YGate, ::QuantumOpticsRepr) = _y
Expand Down
59 changes: 59 additions & 0 deletions test/general/qo_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,62 @@ using QuantumSymbolics
@test embed(b,b,[1],l2)*opt0 ≈ (spre(op21)*spost(op22)*op0)⊗op0a⊗op0b
@test embed(b,b,[1],l2+l3)*opt0 ≈ (spre(op21)*spost(op22)*op0 + spre(op31)*spost(op32)*op0)⊗op0a⊗op0b
end

if !hasproperty(QuantumOpticsRepr(), :lazy)
# Exercise the lazy conversion path before QuantumInterface releases the native flag.
const _test_lazy_quantumoptics_repr_key = :QuantumSymbolicsTestLazyQuantumOpticsRepr

Base.propertynames(r::QuantumOpticsRepr, private::Bool=false) = (:cutoff, :lazy)
function Base.getproperty(r::QuantumOpticsRepr, name::Symbol)
name === :lazy && return get(task_local_storage(), _test_lazy_quantumoptics_repr_key, false)
return getfield(r, name)
end
_test_lazy_repr() = QuantumOpticsRepr()
_with_test_lazy_repr(f) = task_local_storage(f, _test_lazy_quantumoptics_repr_key, true)
_with_test_eager_repr(f) = task_local_storage(f, _test_lazy_quantumoptics_repr_key, false)
else
_test_lazy_repr() = QuantumOpticsRepr(lazy=true)
_with_test_lazy_repr(f) = f()
_with_test_eager_repr(f) = f()
end

_test_eager_qo(x) = _with_test_eager_repr() do
express_nolookup(x, QuantumOpticsRepr())
end

@testset "Lazy QuantumOpticsRepr" begin
using QuantumOpticsBase

_with_test_lazy_repr() do
repr = _test_lazy_repr()

sum_expr = X + 2Y + Z
lazy_sum = express(sum_expr, repr)
@test lazy_sum isa LazySum
@test length(lazy_sum.operators) == 3
@test dense(lazy_sum) ≈ dense(_test_eager_qo(sum_expr))

product_expr = X * (Y + Z)
lazy_product = express(product_expr, repr)
@test lazy_product isa LazyProduct
@test lazy_product.operators[2] isa LazySum
@test dense(lazy_product) ≈ dense(_test_eager_qo(product_expr))

tensor_expr = X ⊗ Y ⊗ Z
lazy_tensor = express(tensor_expr, repr)
@test lazy_tensor isa LazyTensor
@test length(lazy_tensor.operators) == 3
@test dense(lazy_tensor) ≈ dense(_test_eager_qo(tensor_expr))

nested_tensor_expr = X ⊗ (Y + Z)
nested_lazy_tensor = express(nested_tensor_expr, repr)
@test nested_lazy_tensor isa LazyTensor
@test all(op -> op isa DataOperator, nested_lazy_tensor.operators)
@test dense(nested_lazy_tensor) ≈ dense(_test_eager_qo(nested_tensor_expr))

@test express(3 * product_expr, repr) isa LazyProduct
@test express(3 * product_expr, repr).factor == 3
@test express(3 * tensor_expr, repr) isa LazyTensor
@test express(3 * tensor_expr, repr).factor == 3
end
end
4 changes: 4 additions & 0 deletions test/projects/jet/Project.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[deps]
JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
LoweredCodeUtils = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b"
ParallelTestRunner = "d3525ed8-44d0-4b2c-a655-542cee43accc"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
QuantumClifford = "0525e862-1e90-11e9-3e4d-1b39d7109de1"
Expand All @@ -9,3 +10,6 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[sources]
QuantumSymbolics = {path = "../../.."}

[compat]
LoweredCodeUtils = "=3.5.3"
Loading