Changelog
All notable changes to SecondQuantizedAlgebra.jl will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[v0.9.0]
Added
substitutenow replaces operators as well as scalar parameters in a single, simultaneous pass. A rule dictionary is split by key:QSymkeys replace operator leaves, every other key substitutes into coefficients through SymbolicUtils. Operator replacements are applied once to the original leaves and the replacement expression is not searched again for operator keys, so a mode transformation such asa => g*a + h*bis well-defined even though its right-hand side contains the keya. Missing adjoint rules are generated automatically (a => balso impliesa' => b'); passreplace_adjoint=falseto match only the keys supplied explicitly.- A keyword form of
to_numeric(op, basis; parameter, time_parameter, operators, adjoint_ops, op_type). Scalarparameters are substituted first, then the expression is translated with custom numericoperators(missing adjoints are added whenadjoint_ops=true), and each emitted operator is passed throughop_type. Whentime_parameteris non-empty the result is a closuret -> op(t); its values may be numbers or functions of time, and a key may be a bare variablevorconj(v). A vector formto_numeric(ops::AbstractVector, basis; kwargs...)forwards the keywords to each element. to_num(c::Coeff)is now public and documented as the supported way to read the coefficient returned when iterating aQAdd.CoeffandCNumare now declared public, and the package's public-API declarations use SciMLPublic.jl instead of a hand-rolled@publicmacro.
[v0.8.3]
Changed
averageof a many-term operator is now O(n) instead of O(n²): a 500-term sum drops from about 31 ms to 9.6 ms.- Wide symbolic prefactors (large sums or products of parameters, as high-order perturbation theory generates) build in one pass instead of pairwise, O(n²) → O(n log n): at n=800 about 12.5× faster for a sum, 17× for a product.
[v0.8.2]
Changed
commutator(::QAdd, ::QSym)andcommutator(::QAdd, ::QAdd)on operators without summation indices now stream each term-pair contribution straight into one shared result dict instead of materializing intermediates. The previous code wrapped every term of the left operand in a temporary single-termQAddand builta*b,b*a, and their difference as separateQAdds before merging into the accumulator, so a commutator over an n-term operand allocated O(n) throwaway dicts (O(n·m) for two n- and m-term sums). The fast path emits+tₐ·t_band−t_b·tₐdirectly through_emit_product!, which already runs the eager canonicalization, and lets the dict collect like terms. Operators that carry summation indices keep the existing path, where the per-term_absorb_pinned_sumsindex-scope bookkeeping must run. Results are byte-identical toa*b - b*a. Measured on the many-mode operatorΣ_{i,j} g aᵢ† aⱼ:QAdd×QSymabout 1.9× to 2.2× faster with about 5× less memory, andQAdd×QAddabout 2.2× faster with about 5× less memory at M=4/8/12.
Fixed
conjnow folds as an involution in the coefficient algebra and inqadjoint. Conjugating a complex factor twice (or taking the adjoint of a scalarconj(x)) returned a nestedconj(conj(x))that never simplified and survived downstream, leaving dead terms (e.g. aconj(0.0)that failed to collapse to zero after a complex coupling was substituted to a real value). Double conjugation now returns the original factor, andconjof an expression that reduces to a constant folds to a native coefficient.
[v0.8.1]
Added
sumandreduce(+, …)over aVector{QAdd}now accumulate in place instead of folding withBase.:+. Building an n-termQAddby repeated addition is O(n²) because every+copies the whole backing dict before inserting; the new_QAddBuilder(exposed through the MutableArithmetics.jl interface, soMA.@rewriteandoperate!!loops work too) threads one shared dict through the entire reduction and materializes once. Results are byte-identical to the+-chain andBase.:+value semantics are unchanged. The fast path is the bracketed comprehensionsum([ω[k]*a[k]'*a[k] for k in 1:M])(a bare generator stays on the generic fold). Measured on the many-mode HamiltonianΣ_k ω_k a_k† a_k: about 3.9×/5.9×/11.8× faster and 5.5×/6.2×/11.4× less memory at M=8/16/24, the win growing with system size. The product path is intentionally left as repeated*(a prototype builder showed no win; the intrinsic distributive expansion dominates), see the devdocs.
Changed
hash(::Op)short-circuits the common non-indexed case. A profile of the product path found that about a third of the time goes to hashingQTermdict keys, andhash(::Op)was recursing through the sharedNO_INDEXconstant'sNumfields on every call even though most operators carry no index. The hash now foldsNO_INDEXto a precomputed sentinel and only hashes a realIndexwhen one is present. The per-operator hash is about 31% faster and term-building operations (products, powers, commutators,substitute) are about 5% to 6% faster end-to-end; for example(a + a†)^12drops from about 379 µs to 344 µs. Hash values are internal dict keys only (never persisted) andisequalis unchanged, so canonical results are identical.
[v0.8.0]
Changed
Breaking: the seven operator types collapsed into one concrete
Op.Destroy,Create,Transition,Pauli,Spin,Position, andMomentumwere separateQSymsubtypes; they are now a single concretestruct Op <: QSymcarrying akind::OpKindtag plus shared packed fields. The role names stay as constructor functions, so every existingDestroy(h, :a)orTransition(h, :σ, 1, 2)call still builds the right operator with no source change. What breaks is type-level dispatch: code that branched onisa Destroy(or used::Transitionmethod signatures, ortypeof(op) == Pauli) must switch to the exported predicatesis_destroy,is_create,is_transition,is_pauli,is_spin,is_position,is_momentum, or read the role withoptype(op)::OpKind. Direct field reads also moved: aTransition'si/j/ground_state/n_levelsare nowl1/l2/g/nlev, and aPauli/Spinaxisisl1.Opand the predicates are exported. Because operator products are now stored as a concreteVector{Op}(QTerm.ops), the per-operator hooks (_site_compare,_can_commute,_commute_pair,_reduce_pair,hash) dispatch and inline statically instead of through the former abstract-QSymdynamic dispatch, and_commute_pair/_reduce_pairreturn concreteTuple{Op, …}with no tuple boxing; customhash/isequalonOpexclude the index'sNumfields. Measured speedups over the v0.7.1 baseline from the operator collapse alone: Fock(a·a†)^10about 2.7×, single-modeH^4about 2.1×, many-modeH^2(M=8) about 1.7×, with roughly 30% fewer allocations on the Fock-dominated cases (#164).Parameter-polynomial coefficient arithmetic (the
Polytail ofCoeff) allocates less: single-term times single-term (the commonω·Jcase) skips the sort/compact pass, term canonicalisation compacts its owned buffer in place instead of allocating a second vector, and a scalar-times-term product shares the other operand's factor vectors instead of re-merging. Results are byte-identical (canonical form unchanged) and the arithmetic stays fully type-stable. This cuts about 18% of allocations on many-modeH^2(M=8) and 11% on single-modeH^4, bringing the combined speedup over v0.7.1 to about 1.9× and 2.2× on those cases (#164).Breaking:
OpandIndexare nowisbitsvia interned integer ids (#137). Operator and index names are stored as an internedname_id::Int32(resolved back to aSymbolwith the newoperator_name(op)/index_name(idx)accessors), and an index's summation range as an internedrange_id::Int32(recovered as the originalNumwith the newindex_range(idx)accessor).Indexdrops itsrange::Numandsym::Numfields: the symbolic variable is reconstructed on demand by the newindex_sym(idx)(SymbolicUtils hashconsing makes it the same object, so substitution andget_variablesare unchanged), and a per-siteslot::Int32replaces the per-slot/numericsympayload. With every field a bits type,Opisisbitsat 44 bytes (down from 72) andQTerm.ops::Vector{Op}is a dense inline buffer the GC never scans, hashed and compared on pure integers. Canonical ordering is preserved exactly and stays deterministic across sessions via a lexicographic name-rank table (interned ids are insertion-order; ranks are name-order). What breaks: code readingop.name,idx.name,idx.range, oridx.symdirectly must switch tooperator_name/index_name/index_range/index_sym;Op.space_indexandIndex.space_indexare nowInt32. The intern tables are module-global, written only at construction under a lock; their ids are not stable across sessions (never serialize them). Measured end-to-end speedups over v0.8.0: Fock(c·c')^5normal-order about 1.4×, indexed Jaynes-CummingsHsimplify about 1.3×, nested-commutator andH²simplify about 1.1–1.2×, with no regressions.
[v0.7.2]
Changed
- Complex parameters and irreducible scalar couplings now stay on the native
Polycoefficient path instead of escalating to theComplex{Num}symbolic tail, where every later product/sum round-tripped through SymbolicUtils hashconsing._is_atom/_recnow recognize any non-algebraic one-argument call on an atom (real(g),imag(g),exp,sin, ...) as a single opaque polynomial atom; a@variables g::Complexparameter (stored asComplex(real(g), imag(g))) therefore no longer poisons downstream coefficient arithmetic. Conjugation of aPolycoefficient is now native (_conj_poly: conjugate scalars and atoms, re-canonicalize) rather than materializing each term throughto_num. On the downstreamQuantumInputOutput.jlSLH cascade benchmark this lowers a 3-cavityhamiltonian/lindbladbuild from about 140 µs to about 7.6 µs (roughly 18×). - Radicals of a single parameter now canonicalize on the native
Polypath:Monomialexponents areRational{Int}, sosqrt(p)is stored asp^(1//2)(likewisecbrt(p)asp^(1//3), andp^(m//n)). Buildingsqrt(p)and squaring it folds topnatively, matching thesqrt(p)^2 -> pfold Symbolics performs at theNumlevel; previouslysqrt(p)was an opaque integer-exponent atom, so the two construction paths produced distinct, non-isequalcoefficients andQAddlike-term dedup could miss merges. A radical of a non-atom (sqrt(g*κ),(g+κ)^(1//2)) is not distributed (unsound in general) and stays aComplex{Num}symbolic leaf, consistent with how Symbolics treats it.
[v0.7.1]
changed
- The per-term operator machinery is faster on product and power workloads, lowering the floor that remained after the
Coeffrework.QTermnow caches its hash in a field computed once at construction, so the repeated dict probes, inserts, and growth-rehashes that key every canonicalization no longer re-walk the operator vector (whose abstractQSymeltype forces a dynamic dispatch per element);isequalalso short-circuits on the cached hash before comparing operators. The reduce pass gained a type-domain_may_reducegate consulted before_reduce_pair: only same-typeTransitionandPaulipairs can compose, so every Fock, Spin, andPhaseSpacepair (the bulk of bosonic products) now skips the dynamically dispatched_reduce_paircall whose(kind, op, factor)tuple would otherwise box on each adjacent pair. Measured speedups over the post-Coeffbaseline: Fock(a·a†)^10about 1.74×, many-modeH^2(M=8) about 1.81×, single-modeH^4about 1.54×, with roughly half the allocations (#141, #164).
[v0.7.0]
Fixed
- Averaging an indexed sum whose coefficient depends on the summation index no longer drops the scope or lets the index dangle outside the
Σ.averagepreviously stampedSumIndices/SumNonEqualmetadata on the average leaf and emittedcoeff * leaf; SymbolicUtils discards metadata on composite/numerically-scaled nodes, soaverage(Σ(u(kk,k), k))collapsed to a bareu(kk,k)and an index-dependent coefficient was hoisted outside the sum. Index-dependent terms are now wrapped in a dedicated moment-layer node (SumFunc/sym_sum) carrying the summation scope as aSumScopeargument, so the whole averaged body stays inside the sum and the representation survivesAdd/Mulcanonicalization. Because the scope rides as an argument (not metadata, whichisequal/hashignore), differently-scoped sums over the same body no longer wrongly cancel in a subtraction. Newis_indexed_sumpredicate;has_sum_metadata/get_sum_indices/get_sum_non_equalretained and now read the node (#175).
Changed
make_time_dependenton an averaged indexed sum now yieldsΣ(i) ⟨a_i⟩(t)(per-site time-dependent moments under the sum) instead of⟨Σ_i a_i⟩(t)(one collective lumped variable), matching the non-lifted displayΣ(i) ⟨a_i⟩and giving indexable per-site unknowns for indexed equations.- Operator prefactors are stored as a concrete
Coeffwith three forms (a nativeComplexF64fast path, a sparse parameter polynomial for products and sums of named parameters, and aComplex{Num}fallback) instead of alwaysComplex{Num}. Numeric and parameter-polynomial coefficient arithmetic stays native and never routes through SymbolicUtils hashconsing; a coefficient lowers toComplex{Num}only at the symbolic boundaries (substitute/average/printing/prefactor). The polynomial arithmetic is fully type-stable, with factor identity viaobjectid/===(which assumes SymbolicUtils hashconsing is enabled, the default). Polynomial coefficients are kept in canonical expanded form, so(g+h)^2is stored asg^2 + 2*g*h + h^2. Measured speedups overComplex{Num}: numeric power expansion about 2.1×, single-modeH^4about 2.65×, many-modeH^2about 3.5×, nested commutator about 2.4× (#164, #183).
[v0.6.5]
Added
@variables η::Numberdeclares a complex coefficient that stays a single atomic symbol, conjugating to the symbolicconj(η). Unlike::Complex(which splits into independent real/imaginary unknowns and pays(a+bi)(c+di)expansion on every product),::Numberkeeps coefficient arithmetic on one symbol (η * η → η²). Adjoint routes coefficients through a symtype-aware_conj_cnum, so(η a)†(η a)correctly carries|η|² = conj(η)·η, andto_numericreduces such coefficients (e.g. an unfoldedconjof a complex literal) viabuild_function.
[v0.6.4]
Fixed
change_indexnow correctly zerosDoubleIndexedVariablenodes taggedidentical=falseeven when they appear nested inside a larger product or sum. Previously_check_not_identicalonly examined the root of the substituted expression, so aJ(i,j)factor insideJ(i,j) * xwould survive aj → isubstitution intact instead of collapsing to zero. The fix walks the full expression tree, collects everyNotIdentical-tagged node whose two index arguments became equal, and replaces them all via a singleSymbolics.substitutepass.
[v0.6.3]
Changed
- Lifted time-dependent averages now display as
⟨op⟩(t)in both the REPL andlatexifyoutput, instead of exposing the internal_avg_…variable name. TheAverageOperatormetadata drives the rendering through newshow_metadata/_toexpr_metadatahooks, so a moment's MTK unknown reads the same as itsaverage(#173).
[v0.6.2]
Added
index_slot(public): recover the concrete positionkof a per-slot index symbol minted by(i::Index)(k), read from metadata rather than parsed out of the symbol's name.(i::Index)(k)now stampskas metadata; since symbol equality and hashing ignore metadata, a per-slot index still dedup-equals its name-only counterpart (#170).
[v0.6.1]
Added
order_key,term_order_key,qadd_order_key(public): a total, identity-faithful structural ordering for operators, products, and sums, built from one per-typeorder_keymethod. The key ties two operators exactly when they areisequal, giving downstream packages a reproducible way to pick canonical representatives and compare expressions without round-tripping throughshoworhash(#169).
[v0.6.0]
Changed
- Average expressions now use
Numberas their Symbolicssymtype, and the newmake_time_dependenthelper lifts them into ModelingToolkit-style time-dependent unknowns while preserving average metadata for round-tripping.
[v0.5.2]
Fixed
- Commuting commutators with symbolic rational coefficients no longer bloat derived expressions (#162). Deriving
[Hₖ, op]for anHₖthat commutes withopshould vanish, but with a rational coupling such asγ/|xᵢ-xⱼ|³the two halves land asγ/D + (-γ)/D, which Symbolics leaves un-combined and_iszero_cnumdoes not see as zero; the spurious term then survives and inflates every downstream RHS. Fixed at two levels:_addto_key!cancels exact-negation prefactor pairs for symbolic coefficients (the numeric path stays allocation-free).commutatordistributes over terms and skips disjoint-subspace pairs ([aₖ, bₗ] = 0), so the cancelling products are never formed.
[v0.5.1]
Fixed
Σnow propagates index-inequality constraints onto the diagonal when a summed index collapses onto an external one. When splitting the diagonal ofΣ_{i≠j} σᵢ²¹ σₖ¹², thei = kcontribution now inheritsk ≠ jinstead of dropping it. Previously the constraint was discarded, letting a later sum re-admit thek = jpoint and double-count the diagonal of nested and ollective double sums (#161).
Changed
- LaTeX rendering for averaged expressions now uses
\langle \cdots \rangle, and averaged indexed sums preserve their\sumprefix inlatexifyoutput via the new Symbolics LaTeX hooks (#153).
Documentation
- Document why alpha-equivalent sums (
Σᵢ σᵢ + Σⱼ σⱼ) are not auto-collected and clarify the bound-index naming policy in the developer docs; add a "read the devdocs first" note for contributors ([#156]). Resolves [#134].
[v0.5.0]
Breaking release: redesign of the algebraic core. Most public entry points keep their names and meaning. The substantive changes fall into three groups: direct renames, constructs replaced by more general machinery, and behavioural changes in shared API names. This entry is the migration reference for users with code written against SecondQuantizedAlgebra.jl v0.4 or QuantumCumulants.jl, which shares the v0.4 API surface.
Renamed
One-to-one renames. Replace the left column with the right column anywhere it appears in your code.
| v0.4 / QuantumCumulants.jl | v0.5 |
|---|---|
reorder(expr, [(α, β), …]) | assume_distinct_index(expr, [(α, β), …]) |
simplify(expr, h) | expand_completeness(simplify(expr)) |
normal_order(expr, h) | expand_completeness(normal_order(expr)) |
@cnumbers a b c | @variables a b c (reexported from Symbolics) |
@rnumbers a b c | @variables a::Real b::Real c::Real |
Parameter(:x) | @variables x and use x |
Removed
A handful of constructs that lived at the type level in v0.4 have been replaced by mechanisms that scale better and avoid bespoke types.
ClusterSpace. Indexed families of identical subsystems are now expressed through the Index and Σ machinery. Where v0.4 wrote
hc = ClusterSpace(NLevelSpace(:atom, 2), N, k)
σ = Transition(hc, :σ, 1, 2)
H = Δ * sum(σ' * σ)v0.5 writes
ha = NLevelSpace(:atom, 2)
i = Index(ha, :i, N, ha)
σ(i) = IndexedOperator(Transition(ha, :σ, 1, 2), i)
H = Δ * Σ(σ(i)' * σ(i), i)N stays symbolic through equation derivation. The cumulant truncation order, which used to be the k parameter on ClusterSpace, is now a property of the moment expansion and is passed to the downstream solver (e.g. meanfield(…; order=k) in QuantumCumulants.jl) rather than baked into the Hilbert space.
SingleSum / DoubleSum / SpecialIndexedTerm. These wrapper types are gone. Σ(expr, i) constructs a QAdd directly, with the summation index recorded on the QAdd.indices field and per-term inequality constraints recorded on each term's ne vector. The SpecialIndexedTerm role (carrying (α, β) ∈ ne constraints attached to a single product) is filled by the per-term ne field. Code that pattern-matched on these types should iterate the resulting QAdd and inspect term.ne directly.
NumberedOperator / insert_index. v0.5 keeps indices symbolic throughout. Numeric basis instantiation for an indexed family of operators is the responsibility of the downstream numeric layer, not the algebra.
IndexedAverageSum / IndexedAverageDoubleSum. Averaging an indexed QAdd now lifts the indices and per-term ne onto the resulting BasicSymbolic as metadata via SymbolicUtils.setmetadata. There is no dedicated indexed-average type; the same averaging machinery handles indexed and non-indexed cases uniformly.
QMul. All arithmetic returns QAdd. Code that branched on QMul-vs-QAdd can drop the branch: multiplication is uniform-return-type. Iteration over a QAdd yields Pair{QTerm, CNum}; reach into term.ops and term.ne directly for the operator string and its constraints.
order_by_index. Operators with symbolic indices on the same Hilbert subspace are sorted automatically when _partial_sort! can resolve their relationship (sum bound, declared inequality). When the relationship is undetermined, declare the pairs with assume_distinct_index to trigger the sort.
Changed
A few API names survive but their behaviour has shifted. The differences are intentional and pay off in correctness or performance, so be aware of them when porting tests.
σᵍᵍ no longer expands automatically. In v0.4, every product passing through * rewrote ground-state projectors via σᵍᵍ = 1 - Σ_{k≠g} σᵏᵏ. In v0.5, σᵍᵍ is a legitimate canonical-form atom and stays atomic through *, normal_order, and simplify. Use expand_completeness(expr) to materialise the identity explicitly. The change preserves like-term collection across operations and avoids an n_levels^k blow-up for products containing many ground-state factors.
simplify now includes coefficient simplification. In v0.4, simplify applied algebraic identities to operators only. In v0.5, simplify is normal_order followed by Symbolics.simplify on each surviving coefficient and a drop of summation indices that no surviving term depends on. The expensive symbolic simplification deliberately runs once per surviving term rather than on every internal dict insertion, so the cost shows up at the simplify call site rather than inside *.
Transition carries ground_state and n_levels. The constructor signature Transition(h, :σ, i, j) still works and infers these from the surrounding NLevelSpace, but the fields now live on the operator itself rather than being looked up on demand. Downstream code that previously had to consult h to expand completeness no longer needs the Hilbert space.
Free indices stay in physical order under *. Two operators carrying different symbolic indices on the same Hilbert subspace, with no Σ binding either and no declared inequality, are stored in their physical order rather than being sorted alphabetically by name. Declare the pairs with assume_distinct_index(q, [(j, k)]) to trigger sorting (and any same-site collapse that follows).
Migration
Side-by-side, an indexed Tavis-Cummings Hamiltonian.
# v0.4 / QuantumCumulants.jl
hc = FockSpace(:cavity)
ha = NLevelSpace(:atom, 2)
h = hc ⊗ ha
@qnumbers a::Destroy(h, 1)
σ(i, j, k) = IndexedOperator(Transition(h, :σ, i, j), k)
@cnumbers N Δ
g(i) = IndexedVariable(:g, i)
i = Index(h, :i, N, ha)
H = -Δ * a' * a + Σ(g(i) * (a' * σ(1, 2, i) + a * σ(2, 1, i)), i)
# v0.5
hc = FockSpace(:cavity)
ha = NLevelSpace(:atom, 2)
h = hc ⊗ ha
@qnumbers a::Destroy(h, 1)
σ(i, j, k) = IndexedOperator(Transition(h, :σ, i, j, 2), k)
@variables N Δ
g(i) = IndexedVariable(:g, i)
i = Index(h, :i, N, ha)
H = -Δ * a' * a + Σ(g(i) * (a' * σ(1, 2, i) + a * σ(2, 1, i)), i)The differences are local: @cnumbers becomes @variables, and Transition takes an explicit n_levels argument (2 for a two-level atom). Everything downstream (commutators, averaging, equation derivation) uses the same names with the same meaning.
Unchanged
These names keep their meaning across the migration. Code that only uses them should port without changes:
FockSpace, NLevelSpace, PauliSpace, SpinSpace, PhaseSpace, ProductSpace, ⊗, tensor, @qnumbers, Destroy, Create, Transition, Pauli, Spin, Position, Momentum, Index, IndexedOperator, IndexedVariable, DoubleIndexedVariable, Σ, ∑, change_index, get_indices, commutator, anticommutator, acts_on, find_operators, fundamental_operators, unique_ops, prefactor, operators, substitute, expand, normal_order, simplify, normal_to_symmetric, symmetric_to_normal, average, undo_average, is_average, to_numeric, numeric_average.
<!– Links generated by Changelog.jl –>
[v0.5.0]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/releases/tag/v0.5.0 [v0.5.1]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/releases/tag/v0.5.1 [v0.5.2]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/releases/tag/v0.5.2 [v0.6.0]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/releases/tag/v0.6.0 [v0.6.1]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/releases/tag/v0.6.1 [v0.6.2]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/releases/tag/v0.6.2 [v0.6.3]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/releases/tag/v0.6.3 [v0.6.4]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/releases/tag/v0.6.4 [v0.6.5]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/releases/tag/v0.6.5 [v0.7.0]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/releases/tag/v0.7.0 [v0.7.1]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/releases/tag/v0.7.1 [v0.7.2]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/releases/tag/v0.7.2 [v0.8.0]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/releases/tag/v0.8.0 [v0.8.1]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/releases/tag/v0.8.1 [v0.8.2]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/releases/tag/v0.8.2 [v0.8.3]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/releases/tag/v0.8.3 [v0.9.0]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/releases/tag/v0.9.0 [#156]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/issues/156