Skip to content

Add lazy QuantumOpticsRepr express output#203

Open
GrovyleX wants to merge 1 commit into
QuantumSavory:mainfrom
GrovyleX:lazy-express
Open

Add lazy QuantumOpticsRepr express output#203
GrovyleX wants to merge 1 commit into
QuantumSavory:mainfrom
GrovyleX:lazy-express

Conversation

@GrovyleX

@GrovyleX GrovyleX commented Jun 13, 2026

Copy link
Copy Markdown

What this does

express(x, QuantumOpticsRepr()) has always built eager dense/sparse operators. This adds an
opt-in lazy path - express(x, QuantumOpticsRepr(lazy=true)) - that keeps the structure of the
symbolic expression by emitting QuantumOpticsBase's lazy operator types instead of materializing
matrices:

  • operator sums → LazySum
  • operator products → LazyProduct
  • tensor products → LazyTensor
  • commutators / anticommutators → a LazySum of LazyProducts

The default is untouched: with lazy=false (plain QuantumOpticsRepr()) every method falls
straight back to the existing eager conversion, so express(x) behaves exactly as before. This is
the QuantumSymbolics half of qojulia/QuantumOptics.jl#521; the lazy field on QuantumOpticsRepr
itself is a companion QuantumInterface PR (qojulia/QuantumInterface.jl#85).

How it works

Five express_nolookup methods in ext/QuantumOpticsExt/QuantumOpticsExt.jl, one per symbolic
operator type (SAddOperator, SMulOperator, STensorOperator, SCommutator,
SAnticommutator). I tried to make the output look like what you'd assemble by hand in
QuantumOptics.jl - a Hamiltonian written as a sum of local tensor terms comes out as a LazySum
of LazyTensors. Two things I was deliberate about:

  • Coefficients land in LazySum.factors, read straight from the SAdd term→coefficient dict,
    so 2X + 3Y stays a genuinely weighted sum rather than folding the 2 and 3 into the
    operators.
  • A LazyTensor stores only its non-identity factors. tensor(X, I) becomes
    LazyTensor(b, [1], (σx,)) - the identity on the second subsystem is left implicit, which is
    what LazyTensor is for and where the structure/memory win comes from. If a factor can't sit on
    a single subsystem (e.g. an expressed two-qubit gate on a composite basis), that one tensor
    product falls back to the eager - the "where the basis structure supports it" clause from the
    issue.

Scalar prefactors didn't need their own method: prefactorscalings already lifts them out
symbolically, and multiplying a lazy operator by a number carries them through. I didn't touch any
existing conversion method.

Tests & docs

  • test/general/lazy_express_tests.jl covers each operator type, the identity-omission,
    commutators/anticommutators, the Fock cutoff still applying under lazy=true, the eager
    fallback, and dense(lazy) ≈ eager for every case. It builds the symbolic expressions directly,
    so it's deterministic. Against a QuantumInterface without the lazy field it skips with a note,
    so it stays green on the registry version and runs in full once the field is present.
  • docs/src/express.md gets a short "Lazy operator output" section.
  • Ran the full Pkg.test() locally on Julia 1.12.6 - green, including Aqua and the doctests.

Cross-repo note

This uses the lazy field that QuantumOpticsRepr gains in the companion QuantumInterface PR
(qojulia/QuantumInterface.jl#85). The extension reads it defensively (hasproperty), so this PR
loads and stays fully eager against the released QuantumInterface and resolves from the registry —
the lazy output switches on once that field ships (a dev'd checkout locally, or QuantumInterface
≥0.4.3). No [sources] pin or compat bump, so the two PRs don't block each other for CI.

CHANGELOG

Add a lazy=true option to QuantumOpticsRepr. When set, express returns LazySum,
LazyProduct, and LazyTensor for symbolic operator sums, products, and tensor products (and
lazy sums of products for commutators and anticommutators) instead of materializing dense
matrices. The lazy output is available when the running QuantumInterface provides the lazy
field (v0.4.3 or later).

Part of qojulia/QuantumOptics.jl#521

  • The code is properly formatted and commented.
  • Substantial new functionality is documented within the docs.
  • All new functionality is tested.
  • All of the automated tests on github pass.
  • We recently started enforcing formatting checks. If formatting issues are reported in the new code you have written, please correct them. There will be plenty of old code that
    is flagged as we are slowly transitioning to enforced formatting. Please do not worry about or address older formatting issues -- keep your PR just focused on your planned
    contribution.

If you are submitting for a bug bounty:

matrices. The lazy output is available when the running QuantumInterface provides the lazy
field (v0.4.3 or later).

Part of qojulia/QuantumOptics.jl#521

  • The code is properly formatted and commented.
  • Substantial new functionality is documented within the docs.
  • All new functionality is tested.
  • All of the automated tests on github pass.
  • We recently started enforcing formatting checks. If formatting issues are reported in the new code you have written, please correct them. There will be plenty of old code that
    is flagged as we are slowly transitioning to enforced formatting. Please do not worry about or address older formatting issues -- keep your PR just focused on your planned
    contribution.

If you are submitting for a bug bounty:

AI disclosure

I used AI to help analyze the codebase and draft some of the implementation, but the design calls
were mine - coefficients into LazySum.factors, the identity-omission in LazyTensor, the eager
fallback for multi-subsystem factors - and I checked them against QuantumOpticsBase's lazy-operator
semantics. I wrote and ran the tests, debugged what broke, and went through the whole diff before
opening this. I understand every line and I'm happy to walk through any of it.

With `QuantumOpticsRepr(lazy=true)`, symbolic operator sums, products, and tensor products express to `LazySum`, `LazyProduct`, and `LazyTensor`, and commutators and anticommutators to lazy sums of products. A `LazyTensor` stores only the non-identity factors, preserving the local tensor structure instead of materializing dense matrices. The default eager behaviour is untouched. Includes tests under test/general and a docs section.

Part of qojulia/QuantumOptics.jl#521
@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results (Julia v1)

Time benchmarks
main faca250... main / faca250...
creation/ket 30 ± 0 ns 30 ± 0 ns 1 ± 0
creation/large_trees/10_layers 0.788 ± 0.027 ms 0.812 ± 0.031 ms 0.97 ± 0.05
creation/large_trees/50_layers 0.036 ± 0.0032 s 0.0366 ± 0.0038 s 0.985 ± 0.13
creation/op 30 ± 0 ns 30 ± 0 ns 1 ± 0
creation/super_op 30 ± 0 ns 30 ± 0 ns 1 ± 0
express/clifford/simple_ket 1.28 ± 0.06 μs 1.33 ± 0.06 μs 0.962 ± 0.063
express/clifford/simple_observable 2.35 ± 0.22 μs 2.27 ± 0.17 μs 1.04 ± 0.12
express/optics/pauli_op_4 6.41 ± 0.35 μs 6.48 ± 0.29 μs 0.989 ± 0.07
express/optics/pauli_state_8 2.62 ± 0.12 μs 2.53 ± 0.13 μs 1.04 ± 0.071
express/optics/simple_ket 1.84 ± 0.11 μs 1.15 ± 0.089 μs 1.6 ± 0.16
express/optics/simple_op 1.42 ± 0.11 μs 1.4 ± 0.1 μs 1.01 ± 0.11
linalg/anticommutator 0.36 ± 0.021 μs 0.371 ± 0.02 μs 0.97 ± 0.077
linalg/commutator 0.381 ± 0.03 μs 0.441 ± 0.011 μs 0.864 ± 0.071
linalg/conjugate 0.05 ± 0.01 μs 0.05 ± 0.01 μs 1 ± 0.28
linalg/dagger 0.05 ± 0.01 μs 0.05 ± 0.01 μs 1 ± 0.28
linalg/inverse 0.05 ± 0.01 μs 0.05 ± 0.01 μs 1 ± 0.28
linalg/ptrace 12.2 ± 0.24 μs 12.6 ± 0.23 μs 0.968 ± 0.026
linalg/trace 0.05 ± 0.01 μs 0.05 ± 0.01 μs 1 ± 0.28
linalg/transpose 0.05 ± 0.01 μs 0.05 ± 0.01 μs 1 ± 0.28
manipulation/expand/commutator 1.58 ± 0.24 ms 1.59 ± 0.23 ms 0.996 ± 0.21
manipulation/expand/distribution 1.47 ± 0.061 ms 1.5 ± 0.14 ms 0.983 ± 0.1
manipulation/simplify/applicable_rules 4.66 ± 0.082 ms 4.68 ± 0.081 ms 0.995 ± 0.025
manipulation/simplify/commutator 0.0377 ± 0.0006 ms 0.0371 ± 0.00061 ms 1.02 ± 0.023
manipulation/simplify/irrelevant_rules 0.745 ± 0.02 ms 0.749 ± 0.015 ms 0.995 ± 0.033
operations/addition/ket 1.26 ± 0.06 μs 1.21 ± 0.051 μs 1.04 ± 0.066
operations/addition/op 1.19 ± 0.07 μs 1.19 ± 0.05 μs 1 ± 0.072
operations/multiplication/bra_ket 1.12 ± 0.03 μs 1.14 ± 0.02 μs 0.982 ± 0.031
operations/multiplication/bra_op 1.17 ± 0.021 μs 1.28 ± 0.03 μs 0.914 ± 0.027
operations/multiplication/inner 1.12 ± 0.03 μs 1.14 ± 0.03 μs 0.982 ± 0.037
operations/multiplication/many 8.3 ± 0.19 μs 8.01 ± 0.18 μs 1.04 ± 0.033
operations/multiplication/op 1.03 ± 0.04 μs 0.992 ± 0.031 μs 1.04 ± 0.052
operations/multiplication/op_ket 1.18 ± 0.03 μs 1.16 ± 0.03 μs 1.02 ± 0.037
operations/multiplication/outer 1.2 ± 0.03 μs 1.2 ± 0.38 μs 0.999 ± 0.32
operations/scaling/ket 0.07 ± 0 μs 0.07 ± 0 μs 1 ± 0
operations/scaling/op 0.07 ± 0 μs 0.07 ± 0 μs 1 ± 0
operations/tensor/ket 0.961 ± 0.021 μs 0.922 ± 0.021 μs 1.04 ± 0.033
operations/tensor/many 8.47 ± 0.2 μs 8.27 ± 0.19 μs 1.02 ± 0.034
operations/tensor/op 0.942 ± 0.03 μs 0.901 ± 0.021 μs 1.05 ± 0.041
time_to_load 1.59 ± 0.0065 s 1.63 ± 0.048 s 0.979 ± 0.029
Memory benchmarks
main faca250... main / faca250...
creation/ket 0 allocs: 0 B 0 allocs: 0 B
creation/large_trees/10_layers 11.6 k allocs: 0.283 MB 11.6 k allocs: 0.283 MB 1
creation/large_trees/50_layers 0.662 M allocs: 13 MB 0.662 M allocs: 13 MB 1
creation/op 0 allocs: 0 B 0 allocs: 0 B
creation/super_op 0 allocs: 0 B 0 allocs: 0 B
express/clifford/simple_ket 16 allocs: 0.641 kB 16 allocs: 0.641 kB 1
express/clifford/simple_observable 0.06 k allocs: 2.66 kB 0.06 k allocs: 2.66 kB 1
express/optics/pauli_op_4 0.055 k allocs: 3.72 kB 0.055 k allocs: 3.72 kB 1
express/optics/pauli_state_8 31 allocs: 1.27 kB 31 allocs: 1.27 kB 1
express/optics/simple_ket 20 allocs: 0.75 kB 20 allocs: 0.75 kB 1
express/optics/simple_op 26 allocs: 1.09 kB 26 allocs: 1.09 kB 1
linalg/anticommutator 11 allocs: 0.438 kB 11 allocs: 0.438 kB 1
linalg/commutator 11 allocs: 0.438 kB 11 allocs: 0.438 kB 1
linalg/conjugate 3 allocs: 0.125 kB 3 allocs: 0.125 kB 1
linalg/dagger 3 allocs: 0.125 kB 3 allocs: 0.125 kB 1
linalg/inverse 3 allocs: 0.125 kB 3 allocs: 0.125 kB 1
linalg/ptrace 0.098 k allocs: 3.56 kB 0.098 k allocs: 3.56 kB 1
linalg/trace 3 allocs: 0.125 kB 3 allocs: 0.125 kB 1
linalg/transpose 3 allocs: 0.125 kB 3 allocs: 0.125 kB 1
manipulation/expand/commutator 14.5 k allocs: 0.454 MB 14.5 k allocs: 0.453 MB 1
manipulation/expand/distribution 15.4 k allocs: 0.492 MB 15.4 k allocs: 0.492 MB 1
manipulation/simplify/applicable_rules 0.0502 M allocs: 1.69 MB 0.0501 M allocs: 1.69 MB 1
manipulation/simplify/commutator 0.444 k allocs: 14.1 kB 0.444 k allocs: 14.1 kB 1
manipulation/simplify/irrelevant_rules 8.51 k allocs: 0.268 MB 8.51 k allocs: 0.268 MB 1
operations/addition/ket 25 allocs: 1.48 kB 25 allocs: 1.48 kB 1
operations/addition/op 25 allocs: 1.48 kB 25 allocs: 1.48 kB 1
operations/multiplication/bra_ket 12 allocs: 0.453 kB 12 allocs: 0.453 kB 1
operations/multiplication/bra_op 13 allocs: 0.484 kB 13 allocs: 0.484 kB 1
operations/multiplication/inner 12 allocs: 0.453 kB 12 allocs: 0.453 kB 1
operations/multiplication/many 0.083 k allocs: 3.17 kB 0.083 k allocs: 3.17 kB 1
operations/multiplication/op 12 allocs: 0.469 kB 12 allocs: 0.469 kB 1
operations/multiplication/op_ket 13 allocs: 0.484 kB 13 allocs: 0.484 kB 1
operations/multiplication/outer 13 allocs: 0.484 kB 13 allocs: 0.484 kB 1
operations/scaling/ket 4 allocs: 0.156 kB 4 allocs: 0.156 kB 1
operations/scaling/op 4 allocs: 0.156 kB 4 allocs: 0.156 kB 1
operations/tensor/ket 12 allocs: 0.469 kB 12 allocs: 0.469 kB 1
operations/tensor/many 0.078 k allocs: 3.44 kB 0.078 k allocs: 3.44 kB 1
operations/tensor/op 12 allocs: 0.469 kB 12 allocs: 0.469 kB 1
time_to_load 0.149 k allocs: 11.2 kB 0.149 k allocs: 11.2 kB 1

@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 26.92308% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.46%. Comparing base (b3305f6) to head (faca250).

Files with missing lines Patch % Lines
ext/QuantumOpticsExt/QuantumOpticsExt.jl 26.92% 19 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #203      +/-   ##
==========================================
- Coverage   68.53%   67.46%   -1.08%     
==========================================
  Files          21       21              
  Lines         982     1008      +26     
==========================================
+ Hits          673      680       +7     
- Misses        309      328      +19     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant