MORFEFerrite.jl Code Documentation
8 modules · 167 documented entries · generated 5 October 2026
MORFEFerrite.Common — shared Ferrite backend layer.
MeshIO— COMSOL/Abaqus/Gmsh mesh loading and conversion.node_dof,free_dofs_at_nodes— mesh node + direction → DOF index.ParaviewExport—write_paraview_*stubs; implementations live in theMORFEFerriteWriteVTKExtextension (activated byusing WriteVTK).
AbstractAssembledModel
Supertype for a physics backend's assembled full-order data: FE spaces, assembled operators, material data, and any backend-specific factories needed before a reduction is chosen.
Each concrete subtype implements build_model. Dispatch is on the backend's assembled-model type; no additional wrapper "case" type is required.
build_model(m::MORFEFerrite.ParametricGeometry.AssembledParametricModel; master, spectrum, conjugate_permutation, diagnostics, diagnostics_rtol)
build_model(m::MORFEFerrite.StructuralSVK.AssembledMechanicalModel; master, forcing, nev, spectrum, expansion_order, n_monomials)
build_model(m::MORFEFerrite.FluidNavierStokes.AssembledFluidModel, eig; master, outer, n_monomials, expansion_order, scale, normalisation, conjugate_permutation, conjugate_atol)
build_model(pm::MORFEFerrite.FluidNavierStokes.AssembledParametricFluidModel; spectrum, master, conjugate_permutation)
build_model(m::AbstractAssembledModel; kwargs...) -> (; model, spectral, meta)
Construct the full-order model and spectral data consumed by a reduction.
This is the single contract every MORFEFerrite physics module implements. The first two fields are exactly what MORFE.parametrise takes:
(; model, spectral) = build_model(case; master = [1], forcing = …)
W, R = parametrise(model, spectral, expansion_order)
Keyword arguments are backend-specific: they may select master modes, add forcing, configure diagnostics, or request/reuse an eigensolve. The return shape is shared, so everything downstream of build_model can remain physics-independent.
The return is a NamedTuple, not a positional tuple, so that a backend can grow what it reports without breaking callers who only want model and spectral.
model— anNthOrderModelspectral— aSpectralDatameta— backend metadata such as timings, the raw spectrum, forcing records, or DOF maps. It is available to callers and reporting code but is not consumed by MORFE.
Implementations must:
return an
NthOrderModelwhoselinear_termsand nonlinear terms are complete, including any external system the forcing introduces — not "mostly built, the caller adds forcing";return a
SpectralDatareconciled against that model's order. UseSpectralData(model, spectrum; master = …)so MORFE owns any order reconciliation;apply conditioning tweaks (mode scaling, unit changes) to the raw arrays before constructing the bundle;
SpectralDatadeliberately has noscalefield, so such tweaks stay visible at the call site;provide the spectrum's actual conjugate pairing to
SpectralData; if a full reduced variable permutation is needed, extend the master pairing through the model's external system withfull_conjugate_permutationrather than a fixed literal.
Implementations must not build a MultiindexSet, a ResonanceSet or a resonance policy, validate the monomial set, derive conjugate closure, warn about resonances, or solve the cohomological equations. Those are parametrise's responsibilities and are chosen by the caller. A backend may solve its linear eigenproblem here, or reuse spectral data supplied by the caller.
free_dofs_at_nodes(dh::Ferrite.DofHandler, free_to_local::Dict{Int64, Int64}, node_ids::AbstractVector{Int64}, directions::AbstractVector{Int64})
free_dofs_at_nodes(dh, free_to_local, node_ids, directions) -> Vector{Int}
Free-DOF indices (the row indices of K, M and of the parametrisation W) for the given mesh nodes and directions. free_to_local is the map built by mechanical_model and available as model.info.free_to_local.
Throws if a requested node/direction is constrained, since it then has no row.
node_dof(dh::Ferrite.DofHandler, node_id::Int64, direction::Int64)
node_dof(dh, node_id, direction) -> Int
Global DOF index of direction (1 = x, 2 = y, 3 = z) at mesh node node_id. Assumes a single vector field with one DOF per direction per node, which is what mechanical_model builds.
stage_timings(info)
stage_timings(info) -> Vector{Pair{String, Float64}}
Every *_time_s entry of an info NamedTuple, in declaration order, with the suffix stripped.
This is the convention that lets one writer time every backend without knowing any of them: a stage that records eig_time_s in its info is reported as "eig", and a backend adds a timed stage simply by recording it.
summary_entries(case, rom)
summary_entries(case, rom) -> Vector{Pair{String, Any}}
The physics-specific rows a backend contributes to its run summary, in the order they should be written.
Add a method for your assembled-model type to extend the summary; the shared skeleton (sizes, order, stage timings) is written by write_summary and does not need restating. Returning an empty vector — the default — is fine.
function Common.summary_entries(m::AssembledFluidModel, meta::NamedTuple)
return ["Re0" => m.Re₀, "mode_scale" => meta.scale]
end
A row whose key repeats one the skeleton already wrote overrides it, so a physics can also correct a shared row rather than only append to it.
write_summary(io::IO, path::AbstractString, case; rom, title, metadata, append)
write_summary(io, path, case; rom = nothing, title, metadata = Pair[],
append = true) -> Nothing
Print a run banner and timing table to io, and write key: value rows to path.
The skeleton is shared: title, problem size, stage timings gathered by stage_timings from both case.info and the ROM meta, then whatever summary_entries the physics contributes, then metadata from the caller (which wins over both — it is the most specific).
append = true is the default because MORFE.save_rom writes this same file first, with the run's provenance (julia version, commit, timestamp). Opening it with "w" here would silently discard that.
MORFEFerrite.Common.MeshIO — mesh loading and conversion utilities.
The subsystem owns direct COMSOL-to-Ferrite loading and conversions between COMSOL, Abaqus, and Gmsh mesh formats.
load_comsol_grid(mphtxt_path::AbstractString, dirichlet_entity_ids::Set{Int64}; scale)
load_comsol_grid(path, dirichlet_entity_ids; scale = 1.0) -> (grid, constrained_nodes)
Read a COMSOL .mphtxt file and return the Ferrite Grid (P18 quadratic prism cells) together with the Set{Int} of node indices lying on the Dirichlet boundaries. dirichlet_entity_ids is the set of COMSOL geometric entity IDs (1-indexed, i.e. raw file ID + 1) whose surface elements define the clamped region.
scale multiplies every node coordinate on load — use it when the mesh and the material constants are in different unit systems (e.g. scale = 1e-3 for a mesh drawn in millimetres against SI material data). Scaling the geometry here rather than afterwards keeps the DOF numbering and the entity sets untouched.
abaqus_to_gmsh(inp_file::String, gmsh_file::String)
abaqus_to_gmsh(inp_file::String, gmsh_file::String)
Read an Abaqus .inp mesh and write a Gmsh .msh file.
Supported element types: C3D4/10/10M, C3D8/8R/8I, C3D20/20R, C3D6, C3D15, S3/3R, S4/4R, S6, S8/8R, CPS3/4/4R/6/8, CPE3/4/4R/6/8, CAX3/4/4R/6/8/8R, B31, B32, T3D2, T3D3.
Node IDs may be non-contiguous; they are remapped to a compact 1-based Gmsh tag space. Unknown element types are silently skipped.
abaqus_to_gmsh_linear(inp_file::String, gmsh_file::String)
abaqus_to_gmsh_linear(inp_file::String, gmsh_file::String)
Like abaqus_to_gmsh but downgrades quadratic elements to their linear (corner-nodes-only) counterparts before writing.
_read_mesh(mesh_file::String)
_read_mesh( mesh_file::String)
Reads a Comsol .mphtxt file and extracts
n2c: node coordinatese2nT6: node list of elements of type T6 (triangle)e2qT6: label of elements of type T6 (triangle)e2nQ9: node list of elements of type Q9 (rectangle)e2qQ9: label of elements of type Q9 (rectangle)e2nT10: node list of elements of type T10 (triangle)e2qT10: label of elements of type T10 (triangle)e2nP18: node list of elements of type P18 (prism)e2qP18: label of elements of type P18 (prism)e2nH27: node list of elements of type H27 (hex)e2qH27: label of elements of type H27 (hex)
Based on the mesh.jl file from Morfe2.0.
comsol_to_gmsh(comsol_file::String, gmsh_file::String)
comsol_to_gmsh(comsol_file::String, gmsh_file::String)
Reads comsol_file (.mphtxt) and returns a Gmsh (.msh) file. Warning: The Node Re-Ordering is done by hand comparing with the old comsol files from Morfe2.0. So be aware that Comsol may have changed the structure or numbering of nodes. Here is the permutation only implemented for T6, Q9 and P18
comsol_to_gmsh_linear(comsol_file::String, gmsh_file::String)
comsol_to_gmsh_linear(comsol_file::String, gmsh_file::String)
Like comsol_to_gmsh but downgrades quadratic elements to their linear counterparts before writing the Gmsh .msh file.
gmsh_to_comsol(gmsh_file::String, comsol_file::String)
gmsh_to_comsol(gmsh_file::String, comsol_file::String)
Reads gmsh_file (.msh) and writes a COMSOL (.mphtxt) mesh file. This is the inverse of comsol_to_gmsh: it undoes the node-reordering permutations and converts 1-based Gmsh indexing back to 0-based COMSOL indexing.
Assumptions:
The .msh file was produced by
comsol_to_gmsh(node tags 1:nn).Element tags in the Gmsh file carry the COMSOL geometric-entity indices (as written by
comsol_to_gmsh).
MORFEFerrite.ParametricGeometry — parametric mesh coordinate transforms.
A physics-blind module: it implements the general parametric coordinate transform x(θ,x₀) = Σ_α x_α(x₀) θ^α as a multivariate θ-power series — the determinant, adjugate and inverse determinant of its Jacobian — and expands any physics' multilinear maps over it.
J(θ,x₀) = Σ_α J_α(x₀) θ^α det/adj are then exact polynomials
∇u → ∇u · adj J(θ) gradient pullback
dΩ → dΩ · (1/det J(θ))^p weak-form weighting
Dimension-general. d is carried by the tensor type of the Jacobian coefficients and appears in no signature: determinant_series is App. A.1's multilinear expansion over the columns and adjugate_series is App. A.2's recurrence, so no cofactor formula is hardcoded. 2D and 3D are both gated end-to-end (test_moved_mesh_fom_2d.jl, test_series_algebra_2d.jl).
One caveat on App. A.2: the appendix derives the adjugate recurrence assuming J₀ = I. A curved reference configuration has J₀ ≠ I — example 07's arch is exactly that — so the implemented form inverts the leading term, A_σ = J₀⁻¹(c_σ I − Σ J_α A_{σ−α}), reducing to the published one when J₀ = I.
The affine map x₀ + Σᵢ θᵢψᵢ(x₀) is the common case and has its own jacobian_series method; the polynomial form takes multiindex => J_α pairs. The θ-series live over a MORFE MultiindexSet box (per-parameter truncation); the single-parameter arch and the multi-parameter beam are instances of the same engine. θ here is the μ of the theory write-up.
Choose the per-parameter bounds from the geometry, not by symmetry
The box is per-parameter precisely so unequal parameters need not be truncated alike, and getting this wrong is the difference between a converged model and a plausible-looking wrong one. Two things set the bounds:
The exact polynomial degrees.
adj Jhas degree≤ (d−1)·deg Janddet Jdegree≤ d·deg J, per parameter as well as in total, so a form's integrand has degree = (number of gradient factors) ×deg adj J. For an affine map that is 2 / 3 / 4 for the stiffness / quadratic / cubic form when∇ψis nilpotent — example 07 uses exactly[2], [3], [4]and is lossless. Note the bounds scale withdeg J: a map that is quadratic inθᵢneeds2don that axis, notd.The reciprocal. If
det J ≢ 1the1/det Jseries is infinite and its truncation, not the polynomial degrees, dominates. That parameter needs many more terms than a volume-preserving one. Example 04's[4,4]→[8,2]cost two extra terms and was 555× more accurate.
The expansion has a radius, and it is checked
1/det J is expanded about det J = 1, so it converges only where |det J − 1| < 1, and the transform separately needs det J > 0. Outside that radius no truncation order converges while the assembled model still looks well formed. report_geometry_validity measures the range and build_model prints it; geometry_validity_at gives the error at one θ.
Validate against a moved mesh, never against an archived coefficient file
The reference that means anything is a FOM whose geometry actually changed: freeze θ, displace the mesh nodes to x₀ + Σᵢθᵢψᵢ(x₀), reassemble with the physics' ordinary non-parametric code and compare. Same topology ⇒ same DOF numbering ⇒ entry-by-entry comparison with no gauge. See test/ParametricGeometry/test_moved_mesh_fom.jl. Judge accuracy on modal quantities: ‖ΔK‖_F/‖K‖_F of 1e-05 has corresponded to a 267 % error in the fundamental frequency, because the Frobenius norm is dominated by stiff directions and the frequency by the softest mode.
It connects to a physics module through AbstractPullbackKernel, and names no material, stress law or strain measure anywhere. A physics implements the QP integrand; this module owns the coordinate transform, the geometry cache, the assembly loops, the shared-input cache, the MultilinearMap wrapping and the build_model contract. Adding a second physics is one new kernel type — see StructuralSVK.SVKPullbackKernel.
AbstractInverseDeterminant
How a PullbackCache obtains the θ-series of 1/det J at each quadrature point. See PowerSeriesInverseDet (Method 4, the default) and AuxiliaryFieldInverseDet (Method 3).
AbstractPullbackKernel{DEG}
A physics' integrand, expressed over a parametric coordinate transform.
DEG is the arity in the field variable: 2 for a quadratic form g(u₁,u₂), 3 for a cubic h(u₁,u₂,u₃), 0 for a purely geometric (linear-operator) kernel.
A physics module implements a subtype and the methods below; ParametricGeometry then owns everything around them — the cell loop, the gather/scatter, the geometry cache, the all-coefficients sweep, the shared-input cache, and the wrapping of each θ-coefficient as a MORFE MultilinearMap.
Required for a nonlinear kernel (DEG ≥ 2)
det_weight_power(k) -> Int
The p in the (1/det J)^p weighting of this form's integrand. The driver multiplies by it; the kernel must not.
qp_prepare(k, ctx, ∇u_adj::NTuple{DEG, Vector{<:Tensor{2,dim}}}) -> state
Whatever the integrand needs once per quadrature point, independent of the test function — stresses, strain cross-terms. Returning it separately is what keeps that work out of the inner basis-function loop.
qp_integrand!(integ, k, ctx, state, ∇N_adj::Vector{<:Tensor{2,dim}}) -> integ
The θ-series of the integrand for one test function, written into integ (length nterms(ctx.basis)), before the determinant weighting.
Required for a linear-operator kernel
linear_qp_series!(a_ser, b_ser, k, ctx, i, j) -> nothing
The θ-series of the two operator entries (for a structure: stiffness and mass) coupling shape functions i and j at this quadrature point, already weighted.
Note on the gradients
∇u_adj and ∇N_adj arrive already contracted with the adjugate series — that is, ∇u·adj J(θ) rather than ∇u. Contracting is the coordinate transform's job, so the kernel receives the pulled-back gradient and writes its weak form exactly as it would in the reference configuration.
AssembledParametricModel <: AbstractAssembledModel
A base physics model expanded over a parametric mesh coordinate transform.
Holds the FE discretisation and pullback cache, the base physics' assembled model, its θ-expanded linear operators and its nonlinear ParametricMaps. Being physics-blind, it names none of them: the base field is only ever passed back to the physics, and the operators arrive already expanded.
Build one with the physics' own entry point (StructuralSVK.parametric_model), then call build_model.
AuxiliaryFieldInverseDet(ip; qr, lump = false, tol = 1e-10)
Method 3 — represent 1/det J by a scalar FE field s on the interpolation ip, with s · det J = 1 enforced weakly.
Expanding s = Σ_κ s_κ(x₀) θ^κ with s_κ ∈ V_h and collecting θ^γ gives a triangular recurrence in graded-lex order:
γ = 0 : A₀ s₀ = b b_i = ∫ Nᵢ dΩ₀
γ > 0 : A₀ s_γ = − Σ_{0<β≤γ} A_β s_{γ−β} A_β = ∫ Nᵢ c_β Nⱼ dΩ₀
where c_β are the coefficients of the (exactly polynomial) det J series. A₀ is the mass matrix weighted by c₀ = det J₀, which is the plain mass matrix only when the reference configuration is undeformed (J₀ = I); a curved reference has c₀ ≠ 1 and the weight matters. A₀ is SPD exactly when det J₀ > 0, i.e. under the orientation condition the transform already needs.
One factorisation of A₀ serves every multiindex, so the whole expansion costs one assembly sweep, one Cholesky and L−1 backsolves — negligible next to a single order of the reduction.
ip is the accuracy knob: unlike Method 4, whose error is a truncation order, Method 3's error is the FE interpolation error of 1/det J. Raising the order of ip drives it towards Method 4's pointwise-exact answer.
qr must be the quadrature rule the physics CellValues uses, so c_β is read at the points the cache already holds. lump = true replaces A₀ by its row-sum diagonal. tol is the tolerance of the s₀ ≡ 1 consistency check, which is exact to round-off whenever J₀ = I.
Note (1/det J)^p is formed as the p-th power of the interpolant s, not as a separately projected field — a different (and untaken) Method-3 variant.
GeometryParameterBasis{Nθ}
Per-parameter box of θ-exponents (a MORFE MultiindexSet) plus an exponent→position lookup. Position 1 is always the zero multiindex (graded-lex order), i.e. the constant term.
ParametricDiscretisation(dh, cv, free_to_local, n_free, cache)
The FE side of a parametric problem: the DOF handler and quadrature, the free-DOF restriction, and the PullbackCache for this geometry and θ-basis. Shared by every kernel over the same mesh.
ParametricMap
ParametricMap(pd, kernel)
One physics kernel, expanded over the θ-basis and ready to be turned into MultilinearMaps by multilinear_maps.
Holds the shared-input cache: one FE sweep computes all θ-coefficients for the same arithmetic cost as one, so the per-α closures share a single sweep per input tuple. The hit test is an exact content comparison (no hashing), so results are bit-identical to computing each coefficient separately.
ParametricOperator
ParametricOperator(arrays, arity)
One linear operator of the base physics, expanded over the θ-basis: arrays[i] is the θ^α coefficient matrix at basis.mset.exponents[i], and arity names the derivative slot it occupies in the model ((1,0,0) stiffness, (0,1,0) damping, (0,0,1) mass for a second-order structure on the augmented ORD = 3 model; (1,) for a first-order physics).
The base coefficient arrays[1] (α = 0) goes into the model's linear_terms; every α ≠ 0 becomes a MultilinearMap correction.
PowerSeriesInverseDet()
Method 4 — expand 1/det J directly as a θ-power series (theory App. A.3).
Pointwise and exact within the θ-box: no auxiliary field, no extra unknowns, no spatial discretisation error. The expansion is geometric about det J = 1, so it converges only where |det J − 1| < 1; outside that radius NO truncation order converges. report_geometry_validity measures the radius and build_model prints it.
PullbackCache
PullbackCache(dh, cv, geom, basis; det_powers = Int[])
Per-(cell, quadrature-point) θ-series of the coordinate transform.
geom is a geometry provider (see _geom_at); basis is the GeometryParameterBasis the series are truncated to. det_powers lists the inverse-determinant powers the kernels will ask for — each is expanded eagerly at construction, because computing (1/det J)^p inside an assembly sweep would put a nterms(basis)-long convolution in the hot loop.
Fields are indexed [cell][qp]:
adj[ci][q]—Vector{Tens3}, the adj J seriesdet[ci][q]—Vector{Float64}, the det J seriesinv_det[ci][q]—Vector{Float64}, the 1/det J seriesinv_det_pow[p]— the(1/det J)^pseries, same[cell][qp]shape
One cache serves every kernel over the same geometry: the quadratic and cubic forms of a structural physics differ only in which det_powers entry they read.
QPContext
QPContext(cv, q, basis, adj, det, inv_det)
What a kernel sees at one quadrature point: the CellValues (already reinit!-ed) and the point index, so it can reach shape values; the GeometryParameterBasis its series live in; and the geometry's adj J, det J and 1/det J series at this point.
A nonlinear kernel normally leaves the determinant weighting to the driver (see det_weight_power) and never touches det/inv_det; a linear-operator kernel weights its own entries, since stiffness and mass carry different powers.
adj_tensor_type(::MORFEFerrite.ParametricGeometry.PullbackCache{Nθ, TT}) where {Nθ, TT}
adj_tensor_type(cache) -> Type
The tensor type of the adj J coefficients, i.e. Tensor{2,dim,Float64,dim²}. The driver sizes its gradient buffers from this rather than from a hardcoded 3D alias, which is what makes the assembly loops dimension-general.
adjugate_series(J::AbstractVector{TT}, det_ser::AbstractVector, basis::MORFEFerrite.ParametricGeometry.GeometryParameterBasis) where TT<:(Tensor{2})
adjugate_series(J::AbstractVector{TT}, det_ser::AbstractVector, basis::MORFEFerrite.ParametricGeometry.GeometryParameterBasis, supp::Vector{Int64}) where TT<:(Tensor{2})
adjugate_series(J, det_ser, basis[, supp]) -> Vector{<:Tensor{2,dim}}
Exact multivariate coefficients of adj(J(θ)), truncated to the box.
Theory App. A.2 (Adjugate). Matching θ^σ in the defining identity J · adj J = det J · I gives a recurrence that is strictly triangular in graded-lex order (β ≤ σ componentwise with β ≠ σ forces |β| < |σ|):
J₀ A_σ + Σ_{0<α≤σ} J_α A_{σ−α} = c_σ I
The appendix solves this by assuming J₀ = I, which this code cannot. A curved reference configuration has J₀ ≠ I — example 07's arch is exactly that, J₀ = I + ∇w ⊗ e₁ — so the leading term must be inverted explicitly:
A_σ = J₀⁻¹ ( c_σ I − Σ_{0<α≤σ, α ∈ supp J} J_α A_{σ−α} ), A_0 = adj(J₀)
which reduces to the appendix's form when J₀ = I. The same correction applies to Method 3, where the leading matrix is the c₀-weighted mass matrix, not M.
The sum runs only over the support of J — Nθ+1 terms for an affine map, not L — so this costs O(L·|supp J|) per quadrature point. basis.diff supplies the position of σ − α without hashing.
Exactly polynomial with deg adj J ≤ (d−1)·deg J. Truncating det J to the box does not corrupt it: A_σ reads only c_σ, which is in the box whenever A_σ is.
assemble_linear_series!(A_arr::Vector, B_arr::Vector, pd::MORFEFerrite.ParametricGeometry.ParametricDiscretisation, kernel::MORFEFerrite.ParametricGeometry.AbstractPullbackKernel)
assemble_linear_series!(A_arr, B_arr, pd, kernel)
Fill A_arr[m], B_arr[m] with the θ^α coefficient matrices of the two linear operators kernel defines (for a structure: stiffness and mass), at multiindex basis.mset.exponents[m].
A_arr and B_arr must each be nterms(basis)-long vectors of sparse matrices sharing the DOF handler's pattern (allocate_matrix(dh)). The driver owns the cell loop and the assembly; the kernel supplies only the per-entry θ-series through linear_qp_series!.
build_inverse_determinant(::MORFEFerrite.ParametricGeometry.PowerSeriesInverseDet, dh, cv, basis::MORFEFerrite.ParametricGeometry.GeometryParameterBasis, det)
build_inverse_determinant(s::MORFEFerrite.ParametricGeometry.AuxiliaryFieldInverseDet, dh, cv, basis::MORFEFerrite.ParametricGeometry.GeometryParameterBasis, det)
build_inverse_determinant(strategy, dh, cv, basis, det) -> Vector{Vector{Vector{Float64}}}
The θ-series of 1/det J, indexed [cell][qp], from the already-computed det J series in the same layout. The single seam between Methods 3 and 4.
build_linear_corrections(A_arr::Vector, basis::MORFEFerrite.ParametricGeometry.GeometryParameterBasis, arity::NTuple{N, Int64}; external_components) where N
build_linear_corrections(A_arr, basis, arity) -> Vector{MultilinearMap}
−θ^α · A_α · x for every α ≠ 0, where A_arr[αidx] is the operator's θ^α coefficient matrix and arity names the derivative slot x occupies.
The base coefficient (α = 0) is skipped: it belongs in the model's linear_terms, not among its nonlinear terms. Structurally empty coefficients are skipped too — an all-zero correction is a term the solve would evaluate for nothing.
For a second-order structure on the augmented ORD = 3 model: (1,0,0) is a stiffness correction, (0,1,0) damping, (0,0,1) mass. For a first-order physics, (1,).
build_model(m::MORFEFerrite.ParametricGeometry.AssembledParametricModel; master, spectrum, conjugate_permutation, diagnostics, diagnostics_rtol)
build_model(m::AssembledParametricModel; master, spectrum, …)
-> (; model, spectral, meta)
Assemble the augmented full-order model and its spectral data for a parametric problem.
The θ parameters enter as frozen external states: N_EXT = n_geometry_parameters external variables with eigenvalue 0 (θ̇ = 0), so a monomial z^a θ^b in the reduced dynamics is a genuine parameter dependence rather than a transient.
What this method does, and what it deliberately leaves to the caller:
Zero-pads the base physics' linear terms to
model_order, because a correction on the highest derivative (a parametric mass) only exists on a model one order higher.Collects every θ ≠ 0 correction of every operator, plus the nonlinear maps.
Reconciles the spectral data against the augmented model's order with
SpectralData(model, spectrum; master). The base eigenproblem is second-order while the model is third — this is exactly the reconciliation MORFE owns, and hand-rolling it (multiplying the last block byλversus forming a freshλ^{k-1}ψ) is silently wrong.Derives the conjugate permutation with
full_conjugate_permutation, never a literal — the θ states are real, hence self-conjugate, so an oddn_geometry_parametersworks without a special case.Does not build a
MultiindexSet. The θ-box truncation is a modelling choice; pass your ownmsettoparametrise.
master lists physical mode PAIRS, as elsewhere: pair p occupies spectrum entries 2p-1, 2p.
convolve_weight_accumulate!(dst, A::AbstractVector, w::AbstractVector, scale, basis::MORFEFerrite.ParametricGeometry.GeometryParameterBasis)
convolve_weight_accumulate!(dst, A, w, scale, basis)
dst[k] += scale · Σ_{α+β=k} A[α] · w[β] — the determinant weighting fused into the element accumulation.
The driver's last act per basis function is to multiply the integrand series by (1/det J)^p and add it to the element residual. Doing that as poly_mul followed by a loop materialises a length-L temporary for every single (cell, quadrature point, basis function); fusing removes it entirely.
det_adj_series(J::AbstractVector{<:Tensors.Tensor{2}}, basis::MORFEFerrite.ParametricGeometry.GeometryParameterBasis)
det_adj_series(J, basis) -> (det_ser, adj_ser)
det(J(θ)) and adj(J(θ)) together, sharing one scan of J's support.
Both are exactly polynomial — App. A.1 and A.2 — which is what makes the adjugate identity J⁻¹ = adj J / det J worth using: it concentrates all the non-polynomial content of J⁻¹ into the single scalar 1/det J, left to reciprocal_series (Method 4) or AuxiliaryFieldInverseDet (Method 3).
determinant_series(J::AbstractVector{<:Tensors.Tensor{2, dim, T}}, basis::MORFEFerrite.ParametricGeometry.GeometryParameterBasis{Nθ}) where {Nθ, dim, T}
determinant_series(J::AbstractVector{<:Tensors.Tensor{2, dim, T}}, basis::MORFEFerrite.ParametricGeometry.GeometryParameterBasis{Nθ}, supp::Vector{Int64}) where {Nθ, dim, T}
determinant_series(J, basis[, supp]) -> Vector{Float64}
Exact multivariate coefficients of det(J(θ)), truncated to the box.
Theory App. A.1 (Determinant). The determinant is multilinear in the columns of J, so expanding each column's series independently and collecting the terms with α₁+…+α_d = σ gives c_σ directly — one d×d scalar determinant per choice of support multiindex per column. Dimension-general, and no cofactor formula appears.
Exactly polynomial with deg det J ≤ d·deg J, per parameter as well as in total.
free_dof_map(ndofs::Integer, freedofs)
free_dof_map(ndofs, freedofs) -> Vector{Int32}
Dense global-DOF → free-DOF index map, 0 where the DOF is constrained.
A Dict here costs one hash lookup per DOF per cell per θ-multiindex — scatter_local! runs once for each of the L coefficients of every cell — which is a six-figure number of hashes per sweep for no reason. The dense vector is 4·ndofs bytes and indexes directly. Physics modules outside this one keep their Dict form (model.info.free_to_local); only the parametric assembly path uses this.
geometry_validity_at(cache::MORFEFerrite.ParametricGeometry.PullbackCache, θ; points)
geometry_validity_at(cache, θ; points = nothing) -> NamedTuple
Measure Method 4's validity conditions at one concrete θ.
Returns (; det_min, det_max, max_deviation, reciprocal_residual, worst_cell, worst_qp, convergent, orientation_ok):
max_deviation—max |det J(θ) − 1|over the probed points. Must be< 1.det_min— must be> 0; a non-positive determinant means the coordinate transform has folded the mesh and is not invertible.reciprocal_residual—max |det J(θ) · (1/det J)(θ) − 1|, the TRUNCATED series measured against its own defining identity. This is the honest error of the expansion at this θ, computed from what the cache actually holds.
points restricts the sweep to a (cell, qp) subset; nothing sweeps all.
geometry_validity_report(cache::MORFEFerrite.ParametricGeometry.PullbackCache{Nθ}; max_probe_points, cap) where Nθ
geometry_validity_report(cache; max_probe_points = 5000, cap = 4.0) -> NamedTuple
The θ-range over which Method 4's expansion is valid, measured per parameter.
Returns (; radius_pos, radius_neg, unconditional, det_at_reference), where radius_pos[i] / radius_neg[i] is the largest θᵢ along ±eᵢ (all other parameters zero) for which every probed quadrature point satisfies both |det J − 1| < 1 and det J > 0. Inf means the scan reached cap without leaving the radius.
unconditional is true when det J ≡ 1 everywhere in θ — a volume-preserving transform, for which the reciprocal series is the single term 1 and no convergence question arises.
Axis-wise, because a full box scan costs 2^Nθ corners and the axes already expose the parameter responsible. For a specific θ of interest — including cross terms — call geometry_validity_at directly.
Diagnostic only: nothing here changes the model.
inv_det_power(inv_det::AbstractVector{<:Real}, n::Int64, basis::MORFEFerrite.ParametricGeometry.GeometryParameterBasis)
inv_det_power(inv_det, n, basis) -> Vector{Float64}
n-th power of the reciprocal series (n ≥ 1), truncated to the box.
inv_det_power_series(cache::MORFEFerrite.ParametricGeometry.PullbackCache, p::Int64)
inv_det_power_series(cache, p) -> Vector{Vector{Vector{Float64}}}
The precomputed (1/det J)^p series, indexed [cell][qp]. Hoist this out of an assembly loop — the Dict lookup is not meant for the hot path.
inverse_determinant_comparison(a::MORFEFerrite.ParametricGeometry.PullbackCache, b::MORFEFerrite.ParametricGeometry.PullbackCache, θ; points)
inverse_determinant_comparison(a::PullbackCache, b::PullbackCache, θ; points = nothing)
Compare two inverse-determinant strategies at one concrete θ.
a and b must be built on the same mesh and quadrature (same (cell, qp) layout); their θ-bases may differ, since each series is evaluated in its own. Typically a is a PowerSeriesInverseDet cache (Method 4) and b an AuxiliaryFieldInverseDet one (Method 3).
Returns (; max_abs, max_rel, rms, residual_a, residual_b, worst_cell, worst_qp):
max_abs/max_rel/rms— how far apart the two1/det Jfields are at the quadrature points, which is exactly where the difference enters the assembled operators.residual_a/residual_b— each method measured againstdet J · s = 1, its own defining identity. Neither method is used as the reference for the other; they are each compared with the truth they are both approximating.
Diagnostic only: nothing here changes a model.
jacobian_series(Js::NTuple{M, TT}, basis::MORFEFerrite.ParametricGeometry.GeometryParameterBasis{Nθ}) where {M, Nθ, TT<:(Tensor{2})}
jacobian_series(Js::AbstractVector{<:Pair}, basis::MORFEFerrite.ParametricGeometry.GeometryParameterBasis{Nθ}) where Nθ
jacobian_series(Js, basis) -> Vector{<:Tensor{2,dim}}
Assemble the degree-1 Jacobian series aligned to basis from Js = (J₀, ∇ψ₁, …, ∇ψ_{Nθ}): J₀ at the zero multiindex, ∇ψ_i at the unit multiindex e_i. All other coefficients are zero.
This is the AFFINE map x = x₀ + Σ_i θ_i ψ_i(x₀). For the theory's general polynomial x = Σ_α x_α θ^α, pass the coefficients as multiindex ⇒ tensor pairs instead — see the AbstractVector{<:Pair} method.
jacobian_series(Js::AbstractVector{<:Pair}, basis) -> Vector{<:Tensor{2,dim}}
The theory's general polynomial map x(x₀,θ) = Σ_α x_α(x₀) θ^α, given as multiindex => J_α pairs (App. A, opening). The multiindex may be any Nθ-element integer container; repeated multiindices accumulate, matching the Σ_α they stand for.
A coefficient whose multiindex falls outside the box throws rather than being silently dropped. Truncating the series computed from the map is this module's job; truncating the map itself would change which geometry is being modelled without saying so — the exact failure mode that made example 04 wrong.
The affine NTuple method above is the common case and builds this structure.
model_order(m::MORFEFerrite.ParametricGeometry.AssembledParametricModel)
model_order(m::AssembledParametricModel) -> Int
The ORD the augmented NthOrderModel needs: one more than the highest derivative slot any θ-correction occupies.
A second-order structure whose mass is parametric needs ORD = 3 — a mass correction has arity (0,0,1), which only exists on a third-order model, and the fourth linear block is zero. A first-order physics with only a (1,)-arity correction needs no augmentation and must not get one.
multilinear_maps(m::MORFEFerrite.ParametricGeometry.ParametricMap{DEG}; arity, external_components) where DEG
multilinear_maps(m::ParametricMap; arity)
One MORFE MultilinearMap per θ-multiindex α: the θ^α coefficient of the kernel's form, with external multiplicity |α| so the solve multiplies it by the matching product of frozen θ states.
arity is the modal arity tuple — length ORD, with DEG in the slot the form acts on. For a second-order structure with the augmented ORD = 3 model, a quadratic displacement form is (2, 0, 0).
position_of(b::MORFEFerrite.ParametricGeometry.GeometryParameterBasis{Nθ}, e::StaticArraysCore.SVector{Nθ, Int64}) where Nθ
position_of(basis, e) -> Int
Grlex position of exponent e, or 0 when it falls outside the box (including any negative component). The hash-free counterpart of basis.index[e].
reciprocal_series(p::AbstractVector{<:Real}, basis::MORFEFerrite.ParametricGeometry.GeometryParameterBasis{Nθ}) where Nθ
reciprocal_series(p, basis) -> Vector{Float64}
Coefficients of 1/p(θ) truncated to the box, from the graded recurrence q[0] = 1/p[0], q[γ] = -(1/p[0]) Σ_{0≠β≤γ} p[β] q[γ-β]. Graded-lex order makes each q[γ-β] (lower total degree) available before q[γ].
This is Method 4 of the theory write-up (App. A.3, "Reciprocal of the Determinant"). The recurrence is the geometric series for 1/(1 − (1 − det J)) in disguise, so it inherits that series' radius: applied to det J it is valid only where |det J − 1| < 1, and it is unbounded in degree because the reciprocal of a polynomial is not a polynomial. basis.diff supplies the position of γ − β without hashing — this runs at every quadrature point.
See geometry_validity_report for the measured radius, and AuxiliaryFieldInverseDet for Method 3, the alternative.
report_geometry_validity(cache::MORFEFerrite.ParametricGeometry.PullbackCache{Nθ}; kwargs...) where Nθ
report_geometry_validity(cache; kwargs...) -> NamedTuple
Emit the measured convergence radius of the inverse-determinant expansion, and return the underlying geometry_validity_report.
Silent for a volume-preserving transform (det J ≡ 1), which is unconditionally convergent. Errors — not warns — when the REFERENCE configuration itself is already folded (det J ≤ 0 at θ = 0), because every series in the cache is then built about an invalid point.
report_zero_coefficients(m::MORFEFerrite.ParametricGeometry.AssembledParametricModel; rtol, probe)
report_zero_coefficients(m::AssembledParametricModel; rtol = 1e-12, io = stderr)
Emit one @info block naming the θ-multiindices that contribute nothing, and return the underlying zero_coefficient_report.
Silent when every multiindex carries something — the common case should not produce output.
series_extent(A::AbstractVector)
series_extent(A) -> Int
Position of the last non-zero coefficient of a θ-series, 0 if it is all zero.
This is the theory's degree bound, measured rather than assumed. adj J has degree ≤ (d−1)·deg J and det J degree ≤ d·deg J, and graded-lex order makes "total degree ≤ k" a contiguous prefix, so everything past this index is exactly zero and can be skipped without any truncation. It adapts on its own: a volume-preserving transform (det J ≡ 1) reports 1 and its convolutions collapse to a single term, with no degree bookkeeping threaded anywhere.
Scanning from the end costs O(L) iszero calls and usually returns at once; testing the same thing per PAIR costs O(|prod|), which a profile of example 04 showed to be 23 % of the whole run.
sweep_all!(out::Matrix{ComplexF64}, m::MORFEFerrite.ParametricGeometry.ParametricMap{DEG}, us::NTuple{DEG, var"#s179"} where var"#s179"<:(AbstractVector)) where DEG
sweep_all!(out, m::ParametricMap{DEG}, us::NTuple{DEG})
Fill out[:, α] with the θ^α coefficient of the kernel's form evaluated at the DEG displacement inputs us, for every α in the basis.
Computing the whole series costs the same per coefficient as computing one, so this is the only sweep the per-α closures need.
zero_coefficient_report(m::MORFEFerrite.ParametricGeometry.AssembledParametricModel; rtol, probe)
zero_coefficient_report(m::AssembledParametricModel; rtol = 1e-12) -> NamedTuple
Which θ-multiindices contribute nothing to the assembled model.
Returns (; geometry, operators, maps, exponents):
geometry[i]—truewhen multiindexiis negligible in every geometry series (adj J,det J) at every quadrature point. The coordinate transform simply does not reach that degree.operators[i]—truewhen multiindexiis negligible in every linear operator's θ^α coefficient matrix. The transform may reach that degree while the weak form still annihilates it.maps[i]—truewhen multiindexiis negligible in every nonlinear form, probed at a pseudo-random state. A form that is identically zero gives zero for any input; a form that is not gives a nonzero result for a random input with probability 1. Setprobe = falseto skip (it costs one sweep per map).exponents— the multiindices themselves, for reporting.
The three are reported separately because they call for different responses: a geometry gap means the θ-basis can shrink at no cost, whereas an operator or form that annihilates a degree the geometry reaches is a property of the weak form or of a symmetry, and is worth understanding before it is relied on.
rtol is applied against the largest coefficient of the same quantity, so a model in newtons and one in millinewtons give the same answer.
Diagnostic only: nothing here changes the model. Use it to see whether a θ-basis is larger than the geometry justifies, or whether a symmetry you expected is actually present in the discretisation.
MORFEFerrite.StructuralSVK — Ferrite-backed St. Venant-Kirchhoff structural models for autonomous or harmonically forced invariant-manifold reductions.
using MORFE, MORFEFerrite
const SVK = MORFEFerrite.StructuralSVK
beam = SVK.mechanical_model(mesh; material, damping, dirichlet, fe_order, quad_order)
(; model, spectral, meta) = build_model(beam;
master = [1], expansion_order = 9)
W, R = MORFE.parametrise(model, spectral, 9;
resonance = ResonanceConfig(style = :complex_normal_form, tol = 0.05))
This module assembles the mechanical case, implements the shared build_model contract, and provides spectral and resonance-inspection helpers. build_model returns the MORFE.NthOrderModel and MORFE.SpectralData; MORFE.parametrise performs the physics-independent reduction and returns (W, R).
The low-level Ferrite entry points are svk_nonlinearity, which constructs a concrete MORFE.FEMMultilinearMap{2}, and svk_assemble_KM!, which assembles the linear stiffness and mass matrices.
AnisotropicMaterial
AnisotropicMaterial(D, ρ)
St. Venant-Kirchhoff material with a general 6×6 Voigt stiffness D, converted to SMatrix{6,6,Float64}, and density ρ, converted to Float64. The ordering is [11, 22, 33, 23, 13, 12] and the strain vector uses engineering shear components [ε₁₁, ε₂₂, ε₃₃, 2ε₂₃, 2ε₁₃, 2ε₁₂].
Use CubicCrystal for cubic crystals given by c₁₁, c₁₂, c₄₄.
AssembledMechanicalModel <: AbstractAssembledModel
Assembled three-dimensional second-order model on the free DOFs. Its linear operators are K, C, and M; its FEM multilinear maps accumulate the negative quadratic and cubic internal forces, so MORFE's model form represents M*ü + C*u̇ + K*u + f_int,nl(u) = f_ext.
term_factory(degree, max_cols) lazily creates the degree-2 or degree-3 MORFE.FEMMultilinearMap{2} with storage for max_cols batched columns. material, damping, and info retain backend data used by eigensolvers, post-processing, and summaries.
Indexing B
The linear operators live in one field, in derivative order, so B[k + 1] is the coefficient of the k-th time derivative:
m.B[1] # B₀ = K, stiffness
m.B[2] # B₁ = C, damping
m.B[3] # B₂ = M, mass
This is the order MORFE.NthOrderModel.linear_terms wants, and m.B is handed to it unchanged. It is also the reason the field exists: the operators used to be three separate fields declared K, M, C while the model tuple was (K, C, M), so the declaration order and the operator order disagreed and every call site had to restate the mapping. It is stated once now, here.
m.K, m.C and m.M remain available as read-only properties — for a structural problem the physics names read better than an index — and are exactly B[1], B[2], B[3].
CubicCrystal(; c11, c12, c44, ρ, rotation)
CubicCrystal(; c11, c12, c44, ρ, rotation = nothing) -> AnisotropicMaterial
Cubic crystal (silicon, germanium, …) from its three independent constants, with the crystal optionally rotated into the lab frame by rotation (a 3×3 matrix, or an angle in radians about the z axis).
rotation = nothing leaves the crystal axes unchanged; a scalar is interpreted as an angle in radians about the z axis, and a matrix is passed to rotate_voigt.
The isotropic limit is c11 = λ + 2μ, c12 = λ, c44 = μ; it reproduces the constitutive response of an SVKMaterial with those Lamé constants.
FerriteGeometricNonlinearity{DEG, DH, CV, S} <: MORFE.FEMMultilinearMap{2}
Three-dimensional FEM-backed multilinear term for the negative SVK geometric internal force.
DEG = 2: quadratic form with two displacement inputs.DEG = 3: cubic form with three displacement inputs.
The MORFE.FEMMultilinearMap{2} supertype places the term in a second-order model. It uses only position inputs, with multiindex == (DEG, 0), and has no external-state factors. S is the concrete isotropic or anisotropic stress law.
dh, cv, free_to_local, and n_free describe the constrained Ferrite discretisation. The remaining storage is reusable evaluation state: the quadrature-gradient buffer has size (max_unique_cols, getnquadpoints(cv)), and the element vectors have ndofs_per_cell(dh) entries. A term instance is mutable through these buffers and is intended for one evaluation sweep at a time.
FerriteGeometricNonlinearity{DEG}(dh, cv, free_to_local, n_free, λ, μ;
max_unique_cols = DEG,
fully_asymmetric = false)
FerriteGeometricNonlinearity{DEG}(dh, cv, free_to_local, n_free, stress;
max_unique_cols = DEG,
fully_asymmetric = false)
Construct a degree-DEG term from Lamé constants or an internal stress-law object. free_to_local maps global cell DOFs to the free state. Buffers are preallocated from dh, cv, and max_unique_cols; the latter must cover the largest column batch evaluated by MORFE. fully_asymmetric is stored with the same symmetry-policy meaning as on MORFE.MultilinearMap.
HarmonicForcing
HarmonicForcing(; mode, amplitude, Ω = nothing)
Harmonic load specification for f(t) = amplitude · M*ϕ_mode · cos(Ω*t). mode is a positive physical mode-pair index used only for the load shape. If Ω is omitted, build_model uses abs(λ[2mode-1]) from the resolved spectrum.
build_model accepts either one of these or a vector of them (multi-harmonic excitation); each element adds its own pair of external states with eigenvalues ±iΩ, so N_EXT = 2 · length(forcing). mode need not be a master pair — it only supplies the load shape. MORFE's parametrisation may separately warn when a monomial frequency is near an eigenvalue left off the manifold, because that off-manifold solve is ill-conditioned independently of the forcing shape.
RayleighDamping
RayleighDamping(; α, β)
Rayleigh damping coefficients defining C = α M + β K. The constructor promotes α and β to a common numeric type.
SVKMaterial
SVKMaterial(; E, ν, ρ)
St. Venant-Kirchhoff material: Young's modulus E, Poisson ratio ν, density ρ, and derived Lamé constants λ = Eν/((1+ν)(1-2ν)), μ = E/(2(1+ν)). With the Green-Lagrange strain and this linear elastic stress law, the internal force has quadratic and cubic displacement terms and no higher-degree terms.
SVKPullbackKernel
SVKPullbackKernel{DEG}(material)
SVKPullbackKernel{DEG}(stress, ρ = 0.0)
Three-dimensional St. Venant-Kirchhoff quadrature kernel over a parametric coordinate transform. The public constructor accepts an SVKMaterial or AnisotropicMaterial; the lower-level constructor accepts the corresponding internal stress-law object and a density.
DEG = 2— the quadratic elastic formg(u₁,u₂;θ)DEG = 3— the cubic formh(u₁,u₂,u₃;θ)DEG = 0— the linear operators (stiffness and mass)
DEG = 2 and DEG = 3 use only the constitutive stress law. DEG = 0 also reads ρ to assemble the mass series, so its lower-level constructor must be given the material density. The kernel owns reusable quadrature scratch space and is intended for one assembly sweep at a time.
VoigtStress
VoigtStress(D)
Fully anisotropic stress, σ = D : ε, with D the 6×6 Voigt stiffness in the ordering [11, 22, 33, 23, 13, 12]. The input strain tensor is converted to the engineering-shear vector [ε₁₁, ε₂₂, ε₃₃, 2ε₂₃, 2ε₁₃, 2ε₁₂]; the result is returned as a symmetric second Piola-Kirchhoff stress tensor.
_forcings(::Nothing)
_forcings(f::HarmonicForcing)
_forcings(fs::AbstractVector{<:HarmonicForcing})
_forcings(forcing) -> Vector{<:HarmonicForcing}
Normalize the public forcing forms—nothing, one HarmonicForcing, or an abstract vector of them—to a vector. The autonomous case returns an empty vector.
assemble_KM!(K, M, dh, cv, λ::Real, μ::Real, ρ::Real)
assemble_KM!(K, M, dh, cv, stress::MORFEFerrite.StructuralSVK.AbstractStress, ρ::Float64)
assemble_KM!(K, M, dh, cv, λ, μ, ρ)
assemble_KM!(K, M, dh, cv, stress, ρ)
Assemble the three-dimensional global stiffness matrix K and mass matrix M into preallocated sparse matrices using isotropic Lamé constants or an internal stress-law object. The matrices must have the sparsity pattern associated with dh; both are mutated and the function returns nothing.
K_rs = ∫ ε(φ_r) ⊡ σ(ε(φ_s)) dΩ
M_rs = ∫ ρ φ_r · φ_s dΩ
For the Lamé overload, σ(ε) = λ tr(ε)I + 2με.
base_operators(m::MORFEFerrite.ParametricGeometry.AssembledParametricModel)
base_operators(m::AssembledParametricModel) -> (K, M)
Return the θ = 0 stiffness and mass coefficient matrices of an assembled parametric SVK case. These are the base-configuration operators used to solve the eigenproblem supplied to build_model.
build_model(m::MORFEFerrite.StructuralSVK.AssembledMechanicalModel; master, forcing, nev, spectrum, expansion_order, n_monomials)
build_model(m::AssembledMechanicalModel; master = [1], forcing = nothing,
nev = ..., spectrum = nothing, expansion_order = nothing,
n_monomials = nothing) -> (; model, spectral, meta)
Construct the full-order MORFE.NthOrderModel and MORFE.SpectralData consumed by MORFE.parametrise.
master is a nonempty, sorted vector of distinct positive physical mode-pair indices. The eigensolver returns adjacent conjugate eigenvalues, so pair p occupies spectrum entries 2p-1, 2p; for example, master = [1] gives ROM = 2 internal coordinates. Selected pairs may be non-leading and non-contiguous.
forcing accepts nothing, one HarmonicForcing, or a vector of them and represents
f(t) = Σₖ aₖ M ϕ_{pₖ} cos(Ωₖ t).
Forcing k appends external coordinates ROM+2k-1, ROM+2k with eigenvalues +iΩₖ, -iΩₖ. If Ωₖ is omitted it is abs(spectrum.eigenvalues[2pₖ-1]). The shape pair pₖ need not be a master pair.
If spectrum is omitted, spectrum is called with nev, where nev counts physical modes (and the returned spectrum contains 2nev conjugate eigenvalues). A supplied spectrum is reused without another eigensolve. In either case it must contain every master pair and every pair used as a forcing shape.
expansion_order and n_monomials size the batched-column cache owned by the FEM nonlinear terms; they do not construct or select MORFE's monomial set. For the standard total-degree set, expansion_order = p reserves binomial(ROM + N_EXT + p, p) - 1 columns. For a custom set, pass n_monomials = length(mset). n_monomials takes precedence if both are supplied; if neither is supplied, one column is reserved.
The result contains:
model: the autonomous or forcedMORFE.NthOrderModel;spectral: spectral data restricted tomaster, with the spectrum-wide adjacent conjugate pairing and the external pairing supplied by the model's external system;meta: backend reporting data, including the resolved forcings/frequencies, selected spectrum indices, external-state count, eigensolve time, and assembled-case metadata.
eigenfrequencies(m::MORFEFerrite.StructuralSVK.AssembledMechanicalModel; kwargs...)
eigenfrequencies(m::AssembledMechanicalModel; nev = 10, eigensolver = nothing)
-> Vector{ComplexF64}
Return the complex damped eigenvalues from spectrum. Physical mode p occupies adjacent conjugate entries 2p-1, 2p. For an underdamped pair, abs(imag(λ[2p-1]))/(2π) is its damped oscillation frequency in Hz; the undamped natural frequency ω used by the Rayleigh construction is distinct. For an underdamped or critically damped mode abs(λ) = ω; this identity does not hold for each individual eigenvalue of an overdamped pair.
Use the result to inspect the spectrum and choose master before parametrisation.
mechanical_model(grid::Ferrite.Grid, constrained_nodes::Set{Int64}; material, damping, fe_order, quad_order)
mechanical_model(grid::Ferrite.Grid; material, damping, dirichlet, fe_order, quad_order)
mechanical_model(mesh_path::AbstractString; dirichlet, scale, kwargs...)
mechanical_model(grid::Ferrite.Grid, constrained_nodes::Set{Int};
material, damping, fe_order = 2, quad_order = fe_order + 1)
mechanical_model(grid::Ferrite.Grid; material, damping, dirichlet,
fe_order = 2, quad_order = fe_order + 1)
mechanical_model(mesh_path::AbstractString; material, damping, dirichlet,
scale = 1.0, fe_order = 2, quad_order = fe_order + 1)
Assemble a three-dimensional AssembledMechanicalModel from a Ferrite grid or mesh file. The displacement interpolation is Lagrange{RefShape,fe_order}()^3; the returned K, M, and C = αM + βK are restricted to free DOFs, while the model retains the Ferrite handlers needed to construct its quadratic and cubic SVK terms lazily.
material is an SVKMaterial or AnisotropicMaterial, and damping is a RayleighDamping. All three displacement components are fixed on the selected nodes or facets:
The
constrained_nodesoverload accepts an already computed set of node indices.For a Ferrite grid or Gmsh
.mshpath,dirichletis the name of a facet set.For a COMSOL
.mphtxtpath,dirichletis aSet{Int}of 1-based boundary entity IDs (the raw COMSOL IDs plus one).scalerescales COMSOL node coordinates before assembly; it is ignored for Gmsh input.
parametric_model(dh, cv, geometry; geometry_parameter_basis, material, damping, free, base, inverse_determinant)
parametric_model(dh, cv, geometry; geometry_parameter_basis, material, damping,
free = nothing, base = nothing,
inverse_determinant = PowerSeriesInverseDet())
-> AssembledParametricModel
Assemble the parameter expansion of a three-dimensional SVK structure over a parametric mesh coordinate transform.
geometry is a provider returning (J₀, ∇ψ₁, …, ∇ψ_Nθ) per quadrature point, either analytically (geom(x₀)) or from an FE field (geom(x₀, cell, cv, q)).
geometry_parameter_basis is either one GeometryParameterBasis, shared by every form, or a NamedTuple with a required linear entry and optional quadratic and cubic entries. A missing quadratic basis falls back to linear; a missing cubic basis falls back to quadratic, then linear. Separate bases avoid over-expanding low-degree forms—for example, an isochoric affine arch has stiffness degree at most two but cubic-form degree at most four.
material is an SVKMaterial or AnisotropicMaterial. damping is a RayleighDamping and defines the assembled series C(θ) = αM(θ) + βK(θ). If free is omitted, every DOF in dh is retained; otherwise it supplies the global free-DOF indices. base is stored in the returned case for physics-side bookkeeping. inverse_determinant selects how the pullback cache constructs the series for 1/det(J).
This function returns an assembled parametric case, not an NthOrderModel. Calling build_model on it produces ORD = 3: a parameter-dependent mass is a correction on the highest derivative of the original second-order system, so the augmented representation needs one additional, zero linear block.
print_mode_table(eigenvalues::AbstractVector; master, io)
print_mode_table(eigenvalues; master = Int[], io = stdout)
Tabulate adjacent conjugate pairs in eigenvalues, ignoring an unmatched final entry. Each row reports real(λ) as the decay rate and imag(λ) as the damped angular frequency, together with imag(λ)/(2π) in Hz. Pairs listed in master are marked. Output is written to io and the function returns nothing.
print_resonances(mset, resonance_set, master_eigenvalues::Vector{ComplexF64}; io)
print_resonances(mset, resonance_set, master_eigenvalues; io = stdout)
Print the monomials flagged as resonant for each inner target and any diagnostic outer targets. Inner targets are labelled with master_eigenvalues; because this function is not passed the outer eigenvalues, outer target labels use 0 as a placeholder. Inner resonances are the terms retained in the reduced dynamics R; outer flags diagnose off-manifold solves and do not add rows to R.
probe_dof(m::MORFEFerrite.StructuralSVK.AssembledMechanicalModel, node::Integer, direction::Integer)
probe_dof(m::AssembledMechanicalModel, node, direction) -> Int
Free-DOF index of direction (1 = x, 2 = y, 3 = z) at mesh node: the row of K, M and of the parametrisation W that carries that node's displacement.
This is the bridge between a physical quantity named the way a person names it ("the transverse displacement at mid-span") and the integer MORFE.observable_polynomial wants:
u = observable_polynomial(W, SVK.probe_dof(case, 289, 2))
Throws if the node is constrained, since a constrained DOF has no row. Wraps Common.free_dofs_at_nodes for the single-node case.
resonances(eigenvalues::AbstractVector, master::Vector{Int64}, order::Int64; external_eigenvalues, outer_eigenvalues, resonance_tol, resonance_tol_rel, mset)
resonances(eigenvalues, master, order; external_eigenvalues = ComplexF64[],
outer_eigenvalues = ComplexF64[], resonance_tol = 0.05,
resonance_tol_rel = nothing, mset = nothing)
-> (mset, resonance_set, master_eigenvalues)
Build the monomial set and the complex-normal-form ResonanceSet that a parametrise call with the same arguments would use. Call it on the output of eigenfrequencies to preview — before paying for the cohomological solve — which monomials will be kept in the reduced dynamics.
master uses the same sorted physical-pair indexing as build_model: pair p occupies eigenvalues[2p-1:2p]. external_eigenvalues appends prescribed external coordinates to the monomial variables. If mset is omitted, all monomials of total degree 1:order in the internal and external variables are created; otherwise the supplied set is returned and order does not choose its contents.
resonance_tol is the absolute detuning threshold in the eigenvalues' frequency units (rad/s for structural spectra). When resonance_tol_rel is given, the inner target λⱼ instead uses resonance_tol_rel * abs(λⱼ).
outer_eigenvalues adds off-manifold resonance targets, populating the outer_resonances block of the returned set (query it with resonant_multiindices(rset, ROM + j)). Those flags are diagnostic: the cohomological solve reads only the inner block. It cannot be combined with resonance_tol_rel, because MORFE shares one tolerance object between the inner and outer target blocks.
rotate_voigt(D::AbstractMatrix, Q::AbstractMatrix)
rotate_voigt(D, Q) -> SMatrix{6,6}
Rotate a Voigt stiffness matrix by a 3×3 matrix Q, expected to be orthogonal. Only its size is validated.
Q maps crystal axes to lab axes: if D is expressed in the crystal frame, the result is expressed in the lab frame (pass Q' for the opposite convention).
Implemented by expanding D to the 4th-order stiffness C_ijkl, applying the tensor transformation C'_ijkl = Q_ip Q_jq Q_kr Q_ls C_pqrs, and contracting back — so no Bond-matrix convention (and its factor-of-2 traps) is involved.
spectrum(m::MORFEFerrite.StructuralSVK.AssembledMechanicalModel; nev, eigensolver)
spectrum(m::AssembledMechanicalModel; nev = 10, eigensolver = nothing)
-> Spectrum
Solve the assembled model's eigenproblem. nev requests physical modes; the result holds 2nev eigenvalues and eigenmodes in adjacent conjugate pairs.
The default is StructureModalDampingEigensolver built from the model's own Rayleigh coefficients m.damping. It solves the undamped problem K ϕ = ω² M ϕ once by shift-invert, mass-normalises ϕ, and builds the damped eigenvalues and the left eigenvector blocks in closed form, which Rayleigh damping makes possible. It never forms the first-order 2n × 2n pencil, so it stays fast on large meshes.
A supplied eigensolver is used as-is. In particular, callers supplying a different modal-damping solver are responsible for making its damping consistent with m.C.
Pass the returned MORFE.Spectrum to build_model as spectrum = ... to inspect or report it without paying for a second solve—and without letting a repeated iterative eigensolve choose a different basis in a clustered eigenspace.
svk_assemble_KM!(K, M, dh, cv, material::Union{MORFEFerrite.StructuralSVK.AnisotropicMaterial, SVKMaterial})
svk_assemble_KM!(args...; kwargs...)
svk_assemble_KM!(K, M, dh, cv, λ, μ, ρ)
svk_assemble_KM!(K, M, dh, cv, material)
Assemble the three-dimensional linear stiffness K and mass M matrices in place with the Ferrite SVK backend. Supply either Lamé constants and density or an SVKMaterial/AnisotropicMaterial. K and M must be preallocated with the sparsity pattern of dh; the function returns nothing.
svk_nonlinearity(degree::Integer, dh, cv, free_to_local, n_free, material::Union{MORFEFerrite.StructuralSVK.AnisotropicMaterial, SVKMaterial}; kwargs...)
svk_nonlinearity(degree::Integer, args...; kwargs...)
svk_nonlinearity(degree, dh, cv, free_to_local, n_free, λ, μ;
max_unique_cols = degree, fully_asymmetric = false)
svk_nonlinearity(degree, dh, cv, free_to_local, n_free, material;
max_unique_cols = degree, fully_asymmetric = false)
Construct a Ferrite-backed St. Venant-Kirchhoff geometric nonlinearity term of the given polynomial degree (2 for the quadratic form, 3 for the cubic form) as a MORFE.FEMMultilinearMap{2}. The constitutive law may be supplied as Lamé constants λ, μ or as an SVKMaterial/AnisotropicMaterial.
free_to_local maps global Ferrite DOFs into the n_free-component state. The max_unique_cols cache must accommodate the column batches used by the reduction; fully_asymmetric is forwarded to MORFE's multilinear-term symmetry policy.
voigt_stiffness(m::MORFEFerrite.StructuralSVK.AnisotropicMaterial)
voigt_stiffness(m::SVKMaterial)
voigt_stiffness(material) -> SMatrix{6,6}
Return the SMatrix{6,6,Float64} Voigt stiffness of a supported material. AnisotropicMaterial returns its stored matrix; SVKMaterial constructs the isotropic matrix from its Lamé constants. This is useful for inspection and for cross-checking anisotropic input.
MORFEFerrite.FluidNavierStokes — incompressible Navier-Stokes DPIM backend (Taylor-Hood P2/P1, cylinder-flow class of problems), promoted from examples/05_karman_vortex_street.
Pipeline (see the Kármán example driver):
fom = setup_fem(meshfile) # Ferrite P2/P1 spaces + BCs
s0 = solve_steady_state(fom; Re0) # Newton base flow
ops = assemble_linear_operators(s0, fom; Re0) # linearised operators
conv = FluidConvection(fom; max_unique_cols) # f₂(s,s) FEMMultilinearMap{1}
g1 = make_param_coupling(K_visc_free) # −D·η′·K_raw·u′ (Re-parametric)
h0 = make_base_forcing(h₀_vec_free) # base-flow forcing direction
Mesh generation (Gmsh) deliberately stays example-local; setup_fem only reads a .msh via FerriteGmsh.