Skip to content

Add an efficient LazyEmbed / embed_lazy API #523

Description

@Krastanov

This issue is part of unitaryHACK26. You have to be registered to complete this issue.
Learn more about the PR submission process here and about unitaryHACK rules here!

A note about AI Slop: while we are open to collaboration with LLMs for unitaryHACK, fully AI-generated PRs are not acceptable. It is at the maintainers' discretion whether or not LLM-generated PRs are the right fit for the issues, and those that appear fully AI-generated may be immediately rejected. Read unitaryHACK's full AI Policy here.

Note from the maintainer: If you have not contributed before to open source projects, no AI agent use is permitted for this issue.


Context

QuantumOpticsBase.jl has two related concepts today:

  • embed(...), which embeds an operator into a larger composite basis.
  • LazyTensor, which can represent local tensor-product structure without
    materializing the full matrix.

For dense and sparse operators, embed(...) currently builds the embedded
operator eagerly. That is often much more expensive than necessary when the
embedded operator will be applied to a state or used as one term in a Hamiltonian.
For example, embedding a one-site operator in a many-site spin chain should not
need to allocate the full 2^n x 2^n sparse matrix if a lazy representation can
do the same application directly.

There was an old starting-point PR for this idea:

That PR proposed a small embed_lazy helper:

embed_lazy(b::Basis, i, op::AbstractOperator) = LazyTensor(b, i, op)

function embed_lazy(b::Basis, i, op::LazySum)
    _embed_ops(b, i, ops::Tuple) = ((embed_lazy(b, i, o) for o in ops)...,)
    _embed_ops(b, i, ops) = [embed_lazy(b, i, o) for o in ops]
    LazySum(b, b, op.factors, _embed_ops(b, i, op.operators))
end

embed_lazy(b::Basis, indices, op::LazyTensor) =
    LazyTensor(b, b, indices, op.operators, op.factor)

This issue is to turn that idea into a robust, tested, documented API.

Relevant current code:

Why this matters

Local operators embedded into composite Hilbert spaces are a common pattern in
quantum optics and spin-chain simulations. The eager embed path is correct and
useful, but it can allocate large sparse matrices that are avoidable for many
workflows.

An efficient lazy embedding API would let users write:

H = embed_lazy(basis, 1, sx) + embed_lazy(basis, 2, sz)

and get a structure-preserving lazy operator suitable for multiplication by
kets, use inside LazySum, and eventual dense/sparse conversion only when
explicitly requested.

Minimal target example

After this change, the following should work:

using Test
using QuantumOpticsBase

b0 = SpinBasis(1//2)
b = tensor(b0, b0, b0, b0)

sx = sigmax(b0)
sz = sigmaz(b0)

op_eager = embed(b, 2, sx)
op_lazy = embed_lazy(b, 2, sx)

psi = Ket(b, randn(ComplexF64, length(b)))

@test op_lazy isa LazyTensor
@test dense(op_lazy) == dense(op_eager)
@test op_lazy * psi == op_eager * psi

local_h = sx + 0.5 * sz
lazy_h = embed_lazy(b, 3, local_h)

@test lazy_h isa LazySum
@test dense(lazy_h) == dense(embed(b, 3, local_h))
@test lazy_h * psi == embed(b, 3, local_h) * psi

Scope

In scope:

  • Add a public API for lazy embedding. The exact name can be decided by
    maintainers, but embed_lazy is the historical name from PR Codecov #88 and is a good
    starting point.
  • Export the public API from QuantumOpticsBase.jl.
  • Implement lazy embedding for:
    • AbstractOperator / DataOperator, returning a LazyTensor when possible,
    • LazyTensor, re-embedding the existing lazy tensor into the larger basis,
    • LazySum, preserving the LazySum and lazily embedding each term,
    • TimeDependentSum, if straightforward, preserving coefficients while
      lazily embedding the static operator terms.
  • Support both single-index and multi-index embedding, matching the existing
    embed behavior where possible.
  • Preserve basis checks and error behavior: incompatible bases should fail
    clearly rather than silently constructing invalid lazy operators.
  • Add tests showing that lazy embedding agrees with eager embed under
    dense(...) and under application to Ket.
  • Add documentation near the existing lazy-operator docs.
  • Add at least small allocation/time checks or benchmarks that demonstrate the
    lazy path avoids materializing the full embedded matrix for representative
    local operators.

Out of scope:

  • Changing the default behavior of embed(...).
  • Replacing LazyTensor, LazySum, or LazyProduct.
  • Supporting every possible AbstractOperator subtype if the subtype cannot be
    represented lazily without materialization.
  • GPU-specific behavior.

API considerations

Please address these points in the PR:

  • Should the public function be named embed_lazy, lazyembed, or should there
    be a LazyEmbed constructor/type?
  • Should embed_lazy live next to embed in the docs, or in the lazy-operator
    section?
  • Should embed_lazy(basis_l, basis_r, indices, op) be supported in addition to
    embed_lazy(basis, indices, op)?
  • What should happen for a LazyProduct? It may be enough to document that this
    is unsupported initially, or to embed each factor lazily when bases make that
    unambiguous.

The implementation should prefer a simple, maintainable API over broad
coverage. It is acceptable to support the common local-operator and LazySum
cases first.

Acceptance criteria

  • embed_lazy or the chosen public API exists and is exported.
  • Embedding a one-site dense or sparse operator into a composite basis returns a
    lazy operator, not an eager embedded sparse matrix.
  • Embedding a LazySum preserves a LazySum of lazy embedded terms.
  • Embedding a LazyTensor into a larger composite basis preserves the existing
    suboperators and factor.
  • The lazy result agrees with eager embed for representative dense conversion
    and Ket application tests.
  • Tests cover single-index and multi-index embedding, dense and sparse local
    operators, LazySum, and at least one incompatible-basis failure.
  • Documentation includes a short example and explains how this differs from
    embed(...) and from directly constructing LazyTensor.

Difficulty

Intermediate. The core implementation can be small, but the hard part is making
the API precise, preserving existing basis semantics, and adding enough tests to
avoid another under-specified lazy embedding helper.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions