Extending the package
The package has two extension points. A new operator kind is added inside the package, since every operator is one concrete struct. A numeric backend is added from outside, by implementing a small interface.
Adding an operator kind
Every operator is an Op, and its role is the kind field of type OpKind. The algebra never inspects an operator directly. It talks to operators through six functions in src/operators/operators.jl, each a single method on (::Op, ::Op) that branches on kind:
| Function | Returns | Purpose |
|---|---|---|
site_compare(a, b, ne) | Less, Equal, Undetermined or Greater | site relation of two operators, which drives the sort |
can_commute(a, b) | Bool | whether a same-site pair may swap without a residual |
commute_pair(a, b) | (b, a, c1, ops1, c2, ops2) | the swap and up to two residual terms $c_1\,\text{ops}_1 + c_2\,\text{ops}_2$ |
may_reduce(a, b) | Bool | whether reduce_pair can do anything, a cheap pre-check |
reduce_pair(a, b) | (kind, op, c) | a local identity: the pair becomes one operator, a number, or stays |
ground_state_expand(op) | (is_gs, g, n, site) | the completeness data of a ground-state projector |
Two residual slots exist because a collective commutator $[S^{ij}, S^{kl}] = \delta_{jk} S^{il} - \delta_{li} S^{kj}$ has two terms; the other kinds fill only the first.
A new kind touches these places:
- The kind. An
OP_*member ofOpKindinsrc/operators/op.jl, anis_*predicate, and an export. - The constructor. A function in the file of its Hilbert space that validates the arguments and returns an
Op. Levels or axes go in the shared fieldsl1,l2,g,nlev; a kind that needs none leaves them zero. - The algebra. A branch in each function of the table that the kind needs. Kinds on one space that interact, like annihilation and creation, form a family in
site_family. - Adjoint. A branch of
adjointinsrc/operators/operators.jl.order_keyreads the shared fields and needs no change, as long as distinct operators of the kind differ in those fields. - Numerics. A branch of
numeric_operatorin every backend underext/, or a deliberateArgumentErrorwhere a backend has no representation. - Printing. The Unicode printer in
src/printing/printing.jland the LaTeX recipe insrc/printing/latexify_recipes.jl. - Tests. Construction in
test/fundamentals/, the algebra rules intest/algebra/, the conversion intest/numeric/, and the output intest/presentation/. Theorder_keycontract test intest/fundamentals/ordering_test.jlloops over a list of operators, which needs one of the new kind.
An extra payload field on Op, for instance a Union{Nothing, …}, is not an option: Op must stay a concrete bits type, which the quality tests enforce.
Adding a numeric backend
The numeric core in src/numeric/ converts an expression into a list of terms, each a ComplexF64 coefficient with a list of operator factors, and hands them to a backend. A backend is a singleton subtype of NumericBackend with methods for the following functions:
| Function | Responsibility |
|---|---|
numeric_basis(be, h, dims) | build the full basis from a Hilbert space and truncations |
numeric_num_subsystems(be, basis) | number of tensor factors |
numeric_subbasis(be, basis, slot) | one tensor factor |
numeric_operator(be, op, subbasis) | the matrix of one operator |
numeric_embed(be, basis, slot, leaf) | a factor placed in the full space |
numeric_identity(be, basis) | the identity on the full space |
numeric_assemble(be, basis, terms) | the sum of the terms, possibly lazy |
numeric_materialize(be, assembled, op_type) | the representation returned to the user |
numeric_materialize gives op_type a fixed meaning: EagerOperator() returns the backend's ordinary operator, identity returns the assembled object unchanged, and any other function is applied to it. Materialization happens once, after assembly, so a single operator, a product and a sum come out in the same representation.
A minimal backend:
import SecondQuantizedAlgebra as SQAstruct MyBackend <: SQA.NumericBackend endSQA.numeric_basis(::MyBackend, h::SQA.HilbertSpace, dims) = error("build the basis")SQA.numeric_num_subsystems(::MyBackend, basis) = error("count subsystems")SQA.numeric_subbasis(::MyBackend, basis, slot::Int) = error("select a subsystem")SQA.numeric_operator(::MyBackend, op::SQA.Op, subbasis) = error("construct a leaf")SQA.numeric_embed(::MyBackend, basis, slot::Int, leaf) = error("embed a leaf")SQA.numeric_identity(::MyBackend, basis) = error("construct the identity")SQA.numeric_assemble(::MyBackend, basis, terms) = error("assemble terms")SQA.numeric_materialize(::MyBackend, assembled, ::SQA.EagerOperator) = error("make an eager operator")SQA.numeric_materialize(::MyBackend, assembled, ::typeof(identity)) = assembledIt is used as to_numeric(H, h, dims; backend = MyBackend()); only the two bundled backends are found automatically. Further features need further methods:
| Feature | Additional methods |
|---|---|
| time-dependent parameters | numeric_assemble_td |
| conversion with a state in place of a basis | numeric_backend(state), numeric_basis(state) |
numeric_average and expect | the state methods and numeric_expect |
superoperators and to_numeric_master | numeric_superoperator, numeric_matrix, and numeric_scaled_td for time-dependent collapse operators |
Two rules keep a backend fast and inferable. The type of the assembled object must not depend on the number of terms, so a vector-backed lazy sum is used rather than a tuple. And a time-dependent coefficient that reads the solver parameters is a two-argument function (p, t) -> value, recognized by its arity rather than by a wrapper type. The two bundled extensions under ext/ are complete examples.
SecondQuantizedAlgebra.NumericBackend — Type
NumericBackendAbstract supertype for numeric-conversion backends.
A backend is normally a concrete singleton whose methods implement the numeric hooks. See Adding a numeric backend for the interface.
Bundled implementations are selected with QuantumOpticsBackend or QuantumToolboxBackend after loading the corresponding package.
SecondQuantizedAlgebra.numeric_basis — Function
numeric_basis(be, h::HilbertSpace, dims)Build the full numeric basis for Hilbert space h with user dimensions dims (Fock cutoff / spin number; ignored for spaces that carry their own level count). Returns a Basis (QuantumOptics) or integer dims (QuantumToolbox).
SecondQuantizedAlgebra.numeric_num_subsystems — Function
numeric_num_subsystems(be, basis) -> IntNumber of tensor-product subsystems represented by basis. Backends use this hook to validate indexed-site layouts before emitting any numeric operators.
SecondQuantizedAlgebra.numeric_subbasis — Function
numeric_subbasis(be, basis, slot::Int)The subsystem basis for slot of a composite basis (the basis itself for a simple one).
SecondQuantizedAlgebra.numeric_operator — Function
numeric_operator(be, op::Op, subbasis)Map a single-site operator op to its numeric matrix on subbasis (the subsystem basis for op's slot). Backends implement it as a closed k === OP_* value-branch on op.kind.
SecondQuantizedAlgebra.numeric_embed — Function
numeric_embed(be, basis, slot::Int, leaf)Place the single-site numeric operator leaf at slot of basis, returning an operator on the full space (the leaf unchanged for a simple basis, a lazy/concrete tensor for a composite one).
SecondQuantizedAlgebra.numeric_identity — Function
numeric_identity(be, basis)The identity operator on basis (lazy on a composite basis).
SecondQuantizedAlgebra.numeric_assemble — Function
numeric_assemble(be, basis, terms)Assemble a static lazy operator from a backend-neutral terms list. Each entry is (c::ComplexF64, factors::Vector{<leaf>}) with factors already embedded at their slots. The result is one concrete vector-backed lazy type for any term count.
SecondQuantizedAlgebra.numeric_assemble_td — Function
numeric_assemble_td(be, basis, td_terms)Time-dependent counterpart of numeric_assemble.
Each coefficient is Union{ComplexF64, Function}: a constant, a t -> ComplexF64, or a solver-parameter-aware (p, t) -> ComplexF64. The result is the backend's native time-dependent operator.
SecondQuantizedAlgebra.numeric_materialize — Function
numeric_materialize(be, op, op_type)Materialize a static lazily assembled operator op according to op_type, applied exactly once at the public boundary. Implementations must treat EagerOperator as a request for their ordinary eager operator and identity as a request for op unchanged. Other callables are backend-defined. The eager representation need not be sparse, although both bundled backends choose sparse storage.
This hook is not used by numeric_average, which consumes the lazy assembly, or by time-dependent conversion, which returns the backend's native time-dependent operator.
SecondQuantizedAlgebra.EagerOperator — Type
EagerOperator()The default op_type: request the backend's ordinary eager operator.
SecondQuantizedAlgebra.numeric_expect — Function
numeric_expect(be, numop, state)Expectation value ⟨state| numop |state⟩ (a ComplexF64), or a Vector{ComplexF64} when state is a vector of states.
SecondQuantizedAlgebra.numeric_backend — Function
numeric_backend(state) -> NumericBackendReturn the numeric backend owning state. Third-party backends must implement this hook for their state types to support numeric_average and state-based to_numeric.
SecondQuantizedAlgebra.numeric_superoperator — Function
numeric_superoperator(be, ctx::NumericContext, terms::Vector{Tuple{ComplexF64, Vector{Op}, Vector{Op}}})Build the backend's eager superoperator Σ c * sprepost(L, R) from sandwich terms. Each side is converted with the ordinary operator path of ctx.
SecondQuantizedAlgebra.numeric_matrix — Function
numeric_matrix(be, op) -> AbstractMatrixThe matrix of an eager backend operator, used for the Hermiticity check of to_numeric_master.
SecondQuantizedAlgebra.numeric_scaled_td — Function
numeric_scaled_td(be, op, f)Wrap the static backend operator op with the time-dependent scalar coefficient f, a t -> value or (p, t) -> value closure. Only backends with time-dependent collapse operators implement it.
SecondQuantizedAlgebra.NumericContext — Type
NumericContext(backend, basis, op_subs, scalar_subs, sites)Settings of one numeric conversion, passed through the numeric core.
It holds the backend singleton, the opaque per-backend basis, the operator-substitution dict op_subs, the indexed scalar_subs, and the indexed sites map (empty for the non-indexed path). All fields are concrete via the type parameters, so the context never widens inference.