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:

FunctionReturnsPurpose
site_compare(a, b, ne)Less, Equal, Undetermined or Greatersite relation of two operators, which drives the sort
can_commute(a, b)Boolwhether 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)Boolwhether 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:

  1. The kind. An OP_* member of OpKind in src/operators/op.jl, an is_* predicate, and an export.
  2. 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 fields l1, l2, g, nlev; a kind that needs none leaves them zero.
  3. 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.
  4. Adjoint. A branch of adjoint in src/operators/operators.jl. order_key reads the shared fields and needs no change, as long as distinct operators of the kind differ in those fields.
  5. Numerics. A branch of numeric_operator in every backend under ext/, or a deliberate ArgumentError where a backend has no representation.
  6. Printing. The Unicode printer in src/printing/printing.jl and the LaTeX recipe in src/printing/latexify_recipes.jl.
  7. Tests. Construction in test/fundamentals/, the algebra rules in test/algebra/, the conversion in test/numeric/, and the output in test/presentation/. The order_key contract test in test/fundamentals/ordering_test.jl loops 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:

FunctionResponsibility
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)) = assembled

It is used as to_numeric(H, h, dims; backend = MyBackend()); only the two bundled backends are found automatically. Further features need further methods:

FeatureAdditional methods
time-dependent parametersnumeric_assemble_td
conversion with a state in place of a basisnumeric_backend(state), numeric_basis(state)
numeric_average and expectthe state methods and numeric_expect
superoperators and to_numeric_masternumeric_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.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).

source
SecondQuantizedAlgebra.numeric_num_subsystems — Function
numeric_num_subsystems(be, basis) -> Int

Number of tensor-product subsystems represented by basis. Backends use this hook to validate indexed-site layouts before emitting any numeric operators.

source
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.

source
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).

source
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.

source
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.

source
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.

source
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.

source
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.

source