Code Documentation
21 modules · 382 documented entries · generated 5 October 2026
Module Multiindices — graded-lexicographic multiindex sets and combinatorial utilities.
A multiindex is an exponent vector α ∈ ℕᴺ that identifies the monomial z₁^α₁ ⋯ zₙ^αₙ in N variables. This module provides:
MultiindexSet{N}— a sorted, deduplicated collection ofSVector{N, Int}exponents in graded-lexicographic (GrLex) order, with O(1) degree-boundary queries.Generators:
all_multiindices_up_to,multiindices_with_total_degree,all_multiindices_in_box.Non-mutating set operations:
delete_multiindices,Base.filter,is_downward_closed,is_conjugate_closed.Lookup:
find_in_set(degree-bracketed binary search),build_exponent_index_map.Factorisation enumerators:
factorisations_asymmetric,factorisations_fully_symmetric,factorisations_groupwise_symmetric— used byMultilinearTermsto sum nonlinear contributions.Combinatorial utilities:
monomial_rank,bounded_index_tuples,divides,is_constant.
FactorisationEntry
FactorisationEntry
One result from a factorisation function: an ordered list of multiindex-set indices (one per factor slot) together with the number of distinct ordered arrangements of that combination.
All three factorisation functions return Vector{FactorisationEntry}, so call sites do not need to know which variant produced the data.
Fields
factor_indices::Vector{Int}— indices into the multiindex set, one per factor slot.multiplier::Int— number of distinct orderings (permutations) of this combination. Always 1 forfactorisations_asymmetric, which enumerates each ordering as a separate entry instead of folding it into a count.
MultiindexSet
MultiindexSet{N}
A fixed collection of exponent vectors (multiindices) stored as a vector of SVector{N, Int}. The set is guaranteed to be sorted according to the graded lexicographic (Grlex) order.
Fields
exponents::Vector{SVector{N, Int}}: the sorted list of exponent vectors.degree_offsets::Vector{Int}: precomputed boundary table;degree_offsets[d+1]is the last index inexponentswith total degree< d, enabling O(1) degree-range queries.
_last_index_below_degree(set::MultiindexSet, max_total_deg::Int64)
_last_index_below_degree(set::MultiindexSet, max_total_deg::Int64, allowed_indices::AbstractVector{Int64})
_last_index_below_degree(set::MultiindexSet, max_total_deg::Int) -> Int
Return the last column index i in the Grlex‑sorted set such that the total degree of set[i] is strictly less than max_total_deg. If no such index exists, return 0. O(1) via the precomputed degree_offsets field.
_last_index_below_degree(set::MultiindexSet, max_total_deg::Int,
allowed_indices::AbstractVector{Int}) -> Int
Same as the 2‑argument version, but restricted to the indices listed in allowed_indices (must be sorted in increasing order). Uses the O(1) degree_offsets boundary then searchsortedlast to locate the answer; no per-element degree sums are computed.
_lex_cmp_same_degree(v::StaticArraysCore.SVector{N, Int64}, e) where N
_lex_cmp_same_degree(v::SVector{N,Int}, e) -> Int
Three‑way descending‑lexicographic comparison of two exponents of equal total degree: -1 if v precedes e, +1 if e precedes v, 0 if they are equal. Since the degrees are known to match, no degree sums are computed.
_multinomial(e::Int64, k::Vector{Int64})
_multinomial(e::Int, k::Vector{Int}) -> Int
Multinomial coefficient: e! / (k₁! k₂! … kₚ!) where sum(k) = e. Uses iterative multiplication of binomial coefficients to avoid overflow.
_total_degree(exp, ::Val{N}) where N
_total_degree(exp, ::Val{N}) -> Int
Total degree of an N-component exponent. Written as an explicit loop so that it works uniformly for Vector, SVector and NTuple, and returns 0 for N == 0, where sum on an empty tuple is not defined.
all_multiindices_in_box(bound::Vector{Int64})
all_multiindices_in_box(bound::Vector{Int}) -> MultiindexSet
Generate all multi-indices v of length length(bound) such that 0 ≤ v[i] ≤ bound[i] for all i. The vectors are generated and then sorted according to graded lexicographic order.
all_multiindices_up_to(nvars::Int64, max_degree::Int64; min_degree)
all_multiindices_up_to(nvars::Int, max_degree::Int) -> MultiindexSet
Generate all exponent vectors with nvars variables whose total degree ≤ max_degree, sorted according to graded lexicographic order. Returns a MultiindexSet.
bounded_index_tuples(M::Int64, exp::StaticArraysCore.SVector{0, T}) where T
bounded_index_tuples(M::Int64, exp::StaticArraysCore.SVector{N, Int64}) where N
bounded_index_tuples(M, exp::SVector{N,Int})
Enumerate all index tuples of length M whose component counts are bounded by exp.
This function generates all tuples (i₁, ..., i_M) with entries in 1:N such that:
Each index
kappears at mostexp[k]timesThe total length of the tuple is exactly
M
Each tuple is represented in its canonical sorted form (non-decreasing order), so that it uniquely corresponds to a count vector (multiindex).
For each valid tuple, the function also returns:
The associated multiindex
alpha, wherealpha[k]is the number of timeskappearsThe number of distinct permutations of the tuple
Returns a vector of tuples: (indextuple, multiindex, permutationcount)
where:
index_tuple::NTuple{M,Int}is the sorted tuplemultiindex::SVector{N,Int}contains the countspermutation_count::Intis the number of distinct permutations
build_exponent_index_map(set::MultiindexSet{N}) where N
build_exponent_index_map(set::MultiindexSet) -> Dict{SVector{N,Int}, Int}
Build a dictionary mapping each exponent SVector to its 1-based column index in set. Useful for O(1) lookups without repeated binary searches.
delete_multiindices(set::MultiindexSet{N}, victims::MultiindexSet{M}) where {N, M}
delete_multiindices(set::MultiindexSet{N}, exps) where N
delete_multiindices(pred, set::MultiindexSet{N}) where N
delete_multiindices(set::MultiindexSet{N}, exps) -> MultiindexSet{N}
Return a new MultiindexSet holding every exponent of set except those listed in exps. set itself is never modified — there is no in-place variant.
exps may be a single exponent (SVector, Vector{Int} or NTuple{N,Int}), any iterable of such exponents, or another MultiindexSet{N}. Exponents that are not members of set are ignored, matching Base.setdiff; an exponent whose length differs from N throws an ArgumentError.
Deleting cannot reorder a Grlex-sorted list, so the result is built through the pre-sorted path: no re-sort is performed, and degree_offsets is rebuilt so the O(1) degree-boundary queries stay correct even when an entire degree block disappears.
Removing an exponent can break downward closure, which parametrise(model, spectral, mset) requires — check the result with is_downward_closed.
Examples
S = all_multiindices_up_to(2, 2) # 6 monomials
T = delete_multiindices(S, [[2, 0], [0, 2]]) # drop the two pure squares
length(S), length(T) # (6, 4) — S is unchanged
delete_multiindices(pred, set::MultiindexSet{N}) -> MultiindexSet{N}
Return a new MultiindexSet holding the exponents of set for which pred(α) is false; set itself is never modified. pred receives each exponent as an SVector{N,Int}. This is the exact complement of filter(pred, set): together the two partition set.
Examples
S = all_multiindices_up_to(3, 4)
delete_multiindices(α -> sum(α) > 2, S) == all_multiindices_up_to(3, 2) # true
divides(a::AbstractVector{Int64}, b::AbstractVector{Int64})
divides(a::AbstractVector{Int}, b::AbstractVector{Int}) -> Bool
Check whether a divides b componentwise, i.e., a[i] ≤ b[i] for all i.
factorisations_asymmetric(set::MultiindexSet{N}, exp::AbstractVector{Int64}, num_factors::Int64, candidate_indices::AbstractVector{Int64}) where N
factorisations_asymmetric(set, exp, num_factors, candidate_indices) -> Vector{FactorisationEntry}
Return every ordered num_factors-tuple of indices from candidate_indices whose exponent vectors sum to exp. Each ordering is a separate entry with multiplier = 1.
factorisations_fully_symmetric(set::MultiindexSet{N}, exp::AbstractVector{Int64}, num_factors::Int64, candidate_indices::AbstractVector{Int64}) where N
factorisations_fully_symmetric(set, exp, num_factors, candidate_indices) -> Vector{FactorisationEntry}
Return all unordered (non-decreasing index) num_factors-tuples from candidate_indices whose exponent vectors sum to exp. multiplier is the multinomial coefficient num_factors! / (m₁! m₂! … mₖ!) counting the distinct ordered arrangements of each combination.
Duplicate entries in candidate_indices are removed internally.
factorisations_groupwise_symmetric(set::MultiindexSet{N}, exp::AbstractVector{Int64}, group_sizes::NTuple{M, Int64}, candidate_indices::AbstractVector{Int64}) where {N, M}
factorisations_groupwise_symmetric(set, exp, group_sizes, candidate_indices) -> Vector{FactorisationEntry}
Return factorisations of exp into M = length(group_sizes) groups, where group i has group_sizes[i] factor slots and is internally symmetric.
factor_indices in each entry is the concatenation of per-group indices in group order (each group sorted non-decreasingly). multiplier is the product of the per-group permutation counts — the total number of ordered arrangements.
filter(pred, set::MultiindexSet{N}) where N
filter(pred, set::MultiindexSet{N}) -> MultiindexSet{N}
Return a new MultiindexSet holding the exponents of set for which pred(α) is true, leaving set untouched. pred receives each exponent as an SVector{N,Int}.
Filtering preserves Grlex order, so the result is built through the pre-sorted path. Use it to intersect independent conditions, e.g. a total-degree bound on the master coordinates together with a per-parameter box:
mset = filter(α -> α[1] + α[2] ≤ 4 && α[3] ≤ 2, all_multiindices_in_box([4, 4, 2]))
See delete_multiindices for the removal-side counterpart.
find_in_set(set::MultiindexSet{N}, exp::AbstractVector{Int64}) where N
find_in_set(set::MultiindexSet{N}, exp::NTuple{N, Int64}) where N
find_in_set(set::MultiindexSet, exp::AbstractVector{Int}) -> Union{Int, Nothing}
Return the column index of exp in set.exponents, or nothing if exp is not present (including when its length differs from the number of variables).
The total degree of exp brackets the search to a single degree block via degree_offsets, after which a binary search in Grlex (here plain descending lexicographic, the degrees being equal) locates the exponent.
find_in_set(set::MultiindexSet, exp::NTuple{N,Int}) where N -> Union{Int, Nothing}
Tuple version – avoids allocating a vector for the exponent.
grlex_precede(a::AbstractVector{<:Integer}, b::AbstractVector{<:Integer})
grlex_precede(a::AbstractVector{<:Integer}, b::AbstractVector{<:Integer}) -> Bool
Graded lexicographic order: compare total degree first, then lexicographic descending. Returns true if a comes before b in this order.
indices_in_box_with_bounded_degree(set::MultiindexSet{N}, box_upper::AbstractVector{Int64}, degree_lower_bound::Int64, total_deg_upper::Int64) where N
indices_in_box_with_bounded_degree(set::MultiindexSet{N}, box_upper::AbstractVector{Int64}, degree_lower_bound::Int64, total_deg_upper::Int64, allowed_indices::AbstractVector{Int64}) where N
indices_in_box_with_bounded_degree(set::MultiindexSet, box_upper::AbstractVector{Int},
degree_lower_bound::Int, total_deg_upper::Int) -> Vector{Int}
Return the column indices of all multiindices v in set such that
v ≤ box_uppercomponentwise,degree_lower_bound ≤ sum(v[i]) < total_deg_upper.
Uses the degree bounds to limit the search to relevant columns.
indices_in_box_with_bounded_degree(set::MultiindexSet, box_upper::AbstractVector{Int},
degree_lower_bound::Int, total_deg_upper::Int,
allowed_indices::AbstractVector{Int}) -> Vector{Int}
Same as the 4‑argument version, but the search is restricted to the indices listed in allowed_indices (which must be sorted). Only those indices that also satisfy the componentwise bound are returned.
is_conjugate_closed(set::MultiindexSet{N}, perm::AbstractVector{Int64}) where N
is_conjugate_closed(set::MultiindexSet{N}, perm::AbstractVector{Int}) -> Bool
Return true when the conjugate partner of every member of set is itself a member.
perm is an N-length permutation pairing conjugate coordinates (self-paired entries for real ones). It acts on the components of an exponent,
(P·α)[k] = α[perm[k]],
the same convention the cohomological solve uses when it fills a conjugate monomial from its partner. Monomials with P·α == α are self-paired and trivially closed.
parametrise(model, spectral, mset; conjugate_permutation = ...) requires this: a member whose partner is absent is solved directly instead of being filled by conjugation, which costs the pairing optimisation and returns a parametrisation without the conjugate structure the permutation asserts. It is enforced through validate_multiindex_set, which reports which partner is missing; use this predicate when a plain Bool is enough.
Throws an ArgumentError if length(perm) != N.
is_constant(exp::AbstractVector{Int64})
is_constant(exp::AbstractVector{Int}) -> Bool
Return true if the exponent vector is all zeros.
is_downward_closed(set::MultiindexSet{N}) where N
is_downward_closed(set::MultiindexSet{N}) -> Bool
Return true when every divisor of every member of set is itself a member.
This is the downward closure property that parametrise(model, spectral, mset) requires: the graded solve reads lower-order coefficients W[α - eᵢ] while working on α, so a missing divisor would be silently read as zero. Combinatorial truncations (all_multiindices_up_to, all_multiindices_in_box) are closed by construction, but a spectral criterion such as |⟨λ, α⟩| ≤ R generally is not.
parametrise and solve_parametrisation enforce this through validate_multiindex_set, which reports which divisor is missing; use this predicate when a plain Bool is enough.
The zero multiindex is exempt: DPIM sets are built with min_degree = 1, so a degree-1 exponent is not asked to have its constant divisor present.
Only the immediate predecessors α - eᵢ are probed; by induction over the total degree that is equivalent to checking every divisor. Cost is O(N · |set| · log) through find_in_set.
monomial_rank(exp::AbstractVector{Int64}, nvars::Int64, max_degree::Int64)
monomial_rank(exp::AbstractVector{Int}, nvars::Int, max_degree::Int) -> Int
Return the 1‑based rank of the exponent vector exp in the complete set of all multiindices of length nvars with total degree ≤ max_degree, sorted according to graded lexicographic order (Grlex).
The vector exp must satisfy length(exp) == nvars and sum(exp) ≤ max_degree. Uses combinatorial counting formulas for efficiency (O(nvars) time).
If the computed rank exceeds typemax(Int), an error is thrown.
multiindex(components::Int64...)
multiindex(components::Int...) -> Vector{Int}
Convenience constructor for an exponent vector.
multiindices_with_total_degree(nvars::Int64, degree::Int64)
multiindices_with_total_degree(nvars::Int, deg::Int) -> MultiindexSet
Generate all exponent vectors of length nvars with total degree exactly deg, sorted in lexicographic order (the tie‑breaker for Grlex). Returns a MultiindexSet.
num_multiindices_up_to(nvars::Int64, max_degree::Int64)
num_multiindices_up_to(nvars::Int, max_degree::Int) -> Integer
Return the number of exponent vectors of length nvars with total degree ≤ max_degree. For nvars = 0 the set contains one element if max_degree ≥ 0, otherwise zero.
zero_multiindex(n::Int64)
zero_multiindex(n::Int) -> Vector{Int}
Return the zero exponent vector of length n (all components zero).
Module Polynomials — dense multivariate polynomial type aligned to a MultiindexSet.
A DensePolynomial{T, NVAR, RANK} stores coefficients in a single contiguous array whose last axis indexes monomials in the same GrLex order as the associated MultiindexSet. The layout is chosen so that polynomial evaluation reduces to a single BLAS gemv / dot call regardless of the output rank:
Scalar polynomial (
RANK = 1): coefficient array of shape(L,).Vector polynomial (
RANK = 2): coefficient array of shape(K, L).Tensor polynomial (
RANK = k+1): coefficient array of shape(d₁, …, dₖ, L).
Key operations: evaluate, extract_component, each_term, similar_poly, linear_matrix_of_polynomial, coefficient.
DensePolynomial
DensePolynomial{T, NVAR, N, A}
Cache-friendly dense polynomial with a single contiguous coefficient array.
Type parameters
| param | meaning |
|---|---|
T | Scalar element type (Float64, ComplexF64, …). Must be a concrete bits type for best performance. |
NVAR | Number of input variables. |
N | ndims(coefficients). Number of axes: N = 1 for scalar, N = 2 for vector-valued, etc. |
A | Concrete array type (Array{T,N} normally; can be an Mmap array). |
Fields
coefficients::A— shape(d1, …, d_{N-1}, L). The last axis indexes theLmonomials inmultiindex_set. Leading axes describe the coefficient shape (none for scalar,(K,)for a K-vector, etc.).multiindex_set::MultiindexSet{NVAR}— Grlex-ordered monomial basis.max_exponents::SVector{NVAR,Int}— per-variable max exponent (used to pre-allocate the power table inevaluate).
DensePolynomial(coeff_vec::Vector{SVector{K,T}}, mset)
Vector-valued polynomial from the old Vector{SVector} layout. Converts to a contiguous (K × L) matrix, filling columns in parallel.
DensePolynomial(dict::Dict{Vector{Int}, T}) where T<:Number
Scalar polynomial from an exponent → coefficient dictionary.
DensePolynomial(dict::Dict{Vector{Int}, SVector{K,T}}) where {K,T}
Vector-valued polynomial from an exponent → SVector dictionary. Coefficients are laid out column-by-column (parallel fill).
_monomial_vector!(m::Vector{Tv}, poly::DensePolynomial{T, NVAR, N, A} where {N, A<:AbstractArray{T, N}}, vals::AbstractVector{<:Number}) where {Tv, T, NVAR}
_monomial_vector!(m, poly, vals)
Fill the pre-allocated vector m (length L) with the value of each monomial in poly.multiindex_set evaluated at vals. This is the central loop; called once per evaluate.
_precompute_powers(::Type{T}, vals::AbstractVector{<:Number}, max_exps::StaticArraysCore.SVector{NVAR, Int64}) where {T, NVAR}
_precompute_powers(T, vals, max_exps) -> NTuple of Vectors
For each variable j, store vals[j]^e for e = 0 … max_exps[j]. Avoids repeated ^ calls inside the inner loop.
coeff_shape(p::DensePolynomial{T, NVAR, N, A} where A<:AbstractArray{T, N}) where {T, NVAR, N}
coeff_shape(p) -> NTuple
Leading axes of the coefficient array. () for scalar, (K,) for K-vector, (m,n) for a matrix-valued polynomial, etc.
coefficient(p::DensePolynomial{T, NVAR, 1, A} where A<:AbstractVector{T}, exp::AbstractVector{Int64}) where {T, NVAR}
coefficient(p::DensePolynomial{T, NVAR, 2, A} where A<:AbstractMatrix{T}, exp::AbstractVector{Int64}) where {T, NVAR}
coefficient(p, exp) -> scalar or view
Return the coefficient of the monomial with exponent exp. For scalar polynomials this is a T; for vector-valued it is a Vector{T} view into the backing array — no allocation.
compose_linear(poly::DensePolynomial, M::Matrix{TA}) where TA
compose_linear(poly::DensePolynomial, M::Matrix{TA}) where TA -> DensePolynomial
Compose a multivariate polynomial with a linear map.
Arguments
poly: polynomial in variablesx₁, …, x_n. (The coefficient type can be numeric or array‑valued.)M: ann × pmatrix. Composition means replacingx_iby∑_{j=1}^p M[i,j] * y_j, wherey₁, …, y_pare new variables.
Returns
A new polynomial in the variables y₁, …, y_p. The returned polynomial has the same coefficient type as the input poly.
Element type
The accumulators are typed from poly, so a real polynomial composed with a complex M cannot store the products back. Promote the polynomial first, e.g. DensePolynomial(promote_type(eltype(coefficients(p)), TA).(coefficients(p)), multiindex_set(p)).
each_term(poly::DensePolynomial{T, NVAR, 1, A} where A<:AbstractVector{T}) where {T, NVAR}
each_term(poly::DensePolynomial{T, NVAR, 2, A} where A<:AbstractMatrix{T}) where {T, NVAR}
each_term(poly) -> generator of (exponent, coefficient)
Yields (SVector{NVAR,Int}, coeff) for every nonzero monomial.
Scalar (N=1):
coeffis aT.Vector (N=2):
coeffis aSubArray{T,1}view (no allocation).
evaluate(poly::DensePolynomial{T, NVAR, 1, A} where A<:AbstractVector{T}, vals::AbstractVector{<:Number}) where {T, NVAR}
evaluate(poly::DensePolynomial{T, NVAR, 2, A} where A<:AbstractMatrix{T}, vals::AbstractVector{<:Number}) where {T, NVAR}
evaluate(poly::DensePolynomial{T, NVAR, N, A} where A<:AbstractArray{T, N}, vals::AbstractVector{<:Number}) where {T, NVAR, N}
evaluate(poly::DensePolynomial{T, NVAR, 2, A} where A<:AbstractMatrix{T}, vals::AbstractVector{<:Number}, component::Int64) where {T, NVAR}
evaluate(poly::DensePolynomial{T,NVAR,1}, vals) -> T
Scalar polynomial evaluation: sum(c_i * m_i) without conjugation.
evaluate(poly::DensePolynomial{T,NVAR,2}, vals) -> Vector{T}
Vector-valued polynomial evaluation via BLAS gemv:
result = coefficients (K × L) * m (L,) → (K,)
A single BLAS call, no per-term allocation.
evaluate(poly::DensePolynomial{T,NVAR,N}, vals) -> Array
General tensor-valued case: reshape to (prod(leading_dims), L), gemv, reshape back.
evaluate(poly::DensePolynomial{T,NVAR,2}, vals, component::Int) -> T
Evaluate a single component of a vector-valued polynomial without allocating a full output vector.
extract_component(poly::DensePolynomial{T, NVAR, 2, A} where A<:AbstractMatrix{T}, idx::Int64) where {T, NVAR}
extract_component(poly::DensePolynomial{T,NVAR,2}, idx) -> DensePolynomial{T,NVAR,1}
Return the idx-th component as a scalar polynomial. The coefficient vector is a copy of row idx of the matrix.
linear_matrix_of_polynomial(poly::DensePolynomial{T, NVAR, 2, A} where A<:AbstractMatrix{T}) where {T, NVAR}
linear_matrix_of_polynomial(poly::DensePolynomial{T,NVAR,2}) -> Matrix{T}
Return the (K × NVAR) matrix A such that the linear part equals A * x. Reads directly from the coefficient matrix — no intermediate arrays.
mmap_polynomial(path::AbstractString, coeff_size::NTuple{M, Int64}, ::Type{T}, mset::MultiindexSet; write) where {M, T<:Number}
mmap_polynomial(path, coeff_size::NTuple, T::Type, mset; write=false)
Return a DensePolynomial whose coefficient array is backed by a memory-mapped file at path. coeff_size is the leading axes of the coefficient array (empty () for scalar, (K,) for K-vector, …).
The OS page-cache handles RAM pressure automatically: only accessed pages are loaded, so polynomials larger than available RAM work transparently.
Example
# 5-vector polynomial with 1 000 000 monomials, stored on disk
p = mmap_polynomial("coeffs.bin", (5,), Float64, mset; write=true)
restrict_polynomial_to_degree(poly::DensePolynomial, max_degree::Int64)
restrict_polynomial_to_degree(poly::DensePolynomial, max_degree::Int) -> DensePolynomial
Returns a polynom that contains all the monomials of poly that are of degree lower or equal than max_degree.
similar_poly(dict::Dict{StaticArraysCore.SVector{NVAR, Int64}, T}) where {NVAR, T<:Number}
similar_poly(dict::Dict{StaticArraysCore.SVector{NVAR, Int64}, StaticArraysCore.SVector{K, T}}) where {NVAR, K, T<:Number}
similar_poly(dict::Dict{StaticArraysCore.SVector{NVAR, Int64}, Vector{T}}) where {NVAR, T<:Number}
similar_poly(dict::Dict{SVector{NVAR,Int}, T}) where {NVAR, T<:Number}
Scalar polynomial from a dictionary mapping exponent vectors to scalar coefficients.
similar_poly(dict::Dict{SVector{NVAR,Int}, SVector{K,T}}) where {NVAR, K, T<:Number}
Fixed-size-vector polynomial from a dictionary mapping exponent vectors to SVector{K,T} coefficients. The output coefficient matrix has size K × L.
similar_poly(dict::Dict{SVector{NVAR,Int}, Vector{T}}) where {NVAR, T<:Number}
Vector-valued polynomial from a dictionary mapping exponents to Vector{T} coefficients. All vectors must have the same length K.
zero(::Type{DensePolynomial{T, NVAR, N, A} where {NVAR, N, A<:AbstractArray{T, N}}}, mset::MultiindexSet) where T<:Number
zero(::Type{DensePolynomial{T, NVAR, N, A} where {NVAR, N, A<:AbstractArray{T, N}}}, cshape::NTuple{M, Int64}, mset::MultiindexSet) where {T<:Number, M}
zero(DensePolynomial{T}, mset) # scalar
zero(DensePolynomial{T}, coeff_shape, mset) # tensor-valued
Module MultilinearMaps — multilinear nonlinear term representations for NthOrderModel.
Nonlinear terms in the full-order ODE are encoded as AbstractMultilinearMap subtypes:
MultilinearMap{ORD, F}— a single polynomial term of total degreedeg, stored as a callablef!that evaluates the multilinear form plus metadata (multiindex,multiplicity_external,deg).FEMMultilinearMap{ORD}— abstract base for FEM-backed terms that expose element-level primitives (fem_elements,scatter_qp!,accumulate_qp!,assemble_element!, …). Implementing these methods enables the O4 batched RHS-C assembly path inMultilinearTerms.
See MORFEFerrite.jl (StructuralSVK / FluidNavierStokes) and examples/02_clamped_beam_gridap/ for reference FEM backend implementations.
AbstractMultilinearMap{ORD}
Abstract supertype for all multilinear terms accepted by NthOrderModel.
Every concrete subtype must expose the fields multiindex, multiplicity_external, deg, and fully_asymmetric with the same semantics as MultilinearMap.
FEMMultilinearMap
FEMMultilinearMap{ORD} <: AbstractMultilinearMap{ORD}
Abstract type for FEM-backed multilinear terms that expose element-level primitives.
Implementing the interface below enables the RHS batched accumulation path in MultilinearTerms.jl: the mesh is traversed exactly once per (monomial, term, split) rather than once per factorisation entry.
Required fields (same semantics as MultilinearMap):
multiindex,multiplicity_external,degfully_asymmetric::Union{Nothing, Bool}—nothing= not set (triggers@infoatNthOrderModelconstruction if multiindex implies symmetry);false= acknowledged symmetric;true= override toFullyAsymmetric. FEM backends whose integrand is symmetric by construction should default tofalse.
Required methods (extend MORFE.*):
fem_elements(t)→ element iteratorfem_n_qp(t)→ quadrature points per elementfem_ndofs_per_cell(t)→ DOFs per elementscatter_qp!(∇W_col, W_global, element, t)→ fill qp field values for one unique W columnaccumulate_qp!(Fe, ∇W_args::NTuple, mult, element, q, dΩ, t)→ add integrand at one qpassemble_element!(accum, Fe, element, t)→ scatter element residual to globalfem_getdetJdV(element, q, t)→ integration weight at qp qfem_qp_buffer(t)→ pre-allocated scratch buffer for one quadrature point
MultilinearMap
MultilinearMap{ORD, F}
Represents a single monomial term of order deg in the nonlinear function of an NthOrderModel.
A term is represented using a multiindex stored in the NTuple multiindex = (i0, ..., i{ORD-1}) where ik is the multiplicity of the derivative x^(k). So the ik specifies how many times the derivative x^(k) appears as an argument. In addition the function accepts multiplicity_external external variables r1, r2, ... which satisfy the first order dynamic system r' = dynamicsexternal(r), where dynamicsexternal is a DensePolynomial defined in NthOrderModel. The influence in f! is described by multiplicity_external
During evaluation the multilinear map is called as
f!(res,
x^(0), ... repeated i_0 times,
x^(1), ... repeated i_1 times,
...
x^(ORD-1), ...repeated i_{ORD-1} times,
r, ... repeated `multiplicity_external` times)
Important Notes
Each
MultilinearMapmust implement a multilinear map, i.e., it should be linear in each of its arguments independently.The function
f!accumulates (adds) intoresand must be callable with the appropriate number of arguments.If one i_k is larger than 1 we assume the input arguments are symmetric by permutation. For example:
multiindex = (0, 2,...)
f!(res, x^(1)_1, x^(1)_2, ...) = f!(res, x^(1)_2, x^(1)_1, ...)
Set
fully_asymmetric = trueto override the symmetry assumption: the term is treated asFullyAsymmetricregardless ofmultiindex, so every ordered argument permutation is evaluated independently with multiplier 1. Use this whenf!is not symmetric in arguments that share a derivative order.
Default: `fully_asymmetric` not set (`nothing`)
When this keyword is omitted, the following assumptions hold (and an @info message is emitted when the term is added to an NthOrderModel). Explicitly passing fully_asymmetric = false applies the same behaviour silently, without triggering the message.
f!is symmetric within each derivative-order group. For everykwithmultiindex[k] > 1, permuting any two of themultiindex[k]argument slots that belong to derivative orderkleaves the result unchanged.Symmetry type is inferred automatically from
multiindex:All entries ≤ 1 →
FullyAsymmetric: each factor slot uses a distinct derivative order;f!is called directly with multiplier 1.Exactly one entry > 1 →
FullySymmetric: all slots share one derivative order; each unique unordered selection of factor indices is evaluated once, scaled by the multinomial coefficientdeg! / ∏ mᵢ!.Multiple entries > 1 →
GroupwiseSymmetric: slots span several derivative orders; each unique unordered selection is evaluated once, scaled by the product of per-group multinomial coefficients.
Permutations are never evaluated separately. Only one representative per equivalence class of argument orderings is passed to
f!; the combinatorial count is applied as a scalar multiplier on the output.
If f! does not satisfy assumption 1, pass fully_asymmetric = true.
Fields
f!::F— the in-place map itself, accumulating into its first argument. Stored as a type parameter so calls through it are statically dispatched.multiindex::NTuple{ORD, Int}—multiindex[k]is how many times the derivativex^(k-1)appears as an argument.multiplicity_external::Int— how many external variablesrare passed, after the derivative arguments.deg::Int— combined degree,sum(multiindex) + multiplicity_external. Cached because it is compared against the monomial degree on every factorisation.fully_asymmetric::Union{Nothing, Bool}— overrides the symmetry inferred frommultiindex.nothingmeans "not stated", which behaves asfalsebut also emits the@infonote described above; that three-valued form is what lets the model warn about an assumption the caller may not have realised it was making.
Construction
Four constructors are available. The keyword form is the recommended one — it names every argument and can infer multiindex from the system order, the total degree or a per-slot list of derivative orders:
MultilinearMap(f!; multiindex, derivatives, order, degree,
multiplicity_external, fully_asymmetric) # recommended
MultilinearMap(f!) # shape inferred from the arity of f!
MultilinearMap(f!, multiindex; fully_asymmetric)
MultilinearMap(f!, multiindex, multiplicity_external; fully_asymmetric)
In the keyword form an omitted order means ORD = 2, the second-order mechanical setting, and a one-factor f! is read as a pure external forcing term. Every assumed value is reported through an @info; the positional forms assume nothing and stay silent.
MultilinearMap(f!; multiindex = nothing, derivatives = nothing, order = nothing,
degree = nothing, multiplicity_external = nothing,
fully_asymmetric = nothing)
Create a multilinear term, naming every argument. This is the recommended constructor.
Keyword arguments
multiindex: tuple (or vector) of counts,multiindex[k]being how many argument slots use the derivativex^(k-1). Its length is the orderORDof the system.derivatives: the alternative spelling — the 0-based derivative order of each argument slot, in call order.derivatives = (0, 0, 1)is the same term asmultiindex = (2, 1), i.e.f!(res, x, x, ẋ). Must be non-decreasing, because it describes the order in whichevaluate_term!passes the factors. Mutually exclusive withmultiindex.order: the system orderORD. Zero-pads a shortermultiindexup to it, so a quadratic term of a third-order model can be writtenmultiindex = (2,), order = 3instead of(2, 0, 0). It may pad but never truncate. Defaults to2— see below.degree: the total degree, external factors included. Use it when the arity off!cannot be introspected (a varargs closure), or as a cross-check againstmultiindex.multiplicity_external: how many times the external stateris passed tof!, after the derivative arguments. Defaults to0, or to1under the forcing rule below.fully_asymmetric: overrides the symmetry inferred frommultiindex; see the note in theMultilinearMapdocstring. Defaults tonothing("not stated").
Defaults for omitted arguments
| Value | Assumed unless… | Default |
|---|---|---|
multiindex | multiindex or derivatives given | from degree, else from the arity of f!, with every non-external factor on x^(0) |
order | order given, or multiindex given | 2 — the second-order mechanical setting |
multiplicity_external | given | 0, or 1 under the forcing rule |
multiindex pins the order exactly, since its length is ORD; derivatives does not, because it lists argument slots rather than the system order.
Two rules apply only when neither multiindex nor derivatives was given:
Forcing rule. A total degree of 1 with
multiplicity_externalunstated is read as a pure external forcing term (multiplicity_external = 1, no derivative factors). Without it,MultilinearMap(f!)onf!(res, r)would resolve to a degree-1 term in the state, which is linear and cannot be represented here — linear contributions belong in thelinear_termsmatrices ofNthOrderModel.Mixed terms are never inferred. With
multiplicity_external >= 1and a non-zero internal degree, splitting the factors would mean guessingf!(res, x, r); that is anArgumentError. Only the pure-forcing split is inferable. Statingmultiindexlifts the restriction — mixed terms are perfectly legal, just not guessable.
Every assumed value is reported through an @info. A call that pins multiindex (or derivatives plus order) and multiplicity_external is silent, as are all the positional constructors. fully_asymmetric is not reported here: it has its own diagnostic at NthOrderModel construction, which fires exactly when the flag changes the result.
Examples
# f!(res, x, x, ẋ) in a second-order system
MultilinearMap(f!; multiindex = (2, 1))
MultilinearMap(f!; derivatives = (0, 0, 1)) # identical
# cubic term of a third-order system, without hand-written trailing zeros
MultilinearMap(f!; multiindex = (3,), order = 3) # ⇒ (3, 0, 0)
MultilinearMap(f!; degree = 3, order = 3) # ⇒ (3, 0, 0)
# pure external forcing, f!(res, r): shape, order and multiplicity all assumed
MultilinearMap(f!) # ⇒ (0, 0), me 1, ORD 2
# the same term stated in full — silent
MultilinearMap(f!; multiindex = (0, 0), multiplicity_external = 1)
# a first-order term must say so, since the order defaults to 2
MultilinearMap(f!; order = 1) # ⇒ (2,), ORD 1
# f! is not symmetric in its two x^(0) slots
MultilinearMap(f!; multiindex = (2, 0), fully_asymmetric = true)
Errors
Throws ArgumentError if multiindex/derivatives entries are negative, if derivatives is not sorted, if order would truncate, if degree disagrees with multiindex, if a mixed internal/external split would have to be guessed, if the resulting degree is below 2 with no external factors, or if f! cannot be called with deg + 1 arguments.
MultilinearMap(f!, multiindex; fully_asymmetric = nothing)
Create a multilinear term for a system of order ORD without external dynamics.
Arguments
f!: in-place evaluation function, accumulating into its first argumentmultiindex::NTuple{ORD, Int}: how many argument slots use each derivative;multiindex[k]counts the slots takingx^(k-1)
Keyword arguments
fully_asymmetric: overrides the symmetry inferred frommultiindex; see the note in theMultilinearMapdocstring
Equivalent to MultilinearMap(f!; multiindex = multiindex).
MultilinearMap(f!, multiindex, multiplicity_external; fully_asymmetric = nothing)
Create a multilinear term for a system of order ORD that also takes the external state.
f! is called with the derivative arguments selected by multiindex first, then the external state r repeated multiplicity_external times. A term that depends on the external state may have total degree 1 — a pure forcing term is written MultilinearMap(f!, (0, 0), 1), for which MultilinearMap(f!) is a shorthand.
The model this term goes into must have an external system; NthOrderModel rejects a multiplicity_external > 0 term otherwise.
Arguments
f!: in-place evaluation function, accumulating into its first argumentmultiindex::NTuple{ORD, Int}: how many argument slots use each derivativemultiplicity_external::Int: how many timesris passed tof!
Keyword arguments
fully_asymmetric: overrides the symmetry inferred frommultiindex; see the note in theMultilinearMapdocstring
Equivalent to MultilinearMap(f!; multiindex = multiindex, multiplicity_external = multiplicity_external), and — like every positional form — it assumes nothing, so it never emits the @info the keyword constructor uses to report defaulted values.
A pure forcing term of a second-order system, MultilinearMap(f!, (0, 0), 1), is what the keyword constructor infers from a one-factor f! when nothing else is stated; see the keyword constructor's docstring for that shorthand and the assumptions it makes.
_arity_description(fixed, va)
_arity_description(fixed, va) -> String
Human-readable summary of the argument counts a callable accepts, e.g. "3, ≥ 1".
_build_multilinear_map(f!, multiindex::NTuple{ORD, Int64}, multiplicity_external::Int64, fully_asymmetric::Union{Nothing, Bool}) where ORD
_build_multilinear_map(f!, multiindex, multiplicity_external, fully_asymmetric)
Validate a fully resolved term and build it. The single place where the invariants of MultilinearMap are enforced; every public constructor ends up here.
Checks run in the order negatives → degree → arity so that sum(multiindex) is meaningful in every message.
_call_signature(multiindex, multiplicity_external)
_call_signature(multiindex, multiplicity_external) -> String
Render the call f! receives, e.g. "f!(res, x^(0), x^(0), x^(1), r)" for multiindex = (2, 1) and multiplicity_external = 1.
_check_arity(f!, deg, multiindex, multiplicity_external)
_check_arity(f!, deg, multiindex, multiplicity_external)
Throw an ArgumentError unless f! can be called with deg + 1 arguments.
hasmethod is the fast accept path, but it returns false for methods with concrete argument annotations, so a scan of the method table decides rejection. A varargs method that can absorb the arguments is accepted — that is what admits closures built by MORFESymbolicsExt and callable structs. A callable exposing no methods at all is trusted rather than rejected.
_counts_from_derivatives(derivatives::NTuple{N, Int64}) where N
_counts_from_derivatives(derivatives) -> NTuple
Convert a per-slot list of 0-based derivative orders into a multiindex of counts: (0, 0, 1) → (2, 1). The list must be non-decreasing, since it is the argument order f! will be called with.
_definition_site(f!)
_definition_site(f!) -> String
Return " @ file.jl:12" for the first method of f!, or "" when f! exposes no methods. Used by show and by FullOrderModel._term_label to point a diagnostic at the definition of a term. Deliberately uses first rather than only: f! is allowed to carry several methods, and a display helper must never throw.
_infer_degree(f!)
_infer_degree(f!) -> Int
Number of factors f! takes, i.e. its argument count less res. Requires a single fixed arity; varargs or conflicting arities are ambiguous and raise an ArgumentError telling the caller to state the degree explicitly.
_info_assumed(f!, multiindex, multiplicity_external, mi_source, assumed_order, assumed_me, forcing)
_info_assumed(f!, multiindex, multiplicity_external, mi_source, assumed_order,
assumed_me, forcing)
Emit an @info naming every value the constructor had to default, or nothing at all when the caller pinned them. mi_source is :stated, :arity or :degree, and names where an assumed multiindex came from.
fully_asymmetric is deliberately not reported here: it already has a dedicated diagnostic, FullOrderModel._info_implicit_symmetry, which fires at NthOrderModel construction exactly when the flag changes the result.
_method_arities(f!)
_method_arities(f!) -> (fixed, va)
Split the methods of f! by argument count, including res but excluding the callable itself. fixed lists the exact counts of the fixed-arity methods; va lists, for each varargs method, the minimum number of arguments it accepts.
_msg_assumed_compact(f!, multiindex, multiplicity_external, assumed)
_msg_assumed_compact(f!, multiindex, multiplicity_external, assumed) -> String
One-line report of the values the constructor had to default. assumed is the list of field names that were not stated; only those are named, so the closing sentence ("state them explicitly to silence this message") is always true.
_msg_assumed_forcing(f!, multiindex, multiplicity_external)
_msg_assumed_forcing(f!, multiindex, multiplicity_external) -> String
Full report for the one inference that can silently reinterpret a linear term as a forcing term. Every call it presents as silent really is silent under the resolution rules — the testset "suggested silent forms are silent" pins that.
_pad_to_order(base::NTuple{N, Int64}, order::Int64) where N
_pad_to_order(base, order) -> NTuple
Zero-pad base to length order. Padding is symmetry-neutral: trailing zeros change neither all(<=(1), mi) nor count(>(0), mi), so symmetry_type classifies (2,) and (2, 0, 0) identically. Truncation is refused — it would silently drop a slot.
_symmetry_label(multiindex, fully_asymmetric)
_symmetry_label(multiindex, fully_asymmetric) -> String
Name the symmetry class a term will be given by the parametrisation solver.
Mirrors symmetry_type in src/ParametrisationMethod/RightHandSide/MultilinearTerms/Symmetry.jl, which cannot be called from here: MultilinearMaps is loaded twelve includes earlier. The testset "show agrees with symmetry_type" holds the two in step.
accumulate_qp!(Fe, ∇W_args, mult, element, q, dΩ, t::FEMMultilinearMap)
accumulate_qp!(Fe, ∇W_args, mult, element, q, dΩ, t::FEMMultilinearMap) -> nothing
Accumulate the integrand contribution at quadrature point q (weight dΩ) into the element residual vector Fe. ∇W_args is an NTuple of pre-scattered W columns; mult is the combinatorial multiplicity of the term.
assemble_element!(accum, Fe, element, t::FEMMultilinearMap)
assemble_element!(accum, Fe, element, t::FEMMultilinearMap) -> nothing
Scatter the element residual Fe into the global accumulator accum using the DOF map of element.
evaluate_term!(res, term::MultilinearMap{ORD}, xs, r) where ORD
evaluate_term!(res, t::FEMMultilinearMap{ORD}, xs, r) where ORD
evaluate_term!(res, term, xs, r)
Evaluate a single MultilinearMap and accumulate (adds) the result into res.
Arguments
res: output vector (modified in-place)term: multilinear termxs: tuple(x, x^(1), …, x^(ORD-1))of state derivativesr: external state vector (ornothingif not used). Ifrisnothingbut the term expects external arguments, an error is thrown.
`r` is the *physical* external state
See evaluate_nonlinear_terms!: when the external system was re-based, the caller must convert the reduced coordinates r′ with ExternalSystems.to_physical_external first. During the cohomological solve this argument is a basis direction rather than a state — a unit vector eⱼ, or the column Q[:, j] after a re-basing — supplied by ExternalSystems.external_argument_vectors, so f! may receive a complex-valued external argument and must not assume an integer one.
evaluate_term!(res, t::FEMMultilinearMap{ORD}, xs, r)
Direct (uncached) evaluation of a FEM-backed multilinear term at state xs and external state r, accumulating the result into res. To be used in InvarianceError.jl.
Internal argument slots (determined by t.multiindex) are scattered to quadrature-point field quantities via scatter_qp! and assembled element-wise.
For me = 0: ∇W_args is a homogeneous NTuple{N_INT, QP_TYPE} — type-stable.
For me > 0: the external arg slots in ∇W_args receive r directly (not scattered). r is a small NEXT-dimensional external-state vector, not a FOM displacement field. The concrete `accumulateqp!evaluates the full multilinear mapF(∇u₁,…,∇uₙ, r₁,…,rₘₑ)` at the actual inputs.
fem_elements(t::FEMMultilinearMap)
fem_elements(t::FEMMultilinearMap) -> iterator
Return an iterator over the mesh elements (cells) for the FEM term t. Implement this for every concrete FEMMultilinearMap subtype.
fem_getdetJdV(element, q, t::FEMMultilinearMap)
fem_getdetJdV(element, q, t::FEMMultilinearMap) -> Real
Return the integration weight det(J) · w_q at quadrature point index q of element.
fem_n_qp(t::FEMMultilinearMap)
fem_n_qp(t::FEMMultilinearMap) -> Int
Return the number of quadrature points per element for the FEM term t.
fem_ndofs_per_cell(t::FEMMultilinearMap)
fem_ndofs_per_cell(t::FEMMultilinearMap) -> Int
Return the number of degrees of freedom per element (cell) for the FEM term t.
fem_qp_buffer(t::FEMMultilinearMap)
fem_qp_buffer(t::FEMMultilinearMap)
Return a pre-allocated scratch buffer sized for one quadrature point of the FEM term t. Reused across calls to avoid allocation in the inner element loop.
fem_reinit!(element, t::FEMMultilinearMap)
fem_reinit!(element, t::FEMMultilinearMap) -> nothing
Reinitialise the FEM quadrature cache (e.g. CellValues) for element. Called once per element before any scatter_qp! or accumulate_qp! calls.
scatter_qp!(∇W_col, W_global, element, t::FEMMultilinearMap)
scatter_qp!(∇W_col, W_global, element, t::FEMMultilinearMap) -> nothing
Fill ∇W_col in-place with the field values (e.g. gradients) at all quadrature points of element for one column of the parametrisation W_global.
Module ExternalSystems — representation of autonomous external dynamical systems that drive a full-order model.
An ExternalSystem encodes a finite-dimensional autonomous ODE
ṙ = f(r) = A r + higher-order terms
whose state r ∈ ℂᴺᴱˣᵀ appears as a forcing argument in the nonlinear terms of an NthOrderModel. The module stores the polynomial dynamics together with the linear matrix A and its eigenvalues, which enter the cohomological equations as external superharmonic frequencies.
Upper-triangularity, and how it is obtained
A must be upper triangular. The cohomological equations are solved monomial by monomial in GrLex order, which makes the solve causal: every coefficient a monomial needs is already available when it is reached. The |β| = 1 branch of the lower-order coupling needs W[α − eⱼ + eᵢ], a coefficient of the same total degree as α, and that coefficient precedes α in GrLex only when i < j. Accordingly LowerOrderCouplings._sum_degree_one_terms! reads only the strictly upper triangle of the reduced linear dynamics Λ.
Λ is block structured: its master block is diagonal, its lower-left block vanishes because the external system is autonomous, and its lower-right block is A. So Λ is upper triangular exactly when A is, and a strictly-lower entry of A would be discarded without trace.
A non-triangular A is therefore not rejected but re-based: the constructor finds a basis Q in which the linear part is upper triangular, re-expresses the whole polynomial in the new coordinates r′ where r = Q r′,
ṙ′ = U r′ + Q⁻¹ g(Q r′), U = Q⁻¹ A Q upper triangular
and stores Q in the basis field. Everything downstream — W's external columns, the reduced external coordinates, R's external rows — is then expressed in r′; external_basis recovers Q so results can be mapped back to the physical r. The solver feeds nonlinear terms the physical external argument automatically (see external_argument_vectors), so term definitions never need to change.
See _triangularising_basis for how Q is chosen, and why a real system's conjugate structure is preserved exactly.
ExternalSystem
ExternalSystem{N_EXT, T, EigenvalueType}
Represents a dynamical system of the form:
dr/dt = f(r) = A r + higher-order terms
where r ∈ ℝ^{N_EXT} (or ℂ^{N_EXT}), A is a constant upper-triangular matrix, and the dynamics are given by a polynomial expansion. The structure stores the full polynomial, the linear matrix, and its eigenvalues.
A must be upper triangular — see the module docstring for the GrLex-causality reason. A caller-supplied polynomial whose linear part is not triangular is re-based rather than rejected, and basis then records the change of coordinates.
Because it is triangular, A's eigenvalues are its diagonal entries, and they are stored by variable index: eigenvalues[e] == linear_matrix[e, e] is the eigenvalue that belongs to external variable e. This is not a sorted spectrum; the ordering carries meaning, since resonance detection contracts the eigenvalues against multiindex components position by position.
Fields
first_order_dynamics::DensePolynomial{T, N_EXT, 2}— the full polynomial dynamics, vector-valued: maps ℂ^{NEXT} → ℂ^{NEXT}. In the re-based coordinatesr′whenbasis !== nothing.linear_matrix::SMatrix{N_EXT, N_EXT, T}— the linear part, i.e. the Jacobian at the origin. Static, sinceN_EXTis small and known at compile time. Always exactly upper triangular, and always re-derived fromfirst_order_dynamics, so the two can never disagree.eigenvalues::SVector{N_EXT, EigenvalueType}— eigenvalues oflinear_matrix, i.e. its diagonal in variable order, cached because they set the external part of the superharmonics. WhenT <: Realthese are still complex, soEigenvalueType = Complex{T}; otherwiseEigenvalueType = T.basis::Union{Nothing, SMatrix{N_EXT, N_EXT, T}}— the change of external coordinatesQwithr = Q r′, ornothingwhen the supplied dynamics were already triangular and nothing was touched.
Constructors
ExternalSystem(first_order_dynamics)Build from a polynomial. Computes the linear matrix and associated eigenvalues automatically, re-basing first if the linear part is not upper triangular.ExternalSystem(eigenvalues)Construct a purely linear systemdx/dt = diag(eigenvalues) * x, i.e., decoupled linear dynamics. Diagonal by construction, so it always satisfies the triangularity requirement.
_evtype(::Type{T}) -> Type
Return the eigenvalue storage type for scalar type T: Complex{T} when T <: Real, or T itself when T <: Complex.
_rebase(poly::DensePolynomial{T0, N_EXT, 2, A} where A<:AbstractMatrix{T0}, Q, Qinv) where {T0, N_EXT}
_rebase(poly, Q, Qinv) -> DensePolynomial
Re-express ṙ = f(r) in the coordinates r′ with r = Q r′, giving ṙ′ = Q⁻¹ f(Q r′).
The substitution itself is compose_linear (in Polynomials); the left multiplication by Q⁻¹ is a single matrix product on the coefficient array, since the polynomial is vector-valued with one row per component.
The promotion on the first line is load-bearing: compose_linear types its accumulators from the input polynomial, so a real polynomial composed with a complex Q cannot store the products back. promote_type rather than a blanket complex keeps a real A with an all-real spectrum in real arithmetic.
_subdiagonal_offenders(linear_matrix::AbstractMatrix)
_subdiagonal_offenders(linear_matrix) -> Vector{Tuple{Int, Int}}
The (i, j) positions of the non-zero entries strictly below the diagonal.
Monomials are solved in GrLex order, so the |β| = 1 lower-order coupling can only read Λ[i, j] for i < j (LowerOrderCouplings._sum_degree_one_terms!); a strictly-lower entry of the external linear matrix would be silently discarded by the solver rather than producing a wrong-but-visible answer. Such entries are what triggers a re-basing, and this list is what the diagnostic reports. See the ExternalSystems module docstring.
_triangularising_basis(A::StaticArraysCore.SMatrix{N, N, T}) where {N, T}
_triangularising_basis(A) -> Union{Nothing, NamedTuple}
Choose a basis Q in which Q⁻¹ A Q is upper triangular, or nothing when A already is.
Returns (; Q, U, Qinv, route) with route ∈ (:eigen, :schur).
Two requirements, and why the route matters
Triangularity is the GrLex-causality constraint (module docstring); both routes give it.
Conjugate structure is the second, and only one route gives it. Realification.realify applies a single conj_map across all NVAR variables, external ones included, and fill_conjugate_monomial! implements W_{P·γ} = conj(W_γ) for a real FOM. Preserving that needs an involution σ with Q[:, σ(k)] = conj(Q[:, k]) and λ_{σ(k)} = conj(λ_k); a basis that breaks it silently invalidates any conjugate_permutation touching external indices — a wrong answer, not an error.
A Schur basis breaks it in general: in A = Q U Qᴴ the first Schur vector Q[:, 1] is an eigenvector for λ₁, but Q[:, 2] is a Schur vector, not an eigenvector for λ₂, so Q[:, 2] ≠ conj(Q[:, 1]) unless A is normal.
The eigenvector basis of a real A gives both. LAPACK's dgeev returns complex eigenvalues in adjacent conjugate pairs, each pair's eigenvectors stored as the real and imaginary parts of one column pair, so V[:, k+1] == conj(V[:, k]) holds bit-exactly; real eigenvalues get real, self-conjugate eigenvectors. And U = Diagonal(λ) is diagonal, which is strictly better than triangular: it lets the solver take the uncoupled fast path (coupled_external = !isdiag(...) in CohomologicalDriver).
So: real and diagonalisable → eigen; anything else (complex A, or real but near-defective, where the eigenvector basis would be catastrophically ill-conditioned) → complex schur. Note schur on a real matrix returns the real Schur form, which is only quasi-triangular — it must be given a complex matrix.
_unit(::Val{N}, j::Int64) where N
_unit(::Val{N}, j) -> SVector{N, Int}
The j-th unit multiindex / basis vector in N variables.
_zero_subdiagonal_linear_block!(poly::DensePolynomial{T, N_EXT, 2, A} where A<:AbstractMatrix{T}; tol) where {T, N_EXT}
_zero_subdiagonal_linear_block!(poly; tol)
Zero the strictly-lower triangle of poly's linear block, after checking it is only round-off.
compose_linear produces sub-diagonal entries of size ~1e-17·‖A‖ where exact arithmetic would give zero, and istriu tests exact zeros — so without this the "triangularised" system is still not triangular by the package's own predicate, and linear_matrix would disagree with the polynomial it is supposed to be the Jacobian of.
linear_matrix_of_polynomial sets A[:, j] = coefficients[:, i_j] where i_j is the mset position of the unit multiindex eⱼ, so A[k, j] is coefficients[k, i_j] and the strict lower triangle is k > j within each unit-multiindex column.
A residual above tol means the basis is wrong; that must error rather than be truncated away, because truncating it would discard genuine coupling.
external_argument_vectors(sys::Union{Nothing, ExternalSystem}, N_EXT::Int64)
external_argument_vectors(sys, N_EXT) -> Vector{<:SVector{N_EXT}}
The external argument the solver passes for each external variable j.
During the cohomological solve the external factor of a monomial is a basis vector, not a state: the multilinear term is evaluated once per external direction and the coefficient is read off. In the model's own coordinates that vector is the unit vector eⱼ; after a re-basing it is the physical direction Q eⱼ = Q[:, j]. Multilinearity makes the substitution exact — f(…, Qeⱼ, Qe_k) = Σ_{a,b} Q[a,j] Q[b,k] f(…, e_a, e_b).
With basis === nothing this returns the integer unit vectors, i.e. exactly what the solver used before re-basing existed.
external_basis(sys) -> Union{Nothing, SMatrix}
The change of external coordinates Q with r = Q r′, or nothing when the system was left in the coordinates it was given in. nothing is the common case and is what every consumer branches on; it means "reduced external coordinates are the physical ones".
to_physical_external(::Union{Nothing, ExternalSystem}, ::Nothing)
to_physical_external(sys::Union{Nothing, ExternalSystem}, r)
to_physical_external(sys, r) -> r_physical
Map a reduced external coordinate vector r′ to the physical r = Q r′.
The state-valued counterpart of external_argument_vectors, used where an external argument is a genuine state rather than a basis direction — notably the invariance-error residual, which samples reduced coordinates and must feed the model physical ones. Identity when the system was never re-based.
Module FullOrderModel — representation of high-dimensional nonlinear ODEs.
The central type is NthOrderModel{ORD, ORDP1, N_NL, N_EXT, T, MT}, which encodes an ORD-th order system
B_ORD ẋ^(ORD) + … + B₁ ẋ + B₀ x = F(x, ẋ, …, r)
where r satisfies an autonomous external system. Linear terms are stored as an NTuple{ORDP1, MT} of matrices; nonlinear terms as an NTuple{N_NL, AbstractMultilinearMap}.
Key functions: linear_first_order_matrices (produces the companion-form (A, B) pair used by eigensolvers), evaluate_nonlinear_terms!.
NthOrderModel
NthOrderModel{ORD, ORDP1, N_NL, T, MT} <: AbstractFullOrderModel
Representation of an ORD-th (ORDP1=ORD+1) order dynamical system of the form
B_ORD x^(ORD) + ... + B_1 x^(1) + B_0 x = F(x^(ORD-1), …, x^(1), x, r, …, r)
where:
x^(n) is the n-th derivative of x (x^(n) = d_t^n x)
x^(0) = x is the state vector
B_i are the coefficient matrices
F is a multilinear polynomial function of the derivatives and the external state vector r
The external state r satisfies its own first‑order dynamics r' = g(r)
Generic type parameters
ORDdefines the order of the ODE.ORDP1is the number of linear terms (from 0 through ORD). It must satisfy ORDP1 == ORD+1.N_NLis the number of nonlinear terms in the tuple nonlinear_terms.N_EXTis the size of the external system.Tis the numeric type.MTis the matrix type that forms the ORDP1-tuple of linear_terms.
Fields
n_fom::Int— dimension of the full‑order state vectorx.linear_terms::NTuple{ORDP1, MT}— the linear coefficient matrices(B_0, …, B_ORD), all of identical size.nonlinear_terms::NTuple{N_NL, AbstractMultilinearMap{ORD}}— the nonlinear contributions, each aMultilinearMaporFEMMultilinearMap.external_system::Union{Nothing, ExternalSystem{N_EXT}}— the external dynamics, ornothingfor an unforced model.max_nl_degree::Int— the largest combined degree overnonlinear_terms, cached at construction. It bounds the polynomial order at which anything nonlinear can still contribute, and drives the progress indicator's work estimate.
Representation
Linear terms are stored as a tuple
(B_0, …, B_ORD)Nonlinear terms are represented as a collection of
MultilinearMaps
Each MultilinearMap defines:
which derivatives are involved (via
multiindex)how many times the external state appears as an argument (via
multiplicity_external)the combined degree
deg = sum(multiindex) + multiplicity_external
Notes
All matrices must have identical size.
The nonlinear structure is stored in sparse form (only active terms).
_check_external_terms(nonlinear_terms)
_check_external_terms(nonlinear_terms)
Reject terms that read the external state in a model built without one.
Without this the mismatch survives construction and only surfaces mid-solve, as "Term expects external arguments but no external state provided" from evaluate_term!.
evaluate_nonlinear_terms!(res, model::NthOrderModel{ORD, ORDP1, N_NL, N_EXT, T} where {N_EXT, T}, order, state_vectors) where {ORD, ORDP1, N_NL}
evaluate_nonlinear_terms!(res, model::NthOrderModel{ORD, ORDP1, N_NL, N_EXT, T} where {N_EXT, T}, order, state_vectors, r) where {ORD, ORDP1, N_NL}
evaluate_nonlinear_terms!(res, model, order, state_vectors, r = nothing)
Evaluate all nonlinear terms of a given polynomial degree for an NthOrderModel.
Arguments
res: output vector (modified in-place)model: theNthOrderModelorder: degree of the nonlinear terms to evaluatestate_vectors: tuple(x, x^(1), …, x^(ORD-1))of state derivativesr: external state vector (defaultnothing). Must be provided if any term uses external variables.
`r` is the *physical* external state
Terms are defined in the external coordinates the model was written in. When the external system was re-based (its linear matrix was not upper triangular, so ExternalSystem chose a new basis Q), the solver's reduced external coordinates r′ are related to these by r = Q r′, and it is the caller's job to convert: ExternalSystems.to_physical_external(model.external_system, r′) does it, and is the identity for every system that was not re-based. Passing r′ where r is expected silently evaluates the terms at the wrong point.
linear_first_order_matrices(model::NthOrderModel{ORD, ORDP1, N_NL, N_EXT, T, MT}) where {ORD, ORDP1, N_NL, N_EXT, T, MT<:(SparseMatrixCSC{T})}
linear_first_order_matrices(model::NthOrderModel{ORD, ORDP1, N_NL, N_EXT, T, MT}) where {ORD, ORDP1, N_NL, N_EXT, T, MT<:AbstractMatrix{T}}
linear_first_order_matrices(model::NthOrderModel)
Construct the matrices A and B of the equivalent linear first-order system:
B Ẋ = A X
obtained from the ORD-th order model
B_ORD x^(ORD) + ... + B_1 x^(1) + B_0 x = F(...)
by introducing the augmented state vector
X = [x, x^(1), ..., x^(ORD-1)].
and the (ORDn_fom x ORDn_fom)-block matrices
B = [ I 0 0 ⋯ 0
0 I 0 ⋯ 0
⋮ ⋱
0 0 0 ⋯ B_ORD ]
and
A = [ 0 I 0 ⋯ 0
0 0 I ⋯ 0
⋮ ⋱
-B₀ -B₁ -B₂ ⋯ -B_{ORD-1} ]
where I is the n_fom × n_fom identity matrix.
show(io::IO, ::MIME{Symbol("text/plain")}, m::NthOrderModel{ORD, ORDP1, N_NL, N_EXT, T, MT}) where {ORD, ORDP1, N_NL, N_EXT, T, MT}
show(io::IO, m::NthOrderModel{ORD, ORDP1, N_NL, N_EXT, T, MT}) where {ORD, ORDP1, N_NL, N_EXT, T, MT}
Base.show(io::IO, ::MIME"text/plain", m::NthOrderModel)
Print a summary of the model.
Without this, showing a model dumps every field — which for a FEM backend means the entire DofHandler behind each nonlinear term. Everything printed here is already on the type; nothing is computed.
The one-line method exists for the same reason: a model nested inside another container would otherwise expand its whole field tree there too.
Module SpectralDecomposition — solve the generalised eigenproblem for an NthOrderModel and package the result for a parametrisation.
The whole spectral layer, in one module. It replaces the former split between Eigensolvers (a stub that held no solvers) and Eigenproblems (which held them) — a division that named neither half for what it contained.
Files
| File | Contents |
|---|---|
Eigensolvers.jl | AbstractEigensolver and its subtypes, the eigensolve / eigensolve_left interface, generalised_eigenpairs, the Spectrum container, sorting/normalisation, and left-block reconstruction |
SpectralData.jl | ModeBundle and SpectralData — the selected, model-reconciled bundle a parametrisation consumes |
ConjugatePermutation.jl | the conjugate involution derived from a spectrum and an external system |
Two layers, deliberately
Spectrum is raw solver output: every mode computed, no selection state. SpectralData is what a parametrisation actually consumes: the selected master modes, reconciled against a specific model's order, plus the outer eigenvalues resonance detection reads. Keeping them apart means a spectrum can be solved once and several reductions taken from it, and that selection is a pure operation rather than a mutation.
Interface contract — full order-blocks
eigensolve and eigensolve_left must return eigenvectors as FOM × ORD × n arrays containing ALL companion order-blocks, not just the physical slice:
right:
(λB − A) ψ = 0, blocksψ = [ψ_1; …; ψ_ORD]withψ_{k+1} = λ ψ_k;left (sesquilinear):
φᴴ (λB − A) = 0, reported eigenvalueλ(the pencil eigenvalue of the adjoint problem isconj(λ)).
The eigensolver is the single owner of eigenvalue knowledge: it uses λ to define the eigenvector blocks, and downstream code reads the blocks without ever folding eigenvalues. Solvers producing only the physical left slice can reconstruct the rest with left_eigenmode_orders_from_slice.
Naming: spectral data, not eigen-data
SpectralData and ModeBundle store right_blocks / left_blocks, not "eigenvectors", and the names are chosen to keep room. A master set need not consist of eigenpairs: for a defective eigenvalue the invariant subspace is spanned by a Jordan chain of generalised eigenvectors, and such a chain would sit in exactly these fields alongside its eigenvalue. Nothing here implements Jordan chains today, but nothing here assumes their absence either — the block layout and the accessors carry over unchanged.
AbstractEigensolver
AbstractEigensolver
Abstract supertype for all eigensolvers accepted by eigensolve.
Concrete subtypes must implement eigensolve and eigensolve_left for NthOrderModel. Two built-in subtypes are provided: DefaultEigensolver and ArpackEigensolver.
Interface contract — full order-blocks
Both eigensolve and eigensolve_left must return eigenvectors as FOM × ORD × n arrays containing ALL companion order-blocks, not just the physical slice:
right:
(λB − A) ψ = 0, blocksψ = [ψ_1; …; ψ_ORD]withψ_{k+1} = λ ψ_k;left (sesquilinear):
φᴴ (λB − A) = 0, reported eigenvalueλ(the pencil eigenvalue of the adjoint problem isconj(λ)).
The eigensolver is the single owner of eigenvalue knowledge: it uses λ to define the eigenvector blocks, and downstream code (orthogonality and invariance operators) reads the blocks without ever folding eigenvalues. Solvers that naturally produce only the physical left slice can reconstruct the blocks with left_eigenmode_orders_from_slice.
ArpackEigensolver
ArpackEigensolver <: AbstractEigensolver
Sparse eigensolver backed by Arpack.eigs. Computes nev smallest-magnitude eigenpairs. If nev is not specified, Arpack's default count is used.
Fields
nev::Union{Nothing, Int64}— number of eigenpairs to compute.nothingmeans "not chosen yet"; the zero-argument constructor warns, because leaving the count to Arpack on a large model is rarely what the caller wants.eigenvalues::Union{Nothing, Vector{ComplexF64}}— the eigenvalues from the most recenteigensolve, cached so the left problem can reuse them. Left undefined by the constructors, not set tonothing: guard withisdefinedbefore reading it.
DefaultEigensolver
DefaultEigensolver <: AbstractEigensolver
Dense eigensolver backed by LinearAlgebra.eigen. Computes all eigenpairs. Suitable for small to medium full-order models (dense matrices).
ModeBundle
ModeBundle{ORD, EV}
One family of modes — eigenvalues plus, optionally, their right and left eigenvector order-blocks.
EV is the eigenvalue container, which is what lets a single type serve both families: the master bundle uses SVector{ROM, ComplexF64} because the solve requires a statically sized vector, while the outer bundle uses Vector{ComplexF64} because its length varies run to run and must not drive recompilation.
Fields
eigenvalues::EVright_blocks,left_blocks—FOM × ORD × n, ornothingwhen modes were not kept (outer bundles default to eigenvalues only).right_physical,left_physical—FOM × nmaterialised copies of the physical slices.
Why the physical slices are cached copies, not views
Three reasons, all load-bearing:
A
Bool-mask selection is not strided, so a view of it would drop downstream BLAS/sparse products onto the slow generic path — the reason the previous code took copies explicitly.solve_parametrisationtypes its right modes as a concreteMatrix{ComplexF64}.An accessor that sliced would allocate on every call rather than once.
The cost is 2·FOM·n complex numbers, negligible beside the FOM × ORD × L parametrisation.
MorfeEigensolver
MorfeEigensolver <: AbstractEigensolver
Sparse eigensolver for the shifted eigenproblem, backed by Arpack.eigs through generalised_eigenpairs. Shifting targets the eigenvalues nearest a chosen point in the complex plane, which is how master modes around a given frequency are picked out of a large spectrum.
Fields
nev::Union{Nothing, Int64}— number of eigenpairs to compute.nothingis resolved to the full problem size on the firsteigensolve.shift::Union{Nothing, ComplexF64}— the shift point.nothingfalls back to the unshiftedeigs.eigenvalues::Union{Nothing, Vector{ComplexF64}}— eigenvalues from the most recenteigensolve, cached so the left problem can reuse them. Left undefined by the constructors, not set tonothing: guard withisdefinedbefore reading it.
SpectralData
SpectralData{ORD, ROM}
The complete spectral input to a parametrisation: a master ModeBundle (always carrying modes), an outer bundle (eigenvalues always, modes optional), and the conjugate structure of the spectrum they were selected from.
Element type is pinned to ComplexF64 rather than left as a parameter, because the solve is ComplexF64 throughout; a generic element type would only create a silent conversion boundary.
Fields
master::ModeBundle{ORD, SVector{ROM, ComplexF64}}outer::ModeBundle{ORD, Vector{ComplexF64}}— the eigenvalues left off the manifold, which outer resonance detection reads.conjugate_permutation::Union{Nothing, Vector{Int}}— the involutionσover the whole spectrum,1:n_eigs, withλ[σ(i)] = conj(λ[i]);nothingwhen the spectrum has no conjugate structure or none was requested.master_permutation,outer_permutation—σrestricted to each bundle and re-indexed, computed once in the constructor. Read them throughmaster_conjugate_permutation/outer_conjugate_permutation.mode_numbers::Vector{Int}— physical mode number of each spectrum entry, derived fromσ's orbits (see below).
One involution, detected once — and each restriction computed once
Conjugacy is a property of the spectrum, not of a bundle: master and outer modes are subsets of one index set. Detecting per bundle would establish the same fact twice, so σ is detected once over all eigenvalues.
The restrictions are then computed once, in the constructor, and stored — not derived per call. The master restriction is what every solve reads, so recomputing it would put an avoidable allocation on that path. Whether σ was detected or the master pairing was supplied explicitly, the result is settled at construction and thereafter only read.
A restriction is well defined only if the index set is closed under σ — both members of a pair selected, or neither. Splitting a pair across the master/outer boundary is rejected at construction, where the offending entry can be named, rather than surfacing later inside the solve.
Mode rescaling for conditioning (e.g. multiplying both sides by 1e-2) is a caller-side operation on the raw arrays before construction; there is deliberately no scale field.
SpectralData(; eigenvalues, right_modes, left_modes,
right_derivatives = nothing, left_blocks = nothing,
outer_eigenvalues = ComplexF64[], conjugate_permutation = nothing)
Build SpectralData directly from raw arrays — for eigensolvers that never construct a Spectrum (a shift-invert Hopf solve, say).
Two shapes are accepted:
Whole blocks.
right_modesandleft_modesareFOM × ORD × ROMarrays (orFOM × ROMmatrices whenORD == 1), withright_derivativesandleft_blocksleftnothing. Remember the mirrored convention: the left array's last slice is the physical one.Physical slices plus their companions.
right_modesandleft_modesare theFOM × ROMphysical slices,right_derivativesisFOM × (ORD-1) × ROMholdingW^(k)[eᵣ], andleft_blocksisFOM × (ORD-1) × ROMholding the lower-order left blocksφ_{r,j}. This is the shape callers of the old positional solve already had, and taking it here means the mirrored convention is applied in one place instead of at every call site — where a swap is type-correct and compiles silently.
conjugate_permutation is taken as given here (no :detect): a caller assembling raw arrays is in the best position to know the pairing, and eigenvalue-based detection is not sufficient on its own.
It is accepted at either of two lengths:
ROM— the master block. Outer entries are left self-paired, the honest statement that raw arrays say nothing about their conjugate structure.ROM + length(outer_eigenvalues)— the involution over the whole synthetic spectrum, used verbatim. Pass this when the outer modes' pairing IS known, so thatphysical_modenumbers physical modes rather than eigenvalues and a conjugate pair among the outer targets reports once rather than twice. A real (non-oscillatory) outer mode is its own conjugate and maps to itself.
SpectralData(model, eigenproblem; master,
conjugate_permutation = nothing, keep_outer_modes = false)
Select the master modes out of a solved eigenproblem and reconcile their blocks against model's order.
master is either a Vector{Int} of indices or a Vector{Bool} mask.
Three rules that carry correctness weight
Order is never changed. master's index order is the reduced-coordinate order — it determines the conjugate permutation, the monomial-set variable roles and the resonance target numbering. Nothing here sorts; sorting is a property of the eigenproblem, applied before selection.
Blocks are sliced when the orders match, extended only when they don't. If the eigenproblem was solved on an operator of the same order as model, the stored blocks are used as-is — not recomputed, because they share whatever scaling the biorthogonal normalisation applied and a recomputation can drift from that. When model has the higher order (an augmented (K, C, M, 0) fed by a second-order structural eigenproblem), the missing right blocks are generated by multiplying the last available block by λ — not by forming a fresh λ^{k-1} ψ — and the left blocks are rebuilt from the physical slice against model.linear_terms.
conjugate_permutation defaults to nothing. Pass :detect to derive it from the master eigenvalues; unlike bare detect_conjugate_permutation, that path also verifies the eigenvectors (Ψ[:, σ(r)] ≈ conj(Ψ[:, r]) on every order-block, both sides) and returns nothing with an @info if they disagree — eigenvalue pairing alone is necessary but not sufficient, and a wrong permutation silently corrupts W and R. The default is nothing so that enabling conjugate symmetry is always a deliberate act.
An explicit vector is accepted at either of two lengths:
n_eigs— the involution over the whole spectrum, stored verbatim. Prefer this whenever the solver's pairing is known for every entry. A structural solver returning adjacent conjugate pairs, for instance, hasσ = reduce(vcat, [[2p, 2p-1] for p in 1:n_pairs])exactly. Outer entries then carry their true partner, sophysical_modenumbers physical modes rather than individual eigenvalues, and per-mode diagnostics (the outer-resonance warning) name the pair instead of warning once per conjugate.ROM— the master block only. Outer entries are left self-paired, the honest reading of "the caller stated the master pairing, not the spectrum's"; an outer conjugate pair then reports as two separate modes.
Both derive the same master restriction, so moving a call site from the second form to the first leaves the solve bit-identical.
Spectrum
Spectrum{T}
Stores sorted and biorthogonally normalised left/right eigenpairs of the generalised eigenproblem A x = λ B x, together with a master-mode selector.
Left eigenmodes are kept in two forms: the full order-block array and the physical-space (highest-order) slice of it. The full blocks feed the orthogonality row operators directly, so no eigenvalue folding is needed to reconstruct them; the slice is what physical-space post-processing and export want.
Immutable, and it holds no master selection. A spectrum is what the eigensolver computed; which of its modes span the manifold is a separate decision, and it belongs to the SpectralData built from it — SpectralData(model, spectrum; master = …). The type used to be mutable with a master_modes field only so the old select_master_modes_* mutators had somewhere to write, which meant one object could mean different things at different points in a script.
Fields
solver::AbstractEigensolver— the solver that computed the eigenpairs, retained so downstream code can query how they were obtained.eigenvalues::Array{Complex{T}}— sorted eigenvaluesλ.eigenmodes::Array{Complex{T}}— right eigenvectors asFOM × ORD × n_eigs, sorted to matcheigenvalues.left_eigenmodes::Union{Nothing, Matrix{Complex{T}}}— physical-space left eigenvectors,FOM × n_eigs;nothinguntil set.left_eigenmodes_orders::Union{Nothing, Array{Complex{T}, 3}}— full left eigenvector order-blocks,FOM × ORD × n_eigs;nothingwhen the solver supplied only the physical slice.
StructureModalDampingEigensolver <: AbstractEigensolver
solves eigenproblem of the mechanical 2nd order problem:
$M \ddot{U} + C \dot{U} + K U + n(U) = 0$with $C = \alpha*M + \beta*K$ under the assumption that M and K are spd matrices.
The steps are:
calculate eigenpair $\omega_k, \varphi_k$ of: $(K-\omega_k^2*M)\varphi_k=0$
calculate: $\xi_k=0.5(\frac{\alpha}{\omega_k} + \beta * \omega_k)$
calculate eigenvalues: $\lambda_k = -\xi_k*\omega_k \sqrt{1-\xi_k^2}$
Calculates only the first nev eigenvectors, sorted by increasing $\omega_k$: mode k occupies the adjacent conjugate entries 2k-1, 2k.
Fields
eigenvalues::Union{Nothing, Vector}— eigenvalues from the most recenteigensolve,nothingbefore the first one. Cached so the left problem can reuse them instead of re-running Arpack.nev::Int64— number of modes to compute, counted in the second-order problem (sonevfrequenciesωₖ, not2·nevfirst-order eigenvalues).α::Float64— mass-proportional damping coefficient inC = αM + βK.β::Float64— stiffness-proportional damping coefficient inC = αM + βK.
_mode_numbers(σ::Union{Nothing, AbstractVector{Int64}}, n_eigs::Int64)
_mode_numbers(σ, n_eigs) -> Vector{Int}
Number the physical modes of a spectrum: entry i gets out[i], the index of the conjugate pair it belongs to.
The pairs are the orbits of σ, numbered by first appearance — walk the spectrum in order and, on reaching an unvisited entry, assign the next number to it and to σ(i). A self-paired entry (σ(i) = i, a real eigenvalue) is its own mode.
This deliberately does not compute ⌈i/2⌉. That formula assumes the eigensolver emits conjugate partners adjacently, which is a convention of some solvers rather than a fact about spectra — a shift-invert or filtered solve can return {1, 5} as a pair. Deriving the numbering from σ agrees with the adjacent case and stays correct otherwise.
With no conjugate structure (σ === nothing), every entry is its own mode.
_stack_blocks(physical::AbstractArray, companions::Union{Nothing, AbstractArray}, ROM::Int64, side::Symbol)
_stack_blocks(physical, companions, ROM, side) -> Array{ComplexF64, 3}
Assemble one side's order-blocks, applying the mirrored convention here and nowhere else: the right physical slice is block 1 with its derivatives in 2:ORD, the left physical slice is block ORD with its orthogonality blocks in 1:(ORD-1).
With companions === nothing the caller already holds whole blocks and they are used as given.
_structural_left_eigenmode_orders(λ::AbstractVector, Y::AbstractArray{<:Complex, 3}, mass::AbstractMatrix, damping::AbstractMatrix)
_structural_left_eigenmode_orders(λ, Y, mass, damping) -> Array{ComplexF64, 3}
Analytic companion left-eigenvector order-blocks for a proportionally damped second-order structure M ẍ + C ẋ + K x = 0 with real symmetric M, K and C = αM + βK. For the sesquilinear left eigenvector φ (solving φᴴ (λB − A) = 0 on the companion pencil) with real position mode ϕ = Y[:, 1, k]:
φ_2 = ϕ (physical slice)
φ_1 = (conj(λ) M + C) ϕ
These are exactly the blocks eigensolve_left would return — algebraically equal to -(1/conj(λ)) Kᵀϕ via the quadratic eigenrelation, but built from the moderate-norm M, C instead of K (which amplifies eigensolver noise by (ω_max/ω₁)²). No adjoint eigensolve is needed because the proportional damping makes the blocks analytic in ϕ.
check_biorthogonality(sd::SpectralData{ORD, ROM}, model::NthOrderModel) where {ORD, ROM}
check_biorthogonality(sd::SpectralData, model) -> Matrix{ComplexF64}
Return the master-block biorthogonality matrix G[r, s] = φᵣᴴ B ψₛ, which should be the identity for biorthogonally normalised eigenvectors.
This is the numerical guard on the mirrored right/left index convention: swapping the physical slice for a derivative block, or the two sides for each other, destroys G ≈ I loudly, whatever accessor was misused.
Diagnostic only — deliberately not called from parametrise or the solve, so it adds no cost to a normal run. Call it in tests, or when a result looks wrong.
detect_conjugate_permutation(lambda::AbstractVector; atol)
detect_conjugate_permutation(lambda; atol = 1e-8) -> Union{Vector{Int}, Nothing}
Attempt to construct a conjugate-permutation vector from the eigenvalue vector lambda (length NVAR). Returns a Vector{Int} perm such that
lambda[perm[i]] ≈ conj(lambda[i]) for all i
and perm[perm[i]] == i (involution), or nothing if no such perfect pairing exists (e.g. an eigenvalue has no conjugate partner within atol).
Warning — necessary but not sufficient. Two eigenvalues forming a conjugate pair does not guarantee that the corresponding eigenvectors satisfy
master_modes[:, perm[r]] ≈ conj(master_modes[:, r]).
This condition can fail when:
the eigenvalue is degenerate (eigenspace has dimension > 1),
the solver returned a non-conjugate basis for a repeated eigenvalue,
eigenvectors were post-processed with different phases or normalisation.
Always verify eigenvector conjugacy (e.g. check norm(master_modes[:, perm[r]] - conj(master_modes[:, r]))) before passing the returned vector to solve_parametrisation as conjugate_permutation. Passing an incorrect permutation silently corrupts W and R.
Arguments
lambda— eigenvalue vector of lengthNVAR(master + external eigenvalues).atol— absolute tolerance for the conjugate-match test
`|lambda[j] - conj(lambda[i])| < atol`.
Returns
Vector{Int} (involution, 1-based) if a perfect pairing is found; nothing otherwise.
eigensolve(model::NthOrderModel, solver::DefaultEigensolver)
eigensolve(model::NthOrderModel, solver::MorfeEigensolver)
eigensolve(model::NthOrderModel, solver::StructureModalDampingEigensolver)
eigensolve(model::NthOrderModel, solver::AbstractEigensolver, args...)
eigensolve(::AbstractMatrix, ::AbstractMatrix, ::StructureModalDampingEigensolver)
eigensolve(model::NthOrderModel, solver::DefaultEigensolver)
Solves right eigenproblem using eigen from LinearAlgebra. Let A and B be the first order matrices of model. Then it returns the eigenpairs
eigensolve(model::NthOrderModel, solver::MorfeEigensolver)
Solves right eigenproblem using generalised_eigenpairs from MORFE.Eigensolvers. Let A and B be the first order matrices of model. Then it returns the eigenpairs
eigensolve(mass::AbstractMatrix, stiffness::AbstractMatrix, solver::StructureModalDampingEigensolver)
Calculates right eigenvectors in the secod order form. Uses the relations:
$ (K-\omega_k^2*M)\y_{k,U}=0 $ $ y_{k,V}=\lambda_k y_{k,U} $and assumes $C = \alpha*M + \beta*K$.
eigensolve(model::NthOrderModel, solver::StructureModalDampingEigensolver)
Thin wrapper: extracts M = model.linear_terms[end] and K = model.linear_terms[1] and delegates to eigensolve(M, K, solver).
eigensolve_left(model::NthOrderModel, solver::DefaultEigensolver)
eigensolve_left(model::NthOrderModel, solver::MorfeEigensolver)
eigensolve_left(model::NthOrderModel, solver::AbstractEigensolver, args...)
eigensolve_left(model::NthOrderModel, solver::DefaultEigensolver)
Solves left eigenproblem using eigen from LinearAlgebra. let A and B be the first order matrices of model. Then it returns the eigenpairs
eigensolve_left(model::NthOrderModel, solver::MorfeEigensolver)
Solves left eigenproblem using σ-shift to recover the correct eigenvalues.
external_conjugate_permutation(::Nothing; atol)
external_conjugate_permutation(sys::ExternalSystem; atol)
external_conjugate_permutation(sys; atol = 1e-8) -> Union{Vector{Int}, Nothing}
The conjugate involution σ on the N_EXT external variables, or nothing when the external system has no conjugate structure to offer.
σ pairs external variable k with the one carrying conj(λ_k), and — when the system was re-based — additionally guarantees Q[:, σ(k)] == conj(Q[:, k]). Both conditions are needed for the reduction's conjugate symmetry: Realification.realify applies one conj_map across all NVAR variables, external ones included, and fill_conjugate_monomial! implements W_{P·γ} = conj(W_γ). With a real forcing F the external columns satisfy Φ[:, k] = L(λ_k)⁻¹ F Q[:, k], so conj(Φ[:, k]) = Φ[:, σ(k)] follows exactly from the two conditions together.
Eigenvalue pairing alone is not sufficient — see detect_conjugate_permutation's own warning — which is why the basis columns are verified rather than assumed. A system re-based onto a Schur basis generally fails that check and correctly returns nothing: its external variables simply are not conjugate pairs.
Use full_conjugate_permutation to assemble the full NVAR permutation.
full_conjugate_permutation(master_perm::AbstractVector{Int64}, sys::Union{Nothing, ExternalSystem})
full_conjugate_permutation(master_perm, sys) -> Vector{Int}
Assemble the full NVAR-length conjugate_permutation from the master block and the external system, appending ROM .+ σ where σ is the external involution.
Callers otherwise hand-write the whole vector ([2, 1, 3], [2, 1, 4, 3], …), which bakes in a pairing that a change of external coordinates can invalidate, and which has to special -case an odd number of external variables. Deriving the external block instead keeps it correct in both situations.
Throws when the external system has no conjugate structure — see external_conjugate_permutation.
generalised_eigenpairs(args...; kwargs...)
generalised_eigenpairs(A, B; nev, shift=nothing, which=:LM, tol=0.0,
maxiter=3000, ncv=nothing, v0=nothing,
ritzvec=true, sort_largest_real=false)
Solve the generalised eigenproblem A x = lambda B x using Arpack.
Requires Arpack.jl and LinearMaps.jl. Load them to activate the MORFE extension.
indices(b::ModeBundle)
indices(b::ModeBundle) -> Vector{Int}
The positions this bundle's modes occupy in the source spectrum.
Selecting master modes discards the original numbering, so it is recorded here. Two things need it: restricting the spectrum-wide conjugate involution to this bundle, and reporting a mode by the entry a user would index in their own spectrum. Conjugate partners need not be adjacent, so these are not recoverable by arithmetic.
left_eigenmode_orders_from_slice(linear_terms::NTuple{ORDP1, AbstractMatrix}, left_slice::AbstractMatrix, eigenvalues::AbstractVector; apply) where ORDP1
left_eigenmode_orders_from_slice(linear_terms, left_slice, eigenvalues)
-> Array{ComplexF64, 3}
Reconstruct the full left eigenvector order-blocks from the physical-space slice, for callers that compute only the latter. For the sesquilinear left eigenvector φ = [φ_1; …; φ_ORD] of reported eigenvalue λ (physical slice ℓ = φ_ORD, satisfying L(λ)ᴴ ℓ = 0 with L(s) = Σ_k B_k s^k), the companion block equations give
φ_{ORD-1} = conj(λ) · (B_ORDᴴ ℓ) + B_{ORD-1}ᴴ ℓ
φ_j = conj(λ) · φ_{j+1} + B_jᴴ ℓ j = ORD-2, …, 1
The eigenvalue is used only to define the eigenvector from its slice — the per-monomial cohomological solve reads the blocks and never touches it. Prefer eigensolvers that return the full blocks directly (eigensolve_left does); use this only when a slice is all you have.
The slice may carry an arbitrary per-mode scale: the blocks scale with it, and the orthogonality equations are invariant under per-mode row scaling.
apply — which operator each block is hit with
apply(B_j) is what multiplies ℓ; it defaults to adjoint, giving the B_jᴴ form derived above, which is correct for a general pencil.
Pass apply = identity when the pencil is self-adjoint — B_j real symmetric for every j, so L(s)ᵀ = L(s). Two things then hold simultaneously: B_jᴴ = B_j, and the left eigenvector is the conjugate of the right one, so a right position mode may legitimately be handed in as left_slice. That is exactly the structural case, and _structural_left_eigenmode_orders is the thin wrapper for it.
The keyword exists rather than hard-coding adjoint because exact symmetry is expected of assembled M, C but not guaranteed. When B is exactly symmetric the two routes agree bitwise (measured, dense and sparse alike: symmetry makes the scatter and gather traversals visit the same indices in the same order). But a single ulp of asymmetry — Ke[i, j] and Ke[j, i] are separate floating-point expressions in an element routine — makes B * x and B' * x differ at round-off. apply = identity therefore keeps the structural path bit-for-bit unconditionally, without resting on an assumption about the assembler, and states the self-adjointness claim at the call site.
Sharing the recurrence is the point: the φ_ORD = ℓ fill-downward index convention is the easy thing to get backwards, and it is now written once.
left_mode_blocks(b::ModeBundle) -> view or nothing
Blocks 1:(ORD-1) of the left eigenvectors — the ones feeding the orthogonality row operators. nothing when ORD == 1.
left_modes(b::ModeBundle) -> Matrix{ComplexF64}
The physical-space left eigenvectors, FOM × n — the highest-order block, mirroring right_modes, which is the lowest. Cached at construction.
master_by_sorting(nev::Integer)
master_by_sorting(nev::Integer) -> Vector{Int}
The first nev spectrum entries, as master indices — the eigenpairs were already sorted by spectrum.
Pure: it returns indices for SpectralData(model, sp; master = …) rather than marking them on the spectrum. The mutating select_master_modes_by_sorting it replaces wrote a mask into the Spectrum, which made the same object mean different things at different points in a script and left the selection invisible at the call site that used it.
master_by_target_frequency(sp::Spectrum, target_frequencies::AbstractVector, tol::Float64)
master_by_target_frequency(sp::Spectrum, target_frequencies, tol) -> Vector{Int}
The spectrum entries within tol of any of target_frequencies, as master indices.
Distance used: dist(a, b) = abs(real(a - b)) + abs(imag(a - b)). A target that matches nothing warns and contributes no index — it is a mis-specified target, not an empty selection, and silently returning fewer masters than asked for is the failure mode worth naming.
master_conjugate_permutation(sd::SpectralData)
master_conjugate_permutation(sd) -> Union{Nothing, Vector{Int}}
σ restricted to the master modes and re-indexed to 1:ROM — the form the cohomological solve consumes, before it is extended over the external variables.
Computed once in the constructor; this is a field read. Every solve consults it, so it is not something to re-derive per call.
normalise_biorthogonal!(model::NthOrderModel, eigenmodes::Array{T}, left_eigenmodes::Array{T}) where T
normalise_biorthogonal!(
model::NthOrderModel,
eigenmodes::Matrix{T},
left_eigenmodes::Matrix{T})
Normalise eigenmodes to fulfill the equation xi^H * B * yj = δ_{ij}
Both sides are scaled symmetrically: with s = sqrt(x_i^H B y_i), the right eigenmode is divided by s and the left eigenmode by conj(s), so the sesquilinear pairing becomes exactly 1.
outer_conjugate_permutation(sd::SpectralData)
outer_conjugate_permutation(sd) -> Union{Nothing, Vector{Int}}
σ restricted to the outer modes and re-indexed to 1:n_outer. Used to group conjugates when reporting off-manifold near-resonances.
Computed once in the constructor; this is a field read.
physical_mode(sd::SpectralData, i::Integer)
physical_mode(sd::SpectralData, i::Integer) -> Int
The physical mode number of spectrum entry i — conjugate partners share a number.
right_mode_derivatives(b::ModeBundle) -> view or nothing
Blocks 2:ORD of the right eigenvectors — the time derivatives ψ_{k+1} = λ ψ_k. nothing when ORD == 1.
right_modes(b::ModeBundle) -> Matrix{ComplexF64}
The physical-space right eigenvectors, FOM × n. Cached at construction; no indexing.
sort_by_magnitude!(eigenvalues, eigenmodes)
sort_by_magnitude!(eigenvalues, eigenmodes)
Sorts eigenpairs to resemble the order: |λ[1]| ≤ |λ[2]| ≤ ... where λ=eigenvalues.
sort_left_eigenmodes(eigenvalues, left_eigenvalues, left_eigenmodes)
sort_left_eigenmodes(eigenvalues, left_eigenvalues, left_eigenmodes)
Sorts the left eigenpairs to match right eigenpairs, by using the distance function dist(a, b) = abs(real(a - b)) + abs(imag(a - b))
spectrum(model::NthOrderModel, solver::StructureModalDampingEigensolver; sorter!)
spectrum(stiffness::AbstractMatrix, mass::AbstractMatrix, solver::StructureModalDampingEigensolver; sorter!)
spectrum(model::NthOrderModel; solver, sorter!, normaliser!)
spectrum(model::NthOrderModel, solver::StructureModalDampingEigensolver; sorter!)
Specialised path for StructureModalDampingEigensolver: mass-normalisation is built into eigensolve, and the left eigenvector order-blocks are analytic in the position mode (see _structural_left_eigenmode_orders). No adjoint solve or biorthogonal normalisation is needed.
The optional sorter! kwarg has the same semantics as in the general eigensolve: pass (args...) -> nothing to preserve the solver's natural ordering.
spectrum(stiffness, mass, solver::StructureModalDampingEigensolver; sorter!)
Convenience overload: pass K and M directly without constructing an NthOrderModel. The Rayleigh damping matrix C = αM + βK is rebuilt from the solver parameters for the left eigenvector order-blocks.
spectrum(
model::NthOrderModel;
solver::AbstractEigensolver = DefaultEigensolver(),
sorter!::Function = sort_by_magnitude!,
normaliser!::Function = normalise_biorthogonal!)
Computes left and right eigenpairs of the problem described in model by using the defined solver. Additionally sorter! sorts the eigenpairs and normaliser! is used to normalise the eigenmodes.
spectrum_entries(sd::SpectralData, p::Integer)
spectrum_entries(sd::SpectralData, p::Integer) -> Vector{Int}
The spectrum entries making up physical mode p — one for a real mode, two for a conjugate pair. Not necessarily consecutive, which is why they are looked up rather than computed.
Module Realification — convert complex-valued parametrisations and reduced dynamics to real-valued form.
The cohomological equations are solved in ComplexF64 to handle both damped and undamped systems uniformly. For systems with real matrices and complex-conjugate master-mode pairs, the resulting W and R satisfy conjugate-symmetry relations that allow an exact transformation to real arithmetic. This module implements that transformation so that subsequent time integration and post-processing can operate entirely in real arithmetic.
_realify_term(exp_vec::StaticArraysCore.SVector{N, Int64}, coeff::C, n::Int64) where {C, N}
_realify_term(exp_vec::SVector{N,Int}, coeff::C, n::Int)
-> Dict{SVector{N,Int}, C} where {C,N}
Transform a single term (exponent vector exp_vec and coefficient coeff) of a polynomial in the canonical form (z, z̄, w) into a sum of real monomials. Returns a dictionary mapping new exponent vectors (in the real variables) to their coefficients.
Here N = 2n + m, with n conjugate pairs and m real variables.
_reorder_canonical(poly::DensePolynomial{C, N, N1, A} where {N1, A<:AbstractArray{C, N1}}, conj_map::Vector{Int64}) where {C, N}
_reorder_canonical(poly::DensePolynomial{C,N}, conj_map::Vector{Int})
-> (DensePolynomial{C,N}, n, m)
Reorder variables according to a conjugation map conj_map of length N (where N = number of variables).
conj_map[i] = jmeans variableiis conjugate to variablej.If variable
iis real, thenconj_map[i] = i.
The reordering groups variables as (z₁, …, zₙ, conj(z₁), …, conj(zₙ), w₁, …, wₘ) where n is the number of conjugate pairs and m the number of real variables. Terms with the same exponent after reordering are merged.
Returns the canonical polynomial (same concrete type as poly), n, and m.
realify(poly::DensePolynomial, conj_map::Vector{Int64})
realify(poly::DensePolynomial, conj_map::Vector{Int}) -> DensePolynomial
Transform a complex‑valued polynomial (with variables that may be conjugate pairs) into a polynomial in real variables.
Arguments
poly: a polynomial in variablesz₁, …, z_N.conj_map: a vector of lengthNwhereconj_map[i] = jmeans variableiis the conjugate of variablej; ifiis real, thenconj_map[i] = i.
Returns
A new polynomial in real variables x₁, …, x_n, y₁, …, y_n, w₁, …, w_m with n conjugate pairs and m real variables. The transformation uses the formulas z = x + i y, z̄ = x - i y. The returned polynomial has the same concrete type as the input poly (including the same number of variables).
realify_via_linear(poly::DensePolynomial, conj_map::Vector{Int64})
realify_via_linear(poly::DensePolynomial, conj_map::Vector{Int}) -> DensePolynomial
Transform a complex‑valued polynomial into a polynomial in real variables by composing with the linear map that expresses complex variables in terms of real and imaginary parts. This is an alternative implementation to realify that uses the compose_linear function. The returned polynomial has the same concrete type as the input poly (real coefficients).
See also: realify, compose_linear
Module Resonance — resonance detection for the parametrisation method.
Eigenvalue roles
Three eigenvalue groups are distinguished:
master_eigenvalues(required): the ROM eigenvalues. They enter the superharmonics = ⟨λ, α⟩and define the inner resonance targets (rows 1:ROM ofinner_resonances).external_eigenvalues(optional): eigenvalues of the external forcing system. They entersthrough the multiindex coefficients but do not produce a target row. Pass them so thatsis computed over the fullNVAR = ROM + N_EXTindex.outer_eigenvalues(optional): additional resonance targets (eigenvalues not included inmaster_eigenvalues, tested for near-resonance). They define the rows ofouter_resonancesbut do not enters.
Choosing a resonance style
Four constructors are provided:
resonance_set_from_graph_style: every monomial of total degree ≥ 2 is automatically resonant with all master modes (inner resonances); outer resonances are flagged by eigenvalue proximity. Use for non-autonomous Invariant Manifolds (e.g. SSMs) with harmonic forcing.resonance_set_from_complex_normal_form_style: inner resonances determined by eigenvalue proximity tomaster_eigenvalues; suitable for autonomous Invariant Manifolds (e.g. NNMs) with complex conjugate reduced variables.resonance_set_from_real_normal_form_style: like CNF but conjugate pairs share the resonance flag; use when building a real-valued ROM.resonance_set_from_condition_number_estimate: flags near-resonances using a condition-number criterion rather than a fixed tolerance.
ConditionNumberEstimateCondition <: OuterResonanceCondition
Flags a monomial as resonant using the criterion:
|λⱼ - s| * max_cond < spectral_radius * κ(λⱼ)
Scaling the test by the spectral radius and by each eigenvalue's own conditioning makes the criterion dimensionless, so it transfers across models without retuning a raw distance tolerance.
Fields
eigenvalues::Vector{ComplexF64}— the target eigenvalues, in local indexing.spectral_radius::Float64— spectral radius of the full-order system, setting the scale against which|λⱼ - s|is judged.condition_numbers::Vector{Float64}— per-target eigenvalue condition numbersκ(λⱼ).max_cond::Float64— the largest condition number tolerated for the cohomological operator before the monomial counts as resonant.target_indices::Vector{Int}— which local targets this condition applies to.conjugacy_map::Union{Nothing, Vector{Int}}— optional local conjugacy map, used as inRealEigenvalueCondition;nothingwhen pairing is not wanted.
EigenvalueCondition
EigenvalueCondition <: OuterResonanceCondition
Flags a monomial as resonant when |λⱼ - s| < tol.
Fields
eigenvalues::Vector{ComplexF64}— the target eigenvalues, in local indexing.tol::Union{Float64, Vector{Vector{Float64}}}— a scalar tolerance, or a per-monomial, per-target table when the threshold has to vary.target_indices::Vector{Int}— which local targets this condition applies to, typically1:n. Kept explicit so several conditions can cover disjoint targets.
GraphInternal
GraphInternal <: InternalResonance
Every monomial of total degree ≥ 2 is marked resonant with all inner master modes. Linear monomials eᵣ are resonant only with their own mode r.
InternalResonance
InternalResonance
Abstract supertype for strategies that decide which monomials are resonant with the inner (ROM) master modes.
NormalFormInternal
NormalFormInternal <: InternalResonance
No monomial is automatically marked resonant with inner modes; resonance is determined entirely by the eigenvalue-proximity condition.
OuterResonanceCondition
Abstract supertype for conditions that test whether a monomial is resonant with a mode at superharmonic frequency s.
All concrete subtypes must implement is_resonant(cond, target::Int, s::Number, k::Int) -> Bool.
RealEigenvalueCondition <: OuterResonanceCondition
Flags a monomial as resonant when |λⱼ - s| < tol or |λ_{conj(j)} - s| < tol, so that conjugate eigenvalue pairs share the resonance flag.
Fields
eigenvalues::Vector{ComplexF64}— the target eigenvalues, in local indexing.conjugacy_map::Vector{Int}—conjugacy_map[i]is the local index of the conjugate of eigenvaluei. Pairing them keeps the flags symmetric, which a real-valued full-order model requires.tol::Union{Float64, Vector{Vector{Float64}}}— a scalar tolerance, or a per-monomial, per-target table.target_indices::Vector{Int}— which local targets this condition applies to.
ResonanceConfig
ResonanceConfig(; style, tol, tol_relative, conjugacy_map, outer_targets, warn_outer)
Every knob that controls resonance detection, in one place.
Previously these were loose keyword arguments spread across parametrise (resonance, resonance_tol, conjugacy_map) and re-implemented again in MORFEFerrite's structural backend (resonance_tol, resonance_tol_rel, plus a separate off-manifold warning pass). Gathering them means there is one thing to read, and combinations that cannot work are rejected where they are written rather than deep inside the solve.
Fields
style::Symbol = :graph—:graph,:complex_normal_form, or:real_normal_form.tol::Union{Nothing, Real, AbstractVector} = nothing— absolute detuning threshold.nothingmeans "not specified" and resolves to the style's default.tol_relative::Union{Nothing, Real} = nothing— when set, replacestolby the per-target thresholdtol_relative * |λⱼ|, judging each target on its own frequency scale. This is the physically meaningful criterion when the master modes span decades.conjugacy_map::Union{Nothing, Vector{Int}} = nothing— required by:real_normal_form, and an error with any other style rather than silently ignored.outer_targets::Bool = false— also flag resonances against the non-master eigenvalues, populating theouter_resonancesblock. Diagnostic: the solve reads only the inner block.warn_outer::Bool = true— warn when a monomial is near-resonant with an off-manifold mode, whose direction is then solved through a near-singular operator.eigenvalue_projection::Symbol = :full— which part of the master eigenvalues detection compares.:fulluses them as they are;:imaginary_part_onlyreplacesλbyi·Im(λ), so near-resonance is judged on frequency alone and the growth rate is ignored. See below.
eigenvalue_projection
For an oscillatory reduction about a marginally stable state — a Hopf normal form, say — what makes a monomial resonant is that its frequency combination ⟨Im λ, α⟩ lands on a target frequency. The growth rate Re λ is small, and it varies with the continuation parameter, so letting it enter the detuning makes the flag pattern depend on where in a parameter sweep you happen to be. :imaginary_part_only removes that dependence.
It applies to the master eigenvalues only — the external and outer eigenvalues are compared as they are. Note it also changes what tol_relative means, and helpfully so: tol_relative * |λ| becomes tol_relative * |Im λ|, a tolerance measured on the same frequency scale the detection now uses.
The default is :full, so nothing changes unless it is asked for.
Why tol defaults to nothing rather than 1e-2
The guards below fire on explicitly set values. With a numeric default there would be no way to tell "the user asked for this tolerance" from "nobody said anything", so a plain ResonanceConfig() would emit a spurious "tolerance unused" notice on every run — and guards that cry wolf get ignored. nothing is the only honest "unspecified".
ResonanceSet
ResonanceSet{ROM, N_EXT, M}
Boolean look-up table recording which monomials are resonant with which master-mode or outer-mode targets.
Type parameters: ROM = number of master modes, N_EXT = external system size, M = matrix type (typically BitMatrix).
Resonance decides, per monomial, whether the cohomological system gets a border on a given master row — so this table is consulted once per monomial per master mode, and is precomputed as bits rather than re-tested against tolerances during the solve.
Use one of the resonance_set_from_* constructors rather than building this directly.
Fields
multiindices::MultiindexSet— the set over which resonances are defined; itsNVARmust equalROM + N_EXT, which the constructor enforces.inner_resonances::M—ROM × NMON; entry(r, k)istruewhen monomialkis resonant with master moder.outer_resonances::Union{Nothing, M}—n_out × NMONfor outer (non-master) targets, ornothingwhen there are none. Outer resonance is not something the border can absorb; it signals that the master set is too small.
_build_inner_matrix(strategy::MORFE.Resonance.InternalResonance, inner_cond::Union{Nothing, MORFE.Resonance.OuterResonanceCondition}, super_eigenvalues, multiindices::MultiindexSet, n_int::Int64)
Build the n_int × NMON inner resonance matrix.
strategy applies graph/normal-form unconditional flags; inner_cond (optional) applies an eigenvalue-proximity check on the master eigenvalues.
_build_outer_matrix(outer_cond::MORFE.Resonance.OuterResonanceCondition, super_eigenvalues, multiindices::MultiindexSet, n_out::Int64)
Build the n_out × NMON outer resonance matrix using outer_cond.
_first_close_pair(eigenvalues::AbstractVector, tol::Float64, n::Int64)
_first_close_pair(eigenvalues, tol, n) -> (i, j, gap)
The first pair of eigenvalues separated by no more than tol, or (0, 0, 0.0) if there is none. Allocation-free, and stops at the first hit.
Split out from the caller so the @info that reports it sits outside the loop: string interpolation inside a loop body captures the loop variables and boxes them on every iteration, even when the branch is not taken and nothing is logged.
_outer_warn_tolerance(config::ResonanceConfig, outer_eigenvalues::AbstractVector)
_outer_warn_tolerance(config, outer_eigenvalues)
-> Union{Nothing, Float64, Vector{Float64}}
The threshold the off-manifold scan compares |λ_outer[j] - s| against, resolved without going through resolve_tolerances.
That function is wrong for this caller three times over: it re-emits every @info guard a second time, tol_relative makes it build n_monomials identical tolerance rows where the scan wants one, and for an explicitly per-target tol it returns nothing for the outer family — a vector sized for the inner targets cannot be indexed by an outer target number. That last case used to reach _resolve_outer_tol and throw, so a per-target tolerance combined with the default warn_outer = true aborted the solve. It now returns nothing, and the scan is skipped with a notice rather than taking the run down with it.
_resolve_outer_tol(tol, outer_tol, n_out::Int64, n_int::Int64)
_resolve_outer_tol(tol, outer_tol, n_out, n_int)
Pick the tolerance for the outer targets, and reject the one combination that used to be a silent bounds error.
A per-target tolerance is read tol[k][local_idx] with local_idx local to its own condition — 1:n_int for the inner block, 1:n_out for the outer one. The two blocks are built from separate condition objects, so they can perfectly well carry separate tolerances; only the public constructors' single tol argument tied them together, and a per-target vector sized for n_int then overran whenever n_out > n_int.
outer_tol === nothing means "reuse tol", which is exactly right for a scalar (it applies to any number of targets) and impossible for a per-target vector — hence the error, which tells the caller what to pass instead of failing deep inside is_resonant.
_warn_outer_resonances(mset::MultiindexSet, master_eigs, outer_eigs, external_eigs, config::ResonanceConfig, spectral)
_warn_outer_resonances(mset, master_eigs, outer_eigs, external_eigs, config, spectral)
Warn when a monomial is near-resonant with a physical mode that is not on the manifold.
That direction is then solved through a near-singular operator, so the ROM loses accuracy there regardless of how the load is shaped: solve_single_monomial! builds its operator from s = ⟨λ, α⟩ alone, which makes the conditioning independent of the right-hand side. Rounding injects a component along the near-null direction and 1/(λ_s - s) amplifies it.
Runs for autonomous and forced models alike — a monomial built purely from master coordinates that lands on an off-manifold eigenvalue is exactly as near-singular as a forced one, and that its cause is the chosen master set makes it more worth surfacing.
Why this does not build a ResonanceSet
It used to, and that made an on-by-default diagnostic cost O(NMON × n_outer) with a large constant: a whole second set including an n_int × NMON inner block that was discarded, _superharmonics allocating a temporary per monomial twice, and _local_index's findfirst inside is_resonant turning the outer build into O(NMON × n_outer²).
The criterion needs none of that — it is one distance test per (monomial, outer target) — so it is written out directly. A run that flags nothing now costs a constant 64 bytes (measured, unchanged from |mset| = 20 to 54) against the 246 400 the probe cost at |mset| = 35 with 58 outer modes. The test itself is unchanged: |λ_outer[j] - s| < tol, the same EigenvalueCondition comparison the probe applied, for every style. Reusing the already-built outer block instead was rejected deliberately: under :real_normal_form that block ORs each target with its conjugate, which would silently change what gets reported.
apply_internal_resonances!(::AbstractMatrix{Bool}, ::MORFE.Resonance.NormalFormInternal, ::AbstractVector{Int64}, ::Int64, ::Int64)
apply_internal_resonances!(mat::AbstractMatrix{Bool}, ::MORFE.Resonance.GraphInternal, mi::AbstractVector{Int64}, n_int::Int64, k::Int64)
apply_internal_resonances!(mat, strategy, mi, n_int, k)
Set inner-resonance flags in column k of the inner matrix mat for the monomial with exponent vector mi.
GraphInternal: alln_introws flagged for degree ≥ 2; for a linear monomialeᵣonly rowr(ifr ≤ n_int) is flagged.NormalFormInternal: no-op.
build_resonance_set(model::NthOrderModel, mset::MultiindexSet, spectral, config::ResonanceConfig)
build_resonance_set(model, mset, spectral::SpectralData, config::ResonanceConfig)
-> ResonanceSet
Build the resonance set from a SpectralData bundle and a ResonanceConfig — the single entry point for resonance construction.
Reads the master and outer eigenvalues off spectral (replacing the eigenproblem plus master-mask plumbing), resolves the config's tolerances into correctly-sized inner and outer objects via resolve_tolerances, and warns about off-manifold near-resonances when config.warn_outer is set.
empty_resonance_set(multiindices::MultiindexSet{NVAR}, n_int::Int64) where NVAR
empty_resonance_set(multiindices::MultiindexSet{NVAR}, n_int::Int64, n_out::Int64) where NVAR
empty_resonance_set(multiindices, n_internal, n_outer=0) -> ResonanceSet
Construct a ResonanceSet with all resonance flags set to false. n_internal = number of master modes (ROM); n_outer = number of outer targets (0 = none).
is_resonant(rs::ResonanceSet{ROM}, idx::Int64, target::Int64) where ROM
is_resonant(rs::ResonanceSet{ROM}, mi::Vector{Int64}, target::Int64) where ROM
is_resonant(cond::MORFE.Resonance.EigenvalueCondition, target::Int64, s::Number, k::Int64)
is_resonant(cond::MORFE.Resonance.RealEigenvalueCondition, target::Int64, s::Number, k::Int64)
is_resonant(cond::MORFE.Resonance.ConditionNumberEstimateCondition, target::Int64, s::Number, ::Int64)
is_resonant(rs, idx, target) -> Bool
is_resonant(rs, mi, target) -> Bool
Return true when the monomial at position idx (or exponent vector mi) is resonant with target target. Targets 1:ROM query inner_resonances; targets > ROM query outer_resonances (returns false when outer is nothing).
n_internal(::ResonanceSet{ROM}) where ROM
n_internal(rs::ResonanceSet{ROM}) -> ROM
Return the number of internal master modes (compile-time constant from type parameter).
project_eigenvalues(λ::AbstractVector, projection::Symbol)
project_eigenvalues(λ, projection::Symbol) -> Vector
Apply a ResonanceConfig eigenvalue projection: :full is the identity, returning Vector{ComplexF64}; :imaginary_part_only returns the imaginary parts Im(λ) as Vector{Float64}.
A projected eigenvalue i·Im(λ) is carried by the real number Im(λ) — the two are isomorphic for this purpose, because for real a, b
|i·a − i·b| = |a − b|
and the whole of detection is abs(eig - s). So carrying the frequencies as Float64 reproduces the pure-imaginary complex computation exactly, with no i·Im reconstruction and one code path serving both modes.
Project every family the same way — master, external and outer. Mixing a projected family with an unprojected one promotes the superharmonic s = ⟨λ, α⟩ back to complex and reinstates the growth rates through the back door, which is precisely what the projection was asked to remove.
Always returns a plain Vector. The master eigenvalues arrive as a static vector, and a comprehension over one stays static — a SizedVector that the resonance_set_from_* signatures reject. The loops below are the same reason the caller used Vector{ComplexF64}(…) rather than collect.
resolve_tolerances(config::ResonanceConfig, master_eigenvalues::AbstractVector, outer_eigenvalues::AbstractVector, n_monomials::Int64)
resolve_tolerances(config, master_eigenvalues, outer_eigenvalues, n_monomials)
-> (inner_tol, outer_tol)
Turn a ResonanceConfig into the two correctly-sized tolerance objects the resonance-set constructors want, and emit @info for settings that will have no effect.
With tol_relative, each family is sized for its own target count:
inner[k][r] = tol_relative * |λ_master[r]| length n_int
outer[k][j] = tol_relative * |λ_outer[j]| length n_out
which is why the two can now be combined at all.
resonance_set_from_complex_normal_form_style(multiindices::MultiindexSet{NVAR}, master_eigenvalues::AbstractVector{<:Number}, tol::Union{Float64, Vector{Vector{Float64}}}; external_eigenvalues, outer_eigenvalues, outer_tol) where NVAR
resonance_set_from_complex_normal_form_style(
multiindices, master_eigenvalues, tol;
external_eigenvalues, outer_eigenvalues)
Build a ResonanceSet using the complex normal form style: inner resonances flagged by |λᵣ - s| < tol for each master mode r; outer resonances (if any) flagged by proximity to outer_eigenvalues.
Suitable for autonomous Invariant Manifolds (e.g. NNMs) with complex conjugate reduced variables; add external_eigenvalues for non-autonomous systems where the multiindex includes forcing directions.
resonance_set_from_condition_number_estimate(multiindices::MultiindexSet{NVAR}, master_eigenvalues::AbstractVector{<:Number}, spectral_radius::Float64, target_condition_numbers::Vector{Float64}, max_cond::Float64; external_eigenvalues, outer_eigenvalues, inner_target_indices, outer_target_indices, conjugacy_map) where NVAR
resonance_set_from_condition_number_estimate(
multiindices, master_eigenvalues, spectral_radius,
target_condition_numbers, max_cond;
external_eigenvalues, outer_eigenvalues,
inner_target_indices, outer_target_indices, conjugacy_map)
Build a ResonanceSet using a condition-number criterion:
|λⱼ - s| * max_cond < spectral_radius * κ(λⱼ)
target_condition_numbers has n_int + n_out entries: the first n_int are for the master modes, the remainder for the outer modes.
inner_target_indices / outer_target_indices restrict which rows in each sub-matrix are populated (default: all).
resonance_set_from_graph_style(multiindices::MultiindexSet{NVAR}, master_eigenvalues::AbstractVector{<:Number}, external_eigenvalues::AbstractVector{<:Number}, outer_eigenvalues::AbstractVector{<:Number}, tol::Union{Float64, Vector{Vector{Float64}}}) where NVAR
resonance_set_from_graph_style(multiindices::MultiindexSet{NVAR}, master_eigenvalues::AbstractVector{<:Number}, external_eigenvalues::AbstractVector{<:Number}, outer_condition::MORFE.Resonance.OuterResonanceCondition) where NVAR
resonance_set_from_graph_style(
multiindices, master_eigenvalues, external_eigenvalues,
outer_eigenvalues, tol)
Build a ResonanceSet using the graph style: every monomial of total degree ≥ 2 is marked resonant with all ROM master modes. Outer targets are flagged by eigenvalue proximity |λⱼ - s| < tol.
master_eigenvalues: ROM eigenvalues; entersand are inner targets.external_eigenvalues: entersonly (e.g. forcing frequencies in the multiindex, since external_eigenvalues cant be targets).outer_eigenvalues: outer targets (eigenvalues not included inmaster_eigenvalues, tested for near-resonance). PassComplexF64[]when there are no outer targets.
resonance_set_from_graph_style(multiindices, master_eigenvalues, external_eigenvalues, outer_condition)
Advanced overload that accepts a pre-built OuterResonanceCondition for the outer rows. outer_condition.target_indices must use local indices 1:n_out. n_out is inferred as maximum(outer_condition.target_indices) (or 0 if empty).
resonance_set_from_real_normal_form_style(multiindices::MultiindexSet{NVAR}, master_eigenvalues::AbstractVector{<:Number}, conjugacy_map::Vector{Int64}, tol::Union{Float64, Vector{Vector{Float64}}}; external_eigenvalues, outer_eigenvalues, outer_tol) where NVAR
resonance_set_from_real_normal_form_style(
multiindices, master_eigenvalues, conjugacy_map, tol;
external_eigenvalues, outer_eigenvalues)
Build a ResonanceSet using the real normal form style: like CNF but conjugate pairs share the resonance flag — monomial k is resonant with target j when |λⱼ - s| < tol OR |λ_{conj(j)} - s| < tol.
conjugacy_map has length(master_eigenvalues) + length(outer_eigenvalues) entries; the first n_int cover inner targets, the remainder cover outer targets (re-indexed locally). conjugacy_map[i] is the local index of the conjugate of target i.
resonant_multiindices(rs::ResonanceSet{ROM}, target::Int64) where ROM
resonant_multiindices(rs, target) -> Vector{Int}
Return the positions of all monomials resonant with target. Targets 1:ROM query inner_resonances; targets > ROM query outer_resonances.
resonant_targets(rs::ResonanceSet, idx::Int64)
resonant_targets(rs::ResonanceSet, mi::Vector{Int64})
resonant_targets(rs, idx) -> AbstractVector{Bool}
resonant_targets(rs, mi) -> Union{AbstractVector{Bool}, Nothing}
Return a boolean vector indicating which targets are resonant with the monomial at position idx (or exponent vector mi). Concatenates inner and outer rows. Returns nothing when mi is not in the multiindex set.
set_resonance!(rs::ResonanceSet{ROM}, target::Int64, idx::Int64, value::Bool) where ROM
set_resonance!(rs::ResonanceSet{ROM}, target::Int64, mi::Vector{Int64}, value::Bool) where ROM
set_resonance!(rs, target, idx, value) -> rs
set_resonance!(rs, target, mi, value) -> rs
Set the resonance flag for target target and monomial idx (or multiindex vector mi) to value. Targets 1:ROM address inner_resonances; targets > ROM address outer_resonances. Returns rs for chaining. Warns if mi is not found.
Assemble the part of the cohomological equations that corresponds to the FullOrderModel, thereby imposing invariance of the manifold. MasterModeOrthogonality handles the orthogonality conditions induced by the parametrisation styles.
Nomenclature
| Symbol | Meaning |
|---|---|
| FOM | Full-order model dimension |
| ROM | Number of master modes (reduced coordinates) |
| N_EXT | Number of external forcing modes |
| NVAR | ROM + N_EXT (total reduced variables) |
| R | Set of resonant master modes |
Non-resonant master modes have trivial (zero) reduced dynamics and are excluded from the cohomological equations; their columns in C(s) are omitted.
Per-multiindex cohomological equation
For each multi-index k with superharmonic s = Σᵢ kᵢ λᵢ (λᵢ eigenvalues of the master modes), the cohomological equation has the block structure
[ L(s) C(s) ] * [ W_k; f_res ] = RHS_k
where:
L(s)(FOM × FOM) is the parametrisation operator (characteristic-matrix polynomial of the full-order model),C(s)(FOM × |R|) acts on the unknown reduced-dynamics coefficientsf_resof the resonant master modes,RHS_kcontains all known lower-order contributions and external forcing.
External forcing modes are not unknowns; their contributions are handled separately and appear on the right-hand side.
Construction of L(s) and C(s) via Horner
The operator L(s) is defined as
L(s) = Σ_{k=1}^{ORD+1} B[k] s^{k-1}
where B[k] are the coefficient matrices of the linear part of the full-order model (size FOM × FOM). L(s) is evaluated efficiently using Horner's method.
The operator acting on the reduced dynamics is
C(s) = Σ_{j=1}^{ORD} D[j] s^{j-1}
with pre-computed coefficient matrices D[j] (size FOM × NVAR) given by
D[j] = Σ_{k=j+1}^{ORD+1} B[k] · generalised_right_eigenmodes · reduced_dynamics_linear^{k-(j+1)}
Here:
generalised_right_eigenmodes:NVAR × FOMmatrix collecting the generalised eigenvectors of the master modes and the external forcing modes.reduced_dynamics_linear: Jordan matrix of the linear part of the reduced dynamics.
The D[j] matrices are pre-computed once per order using a downward recurrence (similar to the Horner scheme in MasterModeOrthogonality).
Precomputation and assembly
The coefficients D[j] are pre-computed for all NVAR = ROM + N_EXT variables. However, when assembling the linear system for a given multi-index (with superharmonic s), only a subset of the columns of C(s) is used:
For the left-hand-side matrix
[L(s) C(s)], only the columns corresponding to the resonant master modes (size|R|) are extracted fromC(s). Non-resonant master modes are omitted because their reduced dynamics is identically zero.The external forcing modes (
N_EXTcolumns) are handled separately and do not appear as unknowns; their contributions are moved to the right-hand side via the operator-E(s)(see below).
Right-hand-side assembly
RHS_k is the sum of two independent contributions: lower-order terms from the cohomological equation, and external forcing terms. Both are evaluated using fused Horner passes that reuse intermediate matrices to minimise computational cost.
Lower-order RHS (cohomological coupling)
During the Horner evaluation of L(s), the intermediate matrices
L[j](s) = Σ_{k=j+1}^{ORD+1} B[k] · s^{k-(j+1)}, j = 1,…,ORD
are naturally available. Multiplying each L[j](s) by a pre-computed coupling vector ξ[j] (obtained from lower-order solution coefficients) gives the contribution of lower-order terms to the RHS:
RHS_lower = -Σ_{j=1}^{ORD} L[j](s) · ξ[j]
The negative sign arises because these terms originate from the left-hand side of the cohomological equation and are moved to the right-hand side. This accumulation is performed in the same Horner loop that computes L(s), avoiding recomputation of the L[j](s) intermediates.
External forcing RHS
For external forcing modes e = 1,…,N_EXT, the polynomial coefficients E_e[L] (FOM × 1 column vectors) are pre-computed such that
E_e(s) = Σ_{L=1}^{ORD} E_e[L] · s^{L-1}
is the contribution of forcing mode e to the cohomological equation when multiplied by its known amplitude external_dynamics[e]. The total external contribution is
RHS_ext = Σ_{e=1}^{N_EXT} E_e(s) · external_dynamics[e]
To evaluate this efficiently, the coefficients of all active (non-zero) external modes are first combined into a single vector polynomial:
g_L = Σ_{e active} external_dynamics[e] · E_e[L], L = 1,…,ORD
Then g(s) = Σ_{L=1}^{ORD} g_L · s^{L-1} is evaluated in a single Horner pass. The result is added to the RHS accumulator. This fused approach avoids evaluating each E_e(s) independently and scales only with the number of active external modes.
The complete right-hand side is therefore
RHS_k = RHS_lower + RHS_ext
where both parts are computed using dedicated fused Horner passes that share the polynomial evaluation structure of the main operator L(s).
Module contents
| Function | Description |
|---|---|
precompute_column_polynomials | Pre-compute D_{L,j} coefficient arrays for both the system-matrix columns and the external-forcing RHS |
evaluate_system_matrix_and_lower_order_rhs! | Fused Horner pass for L(s) + lower-order RHS |
evaluate_column! | Evaluate one C_r(s) column |
evaluate_external_rhs! | Accumulate external-forcing RHS |
assemble_cohomological_matrix_and_rhs! | Full block-matrix and RHS assembly (in-place) |
assemble_cohomological_matrix_and_rhs!(M::AbstractMatrix, rhs::AbstractVector, s::Number, linear_terms::NTuple{ORDP1, var"#s413"} where var"#s413"<:(AbstractMatrix), C_coeffs::Vector{<:AbstractMatrix}, E_coeffs::Vector{<:AbstractMatrix}, resonance::StaticArraysCore.SVector{ROM, Bool}, lower_order_couplings::AbstractVector{<:AbstractVector}, external_dynamics::AbstractVector, g_buffer::AbstractVector) where {ROM, ORDP1}
assemble_cohomological_matrix_and_rhs!(M, rhs, s, linear_terms, C_coeffs, E_coeffs,
resonance, lower_order_couplings,
external_dynamics, g_buffer) → nothing
In-place variant: writes the invariance-equation system matrix and RHS directly into the caller-supplied M and rhs buffers. No heap allocation occurs.
M must have size FOM × (FOM + ROM) — the border is constant width and the resonance mask selects its content rather than its size: column FOM + r holds C_r(s) when mode r is resonant and is zeroed otherwise. The zeroed columns are harmless because the matching orthogonality row pins R_{r,α} = 0 (see assemble_orthogonality_matrix_and_rhs!).
rhs must have length FOM. Both are overwritten on entry. g_buffer is the pre-allocated FOM-length scratch buffer for the external RHS.
build_sparse_L_and_rhs!(rhs::AbstractVector, L_template::SparseArrays.SparseMatrixCSC, mappings::Vector{Vector{Int64}}, linear_terms::NTuple{ORDP1, var"#s413"} where var"#s413"<:SparseArrays.SparseMatrixCSC, s, lower_order_couplings::AbstractVector{<:AbstractVector}) where ORDP1
build_sparse_L_and_rhs!(rhs, L_template, mappings, linear_terms, s, lower_order_couplings)
-> L_template
In-place Horner evaluation of the parametrisation operator using a pre-allocated union-pattern template and index mappings from precompute_sparse_L_template.
Avoids ALL per-monomial sparse arithmetic allocations regardless of whether the linear_terms share a common sparsity pattern. The template's nzval is overwritten on each call; its colptr/rowval are never modified.
evaluate_column!(c::AbstractVector{T}, s::T, r::Int64, C_coeffs::Vector{<:AbstractMatrix{T}}) where T
evaluate_column!(c, s, r, C_coeffs) -> c
Evaluate the r-th reduced-dynamics operator column
C_r(s) = Σ_{L=1}^{ORD} C_coeffs[r][:, L] · s^{L-1}
in-place via Horner's method, overwriting the pre-allocated FOM-vector c.
c may be a plain Vector{T} or a column view view(M, :, col).
Horner recurrence
L runs from ORD-1 down to 1:
c ← C_coeffs[r][:, ORD] (highest-degree coefficient)
for L = ORD-1, …, 1:
c ← c · s + C_coeffs[r][:, L]
Column access C_coeffs[r][:, L] reads contiguous memory (Julia is column-major), so the loop touches sequential cache lines.
Arguments
c :: AbstractVector{T}– output buffer (lengthFOM), overwritten.s :: T– evaluation frequency.r :: Int– 1-based master-mode index (1 ≤ r ≤ ROM).C_coeffs :: Vector{<:AbstractMatrix{T}}– pre-computed coefficients fromprecompute_column_polynomials;C_coeffs[r]isFOM × ORD.
Complexity
O(ORD · FOM)
evaluate_external_rhs!(rhs::AbstractVector{T}, s::T, external_dynamics::AbstractVector{T}, E_coeffs::Vector{<:AbstractMatrix{T}}, g::AbstractVector{T}) where T
evaluate_external_rhs!(rhs, s, external_dynamics, E_coeffs, g) -> rhs
Accumulate the external-forcing contribution to the cohomological right-hand side:
rhs += Σ_{e=1}^{N_EXT} external_dynamics[e] · E_e(s)
where E_e(s) = Σ_{L=1}^{ORD} E_coeffs[e][:, L] · s^{L-1} is the e-th external column polynomial (pre-computed by precompute_column_polynomials).
The sign flip was already absorbed into the pre‑computed coefficients E_coeffs, since they originate from the left‑hand side of the invariance equation and are moved to the right‑hand side, so the contribution is added to the RHS rather than subtracted.
Sparse exploitation
Only the non-zero entries of external_dynamics are processed. For periodic forcing of a few harmonics this is typically a small subset of N_EXT.
Combined Horner pass
Rather than evaluating each E_e(s) independently, the non-zero contributions are combined into a single degree-(ORD-1) vector polynomial
g(s) = Σ_{e active} external_dynamics[e] · E_e(s)
and evaluated in one Horner pass (ORD-1 scalar-vector multiplies plus ORD-1 saxpy operations over FOM), instead of N_EXT_active separate Horner passes.
Arguments
rhs :: AbstractVector{T}– accumulator (lengthFOM), updated in-place.s :: T– evaluation frequency.external_dynamics :: AbstractVector{T}– known amplitudes of theN_EXTexternal forcing modes; typically sparse.E_coeffs :: Vector{<:AbstractMatrix{T}}– pre-computed external coefficients fromprecompute_column_polynomials;E_coeffs[e]isFOM × ORD.g :: AbstractVector{T}– pre-allocatedFOM-length scratch buffer; zeroed on entry.
Complexity
O(N_EXT_active · FOM · ORD)for combining coefficients.O(FOM · ORD)for the single Horner evaluation.
evaluate_system_matrix_and_lower_order_rhs!(parametrisation_operator::AbstractMatrix, lower_order_rhs::AbstractVector, s::Number, lower_order_couplings::AbstractVector{<:AbstractVector}, linear_terms::NTuple{ORDP1, var"#s412"} where var"#s412"<:(AbstractMatrix)) where ORDP1
evaluate_system_matrix_and_lower_order_rhs!(parametrisation_operator, lower_order_rhs, s, lower_order_couplings, linear_terms)
-> parametrisation_operator
Evaluate the parametrisation operator L(s) and accumulate the lower-order right-hand-side contributions in a single Horner pass, reusing the transient intermediate matrices that are available only during the polynomial evaluation.
Mathematical context
At step j of the Horner recurrence (before the scalar multiply by s), the intermediate matrix
L[j](s) = Σ_{k=j+1}^{ORD+1} B[k] · s^{k-(j+1)}
is available. Multiplying by the pre-computed coupling vector ξ[j] = lower_order_couplings[j] gives the contribution of lower-order solution terms at derivative order j to the right-hand side:
contribution[j] = -L[j](s) · ξ[j]
The negative sign arises because these terms originate from the left-hand side of the cohomological equation and are transposed to the right-hand side.
Summed over j = 1, …, ORD, the full lower-order RHS is
lower_order_rhs = -Σ_{j=1}^{ORD} L[j](s) · ξ[j] = -Σ_{j=1}^{ORD} ( Σ_{k=j+1}^{ORD+1} B[k] · s^{k-(j+1)} ) · ξ[j]
This computation must share the Horner loop with L(s): the L[j] intermediates are transient, and recomputing them would double the O(ORD · FOM²) work.
The coupling vectors are obtained from MORFE.LowerOrderCouplings.compute_lower_order_couplings applied to the lower-order multi-indices associated with each Horner step.
Arguments
parametrisation_operator :: AbstractMatrix{T}– output buffer (FOM × FOM), overwritten withL(s) = Σ_{k=1}^{ORD+1} B[k] · s^{k-1}.lower_order_rhs :: AbstractVector{T}– accumulator (lengthFOM), updated in-place. Must be initialised to zero (or the desired starting value) by the caller.s :: T– evaluation superharmonic.lower_order_couplings :: SVector{ORD, <:AbstractVector{T}}– coupling vectorsξ[j]forj = 1,…,ORD; each element is anAbstractVector{T}of lengthFOM.linear_terms :: NTuple{ORD+1, <:AbstractMatrix{T}}–linear_terms[k] = B[k].
Complexity
O(ORD · FOM²), shared with the L(s) evaluation.
precompute_column_polynomials(fom_matrices::NTuple{ORDP1, var"#s413"} where var"#s413"<:AbstractMatrix{T}, generalised_right_eigenmodes::AbstractMatrix{T}, reduced_dynamics_linear::AbstractMatrix{T}, ROM::Int64) where {ORDP1, T<:Number}
precompute_column_polynomials(fom_matrices, generalised_right_eigenmodes,
reduced_dynamics_linear, ROM)
-> (C_coeffs, E_coeffs)
Pre-compute the polynomial coefficient arrays for the cohomological operator columns (master modes) and the external-forcing right-hand-side columns.
These arrays are computed once per polynomial order and reused for every multi-index at that order.
Return values
C_coeffs :: Vector{Matrix{T}}of lengthROM, whereC_coeffs[r]isFOM × ORD. ColumnjofC_coeffs[r]is the degree-(j-1)coefficient of ther-th reduced-dynamics operator column:C_r(s) = Σ_{j=1}^{ORD} C_coeffs[r][:, j] · s^{j-1}E_coeffs :: Vector{Matrix{T}}of lengthN_EXT = NVAR - ROM, whereE_coeffs[e]isFOM × ORD. ColumnjofE_coeffs[e]is the degree-(j-1)coefficient of thee-th external-forcing operator column:E_e(s) = Σ_{j=1}^{ORD} E_coeffs[e][:, j] · s^{j-1}
Arguments
fom_matrices :: NTuple{ORD+1, <:AbstractMatrix{T}}– linear matrices of the full-order model;fom_matrices[k+1]corresponds toB[k](0-indexed in the ODE).generalised_right_eigenmodes :: AbstractMatrix{T}of sizeFOM × NVAR– generalised eigenvectors; columns1:ROMare the master modes, columnsROM+1:NVARare the external forcing modes.reduced_dynamics_linear :: AbstractMatrix{T}of sizeNVAR × NVAR– Jordan-form matrix of the linear part of the reduced dynamics on the Invariant Manifold.ROM :: Int– number of master modes (dimension of the reduced-order model).
Recurrence
The output matrices are filled by a single downward Horner recurrence (j runs from ORD down to 1) using one FOM × NVAR working buffer D:
D ← B[ORD+1] * generalised_right_eigenmodes (j = ORD)
C_coeffs[r][:, j] ← D[:, r] for r = 1…ROM
E_coeffs[e][:, j] ← D[:, ROM+e] for e = 1…N_EXT
D ← D * reduced_dynamics_linear + B[j+1] * generalised_right_eigenmodes (j = ORD-1, …, 1)
C_coeffs[r][:, j] ← D[:, r] for r = 1…ROM
E_coeffs[e][:, j] ← D[:, ROM+e] for e = 1…N_EXT
After step j, column j of every per-target matrix holds the exact degree-(j-1) coefficient
D[:, ·] = Σ_{k=j+1}^{ORD+1} B[k] * generalised_right_eigenmodes * reduced_dynamics_linear^{k-(j+1)}
Complexity
Time:
O(ORD · FOM · NVAR)(dominated byORDmatrix–matrix products)Storage:
O(ORD · FOM · NVAR)
precompute_external_column_polynomials(fom_matrices::NTuple{ORDP1, var"#s413"} where var"#s413"<:(AbstractMatrix), external_directions::AbstractMatrix, reduced_dynamics_linear::AbstractMatrix, D_master_steps::Vector{<:AbstractMatrix}) where ORDP1
precompute_external_column_polynomials(fom_matrices, external_directions,
reduced_dynamics_linear, D_master_steps)
-> E_coeffs
Φext-dependent half of [`precomputecolumnpolynomials](@ref). Computes the external-mode column polynomialsEe(s)reusing the pre-saved master Horner intermediatesDmastersteps(from [precomputemastercolumn_polynomials`](@ref)) instead of recomputing the master-column work.
Pass external_directions = zeros(FOM, N_EXT) to obtain the partial (Φext = 0) Ecoeffs needed for the initial external-forcing solve.
Arguments
D_master_steps— fromprecompute_master_column_polynomials; lengthORD, eachFOM × ROM.D_master_steps[j]is the master Horner buffer at stepj.
precompute_master_column_polynomials(fom_matrices::NTuple{ORDP1, var"#s413"} where var"#s413"<:(AbstractMatrix), master_modes::AbstractMatrix, Λ_master::AbstractMatrix) where ORDP1
precompute_master_column_polynomials(fom_matrices, master_modes, Λ_master)
-> (C_coeffs, D_master_steps)
Φext-independent half of [`precomputecolumnpolynomials](@ref). Computes only the master-mode column polynomialsCr(s)and saves the intermediate FOM×ROM Horner buffer at every step for later reuse by [precomputeexternalcolumn_polynomials`](@ref).
Because Λ is upper-triangular, the master columns of the Horner buffer D form a closed subsystem under the recurrence D ← D·Λ + B[j+1]·Y: for column r ≤ ROM, (D·Λ)[:,r] = Σ_{k≤r} D[:,k]·Λ[k,r] depends only on master columns. Therefore C_coeffs is independent of the external directions Φ_ext.
Returns
C_coeffs :: Vector{Matrix{T}}— same layout as fromprecompute_column_polynomials.D_master_steps :: Vector{Matrix{T}}— lengthORD;D_master_steps[j]is the FOM×ROM master block of the Horner buffer at the step that wroteC_coeffs[:,j]. Used byprecompute_external_column_polynomialsto avoid recomputing master work.
precompute_sparse_L_template(linear_terms::NTuple{ORDP1, var"#s413"} where var"#s413"<:SparseArrays.SparseMatrixCSC) where ORDP1
precompute_sparse_L_template(linear_terms) -> (L_template, mappings)
Pre-allocate a SparseMatrixCSC{ComplexF64} with the union sparsity pattern of all linear_terms, and compute index mappings so that each entry of linear_terms[k] can be accumulated into the correct position of the template's nzval array.
Returns (L_template, mappings) where mappings[k][i] is the index into L_template.nzval that corresponds to position i of linear_terms[k].nzval.
Used once at context construction; the template is reused across all monomials by the in-place build_sparse_L_and_rhs! overload, eliminating per-monomial sparse arithmetic allocations even when the input matrices do not share a common pattern (e.g. when the damping matrix is C = α*M + β*K and Julia's sparse addition drops entries that cancel to exactly zero).
precompute_sparse_bordered_template(L_template::SparseArrays.SparseMatrixCSC{Tv, Ti}, ROM::Int64) where {Tv, Ti}
precompute_sparse_bordered_template(L_template, ROM) -> (M, border_row_base)
Allocate the constant-size (FOM+ROM) × (FOM+ROM) bordered cohomological matrix
┌ ┐
│ L(s) C(s) P │ FOM rows (invariance)
│ P Ĵ(s) P Ĉ(s) P + τ Q │ ROM rows (orthogonality / R_α = 0)
└ ┘
whose sparsity pattern depends only on the union pattern of L_template and on ROM — never on the resonance mask P = diag(ρ) of the monomial being solved. Non-resonant border entries are carried as numeric zeros in structural positions, which is exactly what allows one symbolic factorisation to be reused for every monomial. The system being represented is documented in the CohomologicalEquations module docstring; what follows is this function's own contract — where each block lands in the CSC arrays, which the assembly in _solve_monomial! writes to directly.
Block layout, in CSC order:
| block | rows | cols | pattern |
|---|---|---|---|
L | 1:FOM | 1:FOM | union pattern of L_template |
C P | 1:FOM | FOM+1:end | dense FOM × ROM |
P Ĵ | FOM+1:end | 1:FOM | dense ROM × FOM |
P Ĉ P + τ Q | FOM+1:end | FOM+1:end | dense ROM × ROM |
so nnz(M) = nnz(L_template) + 2·FOM·ROM + ROM². Appending the border rows to each of the first FOM columns preserves sorted rowval because every union row index is ≤ FOM.
Returns
M :: SparseMatrixCSC— the template; onlynzvalis ever written afterwards.border_row_base :: Vector{Int}— lengthFOM; entryM[FOM+r, c]forc ≤ FOMlives atM.nzval[border_row_base[c] + r - 1].
No L → M index table is returned or needed. Because the L entries of column c are laid down as a contiguous prefix of that column, the map is affine within each column,
L_template.nzval[p] ↦ M.nzval[M.colptr[c] + (p - L_template.colptr[c])]
so scatter_L_into_bordered! is a per-column block copy rather than an indirect gather — which also saves an nnz(L)-length index vector.
Border column positions need no table either: column FOM+q starts at bq = M.colptr[FOM+q], so C_q(s) occupies the contiguous run M.nzval[bq : bq+FOM-1] and the corner entry M[FOM+m, FOM+q] sits at bq + FOM + m - 1.
scatter_L_into_bordered!(M::SparseArrays.SparseMatrixCSC, L_template::SparseArrays.SparseMatrixCSC)
scatter_L_into_bordered!(M, L_template) -> M
Copy the freshly evaluated L(s) from the standalone Horner workspace into the (1,1) block of the bordered template.
L's entries for column c sit contiguously in both matrices — at L_template.colptr[c] … and at M.colptr[c] … respectively (see precompute_sparse_bordered_template) — so each column is a single copyto! block move rather than an element-by-element gather through an index table. Keeping this a separate step is what leaves build_sparse_L_and_rhs! untouched: it needs its own square workspace for the transient Horner intermediates L[j](s) that accumulate the lower-order RHS.
Assemble the orthogonality conditions that arise in the parametrisation method, a reduced-order modelling technique for high-dimensional dynamical systems.
Nomenclature
| Symbol | Meaning |
|---|---|
| FOM | Full-order model dimension |
| ROM | Number of master modes (reduced coordinates) |
| N_EXT | Number of external forcing modes |
| NVAR | ROM + N_EXT (total reduced variables) |
| R | Set of resonant master modes (` |
Non-resonant master modes have trivial (zero) reduced dynamics. They are not dropped from the block: each contributes the trivial row f_r = 0, so the block keeps a constant ROM × (FOM+ROM) shape whatever the resonance pattern.
Mathematical origin: sesquilinear B-orthogonality
For each multi-index γ with superharmonic s = Σᵢ γᵢ λᵢ, the condition for master mode r is the sesquilinear B-orthogonality of the companion left eigenvector φ_r = [φ_{r,1}; …; φ_{r,ORD}] (solving φ_rᴴ (λ_r B − A) = 0) against the first-order state 𝒲 of the monomial:
φ_rᴴ B 𝒲 = 0, 𝒲 = [W_1; …; W_ORD], W_1 = W, W_{j+1} = s W_j + Y_j f + ξ_j
Here Y_j are the right eigenmode order-blocks, f the reduced-dynamics coefficient at γ (master part unknown, external part known forcing) and ξ_j the known lower-order couplings. Solving the recurrence and collecting terms reduces the condition to a single row equation
Ĵ_r(s) W + C_r(s) f_res = − E_r(s) f_ext − Σ_{k=1}^{ORD-1} G_{r,k}(s) ξ_k
with
Ĵ_r(s) = Σ_{j=1}^{ORD} J_r[j] · s^{j-1} (1 × FOM row on W)
G_{r,k}(s) = Σ_{j=k+1}^{ORD} J_r[j] · s^{j-1-k} (Horner tails of Ĵ_r)
C_r(s) = Σ_{k=1}^{ORD-1} G_{r,k}(s) · Y_k^m (coupling to unknown f_res)
E_r(s) = Σ_{k=1}^{ORD-1} G_{r,k}(s) · Y_k^e (known forcing → RHS)
Only the resonant master columns of C_r carry a value; the rest are written as zeros, since the matching trivial rows pin those reduced-dynamics coefficients to zero anyway.
Coefficients from eigenvector order-blocks (no eigenvalue folding)
The row coefficients are read directly off the (conjugated) left eigenvector order-blocks — no eigenvalue appears:
J_r[j] = conj(φ_{r,j}) j = 1, …, ORD-1
J_r[ORD] = conj(B_ORDᴴ φ_{r,ORD})
The conjugation is the sesquilinear ᴴ of the condition; it is baked into the stored coefficients so that every assembled contraction (row · W, row · ξ, row · Y) is bilinear.
C_r/E_r contract the same tails G_{r,k} against the right eigenmode order-blocks: master blocks come from the eigensolver, external blocks from the recurrence Y_{k+1}^e = Y_k^e Λ_e + Y_k^m Λ_me (the master↔external coupling of the reduced linear dynamics — the one confined place an eigenvalue-derived matrix remains).
Precomputation and assembly
J_r, C_r and E_r coefficients are pre-computed once per order. At assembly time, only a subset is used:
LHS matrix
[Ĵ_r Ĉ_r]: the columns ofC_r(s)for non-resonant master modes are zeroed rather than omitted, which keeps the block shape independent of the resonance pattern. Nothing is lost — those coefficients are pinned to zero by their own trivial rows.RHS scalar: only the
N_EXT_activenon-zero external forcing entries ofE_r(s)contribute; their values are multiplied byexternal_dynamicsand accumulated intoRHS_r.
Right-hand-side assembly
RHS_r is the sum of two scalar contributions:
Lower-order RHS
During the Horner evaluation of Ĵ_r(s), the intermediate row vectors
Ĵ_r[j](s) = Σ_{k=j+1}^{ORD} J_r[k] · s^{k-(j+1)}, j = 1, …, ORD-1
are naturally available. Dotting each with the pre-computed coupling vector ξ[j] gives a scalar contribution:
RHS_lower_r = -Σ_{j=1}^{ORD-1} Ĵ_r[j](s) · ξ[j]
This accumulation is performed in the same Horner loop that computes Ĵ_r(s), avoiding recomputation of the Ĵ_r[j] intermediates.
External forcing RHS
For external forcing modes e = 1, …, N_EXT, the scalar-valued polynomial
E_r_e(s) = Σ_{j=1}^{ORD-1} E_coeffs[r][j, e] · s^{j-1}
gives the contribution of mode e to RHS_r when multiplied by external_dynamics[e]. The total external contribution is
RHS_ext_r = -Σ_{e active} external_dynamics[e] · E_r_e(s)
Only active (non-zero) external modes are processed. Their contributions are combined into a single scalar Horner pass, avoiding per-mode evaluations.
The complete right-hand side is therefore
RHS_r = RHS_lower_r + RHS_ext_r
Full system assembly
Stacking one row per master mode — resonant or not — yields the global linear system
[ P Ĵ P Ĉ P + τ Q ] · [ W; f ] = P · RHS_R, P = diag(resonance), Q = I − P
where:
ĴisROM × FOM(rowsĴ_r), kept only on resonant rows,ĈisROM × ROM(columns ofC_rfrom the joint operator), masked on both axes,fis the fullROM-vector of reduced-dynamics coefficients,RHS_Ris the assembledROM-vector of scalar right-hand sides,τ = 1on the non-resonant diagonal, turning rowrintof_r = 0.
The block size is therefore independent of how many modes are resonant, which is what allows the sparse cohomological solver to hold one sparsity pattern — and one cached symbolic factorisation — for the whole solve. Masking loses nothing: the dropped C entries multiply coefficients that the trivial rows pin to zero.
Module contents
| Function | Description |
|---|---|
precompute_orthogonality_operator_coefficients | Pre-compute J_r coefficient arrays for the orthogonality row operators Ĵ_r(s) |
precompute_orthogonality_column_polynomials | Pre-compute Q_r coefficient arrays split into C_coeffs and E_coeffs |
evaluate_orthogonality_row_and_lower_order_rhs! | Fused Horner pass for Ĵ_r(s) (row) + scalar lower-order RHS |
evaluate_orthogonality_column_row! | Evaluate C_r(s) into one row of the C block, masked by the resonance vector |
evaluate_orthogonality_external_rhs | Compute the scalar external-forcing RHS for mode r |
assemble_orthogonality_matrix_and_rhs! | Constant-size ROM × (FOM+ROM) block and RHS assembly (in-place) |
assemble_orthogonality_matrix_and_rhs!(M::AbstractMatrix, rhs::AbstractVector, s::T, J_coeffs::AbstractVector{<:AbstractMatrix{T}}, C_coeffs::Vector{<:AbstractMatrix{T}}, E_coeffs::Vector{<:AbstractMatrix{T}}, resonance::StaticArraysCore.SVector{ROM, Bool}, lower_order_couplings::AbstractVector{<:AbstractVector{T}}, external_dynamics::AbstractVector{T}) where {T, ROM}
assemble_orthogonality_matrix_and_rhs!(M, rhs, s, J_coeffs, C_coeffs, E_coeffs,
resonance, lower_order_couplings,
external_dynamics) → nothing
In-place variant: writes the orthogonality block and its RHS directly into the caller-supplied M (ROM × (FOM+ROM)) and rhs (length ROM) buffers. No heap allocation occurs.
The block has constant size — one row per master mode, resonant or not — and the resonance vector selects each row's content rather than the block's dimensions:
resonant
r: rowris the orthogonality conditionĴ_r(s) W_α + Σ_m Ĉ_{rm}(s) R_{m,α} = g_{r,α}, with the corner entries masked to the resonant modes byevaluate_orthogonality_column_row!;non-resonant
r: rowrbecomes the trivial equationτ R_{r,α} = 0— everything zeroed exceptM[r, FOM+r] = τ = 1— which encodes the style choice that non-resonant reduced-dynamics coefficients vanish.
Keeping the width constant is what lets the sparse path reuse one symbolic factorisation across every monomial. Note that τ is structural only: the solver never reads the trivial rows back out of the solution vector, it writes the hard zeros directly during unpacking.
evaluate_orthogonality_column_row!(c::AbstractVector{T}, s::T, r::Int64, C_coeffs::Vector{<:AbstractMatrix{T}}, resonance::StaticArraysCore.SVector{ROM, Bool}) where {T, ROM}
evaluate_orthogonality_column_row!(c, s, r, C_coeffs, resonance) -> c
Evaluate the joint operator row C_r(s) in-place via Horner's method, overwriting the pre-allocated length-ROM vector c.
C_r(s) = Σ_{j=1}^{ORD-1} C_coeffs[r][j, :] · s^{j-1} is a 1 × ROM row polynomial. Entry c[j] holds C_r(s)[j] when master mode j is resonant and zero(T) otherwise — i.e. the layout is expanded and masked, indexed by the mode j itself rather than compacted into resonant rank order.
Masking rather than compacting is what keeps the bordered cohomological system at the constant size FOM + ROM for every monomial (one sparsity pattern, one cached symbolic factorisation). Dropping the non-resonant entries is lossless: the matching orthogonality row pins R_{j,α} = 0, so those coefficients multiply zero.
c may be a plain Vector{T} or a row view view(M, row, col_range).
Horner recurrence (column-wise, no allocation)
For each resonant column index j independently:
val ← C_coeffs[r][ORD-1, j]
for L = ORD-2, …, 1:
val ← val · s + C_coeffs[r][L, j]
c[j] ← val
Column j of C_coeffs[r] is contiguous in memory (Julia is column-major), so each per-column Horner pass is cache-friendly.
Arguments
c :: AbstractVector{T}– output buffer (lengthROM), fully overwritten: resonant entries withC_r(s), the rest with zero.s :: T– evaluation frequency.r :: Int– 1-based master-mode index for the row equation (1 ≤ r ≤ ROM).C_coeffs :: Vector{<:AbstractMatrix{T}}– pre-computed coefficients fromprecompute_orthogonality_column_polynomials;C_coeffs[r]is(ORD-1) × ROM.resonance :: SVector{ROM, Bool}–resonance[j]istrueiff master modejis resonant at the current multi-index.
Complexity
O((ORD-1) · |R|), with no heap allocation.
evaluate_orthogonality_external_rhs(s::T, r::Int64, external_dynamics::AbstractVector{T}, E_coeffs::Vector{<:AbstractMatrix{T}}) where T
evaluate_orthogonality_external_rhs(s, r, external_dynamics, E_coeffs) -> T
Compute the scalar external-forcing contribution to the right-hand side of the orthogonality equation for master mode r:
RHS_ext_r = -Σ_{e active} external_dynamics[e] · E_r_e(s)
where E_r_e(s) = Σ_{j=1}^{ORD-1} E_coeffs[r][j, e] · s^{j-1} is the scalar polynomial for forcing mode e in the row equation for mode r (pre-computed by precompute_orthogonality_column_polynomials).
The negative sign reflects that these terms are moved from the left-hand side of the cohomological equation to the right-hand side.
Sparse exploitation
Only the non-zero entries of external_dynamics are processed. For periodic forcing of a few harmonics this is typically a small subset of N_EXT.
Combined Horner pass
The non-zero contributions are combined into a single scalar polynomial
g(s) = Σ_{e active} external_dynamics[e] · E_r_e(s)
and evaluated in one Horner pass (ORD-2 scalar multiplies and ORD-2 · N_EXT_active scalar additions), instead of N_EXT_active separate Horner passes.
Arguments
s :: T– evaluation frequency.r :: Int– 1-based master-mode index (1 ≤ r ≤ ROM).external_dynamics :: AbstractVector{T}– known amplitudes of theN_EXTexternal forcing modes; typically sparse.E_coeffs :: Vector{<:AbstractMatrix{T}}– pre-computed coefficients fromprecompute_orthogonality_column_polynomials;E_coeffs[r]is(ORD-1) × N_EXT.
Returns
The scalar RHS_ext_r = -g(s).
Complexity
O(N_EXT_active · (ORD-1)) for combining coefficients plus O(ORD-1) for the single Horner evaluation.
evaluate_orthogonality_row_and_lower_order_rhs!(row::AbstractVector{T}, s::T, lower_order_couplings::AbstractVector{<:AbstractVector{T}}, J_coeffs_r::AbstractMatrix{T}) where T
evaluate_orthogonality_row_and_lower_order_rhs!(row, s,
lower_order_couplings,
J_coeffs_r)
-> scalar_rhs :: T
Evaluate the orthogonality row operator Ĵ_r(s) and compute the scalar lower-order right-hand-side contribution for mode r in a single Horner pass, reusing the transient intermediate row vectors.
Mathematical context
At step j of the Horner recurrence (before the scalar multiply by s), the intermediate row vector
Ĵ_r[j](s) = Σ_{k=j+1}^{ORD} J_r[k, :] · s^{k-(j+1)}
is available. Contracting bilinearly with the pre-computed coupling vector ξ[j] gives the scalar contribution of lower-order solution terms at step j:
contribution[j] = -Ĵ_r[j](s) · ξ[j] = -Σᵢ Ĵ_r[j](s)ᵢ · ξ[j]ᵢ
The contraction must not conjugate: the sesquilinear conjugation of the orthogonality condition is already baked into the row coefficients J_r (see precompute_orthogonality_operator_coefficients), so row holds the Horner tail G_{r,j}(s) with the ᴴ applied. The negative sign arises because these terms originate from the left-hand side of the cohomological equation. Summed over j = 1, …, ORD-1:
RHS_lower_r = -Σ_{j=1}^{ORD-1} Ĵ_r[j](s) · ξ[j]
The sum runs to ORD-1 (one fewer than in InvarianceEquation) because the joint operator Q_r has one fewer degree. Sharing the loop with the Ĵ_r(s) evaluation avoids recomputing the Ĵ_r[j] intermediates.
Arguments
row :: AbstractVector{T}– output buffer (lengthFOM), overwritten withĴ_r(s) = Σ_{j=1}^{ORD} J_r[j, :] · s^{j-1}.s :: T– evaluation superharmonic.lower_order_couplings :: SVector{ORD_M1, <:AbstractVector{T}}– coupling vectorsξ[j]forj = 1, …, ORD-1; each is a length-FOMvector.J_coeffs_r :: AbstractMatrix{T}–ORD × FOMmatrix; rowjisJ_r[j, :], the degree-(j-1)coefficient ofĴ_r. Obtained fromprecompute_orthogonality_operator_coefficients.
Returns
The scalar lower-order RHS accumulation RHS_lower_r = -Σ_{j=1}^{ORD-1} Ĵ_r[j](s) · ξ[j].
Complexity
O(ORD · FOM), shared with the Ĵ_r(s) evaluation.
precompute_orthogonality_column_polynomials(J_coeffs::AbstractVector{<:AbstractMatrix}, right_master_blocks::AbstractArray{<:Number, 3}, external_directions::AbstractMatrix, reduced_dynamics_linear::AbstractMatrix)
precompute_orthogonality_column_polynomials(J_coeffs, right_master_blocks,
external_directions,
reduced_dynamics_linear)
-> (C_coeffs, E_coeffs)
Pre-compute the coefficient arrays of the operators C_r(s) (coupling to the unknown reduced dynamics) and E_r(s) (known external forcing) that appear in the orthogonality equation for master mode r:
J_r(s) W + C_r(s) f_m = − E_r(s) f_e − Σ_k G_{r,k}(s) ξ_k
Mathematical origin
With G_{r,k}(s) = Σ_{j=k+1}^{ORD} J_r[j, :] s^(j-1-k) the Horner tails of the row operator, the couplings are bilinear contractions against the right eigenmode order-blocks Y_k (Y_1 = physical mode, Y_{k+1} = next derivative block):
C_r(s) = Σ_{k=1}^{ORD-1} G_{r,k}(s) · Y_k^m (master blocks, from the eigensolver)
E_r(s) = Σ_{k=1}^{ORD-1} G_{r,k}(s) · Y_k^e (external blocks)
The master blocks are supplied directly (right_master_blocks). The external blocks are generalised — the reduced linear dynamics couples them back to the master modes — and are generated by the block recurrence
Y_1^e = Φ_ext, Y_{k+1}^e = Y_k^e Λ_e + Y_k^m Λ_me
where Λ_me = Λ[1:ROM, ROM+1:NVAR] and Λ_e = Λ[ROM+1:NVAR, ROM+1:NVAR] are the master↔external and external blocks of reduced_dynamics_linear. This is the one place an eigenvalue-derived matrix remains, confined to the per-order precompute.
All contractions are bilinear (Σᵢ J_r[·,i] Y[i,·]): the sesquilinear conjugation of the orthogonality condition is already baked into J_coeffs.
Arguments
J_coeffs :: Vector{<:AbstractMatrix{T}}– output ofprecompute_orthogonality_operator_coefficients;J_coeffs[r]isORD × FOM.right_master_blocks :: AbstractArray{T,3}– right master-mode order-blocks, sizeFOM × ORD × ROM;right_master_blocks[:, k, m] = Y_k^m[:, m](equal to the linear master monomials of the parametrisationW). Only blocksk ≤ ORD-1are used.external_directions :: AbstractMatrix{T}– physical external directionsΦ_ext, sizeFOM × N_EXT.reduced_dynamics_linear :: AbstractMatrix{T}–NVAR × NVARlinear reduced dynamics; only theΛ_meandΛ_eblocks are read.
Return values
C_coeffs :: Vector{Matrix{T}}of lengthROM;C_coeffs[r]is(ORD-1) × ROM, rowp= degree-(p-1)coefficient ofC_r(s).E_coeffs :: Vector{Matrix{T}}of lengthROM;E_coeffs[r]is(ORD-1) × N_EXT, rowp= degree-(p-1)coefficient ofE_r(s).
When ORD == 1 both matrices have zero rows and the operators are identically zero (the corresponding blocks are absent from the assembled system).
Complexity
Time:
O(ROM² · ORD² · FOM)for the contractions plusO(ORD · FOM · NVAR · N_EXT)for the external block recurrence.Storage:
O(ROM · ORD · NVAR)
precompute_orthogonality_operator_coefficients(fom_matrices::NTuple{ORDP1, var"#s413"} where var"#s413"<:(AbstractMatrix), left_eigenmodes::AbstractMatrix) where ORDP1
precompute_orthogonality_operator_coefficients(fom_matrices::NTuple{ORDP1, var"#s412"} where var"#s412"<:(AbstractMatrix), left_eigenmodes::AbstractMatrix, left_modes_derivatives::Union{Nothing, AbstractArray{<:Number, 3}}) where ORDP1
precompute_orthogonality_operator_coefficients(fom_matrices, left_eigenmodes,
left_modes_derivatives = nothing)
-> Vector{Matrix{T}}
Pre-compute the polynomial coefficient arrays for the orthogonality row operators J_r(s) directly from the left-eigenvector order-blocks. No eigenvalue is used.
Mathematical origin
The orthogonality condition for master mode r at a monomial with superharmonic s is the sesquilinear B-orthogonality of the companion left eigenvector φ_r = [φ_{r,1}; …; φ_{r,ORD}] (defined by φ_rᴴ (λ_r B − A) = 0) against the first-order state 𝒲:
φ_rᴴ B 𝒲 = 0, B = blockdiag(I, …, I, B_ORD)
Reducing block-by-block, the row acting on the physical unknown W is
J_r(s) = Σ_{j=1}^{ORD} J_r[j, :] · s^(j-1)
with coefficients read straight off the (conjugated) eigenvector blocks:
J_r[j, :] = conj(φ_{r,j}) j = 1, …, ORD-1
J_r[ORD, :] = conj(B_ORDᴴ φ_{r,ORD}) = conj(B_ORDᴴ ℓ_r)
The conjugation is the sesquilinear ᴴ of the condition, stored so that the assembled matrix row acts bilinearly on W (row · W = Σᵢ rowᵢ Wᵢ).
Arguments
fom_matrices :: NTuple{ORD+1, <:AbstractMatrix{T}}– linear matrices of the full-order model;fom_matrices[k+1]corresponds toB_k(0-indexed).left_eigenmodes :: AbstractMatrix{T}– physical-space (highest-order) left eigenvector slice;left_eigenmodes[:, r]isℓ_r = φ_{r,ORD}(length FOM).left_modes_derivatives :: Union{Nothing, AbstractArray{T,3}}– lower-order left eigenvector blocks, sizeFOM × (ORD-1) × ROM;left_modes_derivatives[:, j, r] = φ_{r,j}. Required whenORD > 1(the eigensolver returns them; seesolve_left). May benothingforORD == 1.
Return value
A Vector{Matrix{T}} of length ROM; entry r is the ORD × FOM matrix J_coeffs[r] whose row j stores the degree-(j-1) coefficient of J_r(s).
Complexity
Time: one
B_ORDᴴ · ℓ_rproduct per mode —O(ROM · FOM²)dense (O(ROM · nnz)sparse); the remaining blocks are copies.Storage:
O(ROM · ORD · FOM)
Module ParametrisationObjects — the coefficient containers of the DPIM parametrisation, and the contract their multiindex set must satisfy.
Defines the two objects that together represent the invariant manifold and the reduced dynamics:
Parametrisation{ORD, NVAR, T}— the mapW : ℂᴺᵛᵃʳ → ℂᶠᵒᵐexpanded as aDensePolynomialwith a(FOM × ORD × L)coefficient tensor; theORDaxis stores the time-derivative orders required by higher-order ODEs.ReducedDynamics{ROM, NVAR, T}— the reduced ODEż = R(z)expanded as aDensePolynomialwith a(NVAR × L)coefficient matrix.
Also provides create_parametrisation_method_objects (allocates both objects for a given MultiindexSet), compute_higher_derivative_coefficients! (fills the derivative-order slices of W from the solved first-order slice and the reduced dynamics), and validate_multiindex_set (the five-clause mset contract).
Why this is a module of its own
CohomologicalEquations needs exactly these definitions, while the user-facing ParametrisationMethod — which owns parametrise — needs CohomologicalEquations. Splitting the containers out breaks what would otherwise be a circular module dependency, and lets ParametrisationMethod load last and simply call the solver. Everything here is re-exported by ParametrisationMethod, so MORFE.ParametrisationMethod.ReducedDynamics and friends keep resolving.
Parametrisation
Parametrisation{ORD, NVAR, T}
A dense polynomial with a contiguous (FOM, ORD, L) coefficient array. Represents a parametrisation mapping from reduced coordinates and forcing variables to the full state.
ORD: native order of the full ODE (1 for first‑order, 2 for second‑order).NVAR: total number of variables = reduced coordinates + forcing variables.T: numeric element type (e.g.,ComplexF64).
Layout: coefficients[:, ord, l] is the full‑state vector (length FOM) for the ord-th time derivative of the l-th monomial coefficient.
Fields
poly::DensePolynomial{T, NVAR, 3, Array{T, 3}}— the coefficient array in the layout above, together with the multiindex set it is aligned to.external_system_size::Int— how many of theNVARvariables are external forcing amplitudes rather than reduced coordinates. The reduced dimension is the remainder, so this is what separates the master block from the forcing block when slicing the coefficients.
ReducedDynamics
ReducedDynamics{ROM, NVAR, T}
A dense polynomial whose coefficients are SVector{ROM, T}. Represents the reduced dynamics on a manifold of dimension ROM.
ROM: dimension of the reduced state (first‑order system).NVAR: total number of variables = ROM + externalsystemsize.T: numeric type.
The dynamics are: ż = R(z, r), where r are the forcing variables.
Fields
poly::DensePolynomial{T, NVAR, 2, Matrix{T}}— coefficients as aNVAR × Lmatrix aligned to the multiindex set. Rows1:ROMare solved for; the trailingexternal_system_sizerows hold the known forcing amplitudes.external_system_size::Int— number of forcing variables, fixing where the master rows end andROM = NVAR - external_system_size.
compute_higher_derivative_coefficients!(param_coeff::AbstractArray{T, 3}, red_coeff::AbstractMatrix{T}, external_dynamics::AbstractVector{T}, superharmonic::T, global_index::Int64, generalised_eigenmodes::AbstractMatrix{T}, lower_order_couplings::AbstractVector{<:AbstractVector{T}}) where T
compute_higher_derivative_coefficients!(
param_coeff, red_coeff, external_dynamics, superharmonic, global_index,
generalised_eigenmodes, lower_order_couplings
) -> nothing
Compute the higher time‑derivative coefficients W^(j+1)[α] for j = 1 … ORD-1 using the superharmonic recurrence
W^(j+1)[α] = s · W^(j)[α] + Φ_master · R[α] + Φ_ext · e_dyn + ξ[j]
where:
s = superharmonicis the frequency⟨λ, α⟩,Φ = generalised_eigenmodes(FOM × NVAR) collects the right eigenmodes,R[α] = red_coeff[:, global_index](ROM‑vector) contains the master‑mode reduced‑dynamics coefficients at the current monomial (already solved),e_dyn = external_dynamics(N_EXT‑vector) contains the known external dynamics at the current monomial,ξ[j] = lower_order_couplings[j](FOM‑vector) contains the coupling from lower‑order monomials at derivative orderj.
Modifies param_coeff in‑place. Does nothing when ORD = 1 (no higher derivatives exist for a first‑order ODE).
Arguments
param_coeff :: AbstractArray{T, 3}— shapeFOM × ORD × L; the coefficient tensor of the parametrisation polynomial.red_coeff :: AbstractMatrix{T}— shapeROM × L; master‑mode reduced‑dynamics coefficients.external_dynamics :: AbstractVector{T}— lengthN_EXT; known external dynamics at the current monomial.superharmonic :: T— scalars = ⟨λ, α⟩.global_index :: Int— monomial index into the last axis ofparam_coeffand the last axis ofred_coeff.generalised_eigenmodes :: AbstractMatrix{T}— shapeFOM × NVAR; right generalised eigenvectors (master modes in columns1:ROM, external modes inROM+1:NVAR).lower_order_couplings :: AbstractVector{<:AbstractVector{T}}— lengthORD; elementjis a length‑FOMvectorξ[j]produced byLowerOrderCouplings.compute_lower_order_couplings.
create_parametrisation_method_objects(mset::MultiindexSet{NVAR}, ORD::Int64, FOM::Int64, ROM::Int64, external_system_size::Int64) where NVAR
create_parametrisation_method_objects(mset::MultiindexSet{NVAR}, ORD::Int64, FOM::Int64, ROM::Int64, external_system_size::Int64, ::Type{T}) where {T<:Number, NVAR}
create_parametrisation_method_objects(mset::MultiindexSet{NVAR}, ORD::Int64, FOM::Int64) where NVAR
create_parametrisation_method_objects(mset::MultiindexSet{NVAR}, ORD::Int64, FOM::Int64, ::Type{T}) where {T<:Number, NVAR}
create_parametrisation_method_objects(mset::MultiindexSet{NVAR}, ORD::Int, FOM::Int, ROM::Int, external_system_size::Int, ::Type{T}=Complex)
Create a consistent pair of polynomials:
W: aParametrisation{ORD, NVAR, T}with zero coefficients,R: aReducedDynamics{ROM, NVAR, T}with zero coefficients.
Both polynomials share the same multiindex set mset and element type T. The total number of variables NVAR must satisfy NVAR == ROM + external_system_size. FOM is the full‑order dimension (size of the state vector). It is not stored but used to initialise the coefficient vectors correctly.
Arguments
mset: multiindex set forNVARvariables.ORD: native order of the full ODE (1 or 2).FOM: dimension of the full‑order state in its native order.ROM: dimension of the reduced state.external_system_size: number of forcing variables (default 0).T: element type.
restrict_Parametrisation_to_degree(W::Parametrisation, max_degree::Int64)
function restrict_Parametrisation_to_degree(W::Parametrisation, max_degree::Int) -> Parametrisation
Returns a new Parametrisation that contains all the monomials of poly that are of degree lower or equal than max_degree.
restrict_ReducedDynamics_to_degree(R::ReducedDynamics, max_degree::Int64)
function restrict_ReducedDynamics_to_degree(R::ReducedDynamics, max_degree::Int) -> ReducedDynamics
Returns a new ReducedDynamics that contains all the monomials of poly that are of degree lower or equal than max_degree.
validate_multiindex_set(mset::MultiindexSet{N}, nvar::Int64, rom::Int64; conjugate_permutation) where N
validate_multiindex_set(mset, nvar, rom; conjugate_permutation = nothing)
Check a custom multiindex set against everything the cohomological solve assumes, and throw an ArgumentError naming the offending exponent on the first violation.
The clauses, and why each one matters:
nvarvariables, matchingROM + N_EXT.Minimum total degree ≥ 1 — the expansion is centred on the fixed point, so the constant monomial has no coefficient to solve for.
Every unit multiindex
eᵢ— the linear part of the parametrisation is initialised from the eigenvectors, one column per unit multiindex.Downward closed — the graded solve reads
W[α - β + eᵢ]while working onαand factorisesα = β₁ + … + β_dover members ofmset. A missing divisor is not an error at run time: it is read as zero, silently corrupting the right-hand side.With a
conjugate_permutation: that it is an involutive permutation of1:nvarmapping1:rominto itself, and thatmsetis closed under it. A member whose partner is absent is solved directly rather than filled by conjugation, so the result loses the conjugate structure the permutation asserts.
Returns nothing. Cost is O(nvar · |mset| · log|mset|) — negligible beside a solve, but parametrise and solve_parametrisation both take a validate_mset = false escape hatch for callers that have already checked.
See is_downward_closed and is_conjugate_closed for the two closure predicates on their own.
Module MultilinearTerms — efficient evaluation of the nonlinear right-hand side of the cohomological equations.
For each monomial α the nonlinear contribution is
Σₜ Σ_{β₁+…+βₖ=α} multiplier · t.f!(W[β₁], …, W[βₖ], r₁, …, rₘ)
where the outer sum runs over all nonlinear terms t of the model and the inner sum enumerates factorisations of α into k sub-exponents from already-computed W columns. This module provides two evaluation paths:
Non-cached (
compute_multilinear_termswith anSVectorexponent): calls the factorisation routines on every invocation. Simple but allocating.Cached (
build_multilinear_terms_cache+compute_multilinear_terms!): precomputes all factorisation bookkeeping inMultilinearTermsCacheonce before the solve loop, then replays it allocation-free at each monomial.
For FEM-backed terms (FEMMultilinearMap) an additional O4 combined element loop merges all me=0 FEM contributions into a single mesh traversal per monomial, avoiding redundant fem_reinit! and scatter_qp! calls.
Three symmetry strategies (FullyAsymmetric, FullySymmetric, GroupwiseSymmetric) are dispatched at compile time from the MultilinearMap.multiindex field.
CachedSplit
CachedSplit
Precomputed bookkeeping for one (monomial l, term t, external-split) triple.
Fields
ext_count::Int— multiplicity of this external-variable split (frombounded_index_tuples); always 1 whenme = 0.args_ext_indices::Vector{Int}— indices into the cache'sexternal_argumentsthat reconstruct the external forcing arguments; empty whenme = 0.is_asymmetric::Bool— true iff the term isFullyAsymmetric, in which case replaying it needs no scratch buffer.orders::Vector{Int}— derivative index for each factor slot; length is the internal degree of the term.entries::Vector{FactorisationEntry}— one entry per factorisation of the remainder exponent, each carrying its own symmetry multiplier.
FEMCachedSplit
FEMCachedSplit{DEG, ME}
Precomputed bookkeeping for one (monomial, FEM-term, external-split) triple.
DEG is the internal degree (t.deg - me) and ME the external multiplicity, so accumulate_qp! is called with DEG + ME == t.deg arguments. ME is a type parameter so the external arguments are appended with a compile-time-known count in _replay_fem_split!.
Fields
ext_count::Int— external multiplicity; 1 whenme = 0.args_ext_indices::Vector{Int}— indices into the cache'sexternal_argumentsthat reconstruct the external arguments; empty whenme = 0.unique_cols::Vector{Tuple{Int, Int}}— deduplicated(derivative_order, W_col_idx)pairs across every entry in this split. Deduplication is what makes the batching pay: each pair is scattered to quadrature-point gradients once per element, however many factorisations reference it.fem_entries::Vector{FEMFactorisationEntry{DEG}}— one entry per factorisation, indexing intounique_colsrather than intoWdirectly. Keyed on the internal degree alone, since it only indexesunique_cols.
FEMFactorisationEntry{DEG}
One factorisation entry for the FEM-batched path.
Fields
multiplier::Int— symmetry count, as inFactorisationEntry.multiplier.local_factor_indices::NTuple{DEG, Int}— for each factor slot, the index into the enclosingFEMCachedSplit.unique_cols. AnNTuplerather than aVectorsontuple(k -> …, Val(DEG))in the hot loop unrolls at compile time.
FEMGlobalEntry
FEMGlobalEntry{DEG}
One factorisation entry in the combined element loop for a given monomial.
Fields
term_idx::Int— index intomodel.nonlinear_terms. Present here but not inFEMFactorisationEntrybecause the combined loop mixes terms.multiplier::Int— symmetry count, as inFEMFactorisationEntry.local_factor_indices::NTuple{DEG, Int}— indices into the enclosingFEMGlobalSplit.global_unique_cols.
FEMGlobalSplit
FEMGlobalSplit{ENTRIES_TUPLE}
All combined-loop bookkeeping for one monomial, covering all me=0 FEM terms.
Fields
global_unique_cols::Vector{Tuple{Int, Int}}— deduplicated(derivative_order, W_col_idx)pairs across allme = 0FEM terms and their splits, scattered once per element. Deduplicating across terms, not just within one, is what the combined loop buys overFEMCachedSplit.entries_by_deg::ENTRIES_TUPLE— oneVector{FEMGlobalEntry{D}}per degreeDpresent, held in a typed tuple so the inner loop dispatches type-stably onDEG.driver_term_idx::Int— the FEM term whose element iterator drives the loop;0when this monomial has nome = 0FEM terms.participating_term_indices::Vector{Int}— sorted distinctterm_idxvalues appearing inentries_by_deg, so assembly touches only the terms involved instead of scanning all of them.
FullyAsymmetric
FullyAsymmetric <: SymmetryType
Tag for terms whose factor slots all use distinct derivatives (multiindex has no repeated entry > 1). No scratch buffer is needed; t.f! writes directly into the accumulator.
FullySymmetric
FullySymmetric <: SymmetryType
Tag for terms where all factor slots share a single derivative order (exactly one positive entry in multiindex). Each factorisation carries a symmetry count.
GroupwiseSymmetric
GroupwiseSymmetric <: SymmetryType
Tag for terms whose factor slots span multiple derivative orders (multiple positive entries in multiindex). Uses factorisations_groupwise_symmetric with a combined per-group symmetry count.
MultilinearTermsCache{T, QP, EV}
All precomputed factorisation bookkeeping for a given (model, parametrisation) pair. splits[l][t_idx] is the list of CachedSplit values for monomial l and term t_idx; an empty list means the term degree exceeds the monomial degree and it contributes nothing.
Type parameters:
T— element type of FOM-length vectors (e.g.ComplexF64)QP— element type of the combined qp gradient bufferglobal_∇W_qp
(e.g. `Tensor{2,3,ComplexF64}` for Ferrite SVK; `Nothing` when no FEM terms)
EV— element type ofexternal_arguments, i.e.SVector{N_EXT, Int}normally and
`SVector{N_EXT, eltype(Q)}` when the external system was re-based. A type
parameter so the hot loop's argument tuple is inferred rather than `Any`.
Build once before the solve loop with build_multilinear_terms_cache. Use by passing to the (model, exp_index, parametrisation, cache) overload of compute_multilinear_terms inside the loop. The cache is valid as long as the multiindex set and the model structure are unchanged (i.e. across all solve steps).
Fields
Three parallel split representations coexist because a model may mix closure-based and FEM-backed nonlinear terms, and the FEM ones are far cheaper to evaluate batched over elements than term by term:
splits::Vector{Vector{Vector{CachedSplit}}}— the generic path.splits[l][t_idx]lists theCachedSplitvalues for monomialland termt_idx; empty when the term degree exceeds the monomial degree.fem_splits::Vector{Vector{Vector{Any}}}— the same indexing for FEM terms, holdingFEMCachedSplit{DEG, ME}values, empty for closure terms. TypedAnybecauseDEGvaries per term; reached through a function barrier so the hot loop stays type-stable.global_fem_splits::Vector{Any}— oneFEMGlobalSplitper monomial, fusing every FEM term into a single element loop.Anyfor the same reason.
Buffers, all reused across monomials to keep the solve loop allocation-free:
result_buffer::Vector{T}— lengthFOM, accumulates the nonlinear contribution returned to the caller.scratch_buffer::Vector{T}— lengthFOM, working space for symmetric terms; unused when a term isFullyAsymmetric.temp_buffer::Vector{T}— lengthFOM, holds one intermediate contraction.external_arguments::Vector{EV}— the external argument passed for each external variable, used to rebuild the external forcing arguments; empty whenN_EXT == 0. These are the unit vectorseⱼin the model's own coordinates, or the columnsQ[:, j]of the change of basis when the external system was re-based — seeExternalSystems.external_argument_vectors. ApplyingQhere, once per solve, is why no term ever has to know about the change of coordinates.fem_Fe::Vector{T}— element-local residual for the external-multiplicity fallback path, sized to the largestndofs_per_cellin the model.global_∇W_qp::Matrix{QP}— shared quadrature-point gradient buffer,max_global_unique × max_n_qp.QPis a type parameter precisely so element access inside the hot loop is statically typed.global_Fe_buffers::Vector{Vector{T}}— per-term element residual buffers, sized to each FEM term'sfem_ndofs_per_cell; empty for closure terms.
SymmetryType
SymmetryType
Abstract tag type used to dispatch the inner factorisation accumulation strategy at compile time. The three concrete subtypes correspond to the three cases that arise from MultilinearMap.multiindex:
FullyAsymmetric— all entries ≤ 1; each factor slot uses a different derivative.FullySymmetric— exactly one entry > 1; all slots share one derivative.GroupwiseSymmetric— multiple entries > 0; slots span several derivatives.
_accumulate_global_entries!(::Any, ::Any, ::Tuple{}, ::Any, ::Any, ::Any, ::Any)
_accumulate_global_entries!(Fe_bufs, ∇W_qp, entries_by_deg::Tuple{Array{MORFE.MultilinearTerms.FEMGlobalEntry{DEG}, 1}, Vararg}, model, element, q, dΩ) where DEG
_accumulate_global_entries!(Fe_bufs, ∇W_qp, entries_by_deg, model, element, q, dΩ)
Recursive type-stable dispatch over the degree-grouped entry tuple. The base case (empty tuple) is a no-op. Each recursive step processes the head degree group — Julia specialises a method per DEG so Val(DEG) in ntuple and the accumulate_qp! dispatch are resolved at compile time — then recurses on the tail.
_accumulate_split!(accum, _scratch, t, ::MORFE.MultilinearTerms.FullyAsymmetric, W, set, rem, deg, candidate_indices, args_ext)
_accumulate_split!(accum, scratch, t, ::MORFE.MultilinearTerms.FullySymmetric, W, set, rem, deg, candidate_indices, args_ext)
_accumulate_split!(accum, scratch, t, ::MORFE.MultilinearTerms.GroupwiseSymmetric, W, set, rem, deg, candidate_indices, args_ext)
_accumulate_split!(accum, scratch, t, sym, W, set, rem, deg, candidate_indices, args_ext)
Accumulate contributions from one (monomial, external-split) pair into accum, dispatching on the SymmetryType tag sym.
FullyAsymmetric: callst.f!directly intoaccumfor each factorisation.FullySymmetric/GroupwiseSymmetric: callst.f!intoscratch, thenaxpy!(multiplier, scratch, accum)to apply the symmetry count.
_build_fem_cached_split(::Val{DEG}, ::Val{ME}, cs::MORFE.MultilinearTerms.CachedSplit) where {DEG, ME}
_build_fem_cached_split(::Val{DEG}, ::Val{ME}, cs) -> FEMCachedSplit{DEG, ME}
Convert a CachedSplit to a FEMCachedSplit{DEG, ME} for the FEM-batched replay path. Deduplicates (derivative_order, W_col_idx) pairs across all entries and remaps each entry's factor slots to local indices into the unique_cols table. DEG is the internal degree and ME the external multiplicity, carried as a type parameter so _replay_fem_split! can append the external arguments with a compile-time-known count. Called once at cache-build time; produces zero allocations in the hot path.
_build_global_fem_split(model, fem_splits_l, fem_term_indices)
_build_global_fem_split(model, fem_splits_l, fem_term_indices) -> FEMGlobalSplit
Build the O4 combined-element-loop bookkeeping for one monomial, merging all me=0 FEM terms into a single FEMGlobalSplit.
Deduplicates
(order, col)pairs across all me=0 FEM splits into a global table.Remaps each
FEMFactorisationEntry's local indices to global table indices.Groups entries by degree into a typed
Tuplefor type-stable compile-time dispatch.Returns an empty
FEMGlobalSplit{Tuple{}}when no me=0 FEM terms are present.
_collect_entries(::MORFE.MultilinearTerms.FullyAsymmetric, t, mset, rem, deg, cands)
_collect_entries(::MORFE.MultilinearTerms.FullySymmetric, t, mset, rem, deg, cands)
_collect_entries(::MORFE.MultilinearTerms.GroupwiseSymmetric, t, mset, rem, deg, cands)
_collect_entries(sym, t, mset, rem, deg, cands) -> Vector{FactorisationEntry}
Call the appropriate factorisation function for cache construction, routing FullyAsymmetric to factorisations_asymmetric, FullySymmetric to factorisations_fully_symmetric, and GroupwiseSymmetric to factorisations_groupwise_symmetric.
_derivative_orders(t::AbstractMultilinearMap)
_derivative_orders(t) → NTuple
Map each factor slot to its 1-based derivative index. Example: multiindex = (2, 1) → (1, 1, 2).
_orders_for_cache(::MORFE.MultilinearTerms.FullySymmetric, t, deg)
_orders_for_cache(::MORFE.MultilinearTerms.SymmetryType, t, deg)
_orders_for_cache(sym, t, deg) -> Vector{Int}
Return the per-slot derivative order vector for storage in a CachedSplit.
FullySymmetric: alldegslots share one derivative index.Other: read from
_derivative_orders(t).
_replay_all_fem_splits!(result, model, W, global_split::MORFE.MultilinearTerms.FEMGlobalSplit, global_∇W_qp, global_Fe_buffers)
_replay_all_fem_splits!(result, model, W, global_split, global_∇W_qp, global_Fe_buffers)
Execute the O4 combined element loop: traverse the mesh once, scatter all globally-unique W columns, then accumulate contributions from ALL me=0 FEM terms at each quadrature point.
fem_reinit! and scatter_qp! are called at most once per unique (element, W-column) pair; accumulate_qp! dispatches per degree group via _accumulate_global_entries!.
_replay_fem_split!(result, t::FEMMultilinearMap, W, fem_split::MORFE.MultilinearTerms.FEMCachedSplit{DEG, ME}, Fe, temp, external_arguments) where {DEG, ME}
_replay_fem_split!(result, t, W, fem_split, Fe, temp, external_arguments)
Replay one FEMCachedSplit{DEG, ME} using the FEM-batched element loop.
For each element: calls fem_reinit! once, scatters each unique (order, col) W column to qp-level field values via scatter_qp!, accumulates all qp contributions via accumulate_qp!, and assembles the element residual into result via assemble_element!. The qp gradient buffer ∇W_qp is obtained from the term via fem_qp_buffer(t) and is owned by the term, not allocated here.
accumulate_qp! receives DEG internal arguments followed by ME external ones, so the tuple has length t.deg — matching what evaluate_term!'s direct FEM overload (_eval_fem_term_direct!) passes. Before this, the external slots were omitted here and ext_count was applied as a bare scalar multiplier, so an me > 0 FEM term could not tell one external direction from another and disagreed with the direct path. The external arguments are hoisted out of the element loop, exactly as in _replay_split!.
_replay_global_fem!(result, model, W, cache::MORFE.MultilinearTerms.MultilinearTermsCache{T, QP}, exp_index) where {T, QP}
_replay_global_fem!(result, model, W, cache, exp_index)
Function barrier: retrieves cache.global_fem_splits[exp_index] (stored as Any) and delegates to _replay_all_fem_splits!. The barrier restores type stability because cache.global_∇W_qp::Matrix{QP} is concretely typed at the call site even though the global_split triggers runtime dispatch on ENTRIES_TUPLE.
_replay_split!(result, scratch, temp, t, W, split, deg, external_arguments)
_replay_split!(result, scratch, temp, t, W, split, deg, external_arguments)
Replay one CachedSplit into result using precomputed factorisation bookkeeping.
me = 0(args_ext_indicesempty): accumulates directly intoresult.me > 0: accumulates intotemp, thenaxpy!(ext_count, temp, result).
Dispatches asymmetric/symmetric accumulation via split.is_asymmetric.
_replay_term!(result, t::MultilinearMap, W, exp_index, t_idx, cache::MORFE.MultilinearTerms.MultilinearTermsCache)
_replay_term!(result, t::FEMMultilinearMap, W, exp_index, t_idx, cache::MORFE.MultilinearTerms.MultilinearTermsCache)
_replay_term!(result, t, W, exp_index, t_idx, cache)
Replay all cached splits for one nonlinear term t and monomial exp_index into result. Dispatches on the concrete term type:
MultilinearMap: replays via_replay_split!(closure path).FEMMultilinearMap: me=0 splits are handled by_replay_global_fem!(O4 combined loop); only me>0 fallback splits are processed here via_replay_fem_split!.
accumulate_multilinear_term!(result, scratch, temp, t::MultilinearMap{ORD}, parametrisation::Parametrisation{ORD, NVAR}, exp::StaticArraysCore.SVector{NVAR}, candidate_indices, external_exp, external_arguments) where {ORD, NVAR}
accumulate_multilinear_term!(result, scratch, temp, t, parametrisation,
exp, candidate_indices, external_exp, external_arguments)
Add the contribution of nonlinear term t for exponent exp to result.
When t has no external (forcing) slots (me = 0) there is exactly one split, so we skip bounded_index_tuples and write directly into result, saving one fill! and one axpy! per term. For me > 0 each split is accumulated into temp first, then scaled into result via axpy!.
build_multilinear_terms_cache(model::NthOrderModel{ORD, ORDP1, N_NL, N_EXT, T} where {ORDP1, N_NL, N_EXT, T}, parametrisation::Parametrisation{ORD, NVAR}) where {ORD, NVAR}
build_multilinear_terms_cache(model::NthOrderModel{ORD, ORDP1, N_NL, N_EXT, T} where {ORDP1, N_NL, N_EXT, T}, parametrisation::Parametrisation{ORD, NVAR}, skip_bits::BitVector) where {ORD, NVAR}
build_multilinear_terms_cache(model, parametrisation[, skip_bits]) → MultilinearTermsCache
Precompute all factorisation data for every monomial and term. Valid as long as the multiindex set is unchanged.
When skip_bits[l] is true the cache entry for monomial l is left empty. This is safe for monomials that will never be replayed (linear monomials, conjugate secondaries): those entries are guarded by the same skip_bits check in the solve loop.
compute_multilinear_terms(model::NthOrderModel{ORD, ORDP1, N_NL, N_EXT, T} where {ORDP1, N_NL, N_EXT, T}, exp::StaticArraysCore.SVector{NVAR}, parametrisation::Parametrisation{ORD, NVAR}) where {ORD, NVAR}
compute_multilinear_terms(model::NthOrderModel{ORD, ORDP1, N_NL, N_EXT, T} where {ORDP1, N_NL, N_EXT, T}, exp_index::Int64, parametrisation::Parametrisation{ORD, NVAR}, cache::MORFE.MultilinearTerms.MultilinearTermsCache) where {ORD, NVAR}
compute_multilinear_terms(model, exp, parametrisation) → Vector
Return the sum of all nonlinear-term contributions for exponent exp.
Scratch buffers and shared data (external_arguments, candidate_indices, external_exp) are allocated once and reused across all terms.
compute_multilinear_terms(model, exp_index, parametrisation, cache) → Vector
Cached variant of compute_multilinear_terms: replays precomputed factorisation data instead of calling the factorisation routines.
exp_index is the 1-based index into parametrisation's multiindex set.
compute_multilinear_terms!(result::AbstractVector, model::NthOrderModel{ORD, ORDP1, N_NL, N_EXT, T} where {ORDP1, N_NL, N_EXT, T}, exp_index::Int64, parametrisation::Parametrisation{ORD, NVAR}, cache::MORFE.MultilinearTerms.MultilinearTermsCache) where {ORD, NVAR}
compute_multilinear_terms!(result, model, exp_index, parametrisation, cache) → nothing
In-place variant: zeros result then accumulates all nonlinear contributions into it. Uses cache.scratch_buffer, cache.temp_buffer, and cache.external_arguments so that no heap allocation occurs during the inner solve loop.
symmetry_type(t) -> SymmetryType
Classify a AbstractMultilinearMap as FullyAsymmetric, FullySymmetric, or GroupwiseSymmetric based on its multiindex field.
Setting fully_asymmetric = true on any term overrides the multiindex-based classification and always returns FullyAsymmetric.
Module LowerOrderCouplings — computation of lower-order coupling vectors ξ[α].
For each monomial α the cohomological right-hand side includes a coupling term
ξ[α] = Σ_{β+γ=α, β after e_i in GrLex} R_β · (|γ₁| W_{γ+e₁} + … + |γₙ| W_{γ+eₙ})
that links the current monomial to all lower-degree coefficients of W and R already computed. compute_lower_order_couplings evaluates this sum efficiently using a precomputed dictionary and candidate index list.
_is_zero_coeff(coeff::AbstractMatrix)
_is_zero_coeff(coeff) -> Bool
Return true when the FOM × ORD coefficient slice coeff is entirely zero. Used to skip trivial contributions without entering the accumulation loop.
_sum_degree_one_terms!(accumulator::Array{Vector{T}, 1}, upper_bound::StaticArraysCore.SVector{NVAR, Int64}, multiindex_dict::Dict{StaticArraysCore.SVector{NVAR, Int64}, Int64}, red_coefficients::AbstractMatrix{T}, param_coefficients::AbstractArray{T, 3}, unit_vectors::AbstractArray{StaticArraysCore.SVector{NVAR, Int64}, 1}) where {NVAR, T}
_sum_degree_one_terms!(accumulator, upper_bound, multiindex_dict,
red_coefficients, param_coefficients, unit_vectors)
Accumulate the |β| = 1 part of the lower-order coupling sum into accumulator.
For each unit-vector eⱼ with eⱼ ≤ upper_bound, and for each i < j, adds R[i, eⱼ] * (upper_bound - eⱼ + eᵢ)[i] * W[:, :, upper_bound - eⱼ + eᵢ] to accumulator. Only reads precomputed multiindex_dict — no allocations.
The i < j restriction is causality, not an optimisation. upper_bound - eⱼ + eᵢ has the same total degree as upper_bound, and GrLex breaks ties by descending lexicographic order, so that exponent precedes upper_bound — and is therefore already solved — exactly when i < j. Consequently this branch reads only the strictly upper triangle of the reduced linear dynamics Λ, and a strictly-lower entry of Λ would be dropped without trace. That is why the external system's linear matrix is required to be upper triangular; see the ExternalSystems module docstring.
_sum_higher_degree_terms!(accumulator::Array{Vector{T}, 1}, upper_bound::StaticArraysCore.SVector{NVAR, Int64}, mset::MultiindexSet, multiindex_dict::Dict{StaticArraysCore.SVector{NVAR, Int64}, Int64}, red_coefficients::AbstractMatrix{T}, param_coefficients::AbstractArray{T, 3}, candidate_idxs::AbstractVector{Int64}, unit_vectors::AbstractArray{StaticArraysCore.SVector{NVAR, Int64}, 1}) where {NVAR, T}
_sum_higher_degree_terms!(accumulator, upper_bound, mset, multiindex_dict,
red_coefficients, param_coefficients,
candidate_idxs, unit_vectors)
Accumulate the |β| ≥ 2 part of the lower-order coupling sum into accumulator.
Iterates over candidate_idxs (precomputed as exponents componentwise ≤ upper_bound with total degree in [2, sum(upper_bound)-1]) and adds the same weighted R[i, β] * W[:, :, upper_bound - β + eᵢ] contributions as _sum_degree_one_terms!.
compute_lower_order_couplings(upper_bound::StaticArraysCore.SVector{NVAR, Int64}, parametrisation::Parametrisation{ORD, NVAR, T}, reduced_dynamics::ReducedDynamics{ROM, NVAR, T}, multiindex_dict::Dict{StaticArraysCore.SVector{NVAR, Int64}, Int64}, accumulator::Array{Vector{T}, 1}, candidate_idxs::AbstractVector{Int64}, unit_vectors::AbstractArray{StaticArraysCore.SVector{NVAR, Int64}, 1}) where {ORD, NVAR, ROM, T}
compute_lower_order_couplings(upper_bound::AbstractVector{Int64}, parametrisation::Parametrisation{ORD, NVAR, T}, reduced_dynamics::ReducedDynamics{ROM, NVAR, T}) where {ORD, NVAR, ROM, T}
compute_lower_order_couplings(upper_bound, parametrisation, reduced_dynamics,
multiindex_dict, accumulator, candidate_idxs,
unit_vectors) -> accumulator
Performance path. Evaluate the lower-order coupling vector ξ[α] for monomial α = upper_bound and accumulate the result into the pre-zeroed accumulator.
All auxiliary data (multiindex_dict, accumulator, candidate_idxs, unit_vectors) must be pre-allocated by the caller (typically via LowerOrderResources) to avoid heap allocation in the inner solve loop. Returns accumulator for convenience.
compute_lower_order_couplings(upper_bound, parametrisation, reduced_dynamics) -> Vector{Vector{T}}
Allocating convenience wrapper. Builds the multiindex dictionary, accumulator, and candidate index list internally, then delegates to the performance path.
Use the 7-argument form inside performance-critical loops; this overload is intended for interactive use or testing.
Numerical linear algebra for MORFE's constant-size bordered systems.
This internal module owns sparse backend selection, constant-pattern solver state, symbolic-factorisation caching, numeric refactorisation, exact factor reuse, residual verification, iterative refinement, and Pardiso extension hooks. It deliberately does not assemble cohomological equations, schedule monomials, manage symmetry, or persist checkpoints.
The supported sparse backends are KLU, UMFPACK, and extension-provided Pardiso. A cached factorisation is retained only after a successful factorisation; failed cached KLU and UMFPACK updates receive one fresh symbolic analysis before failure is reported. Failed KLU native storage is released before that recovery analysis begins.
Source organisation
| File | Purpose |
|---|---|
SolverState.jl | Backend markers, constant-pattern sparse storage, and cache lifetime |
FailureDiagnostics.jl | Contextual failure categories and user-facing diagnostics |
Factorisations.jl | Typed KLU and UMFPACK symbolic/numeric factorisation reuse |
AccuracyControl.jl | Sparse backward errors and iterative refinement |
BorderedSolve.jl | Backend dispatch, exact factor reuse, and in-place solves |
AbstractSparseBackend
Internal dispatch root for sparse bordered solvers. KLUBackend and UMFPACKBackend are always available; PardisoBackend is constructed only when the extension is active.
KLUBackend
KLUBackend{VERIFY}
Marker for the built-in KLU path. VERIFY selects backward-error verification at compile time so the default unchecked loop does not pay for residual bookkeeping.
PardisoBackend
PardisoBackend{P, VERIFY}
Sparse backend state for an extension-provided Pardiso solver. VERIFY selects backward-error verification at compile time.
SparseLinearSolverState{T, B, RT}
Sparse-path resources for the constant-size bordered cohomological system (see the CohomologicalEquations module docstring for the system itself).
bordered is the (FOM+ROM) × (FOM+ROM) matrix actually handed to the factoriser. Its colptr/rowval are fixed for the entire solve — only nzval is rewritten per monomial — which is what keeps the symbolic factorisation cached in fact valid throughout, and is the reason the border is masked rather than compacted.
L_template is the separate square FOM × FOM workspace on which build_sparse_L_and_rhs! runs its fused Horner pass (it needs the transient intermediates L[j](s) to accumulate the lower-order RHS); the resulting L(s) is then block-copied into the (1,1) block of bordered, column by column.
Backend selection follows the caller's parametrisation options: :klu forces KLU, :umfpack forces SuiteSparse UMFPACK, :pardiso requires the extension, and :auto prefers an available Pardiso implementation before falling back to KLU. KLU and UMFPACK reuse cached symbolic analysis while redoing numeric factorisation whenever the bordered matrix changes. KLU uses klu_factor!, not the exported klu!: the latter freezes the pivot sequence.
Fields
bordered::SparseMatrixCSC{T}— the(FOM+ROM)²matrix handed to the factoriser. Itscolptr/rowvalnever change; onlynzvalis rewritten per monomial.L_template::SparseMatrixCSC{T}—FOM × FOMworkspace carrying the union sparsity pattern of alllinear_terms, on whichL(s)is built.L_mappings::Vector{Vector{Int}}— for eachlinear_terms[k], the position inL_template.nzvalof each of its stored entries, so accumulatings^k B_kis an indexed scatter with no pattern search.border_row_base::Vector{Int}— lengthFOM;bordered[FOM+r, c]lives atnzval[border_row_base[c] + r - 1]. The border rows are contiguous within each column, which is what makes writing them a strided copy.solve_scratch::Vector{T}— lengthFOM+ROMRHS copy for Pardiso, whose solve needs distinct input and output vectors. Empty on the KLU and UMFPACK paths, whereldiv!is genuinely in-place.pardiso_matrix::Any— the matrix handed to Pardiso's analysis phase;nothinguntil_pardiso_prepare!has run.fact::Any— cached KLU or UMFPACK factorisation;nothinguntil the first successful one.backend::B— selectedKLUBackend,UMFPACKBackend, orPardisoBackend.residual_tolerance::Union{Nothing, RT}— active backward-error threshold.max_refinement_steps::Int— maximum KLU or UMFPACK refinement corrections.residual_work::Vector{T}— persistent KLU or UMFPACK residual/norm workspace.refinement_work::Vector{T}— lazily allocated correction workspace.max_relative_residual::RT— largest accepted backward error observed so far.refinement_count::Int— total KLU or UMFPACK refinement corrections performed.
mutable is load-bearing, not incidental: the Pardiso branch attaches a finaliser to release C-side memory, and Julia refuses to finalise an immutable object.
SparseLinearSolverState{T}(L_template, L_mappings, FOM, ROM;
options = nothing) -> SparseLinearSolverState
Initialise the sparse solver state and constant-pattern bordered template. Backend selection, residual verification, and refinement storage follow options.
UMFPACKBackend
UMFPACKBackend{VERIFY}
Marker for the opt-in SuiteSparse UMFPACK path. VERIFY selects backward-error verification at compile time so unchecked solves retain the same specialised path as KLU.
_BorderedSolveFailure
Internal contextual failure raised by a bordered cohomological solve.
Fields
category::Symbol—:outer_resonance,:factorisation,:solve, or:accuracy.backend::Symbol— selected linear solver (:dense,:klu,:umfpack, or:pardiso).index::Int— position of the monomial in the active multiindex set.multiindex— exponent vector of the failing monomial.superharmonic— canonical superharmonic used to assemble the matrix.inner_resonance_mask— master-mode resonance mask.outer_resonance_targets::Vector{Int}— configured outer targets flagged at this monomial.recovery_attempted::Bool— whether a failed cached factorisation was discarded and retried from a fresh symbolic analysis.detail::String— backend status or failed accuracy criterion.cause— underlying backend exception, ornothingwhen failure was status-based.
_backend_name(::MORFE.BorderedLinearSolvers.KLUBackend)
_backend_name(::MORFE.BorderedLinearSolvers.UMFPACKBackend)
_backend_name(::MORFE.BorderedLinearSolvers.PardisoBackend)
_backend_name(backend) -> Symbol
Return the stable diagnostic name of a sparse backend (:klu, :umfpack or :pardiso).
_backward_error(ss::SparseLinearSolverState, x, norm_r, norm_b, norm_A)
_backward_error(state, x, norm_r, norm_b, norm_A) -> Real
Return the normwise backward error norm_r / (norm_A * norm(x, Inf) + norm_b), guarded against a zero denominator in the state's real scalar type.
_bordered_solve!(ss::SparseLinearSolverState{T, <:MORFE.BorderedLinearSolvers.KLUBackend{VERIFY}}, solution::AbstractVector, superharmonic, index::Int64, multiindex, resonance, resonance_set; reuse_factor) where {T, VERIFY}
_bordered_solve!(ss::SparseLinearSolverState{T, <:MORFE.BorderedLinearSolvers.UMFPACKBackend{VERIFY}}, solution::AbstractVector, superharmonic, index::Int64, multiindex, resonance, resonance_set; reuse_factor) where {T, VERIFY}
_bordered_solve!(ss::SparseLinearSolverState{T, <:MORFE.BorderedLinearSolvers.PardisoBackend{P, VERIFY}}, solution::AbstractVector, superharmonic, index::Int64, multiindex, resonance, resonance_set; reuse_factor) where {T, P, VERIFY}
_bordered_solve!(state, solution, superharmonic, index, multiindex,
resonance, resonance_set; reuse_factor = Val(false)) -> solution
Solve the bordered system in place through KLU, UMFPACK, or Pardiso. Exact grouped reuse skips numeric factorisation only when the caller proves that the assembled matrix is identical. Monomial data is carried solely to produce contextual failure diagnostics.
_cached_klu_factor(ss::SparseLinearSolverState{T}, ::SparseArrays.SparseMatrixCSC{T, Ti}) where {T, Ti}
_cached_klu_factor(state, matrix) -> KLUFactorization
Return the cached KLU factorisation with value and index types derived from the active sparse matrix. Call only after the first successful factorisation has populated fact.
_cached_sparse_factor(ss::SparseLinearSolverState{T, <:MORFE.BorderedLinearSolvers.KLUBackend}, matrix) where T
_cached_sparse_factor(ss::SparseLinearSolverState{T, <:MORFE.BorderedLinearSolvers.UMFPACKBackend}, matrix) where T
Return the selected backend's cached factor with its concrete type restored.
_cached_umfpack_factor(ss::SparseLinearSolverState{T}, ::SparseArrays.SparseMatrixCSC{T, Ti}) where {T, Ti}
_cached_umfpack_factor(state, matrix) -> UmfpackLU
Return the cached UMFPACK factorisation with value and index types derived from the active sparse matrix. Call only after a successful factorisation has populated fact.
_configured_residual_tolerance(::Type{T}, options) where T
_configured_residual_tolerance(T, options) -> tolerance or nothing
Resolve the backward-error threshold in the real scalar type associated with T. options = nothing selects the standard checked default used by the low-level constructor.
_discard_klu_factor!(ss::SparseLinearSolverState, factorisation::KLU.KLUFactorization)
_discard_klu_factor!(state, factorisation) -> nothing
Remove a failed KLU factorisation from state and release its native symbolic and numeric storage before another analysis begins. Merely assigning nothing to state.fact would leave that storage alive until garbage collection, which can retain damaged KLU state across the immediate recovery attempt on Windows.
Base.finalize is Julia's required external spelling; it runs the finaliser already owned by KLUFactorization and prevents it from running twice later.
_fresh_klu_factor!(ss::SparseLinearSolverState, A::SparseArrays.SparseMatrixCSC)
Create and cache a checked fresh KLU factorisation of A.
_is_unrecoverable_failure(error)
Return whether a caught runtime failure must pass through without solver wrapping.
_outer_resonance_targets(resonance_set, index::Int64)
Return configured outer-resonance target indices for one monomial position.
_pardiso_factorise_solve!(args...)
Run Pardiso numeric factorisation followed by a solve.
_pardiso_prepare!(args...)
Run Pardiso configuration and symbolic analysis for a bordered matrix.
_pardiso_release!(args...)
Release extension-owned Pardiso factorisation storage.
_pardiso_solve!(args...)
Solve with the current Pardiso numeric factorisation.
_refactorise!(ss::SparseLinearSolverState, A::SparseArrays.SparseMatrixCSC)
Backward-compatible internal alias for _refactorise_klu!.
_refactorise_klu!(ss::SparseLinearSolverState, A::SparseArrays.SparseMatrixCSC)
_refactorise_klu!(ss, A) -> KLU factorisation of `A`
Factorise the bordered matrix for the current monomial, reusing the symbolic analysis cached in ss.fact.
The first call analyses and factorises; every later call redoes only the numeric factorisation. Splitting them this way is what makes the constant-size formulation pay off: the sparsity pattern is identical for every monomial, so the ordering and symbolic phase — the expensive part — are computed once for the whole solve.
Why klu_factor! and not klu!
The numeric phase must re-pivot. The bordered matrix changes value on every monomial and its (1,1) block is near-singular at every resonance, where stability depends on pivoting being free to exchange rows across the border. klu_factor! re-pivots while reusing the cached symbolic analysis. KLU's exported klu! maps to klu_refactor, which replays the pivot sequence chosen at the first monomial and so loses accuracy exactly where it is needed.
Preconditions and aliasing
A's sparsity pattern must be identical on every call — SparseLinearSolverState guarantees this by construction, since only nzval is ever written.
klu takes A.nzval by reference rather than copying it, so the factorisation and the template share one value array: per-monomial assembly writes straight into what KLU reads, with no copy-in step. colptr/rowval stay KLU's own (0-based) copies, and are invariant in any case.
A factorisation is cached only if it succeeded. Both fresh and cached numeric factorisations use KLU's checked path because stored zeros in the constant border can make an unchecked status appear successful even when the numeric factor is singular. A failed cached factor has its native storage released immediately and is retried once from a fresh symbolic analysis.
_refactorise_sparse!(ss::SparseLinearSolverState{T, <:MORFE.BorderedLinearSolvers.KLUBackend}, matrix) where T
_refactorise_sparse!(ss::SparseLinearSolverState{T, <:MORFE.BorderedLinearSolvers.UMFPACKBackend}, matrix) where T
Dispatch numeric refactorisation to the selected built-in sparse backend.
_refactorise_umfpack!(ss::SparseLinearSolverState, A::SparseArrays.SparseMatrixCSC)
_refactorise_umfpack!(state, matrix) -> UMFPACK factorisation
Factorise the current bordered matrix through SuiteSparse UMFPACK. Successful symbolic analysis is reused by lu!. A failed cached numeric factorisation is discarded and retried once from a fresh analysis; only a successful factorisation is retained in state.fact.
_release_pardiso!(state::SparseLinearSolverState{T, <:MORFE.BorderedLinearSolvers.KLUBackend}) where T
_release_pardiso!(state::SparseLinearSolverState{T, <:MORFE.BorderedLinearSolvers.UMFPACKBackend}) where T
_release_pardiso!(state::SparseLinearSolverState{T, <:MORFE.BorderedLinearSolvers.PardisoBackend}) where T
_release_pardiso!(state) -> nothing
Finaliser: hand the Pardiso factorisation back. No-op on the KLU and UMFPACK paths, and never allowed to throw — a finaliser that raises would be reported out of context.
_sparse_backward_error!(ss::SparseLinearSolverState, factorisation, solution, norm_b)
_sparse_backward_error!(state, factorisation, solution, norm_b) -> Real
Check the normwise backward error of a KLU or UMFPACK solution and, when necessary, perform at most state.max_refinement_steps correction solves with the supplied typed factorisation. Update the diagnostic maximum and refinement count and return the final backward error.
_sparse_inf_norm!(workspace, matrix::SparseArrays.SparseMatrixCSC)
_sparse_inf_norm!(workspace, matrix) -> Real
Compute norm(matrix, Inf) for a CSC matrix using workspace for row sums and without allocating a dense intermediate.
_sparse_residual!(residual, matrix::SparseArrays.SparseMatrixCSC, solution)
_sparse_residual!(residual, matrix, solution) -> residual
Overwrite a vector containing b with matrix * solution - b using SparseArrays' five-argument mul! kernel.
_suite_sparse_bordered_solve!(ss::SparseLinearSolverState{T}, solution::AbstractVector, superharmonic, index::Int64, multiindex, resonance, resonance_set, reuse_factor::Val{REUSE}, ::Val{VERIFY}) where {T, REUSE, VERIFY}
Shared allocation-conscious solve for the built-in KLU and UMFPACK backends.
_throw_bordered_failure(category::Symbol, backend::Symbol, index::Int64, multiindex, superharmonic, inner_resonance_mask, resonance_set; recovery_attempted, detail, cause)
Construct and throw a contextual _BorderedSolveFailure.
_try_build_pardiso_solver(...)
_try_build_pardiso_solver() -> solver or nothing
Extension hook implemented by MORFEPardisoExt. Return a configured Pardiso solver when the extension is active, or nothing so automatic selection can fall back to KLU.
Solve the cohomological equations that arise in the parametrisation method for computing Direct Normal Forms, NNMs, Spectral Submanifolds (SSMs) and such Invariant Manifolds of high-dimensional dynamical systems.
Problem statement
Given a full-order model of order ORD in FOM degrees of freedom, the parametrisation method seeks a change of coordinates
x(t) = W(z(t)), z ∈ ℂᴺᵛᵃʳ
such that the reduced dynamics ż = R(z) is simpler. Expanding both W and R as dense polynomials in the NVAR = ROM + N_EXT reduced variables (ROM master-mode amplitudes plus N_EXT external forcing amplitudes) and matching coefficients monomial by monomial yields the cohomological equations: one linear system per multi-index α.
System structure for multi-index α
Let s = ⟨λ, α⟩ be the superharmonic frequency and let P = diag(ρ) be the diagonal 0/1 matrix of the master modes resonant at α, Q = I − P. The cohomological linear system is bordered and of constant size FOM + ROM:
┌ ┐ ┌ ┐ ┌ ┐
│ L(s) C(s) P │ │ W[α] │ = │ RHS_inv │ FOM rows (invariance)
│ P Ĵ(s) P Ĉ(s) P + τQ │ │ R[α] │ │ P RHS_ort │ ROM rows (orthogonality)
└ ┘ └ ┘ └ ┘
where:
L(s)(FOM × FOM) is the parametrisation operator,C(s)(FOM × ROM) acts on the unknown reduced-dynamics coefficients,Ĵ(s)(ROM × FOM) is the orthogonality row operator, stacking the rowsĴ_r(s),Ĉ(s)(ROM × ROM) is the orthogonality joint operator,W[α] ∈ ℂᶠᵒᵐis the parametrisation coefficient,R[α] ∈ ℂᴿᴼᴹare the master reduced-dynamics coefficients,τ = 1on the non-resonant diagonal, so those rows readR[r, α] = 0.
Non-resonant master modes stay in the system as trivial equations rather than being compacted away. That is what keeps the size — and on the sparse path the sparsity pattern — independent of α, so a single symbolic factorisation serves every monomial. It costs nothing in accuracy: permuting the unknowns as [W; R_res; R_non] makes the matrix block triangular with an exactly decoupled τI block, and each trivial row is a singleton in both its row and its column. External forcing modes are known and appear only on the right-hand side.
Why the system is bordered rather than reduced
The border is not an optimisation but a conditioning requirement. Inner resonance is flagged by |λ_r − s| < tol, and det L(λ_r) = 0 for every master eigenvalue, so "monomial α is resonant" means precisely "L(s_α) is numerically singular" — and a border is present only on exactly those monomials. Eliminating L(s) first, by forming L(s)⁻¹·RHS and L(s)⁻¹C(s), would therefore apply an inverse that does not usefully exist, with backward error growing like κ(L(s_α)) → ∞.
Factorising the bordered matrix as a whole keeps the backward error at κ of the bordered matrix, which stays bounded because the border spans the near-null directions of L(s) (Keller's bordering lemma). This is why the linear algebra below never inverts L(s) alone, and why its numeric factorisation must re-pivot on every monomial.
Module contents
| Symbol | Description |
|---|---|
InvarianceOperators | Precomputed invariance-equation operator coefficients |
OrthogonalityOperators | Precomputed orthogonality-condition operator coefficients |
LowerOrderResources | Lower-order coupling data and buffers |
CohomologicalBuffers | Pre-allocated system-assembly scratch buffers |
CohomologicalContext | Composed struct bundling all precomputed operators and resources |
solve_single_monomial! | Solve the cohomological system for one multi-index |
Source layout
The module is deliberately organised by responsibility:
| File | Responsibility |
|---|---|
CohomologicalEquations.jl | Focused module composition, imports, and exports |
SolveState.jl | Equation operators, lower-order resources, and reusable assembly buffers |
EquationAssembly.jl | Bordered equation assembly and the dense equation solve |
MonomialSolve.jl | Nonlinear right-hand side, bordered-solver call, and W/R finalisation |
Linear factorisation belongs to BorderedLinearSolvers; scheduling, checkpointing, symmetry, progress, and benchmarking belong to ParametrisationSolver.
CohomologicalBuffers
CohomologicalBuffers{T, RT}
Pre-allocated scratch buffers for the cohomological system assembly and solve.
Both paths solve the same constant-size (FOM+ROM) × (FOM+ROM) bordered system, but they materialise it differently, so only one of the two matrix buffers is allocated:
dense path uses
system_matrix, the bordered matrix itself, LU'd in place;sparse path uses
orthogonality_rowsas the staging area for theROMorthogonality rows, which are then scattered into the strided border positions of the sparse template'snzval(the sparse bordered matrix lives inSparseLinearSolverState).
The unused buffer is a 0×0 placeholder.
Fields
system_matrix::Matrix{T}— the(FOM+ROM) × (FOM+ROM)bordered matrix on the dense path, factorised in place bylu!.0×0on the sparse path.orthogonality_rows::Matrix{T}—ROM × (FOM+ROM)staging area on the sparse path, holding the evaluatedĴ(s)rows and corner before they are scattered into the template.0×0on the dense path.rhs::Vector{T}— lengthFOM+ROM. Holds the right-hand side on entry and, in the same memory, the solution after the solve; the unpacking step readsW[α]from its firstFOMentries and the resonantR[α]from the rest.external_rhs::Vector{T}— lengthFOMscratch forevaluate_external_rhs!.ml_result::Vector{T}— lengthFOM, receiving the nonlinear (multilinear-term) contribution fromcompute_multilinear_terms!.dense_solution::Vector{T}— accepted solution retained while the dense matrix and right-hand side are reassembled for backward-error checks; empty when verification is disabled or on the sparse path.dense_refinement::Vector{T}— lazily allocated dense correction vector used only after a failed backward-error check.residual_tolerance::Union{Nothing, RT}— active scalar-type-aware backward-error threshold, ornothingwhen verification is disabled.max_refinement_steps::Int— maximum dense iterative-refinement corrections.
CohomologicalBuffers(T, MT, FOM, ROM, options = nothing) -> CohomologicalBuffers
Allocate all buffers for a system of full-order dimension FOM and ROM master modes. Dispatches on the FOM matrix type MT: MT <: SparseMatrixCSC selects the sparse layout, everything else the dense one. options controls backward-error workspace and the refinement limit.
CohomologicalContext
CohomologicalContext{T, ORD, ORDP1, NVAR, FOM, LT, MT}
Bundles all precomputed data required to solve the cohomological equations for every monomial.
The solve visits every multi-index in turn, and nothing in this struct varies with the multi-index: operator coefficients, resonance look-ups, buffers and the cached factorisation are all built once by solve_parametrisation and reused. That is what allows solve_single_monomial! to run without heap allocation. Related data is grouped into named sub-structs rather than kept flat, so each field's provenance is visible at the call site (ctx.orthogonality.J_coeffs, not a bare ctx.J_coeffs).
Type parameters
| Parameter | Meaning |
|---|---|
T | Scalar type (typically ComplexF64) |
ORD | Differential-equation order of the full-order model |
ORDP1 | ORD + 1 |
NVAR | Total reduced variables: ROM + N_EXT |
FOM | Full-order state dimension |
LT | Element type of the FOM matrices |
MT | Matrix type; sparse path when MT <: SparseMatrixCSC |
Fields
linear_terms::NTuple{ORDP1, MT}— theORD+1FOM matricesB₀ … B_ORDwhose combinationL(s) = Σ_j s^j B_jforms the(1,1)block of the bordered system.generalised_eigenmodes::Matrix{T}—FOM × NVARmatrix of generalised right eigenmodes, formed by concatenating the master right eigenmodes and the solved external right directions. It propagates higher derivative coefficients after the linear monomials have been initialised. The left eigenmodes are represented separately throughorthogonalityand are never stored in this field.lambda_diag::Vector{T}— theNVARreduced eigenvalues; the superharmonic of a monomial iss = ⟨lambda_diag, α⟩.invariance::InvarianceOperators{T}— border-column coefficients; seeInvarianceOperators.orthogonality::OrthogonalityOperators{T}— orthogonality row, corner and external coefficients; seeOrthogonalityOperators.resonance_set::ResonanceSet— which master modes are resonant with which monomial, deciding per row whether the border is populated or masked out.linear_monomial_skip_set::Set{Int}— positions of master linear monomials fixed from the eigenvectors before the loop. External linear directions are marked separately in the active conjugate-symmetry skip mask after they are solved.lower_order::LowerOrderResources{NVAR, T}— coupling buffers and multiindex look-up; seeLowerOrderResources.buffers::CohomologicalBuffers{T}— assembly and solve scratch; seeCohomologicalBuffers.sparse_solver::Union{Nothing, SparseLinearSolverState{T}}— sparse-path template and factorisation handles, ornothingon the dense path. This field, notMTalone, is what the solve branches on.
InvarianceOperators
InvarianceOperators{T}
Precomputed column-polynomial coefficients for the invariance equation.
Both fields hold coefficients of polynomials in the superharmonic s, evaluated by evaluate_column! once per monomial: the master columns become the C(s) border of the bordered system, the external ones contribute to its right-hand side. Precomputing them per order rather than per monomial is what keeps the inner solve loop allocation-free.
Fields
column_coeffs::Vector{Matrix{T}}— oneFOM × ORDmatrix per master moder, the coefficients of the border columnC_r(s). Distinct in both shape and role fromOrthogonalityOperatorscorner_coeffs, which fills theROM × ROMcorner.E_coeffs::Vector{Matrix{T}}— oneFOM × ORDmatrix per external variable or directione. External amplitudes are known, so these never reach the matrix.
LowerOrderResources
LowerOrderResources{NVAR, T}
Data needed to compute lower-order coupling vectors ξ[j] on every monomial.
Bundled into one struct so the per-monomial buffers and the multiindex lookup are allocated once for the whole solve rather than per call. Everything here is a pure function of the multiindex set, so none of it changes as the solve progresses.
Fields
multiindex_dict::Dict{SVector{NVAR, Int}, Int}— maps an exponent vector to its position in the multiindex set, turning the coupling lookup into a hash rather than a scan.buffer::Vector{Vector{T}}—ORDvectors of lengthFOMholding the coupling termsξ[j]. Reused across monomials and zeroed by the caller before each use.candidate_indices::Vector{Vector{Int}}— for each of theLmonomial positions, the multiindices that can contribute a coupling to it. Degree-1 monomials get an empty list, since a coupling needs two factors of degree ≥ 1.unit_vectors::Vector{SVector{NVAR, Int}}— theNVARunit exponent vectorseᵣ, materialised once because the coupling recurrence subtracts them constantly.
LowerOrderResources{NVAR, T}(mset, ORD, FOM) -> LowerOrderResources
Build the lower-order coupling resources for a multiindex set of NVAR variables, ODE order ORD, and full-order dimension FOM.
candidate_indices is precomputed here, once per monomial position: it lists the multiindices that can contribute a lower-order coupling to that monomial, which is a pure function of the multiindex set and so need not be recomputed during the solve.
OrthogonalityOperators{T}
Precomputed row and column-polynomial coefficients for the orthogonality conditions.
Together the three fields supply the bottom ROM rows of the bordered system: one row per master mode, assembled by assemble_orthogonality_matrix_and_rhs!. They are read off the left eigenvector order-blocks once per order, so evaluating a row at a given s costs one Horner pass and no allocation.
Fields
J_coeffs::Vector{Matrix{T}}— oneORD × FOMmatrix per master moder, the coefficients of the row operatorĴ_r(s)acting onW[α].corner_coeffs::Vector{Matrix{T}}— one(ORD-1) × ROMmatrix per master mode, evaluating to theROM × ROMcorner blockĈ(s)that couples the orthogonality rows to the unknown reduced-dynamics coefficients. SeeInvarianceOperatorscolumn_coeffsfor the differently-shaped border columns.E_coeffs::Vector{Matrix{T}}— one(ORD-1) × N_EXTmatrix per master mode, contracting the known external amplitudes into the scalar right-hand side.
Internal marker selecting the allocation-free, uninstrumented solve path.
_assemble_bordered_system!(ctx::CohomologicalContext{T, ORD, ORDP1, NVAR, FOM, LT, MT}, s, resonance::StaticArraysCore.SVector{ROM, Bool}, lower_order_couplings, external_dynamics) where {T, ORD, ORDP1, NVAR, FOM, LT, MT, ROM}
_assemble_bordered_system!(ctx, s, resonance, lower_order_couplings, external_dynamics)
Assemble the (FOM+ROM) × (FOM+ROM) bordered cohomological system into ctx.buffers.system_matrix and ctx.buffers.rhs.
The first
FOMrows come from the invariance equation (operator + nonlinear RHS).The last
ROMrows come from the orthogonality conditions, with non-resonant modes contributing the trivial rowR[r, α] = 0.
Called by the dense-path _solve_monomial!.
_assemble_nonlinear_rhs!(::MORFE.CohomologicalEquations._NoMonomialInstrumentation, ctx, model, idx, W, ml_cache)
_assemble_nonlinear_rhs!(instrumentation, ctx, model, idx, W, ml_cache)
Instrumentation hook for nonlinear right-hand-side assembly. The production method returns nothing; benchmark instrumentation returns the corresponding @timed result.
_dense_backward_error(A, x, b)
_dense_backward_error(A, x, b) -> Real
Compute norm(A*x-b, Inf) / (norm(A, Inf)*norm(x, Inf) + norm(b, Inf)) without allocating a residual vector. The denominator is floored at the real scalar type's smallest positive normal value.
_finalise_monomial!(W::Parametrisation{ORD, NVAR, T}, R::ReducedDynamics{ROM, NVAR, T}, idx::Int64, ctx::CohomologicalContext{T, ORD, ORDP1, NVAR, FOM, LT} where LT, s, resonance::StaticArraysCore.SVector{ROM, Bool}, external_dynamics, lower_order_couplings) where {ORD, NVAR, T, ROM, FOM, ORDP1}
_finalise_monomial!(W, R, idx, ctx, s, resonance, external_dynamics,
lower_order_couplings) -> nothing
Unpack the bordered solution into the primary coefficients of W and the resonant rows of R, write exact zeros to non-resonant master rows, and propagate the higher derivative coefficients using the generalised right eigenmodes.
_monomial_metrics(::MORFE.CohomologicalEquations._NoMonomialInstrumentation, ::Nothing, ::Nothing)
_monomial_metrics(instrumentation, rhs_result, solve_result)
Convert instrumentation-specific phase results into the metrics delivered to solve observers. The production path returns nothing.
_resonance_vector(resonance_set::ResonanceSet, monomial_idx::Int64, ::Val{ROM}) where ROM
_resonance_vector(resonance_set, monomial_idx, ::Val{ROM}) -> SVector{ROM, Bool}
Return a compile-time-sized boolean vector indicating which of the ROM master modes are resonant with the monomial at position monomial_idx in the multiindex set. Using Val{ROM} enables the compiler to emit a fully unrolled ntuple loop.
_run_single_monomial!(instrumentation, W::Parametrisation{ORD, NVAR, T}, R::ReducedDynamics{ROM, NVAR, T}, idx::Int64, ctx::CohomologicalContext{T, ORD, ORDP1, NVAR, FOM, LT, MT}, model::NthOrderModel, ml_cache::MORFE.MultilinearTerms.MultilinearTermsCache) where {ORD, NVAR, T, ROM, FOM, ORDP1, LT, MT}
_run_single_monomial!(instrumentation, W::Parametrisation{ORD, NVAR, T}, R::ReducedDynamics{ROM, NVAR, T}, idx::Int64, ctx::CohomologicalContext{T, ORD, ORDP1, NVAR, FOM, LT, MT}, model::NthOrderModel, ml_cache::MORFE.MultilinearTerms.MultilinearTermsCache, reuse_factor::Val{REUSE}) where {ORD, NVAR, T, ROM, FOM, ORDP1, LT, MT, REUSE}
_run_single_monomial!(instrumentation, W::Parametrisation{ORD, NVAR, T}, R::ReducedDynamics{ROM, NVAR, T}, idx::Int64, ctx::CohomologicalContext{T, ORD, ORDP1, NVAR, FOM, LT, MT}, model::NthOrderModel, ml_cache::MORFE.MultilinearTerms.MultilinearTermsCache, reuse_factor::Val{REUSE}, superharmonic) where {ORD, NVAR, T, ROM, FOM, ORDP1, LT, MT, REUSE}
_run_single_monomial!(instrumentation, W, R, idx, ctx, model, ml_cache,
reuse_factor, superharmonic = nothing) -> metrics
Canonical per-monomial pipeline shared by production and benchmark execution. Only nonlinear-right-hand-side assembly and the bordered solve are instrumentation hooks; preparation and coefficient finalisation stay outside benchmark timings.
Grouped execution supplies one canonical superharmonic for the entire structural factor group. Direct and public single-monomial execution leave it as nothing and compute the value from the monomial itself.
_solve_monomial!(ctx::CohomologicalContext{T, ORD, ORDP1, NVAR, FOM, LT, MT}, index::Int64, multiindex, s, resonance::StaticArraysCore.SVector{ROM, Bool}, lower_order_couplings, external_dynamics) where {T, ORD, ORDP1, NVAR, FOM, LT, MT<:SparseMatrixCSC, ROM}
_solve_monomial!(ctx::CohomologicalContext{T, ORD, ORDP1, NVAR, FOM, LT, MT}, index::Int64, multiindex, s, resonance::StaticArraysCore.SVector{ROM, Bool}, lower_order_couplings, external_dynamics) where {T, ORD, ORDP1, NVAR, FOM, LT, MT, ROM}
_solve_monomial!(ctx::CohomologicalContext{T, ORD, ORDP1, NVAR, FOM, LT, MT}, index::Int64, multiindex, s, resonance::StaticArraysCore.SVector{ROM, Bool}, lower_order_couplings, external_dynamics, reuse_factor::Val{REUSE}) where {T, ORD, ORDP1, NVAR, FOM, LT, MT<:SparseMatrixCSC, ROM, REUSE}
_solve_monomial!(ctx::CohomologicalContext{T, ORD, ORDP1, NVAR, FOM, LT, MT}, index::Int64, multiindex, s, resonance::StaticArraysCore.SVector{ROM, Bool}, lower_order_couplings, external_dynamics, reuse_factor::Val{REUSE}) where {T, ORD, ORDP1, NVAR, FOM, LT, MT, ROM, REUSE}
_solve_monomial!(ctx, index, multiindex, s, resonance,
lower_order_couplings, external_dynamics)
Dense path. Assemble the (FOM+ROM) bordered system via _assemble_bordered_system!, then solve it in-place with lu! + ldiv!. The solution is written into ctx.buffers.rhs[1:FOM+ROM].
_solve_monomial!(ctx, index, multiindex, s, resonance,
lower_order_couplings, external_dynamics)
Sparse path (dispatched when MT <: SparseMatrixCSC). Writes the bordered matrix into the constant-pattern template held by ctx.sparse_solver and solves it with at most one new numeric factorisation. When reuse_factor == Val(true), the existing factorisation is reused because the caller has established exact matrix identity.
The (1,1) block is evaluated by the untouched build_sparse_L_and_rhs! Horner pass on its own workspace — it needs the transient intermediates L[j](s) to accumulate the lower-order RHS — and is then scattered into the template. The border blocks are staged in ctx.buffers.orthogonality_rows by the same assembly routine the dense path uses, then scattered into their strided positions in nzval.
Results are written into ctx.buffers.rhs[1:FOM+ROM].
_solve_prepared_system!(::MORFE.CohomologicalEquations._NoMonomialInstrumentation, ctx, index, multiindex, s, resonance, lower_order_couplings, external_dynamics, reuse_factor::Val)
_solve_prepared_system!(instrumentation, ctx, index, multiindex, s,
resonance, lower_order_couplings, external_dynamics, reuse_factor)
Instrumentation hook for the bordered solve after all monomial-dependent inputs have been prepared. reuse_factor may be true only for an exactly identical grouped matrix.
_superharmonic(multi, lambda_diag)
Return the monomial superharmonic sum(alpha[i] * lambda_diag[i]).
solve_single_monomial!(W, R, idx::Int64, ctx, model, ml_cache)
solve_single_monomial!(W, R, idx::Int64, ctx, sym, model, ml_cache, reuse_factor::Bool)
solve_single_monomial!(W, R, idx::Int64, ctx, sym, model, ml_cache)
solve_single_monomial!(W, R, idx::Int64, ctx, sym, model, ml_cache, reuse_factor::Val)
solve_single_monomial!(W, R, idx, ctx, model, ml_cache) -> nothing
Solve the cohomological equations for one multiindex-set position, updating W and R.
Operational workflow for constructing a parametrisation and reduced dynamics.
This internal submodule owns the work surrounding the mathematical cohomological equation: configuration, W/R storage preparation, checkpoint restoration and commits, conjugate reconstruction, causal monomial scheduling, progress, and benchmarking. It delegates each individual cohomological equation to CohomologicalEquations and delegates sparse factorisation to BorderedLinearSolvers.
Source organisation
| File | Purpose |
|---|---|
Configuration.jl | User-facing execution and checkpoint options |
Checkpointing.jl | Validated, atomic checkpoint persistence and restoration |
ConjugateSymmetry.jl | Conjugate-pair bookkeeping and coefficient reconstruction |
ProgressIndicator.jl | Allocation-conscious terminal progress reporting |
SolveSchedule.jl | Causal jobs and exact structural factor groups |
SolveExecution.jl | Typed observers and plan execution |
Benchmarking.jl | Timing instrumentation and CSV reporting |
SolutionStorage.jl | Creation, reuse, initialisation, and checkpoint restoration of W/R |
ExternalDirections.jl | Ordered external solves and conjugate-block validation |
SolvePreparation.jl | Backend, operator, symmetry, resources, and complete context |
SolveProblem.jl | Short top-level phase orchestration |
The module root contains only dependencies, includes, and exports so that these responsibilities remain visible rather than accumulating in one driver file.
CheckpointOptions
CheckpointOptions(path; problem_id, resume = true, granularity = :factor_group)
Configure durable checkpoints for parametrise. Pass the resulting object as the checkpoint field of ParametrisationOptions.
Arguments
path::AbstractString— checkpoint directory. MORFE creates it and stores a manifest plus checksummed coefficient chunks beneath it.problem_id::AbstractString— required, non-empty identifier for the physical problem. It is checked when resuming so that a checkpoint cannot silently be used for a different run.resume::Bool = true— restore compatible completed work found atpath. Withresume = false, MORFE refuses an existing checkpoint manifest instead of overwriting it; use a new directory for a fresh run.granularity::Symbol = :factor_group— when data are committed::factor_groupwrites after every exact factor-reuse group, giving finer restart points and more chunk files;:degreewrites once after each completed total degree, giving fewer, larger chunks.
A checkpoint is accepted only when its fingerprint matches the model, spectral data, resonance set, multiindex set, conjugate permutation, and numerical solver policy. Models containing application-defined callable kernels must implement checkpoint_fingerprint_data before they can be checkpointed.
Example
checkpoint = CheckpointOptions("checkpoints/beam-order-9";
problem_id = "clamped-beam-v1",
granularity = :degree)
options = ParametrisationOptions(checkpoint = checkpoint)
W, R = parametrise(model, spectral, 9; options)
CheckpointSession
CheckpointSession
Mutable state for one checkpoint directory: the validated CheckpointOptions, the problem fingerprint, and the most recently committed manifest. The manifest is refreshed under the writer lock after every chunk or degree commit.
Fields
options::CheckpointOptions— checkpoint path, resume policy, problem identifier, and commit granularity for this session.fingerprint::String— SHA-256 identity of the mathematical problem and numerical policy against which every resumed manifest is checked.manifest::Dict{String, Any}— in-memory copy of the last atomically committed manifest.
ConjugateSymmetryData{CP}
Self-contained optimisation layer for exploiting complex-conjugate symmetry in the cohomological solve.
Type parameters
| Parameter | Meaning |
|---|---|
CP | NoConjugatePermutation (inactive) or SVector{NVAR, Int} (active) |
Compile-time dispatch on CP eliminates secondary-monomial bookkeeping when inactive. When active, secondary monomials (those whose conjugate partner has a lower multiindex-set index) are marked in skip_bits and skipped in the outer solve loop; their coefficients are filled by fill_conjugate_monomial!.
Fields
permutation::CP— the involutory mode permutationPswapping conjugate pairs, orNoConjugatePermutationwhen the optimisation is off.monomial_map::Vector{Int}—monomial_map[i]is the position of the conjugate monomialP·γforγ = mset[i], or0when it falls outside the multiindex set.skip_bits::BitVector— lengthL;truemarks a monomial the outer loop must not solve. Covers both the linear monomials (already known from eigenvectors) and the secondary monomials obtained by conjugation, so the loop needs one test rather than two.
NoConjugatePermutation
Sentinel type indicating that conjugate-symmetry exploitation is disabled. Dispatch on this type eliminates all symmetry bookkeeping at compile time.
ParametrisationOptions(;
backend = :auto,
grouping = :auto,
residual_check = :off,
residual_tolerance = nothing,
max_refinement_steps = 3,
validate_mset = true,
checkpoint = nothing,
show_progress = true,
verbose = true,
setup_io = stderr)
Example of operational controls passed to parametrise through its options keyword:
options = ParametrisationOptions(
backend = :klu,
residual_check = :backward_error,
residual_tolerance = 1e-10,
show_progress = false)
W, R = parametrise(model, spectral, expansion_order; options)
The fields below are constructor keywords of ParametrisationOptions; they are not direct keywords of parametrise. Mathematical choices (resonance and conjugate_permutation) deliberately remain explicit keywords of parametrise.
Solver options
backend::Symbol = :auto— linear solver for the bordered cohomological systems::autouses dense LU for dense full-order matrices; for sparse matrices it uses
Pardiso when a Pardiso extension is active and otherwise KLU;
:klurequires sparse full-order matrices and forces KLU;:umfpackrequires sparse full-order matrices and forces Julia's built-in
SuiteSparse UMFPACK solver;
:pardisorequires sparse full-order matrices and an active Pardiso extension,
otherwise construction of the solver fails.
grouping::Symbol = :auto— exact reuse of a factorisation by monomials whose cohomological matrices are structurally identical::autogroups only when repeated or zero eigenvalues make reuse possible and grouping
actually reduces the number of factorisations;
:onalways builds and processes the exact structural groups;:offsolves directly in graded-lexicographic order without grouping.
Grouping is exact; it never clusters approximately equal eigenvalues. Each group uses its first monomial's superharmonic consistently for every member.
residual_check::Symbol = :off—:backward_errorverifies every bordered solve;:offskips this additional check.residual_tolerance::Union{Nothing, Real} = nothing— positive backward-error limit. It is used only withresidual_check = :backward_error.nothingselects the scalar-type-aware defaultsqrt(eps(real(T))) / 100(approximately1.49e-10forFloat64).max_refinement_steps::Integer = 3— maximum number of iterative-refinement corrections after a failed backward-error check on the dense, KLU and UMFPACK paths. Must be non-negative. A solve still throws if it remains outside the tolerance.
Validation and restart options
validate_mset::Bool = true— validate the multiindex set before solving: dimensions, allowed degrees, required master unit vectors, downward closure, and (when active) conjugate closure. Set this tofalseonly when the same set has already been validated.checkpoint::Union{Nothing, CheckpointOptions} = nothing— durable checkpoint and resume policy.nothingdisables checkpoint I/O; seeCheckpointOptions.
Output options
show_progress::Bool = true— show the in-place monomial progress indicator onstderrwhen it is an interactive terminal. It remains silent in redirected output and CI logs.verbose::Bool = true— print the model/setup summary before building the resonance set. With the defaultsetup_io = stderr, the summary is printed only whenstderris interactive; an explicitly supplied output stream is always honoured.setup_io::IO = stderr— destination for the setup summary controlled byverbose. This does not redirect the progress indicator, which always usesstderr.
Common configurations
Quiet run:
options = ParametrisationOptions(show_progress = false, verbose = false)
Verified sparse solve with automatic exact factor reuse:
options = ParametrisationOptions(
backend = :auto,
grouping = :auto,
residual_check = :backward_error,
residual_tolerance = 1e-10,
max_refinement_steps = 3)
All symbolic options are validated when ParametrisationOptions is constructed, so a misspelling such as backend = :suitesparse or grouping = :approximate fails immediately.
StructuralFactorKey
StructuralFactorKey{NVAR, ROM}
Exact mathematical identity of a bordered matrix for factor-reuse grouping.
Fields
exponents::SVector{NVAR, Int}— monomial powers accumulated by equal nonzero eigenvalue; powers belonging to zero eigenvalues do not affect the superharmonic.resonance::SVector{ROM, Bool}— complete master-mode border mask.
_AbstractSolveObserver
Internal lifecycle interface notified after jobs, factor groups, completed degrees, and the complete plan. Concrete observers keep progress, checkpointing, and benchmarking out of the numerical executor.
_AbstractSolvePlan
Internal dispatch root for direct and exact-factor-grouped solve plans.
_BenchmarkInstrumentation
Instrumentation marker that times nonlinear right-hand-side assembly and the bordered solve while leaving preparation and coefficient finalisation outside the measurements.
_BenchmarkSolveObserver{M}
Accumulate per-monomial and per-degree benchmark rows in memory and write them after a successful solve.
Fields
mset::M— multiindex set used to render monomial exponents.monomial_io::IOBuffer— buffered per-monomial CSV rows.order_io::IOBuffer— buffered per-degree CSV rows.benchmark_dir::String— destination directory for both CSV files.cumulative_time::Float64— cumulative measured RHS and solve time.current_order::Int— degree of the most recently measured job.order_accumulator::_OrderAccum— reusable totals for the current degree.
_CheckpointSolveObserver{S, W, R, M, SS}
Commit coefficient chunks and degree markers at lifecycle boundaries.
Fields
session::S— activeCheckpointSession.W::W— parametrisation whose completed coefficient slices are persisted.R::R— reduced dynamics persisted alongsideW.mset::M— multiindex set used to collect all indices in degree-granularity mode.sparse_solver::SS— sparse solver diagnostics, ornothingon the dense path.
_CompositeSolveObserver{A, B}
Forward every lifecycle event to two observers in order.
Fields
first::A— observer notified first.second::B— observer notified afterfirstcompletes.
_DirectSolvePlan
_DirectSolvePlan
Causal jobs executed as independent singleton factorisation groups.
Fields
jobs::Vector{_SolveJob}— solve jobs in graded-lexicographic causal order.
_GroupedSolvePlan
_GroupedSolvePlan{T}
Degree-local groups whose jobs share exactly identical bordered matrices.
Fields
groups::Vector{_SolveGroup{T}}— ordered factor-reuse groups, never spanning degrees.n_jobs::Int— total primary solves across all groups, used for progress reporting.
_NoSolveObserver
No-op solve observer used when no optional lifecycle action is requested.
_OrderAccum
_OrderAccum
Mutable per-degree benchmark totals, reset and reused between polynomial orders.
Fields
n_solved::Int— primary monomials measured in the current degree.rhs_time::Float64— cumulative nonlinear right-hand-side assembly time.rhs_bytes::Int— cumulative allocations during right-hand-side assembly.solve_time::Float64— cumulative bordered-solve time.solve_bytes::Int— cumulative allocations during bordered solves.
_ProgressSolveObserver
Track completed primary jobs and update the terminal progress indicator.
Fields
progress::_SimpleProgress— configured terminal progress state.n_done::Int— number of primary jobs reported so far.
_SimpleProgress
_SimpleProgress
Lightweight progress state for the -based terminal progress indicator.
Fields
n_total::Int— number of monomials that will actually be solved, which is fewer than the multiindex-set size whenever linear or conjugate-secondary monomials are skipped. The reported fraction is against this, not against the set size.enabled::Bool—falsewhenstderris not a TTY, so CI logs stay clean without every call site having to test for it.max_nl_degree::Int— highest nonlinearity degree in the model. Work per monomial grows with degree, so the fraction is raised to this power to make the displayed percentage track elapsed time rather than monomial count.
_SolveGroup
_SolveGroup{T}
Jobs sharing one canonical superharmonic and exactly the same bordered matrix.
Fields
superharmonic::T— common value ofs = dot(alpha, lambda_diag)used to assemble and factor the first job.jobs::Vector{_SolveJob}— jobs that reuse that numeric factorisation in causal order.
_SolveJob
_SolveJob
A primary monomial solve and its optional conjugate reconstruction target.
Fields
index::Int— multiindex-set position solved by the cohomological system.conjugate_target::Int— secondary position filled by conjugation afterwards, or0when no reconstruction is required.
_announce_conjugate_symmetry(::NoConjugatePermutation)
_announce_conjugate_symmetry(::StaticArraysCore.SVector)
_announce_conjugate_symmetry(permutation)
Emit the conjugate-symmetry assumptions once when a concrete conjugate permutation is active. The no-symmetry method is a compile-time no-op.
_atomic_manifest(path, manifest)
_atomic_manifest(path, manifest) -> nothing
Write a TOML manifest to a process-specific temporary file and atomically replace path only after the complete document has been flushed.
_build_complete_context(W, dimensions::NTuple{5, Int64}, unit_offset::Int64, linear_terms, master_right_modes, lambda_matrix, lambda_diag, invariance_C_coeffs, master_derivative_steps, orthogonality_J_coeffs, right_master_blocks, resonance_set, linear_skip_set, lower_order, buffers, sparse_solver)
_build_complete_context(W, dimensions, unit_offset, ...)
Read the final external directions from W, build the complete external operators, and return the context used by the nonlinear solve.
_build_conjugate_symmetry(::NoConjugatePermutation, linear_skip_set::Set{Int64}, L::Int64)
_build_conjugate_symmetry(perm::StaticArraysCore.SVector{NVAR, Int64}, linear_skip_set::Set{Int64}, mset::MultiindexSet{NVAR}, mdict::Dict{StaticArraysCore.SVector{NVAR, Int64}, Int64}) where NVAR
_build_conjugate_symmetry(perm_or_sentinel, linear_skip_set, ...) -> ConjugateSymmetryData
Factory for ConjugateSymmetryData.
Inactive (first argument is
NoConjugatePermutation()): wrapslinear_skip_setasskip_bitsand returns aConjugateSymmetryData{NoConjugatePermutation}.Active (first argument is an
SVector{NVAR,Int}involution): builds the monomial conjugate map, identifies primary/secondary pairs, populatesskip_bitsfor both linear monomials and secondary conjugate monomials, and returns aConjugateSymmetryData{SVector{NVAR,Int}}.
_build_context(linear_terms::NTuple{ORDP1, MT}, generalised_right_eigenmodes::Matrix{T}, lambda_diag::Vector{T}, inv_ops::InvarianceOperators{T}, orth_ops::OrthogonalityOperators{T}, resonance_set::ResonanceSet, linear_skip_set::Set{Int64}, lower_order::LowerOrderResources{NVAR, T}, buffers::CohomologicalBuffers{T}, sparse_solver) where {ORDP1, MT, T, NVAR}
_build_context(linear_terms, generalised_right_eigenmodes, lambda_diag,
inv_ops, orth_ops, resonance_set, linear_skip_set,
lower_order, buffers, sparse_solver) -> CohomologicalContext
Construct a CohomologicalContext from pre-assembled operator data and shared resources. All type parameters are inferred from the arguments.
_build_monomial_map(mset::MultiindexSet{NVAR}, perm::StaticArraysCore.SVector{NVAR, Int64}, mdict::Dict{StaticArraysCore.SVector{NVAR, Int64}, Int64}) where NVAR
_build_monomial_map(mset, perm, mdict) -> Vector{Int}
Build the monomial conjugate map induced by the mode permutation perm. monomial_map[i] is the index in mset of the monomial P·γ where γ = mset[i] and (P·γ)[k] = γ[perm[k]]. Returns 0 when P·γ is not in mset.
_build_solve_jobs(sym::ConjugateSymmetryData)
_build_solve_jobs(sym) -> Vector{_SolveJob}
Build jobs from the current skip mask. Checkpoint restoration and external-direction initialisation mutate that mask after symmetry discovery, so jobs must not be cached in ConjugateSymmetryData.
_build_solve_plan(ctx, sym, mset, grouping::Symbol, ::Val{ROM}) where ROM
_build_solve_plan(ctx, sym, mset, grouping, Val(ROM)) -> _AbstractSolvePlan
Create a causal direct or grouped plan. Grouping is exact and never crosses a total degree. :auto returns the direct plan unless structural reuse is possible and actually reduces the number of numeric factorisations.
_check_external_conjugate_block(conjugate_permutation, sys, ROM::Int64, NVAR::Int64)
_check_external_conjugate_block(conjugate_permutation, sys, ROM, NVAR)
Reject a conjugate_permutation whose external block disagrees with the external system's own conjugate involution.
Only checked when the system was re-based (external_basis(sys) !== nothing), because that is the only situation in which a hand-written permutation can be stale: the caller's external indices then refer to coordinates r′ that the constructor chose, not the ones they wrote down. For every system left in its own coordinates this is a no-op, so no existing model can trip it.
Getting this wrong is silent — fill_conjugate_monomial! would fill external monomials from the wrong partner — hence an error rather than a warning. Use full_conjugate_permutation to build the vector instead of writing it by hand.
_commit_initial_degree!(session, W, R, mset, sparse_solver, completed_degree::Int64)
Commit degree-one coefficients once external directions are final.
_completed_degrees(session::MORFE.ParametrisationSolver.CheckpointSession)
_completed_degrees(session) -> Vector{Int}
Return the polynomial degrees recorded as completely committed in session.manifest. The returned vector is converted to Int for consistency across TOML implementations.
_completed_indices(job::MORFE.ParametrisationSolver._SolveJob)
_completed_indices(group::Vector{MORFE.ParametrisationSolver._SolveJob})
_completed_indices(job_or_group) -> Vector{Int}
Return the sorted, unique coefficient positions made final by a job or factor group, including reconstructed conjugate secondaries.
_compose_observers(a::MORFE.ParametrisationSolver._NoSolveObserver, b::MORFE.ParametrisationSolver._NoSolveObserver)
_compose_observers(a::MORFE.ParametrisationSolver._NoSolveObserver, b::MORFE.ParametrisationSolver._AbstractSolveObserver)
_compose_observers(a::MORFE.ParametrisationSolver._AbstractSolveObserver, b::MORFE.ParametrisationSolver._NoSolveObserver)
_compose_observers(a::MORFE.ParametrisationSolver._AbstractSolveObserver, b::MORFE.ParametrisationSolver._AbstractSolveObserver)
_compose_observers(a, b) -> _AbstractSolveObserver
Elide no-op observers and otherwise return a composite that preserves notification order.
_digest_text!(ctx, value)
_digest_text!(ctx, value) -> nothing
Append a textual fingerprint token and a zero-byte separator to the SHA context. The separator prevents adjacent values from producing ambiguous concatenations.
_eigenvalue_representatives(lambda_diag)
_eigenvalue_representatives(lambda_diag) -> Vector{Int}
Assign equal nonzero eigenvalues the same representative index and assign zero eigenvalues the sentinel 0. The result defines the exact exponent aggregation used by factor keys.
_embed_external_dynamics!(R::ReducedDynamics{ROM, NVAR, T}, external_polynomial::DensePolynomial{T, N_EXT, 2, A} where A<:AbstractMatrix{T}, mset::MultiindexSet{NVAR}) where {ROM, NVAR, T, N_EXT}
_embed_external_dynamics!(R, external_polynomial, mset)
Copy coefficients from the N_EXT-variable external polynomial into the last N_EXT rows of R, embedding them in the full NVAR = ROM + N_EXT multiindex set. Throw when a non-zero external coefficient has no destination, since silently dropping it would change the external dynamics.
_execute_benchmarked_schedule!(W::Parametrisation{ORD, NVAR, T}, R::ReducedDynamics{ROM, NVAR, T}, ctx::CohomologicalContext{T, ORD, ORDP1, NVAR, FOM, LT, MT}, sym::ConjugateSymmetryData, model::NthOrderModel, ml_cache::MORFE.MultilinearTerms.MultilinearTermsCache; benchmark_dir, show_progress) where {ORD, NVAR, T, ROM, FOM, ORDP1, LT, MT}
_execute_benchmarked_schedule!(W, R, context, symmetry, model, cache;
benchmark_dir, show_progress = true) -> nothing
Run the causal cohomological solve while timing nonlinear right-hand-side assembly and the bordered linear solve separately. On successful completion it overwrites two files:
benchmark_per_monomial.csvcontains order, monomial index and exponents, time and allocations for each measured phase, monomial total, and cumulative measured time.benchmark_per_order.csvaggregates those quantities by polynomial degree and records live GC bytes when each order is flushed.
Rows are buffered in memory and written only after the solve completes. The measured time does not include lower-order coupling assembly, coefficient unpacking, higher-derivative updates, progress output, or CSV writing. This diagnostic loop supports conjugate filling but deliberately does not use structural factor grouping or checkpoint callbacks.
benchmark_dir is required and is created when necessary. show_progress follows the ordinary schedule's TTY behaviour.
_execute_job!(instrumentation, observer, W, R, job, degree, ctx, sym, model, ml_cache, reuse_factor::Val)
_execute_job!(instrumentation, observer, W, R, job, degree, ctx, sym, model, ml_cache, reuse_factor::Val, superharmonic)
_execute_job!(instrumentation, observer, W, R, job, degree, ctx, sym, model,
ml_cache, reuse_factor, superharmonic = nothing) -> nothing
Run one canonical monomial pipeline, reconstruct its optional conjugate, and notify the observer only after both coefficient positions are final.
_execute_problem_schedule!(W, R, ctx, symmetry, model, cache, mset, sparse_solver, checkpoint_session, benchmark_dir, options)
Execute benchmark or ordinary scheduling with the appropriate concrete observer.
_execute_solve_plan!(plan::MORFE.ParametrisationSolver._DirectSolvePlan, instrumentation, observer, W, R, ctx, sym, model, ml_cache)
_execute_solve_plan!(plan::MORFE.ParametrisationSolver._GroupedSolvePlan, instrumentation, observer, W, R, ctx, sym, model, ml_cache)
_execute_solve_plan!(plan, instrumentation, observer, W, R, ctx, sym, model,
ml_cache) -> nothing
Execute a direct or grouped plan in causal degree order. The grouped overload factorises the first job in each group and passes Val(true) only to later jobs with a provably identical bordered matrix.
_execute_solve_schedule!(W::Parametrisation{ORD, NVAR, T}, R::ReducedDynamics{ROM, NVAR, T}, ctx, sym, model, ml_cache; show_progress, grouping, observer, instrumentation) where {ORD, NVAR, T, ROM}
_execute_solve_schedule!(W, R, context, symmetry, model, cache; ...) -> nothing
Build the solve plan, compose progress with the requested observer, and execute the plan. The concrete coefficient-container signature forms the inference boundary directly.
_file_sha256(path)
_file_sha256(path) -> String
Return the lowercase SHA-256 digest of a checkpoint chunk without loading the complete file into memory.
_fill_job_conjugate!(W, R, job, ::ConjugateSymmetryData{NoConjugatePermutation})
_fill_job_conjugate!(W, R, job, sym::ConjugateSymmetryData{<:StaticArraysCore.SVector})
_fill_job_conjugate!(W, R, job, sym) -> nothing
Reconstruct a job's conjugate secondary after its primary coefficients are final. This is a compile-time no-op when conjugate symmetry is disabled.
_fingerprint_value!(ctx, value)
_fingerprint_value!(ctx, value) -> nothing
Recursively append a deterministic, type-aware representation of value to a SHA context. Arrays include their shape, dictionaries are traversed in sorted-key order, and opaque callables must provide checkpoint_fingerprint_data.
_finish_observer!(::MORFE.ParametrisationSolver._NoSolveObserver)
_finish_observer!(observer::MORFE.ParametrisationSolver._ProgressSolveObserver)
_finish_observer!(observer::MORFE.ParametrisationSolver._CompositeSolveObserver)
_finish_observer!(::MORFE.ParametrisationSolver._CheckpointSolveObserver)
_finish_observer!(observer::MORFE.ParametrisationSolver._BenchmarkSolveObserver)
Finish a solve observer after the complete plan has executed successfully.
_flush_order_row!(order_io::IO, order::Int64, a::MORFE.ParametrisationSolver._OrderAccum, cumul_time::Float64)
Append one completed degree's aggregate benchmark row to order_io.
_group_solve_jobs(ctx, mset, jobs::Vector{MORFE.ParametrisationSolver._SolveJob}, ::Val{ROM}) where ROM
_group_solve_jobs(ctx, mset, jobs, Val(ROM)) -> Vector{_SolveGroup}
Partition causal jobs into degree-local groups with equal StructuralFactorKey. Group order follows the first occurrence of each key, and job order is preserved within a group.
_has_structural_factor_reuse(lambda_diag)
_has_structural_factor_reuse(lambda_diag) -> Bool
Return whether zero or repeated eigenvalues make two distinct monomials capable of sharing an identical superharmonic expression and numeric factorisation.
_initialise_waveform!(W::Parametrisation, R::ReducedDynamics, master_right_modes, master_eigenvalues, master_right_mode_derivatives, unit_offset::Int64, model)
_initialise_waveform!(W, R, master_right_modes, master_eigenvalues,
master_right_mode_derivatives, unit_offset, model)
Initialise the linear-monomial coefficients of W and R from spectral data:
W[:, 1, eᵣ] = master_right_modes[:, r]and, forORD > 1,W[:, k, eᵣ] = master_right_mode_derivatives[:, k-1, r].R[r, eᵣ] = master_eigenvalues[r].
Also embeds the external-system linear dynamics into the external rows of R via _embed_external_dynamics! when model.external_system !== nothing.
_job_target(::ConjugateSymmetryData{NoConjugatePermutation}, ::Int64)
_job_target(sym::ConjugateSymmetryData{<:StaticArraysCore.SVector}, index::Int64)
_job_target(sym, index) -> Int
Return the larger conjugate-secondary position reconstructed after solving index, or 0 when symmetry is inactive, the monomial is self-conjugate, or no partner is present.
_linear_monomial_indices(mset::MultiindexSet{NVAR}) where NVAR
_linear_monomial_indices(mset) -> Vector{Int}
Return the positions of every unit-vector monomial in mset. These are the linear coefficients initialised from eigenvectors or external directions rather than by the main nonlinear solve.
_lock_path(root)
Return the single-writer lock-directory path for a checkpoint.
_make_progress(n_total::Int64, show_progress::Bool, max_nl_degree::Int64)
_make_progress(n_total, show_progress, max_nl_degree) -> _SimpleProgress
Construct a _SimpleProgress tracker. max_nl_degree controls the work-weighted percentage. Output is disabled automatically when stderr is not a TTY.
_make_sparse_solver(::Type{<:SparseArrays.SparseMatrixCSC}, linear_terms, FOM::Int64, ROM::Int64)
_make_sparse_solver(::Type{<:AbstractMatrix}, ::Any, ::Int64, ::Int64)
_make_sparse_solver(::Type{<:SparseArrays.SparseMatrixCSC}, linear_terms, FOM::Int64, ROM::Int64, options::ParametrisationOptions)
_make_sparse_solver(::Type{<:AbstractMatrix}, ::Any, ::Int64, ::Int64, options::ParametrisationOptions)
_make_sparse_solver(MT, linear_terms, FOM, ROM,
options = ParametrisationOptions()) -> Union{SparseLinearSolverState, Nothing}
Dispatch helper: returns a SparseLinearSolverState when MT <: SparseMatrixCSC (sparse path), or nothing for all other matrix types (dense path). An explicit sparse backend request is rejected for dense matrices; sparse backend selection follows options.
_manifest_path(root)
Return the manifest path inside a checkpoint directory.
_mark_degree_complete!(session::MORFE.ParametrisationSolver.CheckpointSession, degree::Int64)
_mark_degree_complete!(session, degree) -> nothing
Commit a degree-completion marker under the writer lock without writing another coefficient chunk. Used after all factor-group chunks for the degree are durable.
_normalise_manifest_lists!(manifest)
_normalise_manifest_lists!(manifest)
TOML.jl types an empty array from a parsed file as Vector{Union{}} on Julia 1.10 (older Base TOML.jl), whereas newer TOML.jl releases (Julia 1.12+) type it as Vector{Any}. A Vector{Union{}} can never accept a push!, since Union{} has no instances, so an empty chunks or completed_degrees array parsed back from disk would make the very first append fail depending solely on the Julia version in use. Call this right after every TOML.parsefile to make the manifest's list fields concrete and writable regardless of which TOML.jl version produced them.
_on_degree_complete!(::MORFE.ParametrisationSolver._NoSolveObserver, args...)
_on_degree_complete!(::MORFE.ParametrisationSolver._ProgressSolveObserver, args...)
_on_degree_complete!(observer::MORFE.ParametrisationSolver._CompositeSolveObserver, args...)
_on_degree_complete!(observer::MORFE.ParametrisationSolver._CheckpointSolveObserver, degree)
_on_degree_complete!(observer::MORFE.ParametrisationSolver._BenchmarkSolveObserver, degree)
Notify a solve observer after every job in one polynomial degree is complete.
_on_group_complete!(::MORFE.ParametrisationSolver._NoSolveObserver, args...)
_on_group_complete!(::MORFE.ParametrisationSolver._ProgressSolveObserver, args...)
_on_group_complete!(observer::MORFE.ParametrisationSolver._CompositeSolveObserver, args...)
_on_group_complete!(observer::MORFE.ParametrisationSolver._CheckpointSolveObserver, degree, group)
_on_group_complete!(::MORFE.ParametrisationSolver._BenchmarkSolveObserver, args...)
Notify a solve observer after one factor group has been fully committed in memory.
_on_job_complete!(::MORFE.ParametrisationSolver._NoSolveObserver, args...)
_on_job_complete!(observer::MORFE.ParametrisationSolver._ProgressSolveObserver, job, degree, metrics)
_on_job_complete!(observer::MORFE.ParametrisationSolver._CompositeSolveObserver, args...)
_on_job_complete!(::MORFE.ParametrisationSolver._CheckpointSolveObserver, args...)
_on_job_complete!(observer::MORFE.ParametrisationSolver._BenchmarkSolveObserver, job, degree, metrics)
_on_job_complete!(observer, job, degree, metrics)
_on_group_complete!(observer, degree, group)
_on_degree_complete!(observer, degree)
_finish_observer!(observer)
Lifecycle hooks implemented by solve observers. Every execution path emits job events in causal order, one group event after its coefficients (including conjugate secondaries) are final, degree events only after all groups in that degree, and one final event.
_open_checkpoint(options::CheckpointOptions, fingerprint, metadata)
_open_checkpoint(options, fingerprint, metadata) -> CheckpointSession
Create the directory-format checkpoint or validate an existing resumable manifest against its schema, problem identifier, fingerprint, and byte order. Existing checkpoints are never overwritten when resume == false.
_open_problem_checkpoint(::Nothing, args...)
_open_problem_checkpoint(checkpoint::CheckpointOptions, model, spectral, resonance_set, mset, conjugate_permutation, options::ParametrisationOptions, dimensions::NTuple{5, Int64}, ::Type{T}) where T
_open_problem_checkpoint(checkpoint, model, spectral, resonance_set, mset,
conjugate_permutation, options, dimensions, T)
Return nothing when checkpointing is disabled. Otherwise calculate the problem fingerprint and open or validate its checkpoint before solver and coefficient storage are allocated. dimensions is (ORD, FOM, ROM, N_EXT, NVAR).
_prepare_external_directions!(W, R, supplied::Bool, completed_indices, model, cache, symmetry, dimensions::NTuple{5, Int64}, unit_offset::Int64, lambda_matrix, lambda_diag, linear_terms, master_right_modes, invariance_C_coeffs, master_derivative_steps, orthogonality_J_coeffs, right_master_blocks, resonance_set, linear_skip_set, lower_order, buffers, sparse_solver)
_prepare_external_directions!(W, R, supplied, completed_indices, ...)
Solve fresh external linear directions in causal variable order, or accept directions from supplied/restored storage. Every external linear monomial is marked skipped before this function returns.
_prepare_master_operators(linear_terms, W, R, master_right_modes, master_left_modes, master_left_mode_blocks, unit_offset::Int64, ROM::Int64)
_prepare_master_operators(linear_terms, W, R, master modes and blocks,
unit_offset, ROM)
Precompute the external-independent orthogonality and invariance operators. Return the orthogonality row coefficients, the master order-block view, the invariance columns, and the master derivative steps.
_prepare_problem_inputs(model::NthOrderModel{ORD, ORDP1, N_NL, N_EXT, LT, MT}, mset::MultiindexSet{NVAR}, spectral::SpectralData{ORD, ROM}, conjugate_permutation, options::ParametrisationOptions) where {ORD, ORDP1, N_NL, N_EXT, LT, MT, NVAR, ROM}
_prepare_problem_inputs(model, mset, spectral, conjugate_permutation, options)
Resolve concrete spectral arrays, validate dimensions and conjugate structure, and return the data required by storage and operator preparation. This is the single setup boundary for SpectralData fields whose higher-order blocks may be nothing.
_prepare_solution_storage(initial_solution, mset, model, master_right_modes, master_eigenvalues, master_right_mode_derivatives, dimensions::NTuple{5, Int64}, unit_offset::Int64, ::Type{T}) where T
_prepare_solution_storage(initial_solution, mset, model, master_right_modes,
master_eigenvalues, master_right_mode_derivatives, dimensions, unit_offset, T)
Return (W, R, supplied). Supplied objects are returned unchanged and by identity. When no storage is supplied, allocate zeroed coefficient arrays and initialise their known linear coefficients from the spectral and external-system data.
_problem_fingerprint(model, spectral, resonance_set, mset, conj_perm, options::ParametrisationOptions)
_problem_fingerprint(model, spectral, resonance_set, mset, conjugate_permutation,
options) -> String
Return the SHA-256 identity of all mathematical inputs and numerical solver policy that affect checkpoint compatibility. Presentation and destination options are intentionally excluded. Opaque application callables must implement checkpoint_fingerprint_data.
_progress_done!(progress::MORFE.ParametrisationSolver._SimpleProgress, completed::Int64)
_progress_done!(progress, completed)
Print the final completion line to stderr and clear trailing characters from the last progress update. This is a no-op when progress is disabled.
_progress_tick!(progress::MORFE.ParametrisationSolver._SimpleProgress, completed::Int64, degree::Int64)
_progress_tick!(progress, completed, degree)
Print an in-place -overwritten progress line to stderr showing the current polynomial degree and the fraction of monomials solved. This is a no-op when progress is disabled.
_replace_options(options::ParametrisationOptions; backend, grouping, residual_check, residual_tolerance, max_refinement_steps, validate_mset, checkpoint, show_progress, verbose, setup_io)
_replace_options(options; overrides...) -> ParametrisationOptions
Return a validated copy of options, replacing only the supplied keyword fields. This is the internal equivalent of an immutable record update and preserves the concrete setup_io type when no replacement is requested.
_reset!(a::MORFE.ParametrisationSolver._OrderAccum)
Reset every per-degree benchmark accumulator field in place.
_restore_checkpoint!(session::MORFE.ParametrisationSolver.CheckpointSession, W, R; verify_existing)
_restore_checkpoint!(session, W, R; verify_existing = false) -> BitSet
Verify every recorded chunk's presence, checksum, header, shape, and payload boundary, then restore its coefficient slices into W and R. With verify_existing, reject any restored slice that disagrees with the caller-provided initial solution. Return restored indices.
_restore_solution_checkpoint!(::Nothing, W, R, supplied::Bool)
_restore_solution_checkpoint!(session::MORFE.ParametrisationSolver.CheckpointSession, W, R, supplied::Bool)
_restore_solution_checkpoint!(session, W, R, supplied)
Restore committed coefficient slices into the selected storage and return (completed_indices, completed_degree). When supplied is true, every committed slice must already agree with the caller's storage. With no checkpoint, return (nothing, 0).
_solve_external_directions!(W, R, partial_ctx_for, model, ml_cache, sym::ConjugateSymmetryData{NoConjugatePermutation}, N_EXT::Int64, ROM::Int64, unit_offset::Int64)
_solve_external_directions!(W, R, partial_ctx_for, model, ml_cache, sym::ConjugateSymmetryData{<:StaticArraysCore.SVector}, N_EXT::Int64, ROM::Int64, unit_offset::Int64)
_solve_external_directions!(W, R, partial_ctx_for, model, ml_cache,
symmetry, N_EXT, ROM, unit_offset)
Solve external forcing directions in increasing variable order. Partial contexts contain only directions already solved, as required by upper-triangular external dynamics. Conjugate secondaries are reconstructed immediately, and every external linear monomial is marked skipped before the nonlinear schedule is built.
_spectral_conjugate_permutation(request, spectral::SpectralData, sys)
_spectral_conjugate_permutation(request, spectral, external_system)
Resolve :from_spectral by taking the master permutation stored in spectral and, when present, appending the conjugate block derived from external_system. Explicit vectors and nothing pass through unchanged.
_structural_factor_key(multi::StaticArraysCore.SVector{NVAR, Int64}, resonance::StaticArraysCore.SVector{ROM, Bool}, representatives) where {NVAR, ROM}
_structural_factor_key(multi, resonance, representatives) -> StructuralFactorKey
Build the exact factor-reuse key from aggregated monomial powers and the full resonance mask. Equality of these keys implies equality of the bordered matrix, not merely closeness of the floating-point superharmonics.
_with_checkpoint_lock(f, root)
_with_checkpoint_lock(f, root)
Execute f() while owning the checkpoint directory's single-writer lock. A pre-existing lock is treated as an active writer and raises instead of risking concurrent manifest updates; the lock is released in a finally block.
_write_benchmark_csvs(mono_io::IOBuffer, order_io::IOBuffer, benchmark_dir::AbstractString)
_write_benchmark_csvs(monomial_io, order_io, benchmark_dir) -> nothing
Create benchmark_dir and write both completed in-memory CSV buffers to their stable filenames.
_write_benchmark_headers(mono_io::IO, order_io::IO)
Write the fixed per-monomial and per-degree benchmark CSV headers.
_write_chunk!(session::MORFE.ParametrisationSolver.CheckpointSession, W, R, degree::Int64, indices, sparse_solver; degree_complete)
_write_chunk!(session, W, R, degree, indices, sparse_solver;
degree_complete) -> nothing
Atomically write a checksummed coefficient slice, then commit its manifest entry under the writer lock. When degree_complete is true, mark the degree complete in the same manifest update. Sparse solver diagnostics are included when available.
checkpoint_fingerprint_data(::Any)
checkpoint_fingerprint_data(callable)
Return immutable, deterministic data identifying a nonlinear callable used by a checkpointed model. Extend this method for application-defined kernels. The default returns nothing, causing checkpoint creation to reject the opaque callable rather than treating a caller label or a source location as mathematical identity.
fill_conjugate_monomial!(W::Parametrisation{ORD, NVAR, T}, R::ReducedDynamics{ROM, NVAR, T}, conj_idx::Int64, source_idx::Int64, sym::ConjugateSymmetryData{StaticArraysCore.SVector{NVAR, Int64}}) where {ORD, NVAR, T, ROM}
fill_conjugate_monomial!(W, R, conj_idx, source_idx, sym)
Fill the conjugate monomial at conj_idx from the already-solved source_idx using the W- and f-symmetry relations for a real-valued FOM:
W_{P·γ} = conj(W_γ)
f_{P·β}[r] = conj(f_β[perm[r]]) (master-mode rows only)
External rows of R at conj_idx are left untouched; they are already correct either because they are zero (mixed monomials) or because _embed_external_dynamics! has set them from the conjugate-symmetric external dynamics polynomial.
Precondition: all ORD time-derivative orders of W and master-mode rows (1:ROM) of R at source_idx must be finalised before this is called.
solve_parametrisation(model::NthOrderModel{ORD, ORDP1, N_NL, N_EXT, LT, MT}, mset::MultiindexSet{NVAR}, spectral::SpectralData{ORD, ROM}, resonance_set::ResonanceSet; initial_solution, conjugate_permutation, benchmark_dir, options) where {ORD, ORDP1, N_NL, N_EXT, LT, MT, NVAR, ROM}
solve_parametrisation(
model, mset, spectral::SpectralData, resonance_set;
initial_solution = nothing,
conjugate_permutation = :from_spectral,
benchmark_dir = nothing,
options = ParametrisationOptions()
) -> (W, R)
Prepare all workflow data, solve the requested parametrisation coefficients, and return the parametrisation and reduced dynamics.
The spectral input is one object. It replaces five separately maintained inputs: master eigenvalues, master right modes, master left modes, right-mode derivative blocks, and left-mode order blocks. Every former call site had to slice and keep those arrays mutually consistent — including the mirrored right/left block convention, where a swap is type-correct and compiles silently. SpectralData's explicitly named accessors own that convention now, and it is checked numerically by check_biorthogonality.
The external dynamics enter through model.external_system.first_order_dynamics, which _embed_external_dynamics! copies into the external rows of R; the superharmonics s are then contracted against diag(Λ) read back from R, so the external part of s is the diagonal of the external linear matrix. That matrix must be upper triangular — the ExternalSystem constructors guarantee it, re-basing the external coordinates when the supplied matrix is not, as explained in the ExternalSystems module docstring. When that happens the reduced external coordinates returned here are the re-based r′, related to the physical r by r = Q r′ with Q = external_basis(model.external_system). The linear-operator tuple is read from model.linear_terms.
Steps
Select the coefficient storage: allocate and initialise
WandR, or use the exact objects supplied throughinitial_solution.Restore checkpoint-committed coefficient slices into that storage. A supplied solution must agree with every committed slice; non-zero coefficients alone never count as completed work.
Build symmetry and shared resources after restored indices have entered the skip mask.
Solve the linear cohomological equations for each fresh external forcing direction via a partial context in which the not-yet-solved external columns of the generalised right-eigenmode matrix are set to zero. Supplied or restored external directions are accepted rather than recomputed.
Build the full
generalised_right_eigenmodesmatrix by concatenating the master right eigenmodes with the solved external right directions. This matrix is stored asCohomologicalContext.generalised_eigenmodes; left eigenmodes enter only through the orthogonality operators.Assemble the full context and solve every monomial not marked linear, conjugate-secondary, or checkpoint-complete.
Arguments
model :: NthOrderModel— full-order model;model.linear_termsprovides(B₀,…,B_ORD).mset :: MultiindexSet{NVAR}— multiindex set over allNVAR = ROM + N_EXTvariables.spectral :: SpectralData{ORD, ROM}— master eigenvalues, the right physical modes and their derivative blocks, the left physical modes and their orthogonality blocks, and the conjugate involution. Build it withSpectralData(model, spectrum; master = …). The orthogonality row operators are read directly off the left blocks — no eigenvalue folding.resonance_set :: ResonanceSet.initial_solution— optionally supply an already-initialised(W, R)tuple. The solver mutates and returns those same objects, trusts their master and external linear data, and recomputes scheduled nonlinear coefficients. This is storage reuse, not implicit checkpoint completion or an iterative warm start.conjugate_permutation—:from_spectral(default) takes the bundle's master-block involution and extends it over the external variables using the model's external system. Pass anNVAR-length vector to override it —perm[i] = jmeans modejis the complex conjugate of modei— ornothingto disable conjugate symmetry for this solve. A supplied permutation is the caller's assertion about the eigenvectors: two eigenvalues forming a conjugate pair is necessary but not sufficient (the eigenspace must be one-dimensional, or the eigenvectors chosen conjugately).SpectralData's:detectverifies exactly that before returning one.options— execution, validation, residual-verification, and checkpoint policy in aParametrisationOptions. Mathematical choices remain separate arguments.benchmark_dir—nothingfor the normal grouped/checkpoint-capable solve, or a directory in which the benchmark observer writes timing CSV files. Benchmark mode uses the shared direct execution plan and therefore does not perform factor grouping. It cannot be combined with checkpointing because benchmark timings do not form resumable checkpoint commits.
Returns
(W, R) — the solved Parametrisation and ReducedDynamics.
Module ParametrisationMethod — the user-facing entry point to the DPIM parametrisation.
Owns parametrise, the public entry point that turns a full-order model plus spectral data into a solved invariant manifold (W, R). It coordinates the following separate functions, so a new policy is a new method rather than a new branch:
| Step | Function | Dispatches on |
|---|---|---|
| build the monomial set | build_multiindex_set | the expansion order |
| build the resonance set | build_resonance_set | the resonance style |
| solve | solve_parametrisation | dense vs sparse model |
The coefficient containers themselves (Parametrisation, ReducedDynamics, …) live in ParametrisationObjects and are re-exported here, so this module remains the single namespace users need.
Load order
This module is included after ParametrisationSolver, whose documented workflow owns solve_parametrisation. The mathematical CohomologicalEquations module and the numerical BorderedLinearSolvers module therefore remain independent of this user-facing entry point. Checkpointing, symmetry, scheduling, progress, and benchmarking do not live in this module.
build_multiindex_set(mset::MultiindexSet, ::Int64)
build_multiindex_set(expansion_order::Integer, nvar::Int64)
build_multiindex_set(x, ::Int64)
build_multiindex_set(expansion_order, nvar) -> MultiindexSet
Turn an expansion order into the monomial set the solve will run over. This is the dispatch seam for expansion policies: parametrise leaves its third argument untyped and delegates here, so a new policy is one new method and parametrise never changes.
Two policies ship today:
expansion_order::Integer— total-degree truncation,all_multiindices_up_to(nvar, order; min_degree = 1).expansion_order::MultiindexSet— a set the caller built (e.g. the anisotropic z-total × θ-box sets used by parametric ROMs), used exactly as given.
Anything else raises an ArgumentError naming what is accepted, rather than a bare MethodError.
This function builds only; it does not validate. parametrise runs validate_multiindex_set once on the result, whatever its source, then tells the solve to skip its own check so the set is walked exactly once. Validation needs ROM and the conjugate permutation, neither of which an expansion policy has any business knowing.
parametrise(model::NthOrderModel{ORD, ORDP1, N_NL, N_EXT, LT, MT}, spectral::SpectralData{ORD, ROM}, expansion_order; resonance, conjugate_permutation, options) where {ORD, ORDP1, N_NL, N_EXT, LT, MT, ROM}
parametrise(model, spectral::SpectralData, expansion_order;
resonance = ResonanceConfig(),
conjugate_permutation = :from_spectral,
options = ParametrisationOptions()) -> (W, R)
Compute the invariant manifold and its reduced dynamics. This is the entry point.
Two positional arguments carry everything the reduction needs — the full-order model and the spectral data — and the third says how far to expand:
(; model, spectral) = build_model(case) # or SpectralData(model, spectrum; master = …)
W, R = parametrise(model, spectral, 5) # total degree ≤ 5
W, R = parametrise(model, spectral, mset) # a monomial set you built
Arguments
model::NthOrderModel— full-order model; supplies the linear operators, the nonlinear terms, and the external system.spectral::SpectralData— master eigenvalues and their right/left blocks, the outer eigenvalues resonance detection reads, and the master-block conjugate involution. Build it withSpectralData(model, spectrum; master = …).expansion_order— anInteger(total-degree truncation) or aMultiindexSetused as given. Dispatch happens inbuild_multiindex_set, so a new expansion policy is a new method there and never a change here.
Keyword arguments
resonance::Union{ResonanceConfig, ResonanceSet} = ResonanceConfig()— either the policy (ResonanceConfiggathers style, tolerances and the outer-target settings) or aResonanceSetyou built yourself, used verbatim.conjugate_permutation = :from_spectral— by default the master-block involution fromspectral, extended over the external variables using the model's external system. Pass an explicitNVAR-length vector to override, ornothingto disable conjugate symmetry for this solve.options::ParametrisationOptions = ParametrisationOptions()— backend selection, exact structural grouping, backward-error verification, multiindex validation, durable checkpointing, and presentation controls. Mathematical choices are not hidden in this object. SeeParametrisationOptionsfor every accepted field, value, default, and its effect.
The operational fields are supplied when constructing ParametrisationOptions; they are not direct keywords of parametrise. For example:
options = ParametrisationOptions(
backend = :auto,
grouping = :auto,
residual_check = :backward_error,
residual_tolerance = 1e-10,
show_progress = false,
verbose = false)
W, R = parametrise(model, spectral, 7;
resonance = ResonanceConfig(style = :complex_normal_form, tol = 0.05),
options = options)
Thus use parametrise(...; options = ParametrisationOptions(backend = :umfpack)), not parametrise(...; backend = :umfpack). The available explicit sparse backends are KLU, UMFPACK, and Pardiso; see ParametrisationOptions for availability and automatic selection rules.
Returns
(W, R) — the solved Parametrisation and ReducedDynamics.
print_setup(io::IO, model::NthOrderModel{ORD, ORDP1, N_NL, N_EXT, T, MT}, spectral::SpectralData{ORD, ROM}, mset::MultiindexSet{NVAR}, resonance) where {ORD, ORDP1, N_NL, N_EXT, T, MT, NVAR, ROM}
print_setup(io, model, spectral, mset, resonance)
Print a summary of what parametrise is about to solve: model size and layout, nonlinear terms, external system, reduced dimensions, master eigenvalues, the monomial count, the resonance policy and the conjugate involution.
A normal function, so a caller who wants a different banner defines a method rather than editing parametrise. It prints only what the arguments genuinely carry: parametrise receives an NthOrderModel, and every backend's model is that same type, so there is nothing backend-specific to dispatch on here. Backends with richer information print their own summary before calling — they already have show methods for it.
Called by parametrise only when the output is going somewhere interactive; see the verbose / setup_io keywords there.
make_bk_problem
make_bk_problem(R::ReducedDynamics; bifparam_index=1, z0=nothing, p0=nothing)
Wrap the reduced dynamics R as a BifurcationKit.BifurcationProblem for continuation and bifurcation analysis.
The reduced dynamics ż = R(z, p) is split into master-mode coordinates z (first ROM variables) and external parameters p (the remaining external_system_size variables). bifparam_index selects which component of p is the continuation parameter.
Requires BifurcationKit.jl. Load it with using BifurcationKit to activate the MORFE extension.
Module InvarianceError — a posteriori validation of the computed invariant manifold.
After solve_parametrisation produces (W, R), this module measures how well the parametrisation satisfies the invariance equation
∂W/∂z · R(z) = F(W(z))
by evaluating both sides on random points z on the manifold and computing the relative residual. Results are plotted as convergence curves against the polynomial order and returned as structured data for further analysis.
InvarianceErrorWorkspace(model, W[, T = ComplexF64])
Reusable storage for invariance_error_residual!. Construct one workspace and reuse it when evaluating many points, for example along a phase orbit. The workspace contains only numerical scratch arrays; it does not retain a reduced coordinate or an external-parameter value.
_jvp_last_block!(result::AbstractVector, W_poly::DensePolynomial{T, NVAR, N, A} where {N, A<:AbstractArray{T, N}}, z::AbstractVector, v::AbstractVector, pw_buf::AbstractMatrix) where {T, NVAR}
_jvp_last_block!(result, W_poly, z, v, pw_buf)
Accumulate J_{W[:,ORD,:]}(z) · v into result using the analytic Jacobian of the last stored derivative block. pw_buf is a pre-allocated (NVAR, max_exp+1) matrix reused across calls; on entry it is filled with pw_buf[j, e+1] = z[j]^e.
_log_log_regression(radii, errors; min_points)
_log_log_regression(radii, errors; min_points=5) → (slope, intercept)
OLS fit of log(error) ~ slope·log(radius) + intercept. Points are sorted by radius and removed one at a time from the left (smallest radius first) as long as doing so increases the slope. This adaptively discards points saturated near machine precision without a hard threshold.
Running Σ-updates make each step O(1); total cost O(N). Returns (NaN, NaN) when fewer than min_points points remain.
invariance_error_convergence(model::NthOrderModel{ORD, ORDP1, N_NL, N_EXT, T} where T, W::Parametrisation{ORD, NVAR}, R::ReducedDynamics; n_samples, r_magnitudes, r_external, master_amplitude, rng) where {ORD, ORDP1, N_NL, N_EXT, NVAR}
invariance_error_convergence(model, W, R;
n_samples = 1000,
r_magnitudes = [0.0],
r_external = nothing,
master_amplitude = 1.0,
rng = Random.default_rng())
→ Vector of NamedTuples, one per entry in `r_magnitudes`
For each forcing magnitude |r| in r_magnitudes, draw n_samples points with master-mode coordinates from N(0,1) and external coordinates sampled uniformly on a sphere of radius |r|. Each result NamedTuple contains:
radii—‖z_k‖for each sampleradii_master—‖(z_k)_master‖for each sampleforce_errors—‖E(z_k)‖₂(force residual)state_errors—‖L(s̄)⁻¹ E(z_k)‖₂(state error estimate)s_bar— median local superharmonics̄ = median(⟨z,R(z)⟩/⟨z,z⟩)max_order— total degree of the highest monomial in Wr_magnitude— the|r|value for this levelconvergence_rate— OLS log-log slope offorce_errorsvsradii_master
(saturated points auto-trimmed)
Use plot_invariance_convergence to visualise the result.
To validate a physical parameter point, pass its exact reduced external coordinate as r_external. In this mode the external tail is fixed for the entire point cloud and master_amplitude scales only the randomly sampled master coordinates. The manifold, reduced dynamics, and full-order model are all evaluated at that same external point. r_magnitudes must remain [0.0] in fixed-target mode.
invariance_error_norms(model::NthOrderModel{ORD, ORDP1, N_NL, N_EXT, T} where T, W::Parametrisation{ORD, NVAR}, R::ReducedDynamics; n_samples, amplitude, r_external, rng) where {ORD, ORDP1, N_NL, N_EXT, NVAR}
invariance_error_norms(model, W, R;
n_samples = 1000,
amplitude = 1.0,
r_external = nothing,
rng = Random.default_rng())
→ NamedTuple{(:max, :mean, :rms, :pointwise)}
Evaluate the invariance-equation residual ‖E(z)‖₂ over a Gaussian point cloud in reduced coordinates.
amplitude is the standard deviation of each complex master-mode component (real and imaginary parts drawn i.i.d. from N(0, amplitude²/2)). External coordinates are fixed to zero unless r_external is provided.
invariance_error_residual!(E::AbstractVector{T}, workspace::InvarianceErrorWorkspace{T, VT, MT} where {VT<:AbstractVector{T}, MT<:AbstractMatrix{T}}, model::NthOrderModel{ORD, ORDP1, N_NL, N_EXT, T} where T, W::Parametrisation{ORD, NVAR}, R::ReducedDynamics, z::AbstractVector; r_external) where {T, ORD, ORDP1, N_NL, N_EXT, NVAR}
invariance_error_residual!(E, workspace, model, W, R, z;
r_external = nothing)
Evaluate the vector invariance defect at the reduced coordinate z, without allocating the large full-order scratch arrays on each call. If r_external is supplied, it must equal the external-coordinate tail of z; this explicit check prevents evaluating the manifold and the full-order model at different parameter values.
The returned value is E. Positive or negative signs of the defect are an internal convention; its norm and modal projections are invariant to that convention.
plot_invariance_convergence(args...; kwargs...)
plot_invariance_convergence(results; kwargs...)
Requires Plots.jl. Load it with using Plots to activate the MORFE extension.
Module RomIO — backend-agnostic persistence for computed ROMs.
save_rom writes the standard result layout every MORFE workflow shares:
dir/
data/
W.jls — serialised `Parametrisation`
R.jls — serialised `ReducedDynamics`
R_coefficients.csv — complex reduced dynamics, one monomial per row
figures/ — created empty, for downstream plots
summary.txt — caller metadata + julia version, git commit, timestamp
The CSV schema is exp_1,…,exp_NVAR,R1_re,R1_im,… with rows whose coefficients are all below drop_below omitted. read_rom_coefficients parses that schema back; it is the entry point every validation script uses.
normal_form_branch is the other direction: it turns a solved ReducedDynamics into the periodic-orbit data a plot needs, without depending on any plotting package.
_real_roots(p::Vector{Float64})
_real_roots(p) -> Vector{Float64}
Real roots of the polynomial whose coefficients p are given in ascending powers, by companion-matrix eigenvalues.
Trailing near-zero coefficients are trimmed first: they would otherwise make the companion matrix singular and manufacture spurious roots at infinity. A root counts as real when its imaginary part is negligible against its own magnitude, since an exactly real root of a floating-point companion matrix generally comes back with a tiny imaginary part.
cycle_amplitude(P::DensePolynomial{T, NVAR, 1, A} where A<:AbstractVector{T}, ρ::Real; ...) where {T, NVAR}
cycle_amplitude(P::DensePolynomial{T, NVAR, 1, A} where A<:AbstractVector{T}, ρ::Real, external; n_phase) where {T, NVAR}
cycle_amplitude(P, ρ, external = (); n_phase = 4096) -> Float64
Amplitude of the scalar observable P on the periodic orbit of radius ρ: half the peak-to-peak excursion of
real P(ρ·exp(iφ), ρ·exp(-iφ), external…)
over one phase cycle, sampled at n_phase points.
P is a scalar polynomial in the reduced coordinates, as returned by observable_polynomial. external supplies the external coordinates, and must have NVAR - 2 entries; an autonomous ROM needs none.
Why half peak-to-peak, and why over phase
The orbit is z₁ = ρ·exp(iφ) traversed once, so a sweep over φ is a sweep over the period, and this is the physical peak amplitude of the oscillation. Taking the excursion rather than the value at one phase matters as soon as the manifold is curved: harmonics beyond the first make the orbit asymmetric, and a single-phase reading picks an arbitrary point on it.
Because it is an extremum over a full cycle, the result is invariant to the eigenvector gauge. Rescaling the master eigenvector by exp(iψ) multiplies the coefficient of z₁^a z̄₁^b by exp(i(a-b)ψ), which is a rigid shift φ → φ + ψ of the sampled signal and leaves its extrema alone. Raw W coefficients are not comparable between runs when the eigensolver picks a different phase; this quantity is, which makes it the right thing to bless in a reference.
n_phase defaults to the resolution the MORFE website's backbone figures use. The signal is band-limited by the expansion order, so far fewer samples serve for plotting: the error in an extremum falls as n_phase^-2.
See also observable_polynomial, normal_form_branch.
normal_form_branch(R::ReducedDynamics{ROM, NVAR}; parameter, amplitudes, parameter_range, external_point, sheet, jump) where {ROM, NVAR}
normal_form_branch(R; parameter = nothing,
amplitudes = range(0, 3; length = 600),
parameter_range = (-Inf, Inf),
external_point = nothing,
sheet = :all, jump = 1e-3)
-> (; amplitude, frequency, parameter, stable)
Periodic-orbit data for a reduced dynamics in complex normal form spanned by a single conjugate pair: the limit-cycle branch, or the backbone, as plain vectors ready to plot.
What it computes
With one conjugate pair the polar substitution z₁ = ρ·exp(iθ) removes the phase. Every monomial surviving a complex-normal-form reduction has a - b = 1 in its first two exponents, so z₁^a z̄₁^b η^c contributes ρ^(a+b) exp(iθ) η^c and the first row factors:
R₁(ρ, ρ, η) = ρ · g(ρ, η), g(ρ, η) = Σ c_{abc} ρ^(a+b-1) η^c
which splits the dynamics into an amplitude equation and a phase equation,
ρ̇ = ρ · Re g(ρ, η) Ω = θ̇ = Im g(ρ, η)
Everything returned is read off g. A limit cycle is a root of Re g, its frequency is Im g there, and it is stable where ∂ρ(ρ · Re g) < 0.
Two modes
parameter = nothing— the backbone.frequencyisIm g(ρ)overamplitudes, evaluated atexternal_point;parametercomes back empty. No root-find is involved, and it needs no external coordinate, so an autonomous ROM (a conservative oscillator, say) is covered.parameter = k— the bifurcation diagram in thek-th external coordinate. For each amplitude the corresponding parameter values are solved for, so a fold inρagainst the parameter is traced without any special handling.
Why it solves for the parameter, not the amplitude
At fixed ρ, Re g(ρ, η) is a real polynomial in η_k, so its roots come from one companion-matrix eigenvalue solve. There is no iteration, no initial guess, no continuation and no step-size control, and a fold is simply a monotone η(ρ).
The other orientation is worse than merely inconvenient. Continuing in the parameter makes ρ the unknown, which is the direction whose series diverges over most of a typical sweep; in examples/05_karman_vortex_street that arrangement once drove an order-7 branch to ρ = 234 before it was abandoned for this one.
This function assumes a single conjugate pair in complex normal form
ROM ≠ 2 is rejected, and so is any first-row monomial with a - b ≠ 1. The second check is the important one: such a monomial keeps a residual exp(i(a-b-1)θ), so R₁/z₁ is not a function of ρ alone and the polar reduction above is invalid. Nothing about the resulting numbers would look wrong, which is why this is an error rather than a warning. A :graph style reduction will fail here by design.
One root per amplitude, or all of them
Re g(ρ, ·) has as many roots as its degree in η_k, and a high-order reduction can put more than one of them inside a physically sensible window. They are different sheets: only the one growing out of the bifurcation is the continuation of the branch, and the others are the truncated series crossing zero again somewhere it has stopped converging.
sheet = :all(the default) returns every real root in range, so an amplitude with several roots appears several times. Nothing is decided for you.sheet = :primaryfollows the sheet the sweep starts on: at each amplitude it keeps the root nearest the previous one, seeded fromη = 0, and stops when the nearest root is farther thanjump. That gap means the tracked sheet has ended and the next root belongs to a different one, so continuing would splice two branches into one curve.jumpis in the parameter's own units; the default suits a coordinate of order1e-2.
Marching in ρ is what makes this reliable: the sheets are single-valued in amplitude even where they fold in the parameter, so no fold-handling is needed.
Arguments
amplitudes— theρgrid. Zero is fine and gives the linear behaviour.parameter_range—(lo, hi)bounds; roots outside are dropped, as are complex roots.external_point— values of the other external coordinates, defaulting to zeros. Inparameter = kmode entrykis ignored, being what is solved for.sheet,jump— branch selection, above.
Returns
Four vectors of equal length. In parameter = nothing mode parameter is empty and the other three follow amplitudes; otherwise each entry is one (ρ, η) point of the branch.
Plotting
MORFE draws nothing. Load a Makie backend and plot the vectors:
using CairoMakie
b = normal_form_branch(R; parameter = 1)
lines(b.parameter, b.amplitude; axis = (xlabel = "η", ylabel = "ρ"))
The cohomological solve is graded, so one curve per truncation order comes from truncating R with restrict_ReducedDynamics_to_degree and calling this again; no re-solve is needed.
See also ReducedDynamics, restrict_ReducedDynamics_to_degree.
observable_polynomial(W::Parametrisation, i::Integer)
observable_polynomial(W::Parametrisation, l::AbstractVector)
observable_polynomial(W, i::Int) -> DensePolynomial
observable_polynomial(W, l::AbstractVector) -> DensePolynomial
Project a Parametrisation onto a single scalar observable, giving a polynomial in the reduced coordinates (z₁, z̄₁, η…) alone.
i picks one degree of freedom of the state; l applies a linear functional lᵀ u over all of them, which is how an integrated quantity (a lift coefficient, a reaction force) is obtained. Both read the displacement level of W, coefficients(W)[:, 1, :]; the higher derivative levels describe the same manifold and carry no extra positional information.
The point is size. W is (FOM, ORD, L) and FOM is the full model, so evaluating it once per phase sample over an amplitude sweep is out of the question; the projection is done once, on the coefficients, and every later evaluation costs L terms. The product is bilinear, not sesquilinear: transpose, never adjoint. z̄₁ is already carried by its own exponent, so conjugating here would conjugate it twice.
Truncate the result with restrict_polynomial_to_degree to get one observable per expansion order out of a single solve, exactly as restrict_ReducedDynamics_to_degree does for the dynamics.
u = observable_polynomial(W, dof) # transverse displacement at one node
a = cycle_amplitude.(Ref(u), branch.amplitude) # its amplitude along a branch
See also cycle_amplitude, normal_form_branch.
read_rom_coefficients(csv::AbstractString)
read_rom_coefficients(csv) -> (exponents::Matrix{Int}, coefficients::Matrix{ComplexF64})
Parse an R_coefficients.csv written by write_rom_coefficients_csv. Returns exponents of size (L, NVAR) (one monomial per row) and coefficients of size (L, NR).
save_rom(dir::AbstractString, W::Parametrisation, R::ReducedDynamics; external_system, metadata, drop_below)
save_rom(dir, W, R; external_system = nothing,
metadata = Pair{String, <:Any}[], drop_below = 1e-14)
Write the standard ROM result layout (see module docstring) under dir. metadata pairs are written first into summary.txt, followed by julia_version, the current git commit (when available) and a timestamp.
Pass external_system whenever the model had one. W and R record no external metadata of their own, so without it an archive of a re-based system cannot be mapped back to the physical external coordinates: its external columns and rows are expressed in r′, and only the basis Q recovers r = Q r′. When the system carries a basis it is serialised to data/external_basis.jls and reported in summary.txt; a system that was never re-based writes nothing, since r′ and r coincide.
write_rom_coefficients_csv(path::AbstractString, exponents, coefficients::AbstractMatrix; drop_below)
write_rom_coefficients_csv(path, exponents, coefficients; drop_below = 1e-14)
Write the standard R_coefficients.csv. exponents is the vector of multiindex exponents (length L); coefficients is the (NR, L) complex matrix. Rows with all |c| ≤ drop_below are omitted.
This module is an extension using Symbolics.jl to introduce a clean and nice looking interface for defining and generating the NthOrderModel and ExternalSystem
_differential_equations_helper(f!, order::Int64, nvars::Int64; p, ext_vars)
_differential_equations_helper(f!, order::Int, nvars::Int; p = ())
Helper for mirroring DifferentialEquations.jl interface. Used in MORFE.modelfromsymbolics(f, nvars::Int; p = ()).
_differential_equations_helper_external(f, nvars::Int64; p)
_differential_equations_helper_external(f, order::Int, nvars::Int; p = ())
Helper for mirroring DifferentialEquations.jl interface. Used in MORFE.externalsystemfromsymbolics(f, nvars::Int; p = ()).
_factor_info(e, F_groups::NTuple{NG, Vector{<:Union{Complex{Num}, Num}}}) where NG
_factor_info(e, F_groups)
Decompose monomial e into a scalar coefficient and a list of (group_index, var_index_in_group, exponent) tuples, one per symbolic factor. For polarization.
_findgroup(sym, groups::NTuple{NG, Vector{Num}}) where NG
_findgroup(sym, groups)
Helper-function that returns in which group the symbol sym is.
_findgroup_index(sym, groups::NTuple{NG, Vector{<:Union{Complex{Num}, Num}}}) where NG
_findgroup_index(sym, groups)
Return (group_index, var_index) such that groups[group_index][var_index] equals sym. For polarization.
_get_taylor_expansion_around_0(expr::Num, all_vars::Vector{Num}, d::Int64)
_get_taylor_expansion_around_0(expr::Complex{Num}, all_vars::Vector{Num}, d::Int64)
_get_taylor_expansion_around_0(expr, all_vars, d)
Helper function to determine wether a expression expr is a monome. Used in is_polynomial. Uses the function Symbolics.taylor to calulate a Taylor expansion around zero.
_monomial_to_MultilinearMap(polarized_monomial::Vector{Union{Complex{Num}, Num}}, polarized_variables::NTuple{ORD, Vector{Vector{Num}}}, multiindex::NTuple{ORD, Int64}; has_ext) where ORD
_monomial_to_MultilinearMap
Defines a MultilinearMap for one monomial.
Keyword arguments
has_ext::Bool = false: boolean thet tells the MultilinearMap wether it is an external system or not
_numeric_eltype
Helper for the extraction of coefficients.
_to_MyNum(m::Num)
_to_MyNum(m::Complex{Num})
_to_MyNum(c::Complex)
_to_MyNum(c::Real)
_to_MyNum(m)
_to_MyNum
Helpers that define the correct build of MyNum for numbers.
all_monomials_to_MultilinearMaps(F_by_multiindex_polarized::Dict{NTuple{ORD, Int64}, Vector{Union{Complex{Num}, Num}}}, dict_pol_vars::Dict{NTuple{ORD, Int64}, NTuple{ORD, Vector{Vector{Num}}}}; has_ext) where ORD
all_monomials_to_MultilinearMaps(F_by_multiindex_polarized, dict_pol_vars)
Defines and collects MultilinearMaps in a Tuple for every monomal in F_by_multiindex_polarized.
Arguments
F_by_multiindex_polarized: Dictionary that maps multiindices to a polarized monomial vector.dict_pol_vars: Dictionary that maps multiindices to the autmatically generated variables that appear in the polarized monomial.
Keyword arguments
has_ext::Bool = false: boolean thet tells the MultilinearMap wether it is an external system or not
check_all_vars_used(exprs::Vector{Num}, all_vars::Vector{Num})
check_all_vars_used(exprs::Vector{Complex{Num}}, all_vars::Vector{Num})
check_all_vars_used(exprs, all_vars)
Returns variables from all_vars that do not appear in any expression in exprs. Empty vector means all variables are used.
check_constant_terms(exprs::Vector{NT}, all_vars::Vector{Num}) where NT
check_constant_terms(exprs, all_vars)
Returns a list of indices where exprs[i] has a nonzero constant term. Empty vector means all expressions vanish at the origin.
check_expr(exprs::Vector{NT}, all_vars::Vector{Num}) where NT
check_expr(exprs, all_vars)
Make some checks wether exprs is correctly defined.
degree_of_monomial
Computes the degree of a monomial. Works on a single not vector expression!
externalsystem_from_symbolics(f, nvars::Int64; p)
externalsystem_from_symbolics(exprs::Vector{<:Union{Complex{Num}, Num}}, var::Vector{Num})
MORFE.externalsystem_from_symbolics(exprs, var)
Generates an MORFE.ExternalSystem. Expects the ODE describing the external system in the form
dr/dt var = exprs
MORFE.externalsystem_from_symbolics(f, nvars::Int; p = ())
Mirrors DifferentialEquations.jl's convention of defining ODEs, for the autonomous driver ṙ = E(r) in nvars external states.
Both layouts are accepted:
in-place, mutating the first argument —
f(dr, r, p, t)out-of-place, returning
dr—f(r, p, t) -> dr
f must be polynomial in r and must not depend on t. p is passed through to f unchanged, so parameters may be closed over or supplied here.
Note that the expression method externalsystem_from_symbolics(exprs, var) this ultimately calls performs no polynomial or equilibrium-at-origin check: a non-polynomial right-hand side surfaces later as a _findgroup error, and a non-zero constant term is absorbed silently. Those checks run only on the model_from_symbolics path, via check_expr.
extract_linear_matrices(exprs::Vector{NT}, groups::NTuple{ORDP1, Vector{Num}}) where {NT, ORDP1}
extract_linear_matrices(exprs::Vector{NT}, groups::NTuple{ORDP1, Vector{Num}}, ext_var::Vector{Num}) where {NT, ORDP1}
extract_linear_matrices
Assemble linear_terms matrices for NthOrderModel. Inputs are a ODE of the variables defined in groups that is a NTuple, where every part of the Tuple consists of the state variables or the derivatives. e.g: ([z1, z2, z3], [dz1, dz2, dz3], ...) Returns tuple of matrices with B[i] is matric w.r.t. groups[i] so the ith derivative.
extract_nonlinear_monomials(exprs::Vector{<:Union{Complex{Num}, Num}}, groups::NTuple{ORDP1, Vector{Num}}, linear_terms) where ORDP1
extract_nonlinear_monomials(exprs::Vector{<:Union{Complex{Num}, Num}}, F_groups_ext::NTuple{NG_EXT, Vector{Num}}, linear_terms, groups::NTuple{ORDP1, Vector{Num}}) where {NG_EXT, ORDP1}
extract_nonlinear_monomials(exprs::Vector{NT}, groups::NTuple{ORDP1, Vector{Num}}) where {NT, ORDP1}
extract_nonlinear_monomials(exprs, groups, linear_terms)
Calculates the nonlinear part of exprs by substracting linear_terms. Seperates the nonlinear remainder into monomials and saves them in the Vector monomials and generates additional a Vector multideg_monomials that contains the multiindexdegree calculated by `multidegreemonomials`.
extract_nonlinear_monomials(exprs, F_groups_ext, linear_terms, groups)
Variant for NthOrderModel with external variables.
F_groups_ext = (groups[1:end-1]..., ext_var) — the groups used to compute multidegrees of nonlinear monomials. groups is the full derivative-group tuple and is used only to subtract the linear part (via nonlinear_remainder).
The returned multideg_monomials has tuples of length length(F_groups_ext), i.e. one entry per state-derivative group (excl. highest) plus one entry for the external group.
generate_polynomial(dict::Dict{NTuple{ORD, Int64}, Vector{Union{Complex{Num}, Num}}}, var::Vector{Num}, N::Int64) where ORD
generate_polynomial(dict, var, N)
Generates a MORFE.DensePolynomial from a dictionary calculated from group_monomials.
get_coefficients(exprs::Vector{<:Union{Complex{Num}, Num}}, var::Vector{Num})
get_coefficients(exprs, var)
Returns the coefficient of a monomial by evaluating every variable in var with 1. Attention: There is no check wether exprs is a monomial or not!
group_monomials(monomials::Vector, multideg_monomials::Array{Array{NTuple{ORD, Int64}, 1}, 1}, N::Int64) where ORD
group_monomials(monomials::Vector, deg_monomials::Vector{Vector{Int64}}, N::Int64)
group_monomials(monomials, multideg_monomials)
Collects monomials of the same multiindex in different components of the vector and defines a mapping F_by_multiindex that maps from the multiindices to the grouped monomial.
group_monomials(monomials, multideg_monomials)
Collects monomials of the same multiindex in different components of the vector and defines a mapping F_by_multiindex that maps from the multiindices to the grouped monomial.
is_polynomial(exprs::Vector{NT}, all_vars::Vector{Num}) where NT
is_polynomial(exprs, all_vars)
Check whether all expressions in exprs are polynomial in all_vars.
Uses the Taylor ansatz: substitute all vars → ε * var, Taylor-expand in ε around 0 up to the maximum degree found in exprs. If the expression is polynomial of degree ≤ d, the Taylor expansion is exact and the difference to the original is zero. If any transcendental or rational term is present, the Taylor expansion differs from the original.
model_from_symbolics(f!, order::Int64, nvars::Int64; p)
model_from_symbolics(f!, order::Int64, nvars::Int64, f_ext, nvars_ext::Int64; p, p_ext)
model_from_symbolics(exprs::Vector{<:Union{Complex{Num}, Num}}, groups::NTuple{ORDP1, Vector{Num}}) where ORDP1
model_from_symbolics(exprs::Vector{<:Union{Complex{Num}, Num}}, groups::NTuple{ORDP1, Vector{Num}}, ext_var::Vector{Num}, ext_exprs::Vector{<:Union{Complex{Num}, Num}}) where ORDP1
model_from_symbolics
Generates NthOrderModel. Inputs are a ODE of the variables defined in groups that is a NTuple, where every part of the Tuple consists of the state variables or the derivatives. e.g: ([z1, z2, z3], [dz1, dz2, dz3], ...). The ODE is supposed to be a vector equal to zero and is inputed in the variable exprs.
MORFE.model_from_symbolics(f!, order, nvars; p = ())
Mirrors DifferentialEquations.jl's convention of defining ODEs, generalised to order order.
f! must be in-place (arity order + 1) and mutates its first argument:
f!(dᵏu, dᵏ⁻¹u, ..., du, u, p, t)
Each dⁱu is a vector of length nvars. A non-mutating f(u, p, t) is not accepted here — only externalsystem_from_symbolics supports that layout.
f! must be polynomial in the state and must not depend on t; p is passed through to f! unchanged, so parameters may be closed over or supplied here.
model_from_symbolics
Generates NthOrderModel where the nonlinear forcing terms may also depend on external variables ext_var (the state of an ExternalSystem).
The multiindices of the resulting MultilinearMaps have ORD + 1 entries: (dx, dẋ, …, d_r) where the last entry counts the degree in the external variables.
Arguments
exprs: vector of ODE expressions (= 0), may contain bothgroupsvariables andext_vargroups: NTuple of variable groups as inmodel_from_symbolicsext_var: vector of external/forcing variables (state of the ExternalSystem)
MORFE.model_from_symbolics(f!, order, nvars, f_ext, nvars_ext; p = (), p_ext = ())
Mirrors DifferentialEquations.jl's convention of defining ODEs, generalised to order order = k, for a model coupled to an externalsystem_from_symbolics driver.
f! must be in-place and takes the external state r as one extra argument, after u and before p — so its arity is order + 2:
f!(dᵏu, dᵏ⁻¹u, ..., du, u, r, p, t)
Each dⁱu is a vector of length nvars and r one of length nvars_ext; referencing r in f! is how forcing enters the model. f_ext describes the external dynamics ṙ = E(r) and may be either layout accepted by externalsystem_from_symbolics: in-place f_ext(dr, r, p, t) or out-of-place f_ext(r, p, t) -> dr.
Both right-hand sides must be polynomial and independent of t. p is passed to f! and p_ext to f_ext, unchanged.
multidegree_of_monomial(term::Complex{Num}, groups::NTuple{NG, Vector{Num}}) where NG
multidegree_of_monomial(term, groups::NTuple{NG, Vector{Num}}) where NG
multidegree_of_monomial(term, groups)
Computes the degree of a monomial but separated in to the derivative orders defined in groups.
nonlinear_remainder(exprs::Vector{NT}, groups::NTuple{ORDP1, Vector{Num}}, B) where {NT, ORDP1}
nonlinear_remainder
returns the nonlinear part of the equation as the same size as exprs with a minus so that it holds: expr: linearterms = - nonlinearremainder Also checks that the nonlinear part is not allowed to depend on the highest derivative variables.
polarize(F_by_multiindex::Dict{NTuple{ORD, Int64}, Vector{Union{Complex{Num}, Num}}}, F_groups::NTuple{ORD, Vector{<:Union{Complex{Num}, Num}}}, N::Int64) where ORD
polarize(F_by_multiindex, F_groups, N)
Returns dictionary Fbymultiindex but polarized.
F_groups is the tuple of variable groups used for polarization — either groups[1:end-1] (pure state case) or (groups[1:end-1]..., ext_var) (with external forcing). The multiindex length must equal length(F_groups).
polarize_monomial(term, F_groups::NTuple{NG, Vector{<:Union{Complex{Num}, Num}}}) where NG
polarize_monomial(term, F_groups::NTuple{NG, Vector{<:Union{Complex{Num}, Num}}}, mi::Union{Nothing, NTuple{NG, Int64}}) where NG
polarize_monomial(term, F_groups, mi=nothing)
Polarize a single monomial term with respect to F_groups (i.e. groups[1:end-1]).
Every factor v^p, where v is the i-th variable of group g, is replaced by the product of p distinct "slot copies" of v: v_1 * v_2 * ... * v_p. Slots are consumed group-by-group, in the order the factors appear in term.
Returns (polarized_expr, slotvars):
polarized_expr :: Num— the polarized monomial (coefficient included).slotvars :: NTuple{NG, Vector{Vector{Num}}}—slotvars[g][s]is the length-Nvector of slot-scopies of the variables inF_groups[g](this is the "x_s" vector you'd pass intoeval).
If mi is not given, it is computed via multidegree_of_monomial.
seperate_into_monomials(expr::Num)
seperate_into_monomials(expr::Complex{Num})
seperate_into_monomials(exprs::Vector{<:Union{Complex{Num}, Num}}, var::Vector{Num})
seperate_into_monomials
Seperates nonlinear part in to the seperate mononmials. Works on a single not vector expression!