Projection
Closest tensor of a prescribed material symmetry, and symmetry detection. Theory: Projection onto a symmetry class; usage: Projection.
The orientation-optimizing methods of proj_tens require NLopt (package extension TensNDNLoptExt).
TensND.proj_tens Function
proj_tens(::Val{:TI}, A::AbstractArray{T,4}, n) → (TensTI{4, T, 5}, d, drel)Project a 4th-order tensor A (3×3×3×3) onto the transversely isotropic subspace with fixed symmetry axis n. Returns a major-symmetric TensTI{4, T, 5}.
The projection minimizes the Frobenius distance ‖B − A‖ over all TI tensors B with axis n.
Returns a 3-tuple (B, d, drel):
B: the projectedTensTI{4, T, 5}d: absolute Frobenius distance‖B − A‖drel: relative distanced / ‖A‖
Examples
julia> n = [0., 0., 1.];
julia> C = tens_TI(10., 3., 2.5, 12., 2., n);
julia> B, d, drel = proj_tens(:TI, get_array(C), n);
julia> d < 1e-12
true
julia> arg_TI(B) == arg_TI(C)
trueproj_tens(::Val{:TI}, A::AbstractArray{T,2}, n) → (TensTI{2,T,2}, d, drel)Project a 2nd-order tensor A (3×3) onto the transversely isotropic subspace with fixed symmetry axis n. Returns a TensTI{2,T,2}.
In the rotated frame where e₃ = n:
a = (M[1,1] + M[2,2]) / 2(transverse)b = M[3,3](axial)
Examples
julia> n = [0., 0., 1.];
julia> A = [5. 0 0; 0 5 0; 0 0 8];
julia> B, d, drel = proj_tens(:TI, A, n);
julia> B.data
(5.0, 8.0)proj_tens(::Val{:ORTHO}, A::AbstractArray{T,4}, frame::OrthonormalBasis{3}) → (TensOrtho{T}, d, drel)Project a 4th-order tensor A (3×3×3×3) onto the orthotropic subspace with fixed material frame frame. Returns a TensOrtho{T}.
Examples
julia> frame = CanonicalBasis{3,Float64}();
julia> t = TensOrtho(10., 8., 12., 3., 2.5, 1.5, 2., 3., 3.5, frame);
julia> B, d, drel = proj_tens(:ORTHO, get_array(t), frame);
julia> d < 1e-12
trueproj_tens(::Val{:CUBIC}, A::AbstractArray{T,4}, frame::OrthonormalBasis{3}) → (TensCubic{T}, d, drel)Project a 4th-order tensor onto the cubic subspace with cube axes frame.
Read drel rather than trusting the projection. Where the morphology and the medium leave the octahedral group invariant, the answer belongs to the class by group theory, so its distance to it is discretization error and nothing else — an error estimate that costs nothing and involves no reference solution. Where they do not, drel says how much was discarded, and the projection is a fit like any other.
Examples
julia> frame = CanonicalBasis{3,Float64}();
julia> t = tens_cubic(10.0, 4.0, 2.0, frame);
julia> B, d, drel = proj_tens(:CUBIC, get_array(t), frame);
julia> d < 1e-12
trueSee also best_fit_cubic, cubic_anisotropy.
proj_tens(::Val{:CUBIC}, A::AbstractArray{T,2}, frame) → (TensISO{2,3,T}, d, drel)At order two the cubic class is the isotropic class, the octahedral group leaving no second-order tensor invariant but a multiple of the identity. The frame is accepted and ignored, and the isotropic projection is returned.
That is not a shortcut: it is why a cube-symmetric pore has a single scalar resistivity contribution while its compliance contribution needs three constants, and why a conduction computation on such a shape carries no anisotropy signal whatever the shape does in elasticity.
proj_tens(::Val{:ORTHO}, A::AbstractArray{T,2}, frame::OrthonormalBasis{3}) → (Array{T,2}, d, drel)Project a 2nd-order tensor A (3×3) onto the orthotropic subspace with fixed material frame frame. The projection is diag(M₁₁, M₂₂, M₃₃) in the material frame.
Examples
julia> frame = CanonicalBasis{3,Float64}();
julia> A = [5. 1 2; 1 8 3; 2 3 12];
julia> B, d, drel = proj_tens(:ORTHO, A, frame);
julia> B ≈ diagm([5., 8., 12.])
trueproj_tens(sym::Symbol, A::AbstractArray, arg) → (projected, d, drel)Convenience dispatch: proj_tens(:TI, A, n) calls proj_tens(Val(:TI), A, n).
proj_tens(::Val{:TI}, A::AbstractArray{T,4}) where {T<:AbstractFloat}Find the best TI approximation of A by optimizing over all possible symmetry axes. Requires the NLopt package: using NLopt.
See also proj_tens(::Val{:TI}, A, n) for fixed-axis projection.
proj_tens(::Val{:TI}, A::AbstractArray{T,2}) where {T<:AbstractFloat}Find the best TI approximation of a 2nd-order tensor A by optimizing the symmetry axis. Requires the NLopt package: using NLopt.
proj_tens(::Val{:ORTHO}, A::AbstractArray{T,4}) where {T<:AbstractFloat}Find the best orthotropic approximation of A by optimizing over all possible material frames. Requires the NLopt package: using NLopt.
proj_tens(::Val{:CUBIC}, A::AbstractArray{T,4}) where {T<:AbstractFloat}Find the best cubic approximation of A by optimizing over all possible cube orientations. Requires the NLopt package: using NLopt.
The octahedral group is discrete, but the orientation of the cube is not: it is an ordinary rotation, so the objective is smooth in the Euler angles exactly as it is for a TI axis or an orthotropic frame. The only trace the group leaves is that the optimum is 24-fold degenerate — the optimizer returns one of the 24 equivalent frames, and they all describe the same tensor with the same coefficients (see TensCubic).
See also proj_tens(::Val{:CUBIC}, A, frame) for fixed-frame projection.
proj_tens(::Val{:CUBIC}, A::AbstractArray{T,2}) where {T<:AbstractFloat}At order two the cubic class is the isotropic class, which has no orientation to optimize, so this is the isotropic projection and needs no NLopt.
proj_tens(::Val{:ORTHO}, A::AbstractArray{T,2}) where {T<:AbstractFloat}Find the best orthotropic approximation of a 2nd-order tensor A by optimizing the material frame. Requires the NLopt package: using NLopt.
TensND.best_sym_tens Function
best_sym_tens(t; proj=(:ISO, :CUBIC, :TI, :ORTHO), ε=1e-6, optimize_angles=false)Find the best (most restrictive) symmetry of tensor t by trying each symmetry class in proj (from most to least symmetric) and accepting the first whose relative projection error is below ε.
optimize_angles=false(default, cheap path, no optimization): the:ISOprojection is closed-form; for:TIthe symmetry axis is taken fromtitself (if it is a structured TI container) or derived from the Kelvin-Mandel eigenstructure (otherwise); for:ORTHOthe material frame is taken fromtor derived likewise. No external optimizer needed.optimize_angles=true: the:TIaxis and:ORTHOframe are found by nonlinear optimization (multistart L-BFGS) — requiresusing NLopt.
Returns (projected, d, drel, sym) where sym ∈ {:ISO, :CUBIC, :TI, :ORTHO, :ANISO}.
Behavior change (vs. pre-2026 versions): the default no-argument call no longer throws when NLopt is absent; set optimize_angles=true to restore the previous angle-optimized behavior.
Examples
julia> n = [0., 0., 1.];
julia> C = tens_TI(10., 3., 2.5, 12., 2., n);
julia> _, _, _, sym = best_sym_tens(C);
julia> sym === :TI
trueSee also best_sym_tens(t, n_or_frame) for fixed-axis/frame use, proj_tens.
best_sym_tens(t, n_or_frame; proj=(:ISO, :CUBIC, :TI, :ORTHO), ε=1e-6)Find the best symmetry of tensor t with a fixed symmetry axis n (for TI) or material frame frame (for ORTHO). No rotation optimization is performed.
n_or_frame: a vector (axis for TI) orOrthonormalBasis{3}(frame for ORTHO and CUBIC). For ISO projection the extra argument is ignored.
Because there is one argument for two kinds of orientation, the classes it cannot serve are dropped from proj: an axis leaves :ISO and :TI, a frame leaves :ISO, :CUBIC and :ORTHO. Pass the kind that matches the classes you mean to test.
Returns (projected, d, drel, sym).
Examples
julia> n = [0., 0., 1.];
julia> C = tens_TI(10., 3., 2.5, 12., 2., n);
julia> _, _, drel, sym = best_sym_tens(C, n);
julia> sym == :TI && drel < 1e-12
truebest_sym_tens(t; proj=(:ISO, :CUBIC, :TI, :ORTHO), ε=1e-6, optimize_angles=false)Find the best (most restrictive) symmetry of tensor t by trying each symmetry class in proj (from most to least symmetric) and accepting the first whose relative projection error is below ε.
optimize_angles=false(default, cheap path, no optimization): the:ISOprojection is closed-form; for:TIthe symmetry axis is taken fromtitself (if it is a structured TI container) or derived from the Kelvin-Mandel eigenstructure (otherwise); for:ORTHOthe material frame is taken fromtor derived likewise. No external optimizer needed.optimize_angles=true: the:TIaxis and:ORTHOframe are found by nonlinear optimization (multistart L-BFGS) — requiresusing NLopt.
Returns (projected, d, drel, sym) where sym ∈ {:ISO, :CUBIC, :TI, :ORTHO, :ANISO}.
Behavior change (vs. pre-2026 versions): the default no-argument call no longer throws when NLopt is absent; set optimize_angles=true to restore the previous angle-optimized behavior.
Examples
julia> n = [0., 0., 1.];
julia> C = tens_TI(10., 3., 2.5, 12., 2., n);
julia> _, _, _, sym = best_sym_tens(C);
julia> sym === :TI
trueSee also best_sym_tens(t, n_or_frame) for fixed-axis/frame use, proj_tens.
best_sym_tens(t, n_or_frame; proj=(:ISO, :CUBIC, :TI, :ORTHO), ε=1e-6)Find the best symmetry of tensor t with a fixed symmetry axis n (for TI) or material frame frame (for ORTHO). No rotation optimization is performed.
n_or_frame: a vector (axis for TI) orOrthonormalBasis{3}(frame for ORTHO and CUBIC). For ISO projection the extra argument is ignored.
Because there is one argument for two kinds of orientation, the classes it cannot serve are dropped from proj: an axis leaves :ISO and :TI, a frame leaves :ISO, :CUBIC and :ORTHO. Pass the kind that matches the classes you mean to test.
Returns (projected, d, drel, sym).
Examples
julia> n = [0., 0., 1.];
julia> C = tens_TI(10., 3., 2.5, 12., 2., n);
julia> _, _, drel, sym = best_sym_tens(C, n);
julia> sym == :TI && drel < 1e-12
trueTensND.is_ISO Function
is_ISO(A::AbstractArray; ε=1e-6) → BoolReturn true when the components of A satisfy material isotropy up to relative tolerance ε (closed-form projection: no optimization).
The value-level predicate complements the type-level is_ISO(::TensISO) = true defined in tens_isotropic.jl: the type version asks whether the container imposes isotropy, this version asks whether the numerical components satisfy it.
Examples
julia> C = [1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0];
julia> is_ISO(C)
true
julia> is_ISO([1.0 2.0 0.0; 2.0 3.0 0.0; 0.0 0.0 1.0])
falseTensND.is_TI Function
is_TI(A)Return true if A is a TensTI{4}, indicating transverse isotropy.
is_TI(A::AbstractArray, n; ε=1e-6) → BoolReturn true when A is transversely isotropic about the given axis n up to relative tolerance ε (closed-form projection on the fixed axis).
Examples
julia> n = [0.0, 0.0, 1.0];
julia> C = tens_TI(10., 3., 2.5, 12., 2., n);
julia> is_TI(get_array(C), n)
true
julia> is_TI(get_array(C), [1.0, 0.0, 0.0])
falseis_TI(A::AbstractArray; ε=1e-6, optimize_angles=false) → BoolCheap default: propose a candidate TI axis from the Kelvin-Mandel eigendecomposition of A and check the residual. With optimize_angles=true, runs the NLopt-backed axis search — requires using NLopt.
See also is_TI(A, n), _candidate_TI_axis.
TensND.is_ORTHO Function
is_ORTHO(A::AbstractArray, frame::OrthonormalBasis{3}; ε=1e-6) → BoolReturn true when A is orthotropic in the given material frame up to relative tolerance ε (closed-form projection on the fixed frame).
is_ORTHO(A::AbstractArray; ε=1e-6, optimize_angles=false) → BoolCheap default: propose a candidate orthotropic frame from the Kelvin-Mandel eigendecomposition and check the residual. With optimize_angles=true, runs the NLopt-backed frame search — requires using NLopt.
TensND.is_CUBIC Function
is_CUBIC(t) -> BoolWhether t is stored as a TensCubic. A type-level query, like is_ISO and is_TI: it says what the container guarantees, not whether the components happen to satisfy a tighter symmetry.
is_CUBIC(A::AbstractArray, frame::OrthonormalBasis{3}; ε = 1.0e-6) -> BoolWhether A is cubic about frame to within ε, measured as the relative Frobenius distance to the class.
The value-level companion of the type-level is_CUBIC(::TensCubic), exactly as for is_ISO, is_TI and is_ORTHO: the first asks what the components say, the second what the container guarantees.
is_CUBIC(A::AbstractArray; ε = 1.0e-6) -> BoolWhether A is cubic about some cube. Requires NLopt, and unlike is_TI and is_ORTHO it has no cheap alternative: every second-order contraction of a cubic tensor is isotropic, so no eigenframe reveals where the cube points and there is nothing to start from but a search. See the note above _candidate_ORTHO_frame's cubic counterpart in this file.
Give the frame — is_CUBIC(A, frame) — whenever it is known; that path is exact and costs one projection.
TensND.ti_params_from_KM Function
ti_params_from_KM(C::AbstractMatrix) → (ℓ₁, ℓ₂, ℓ₃, ℓ₅, ℓ₆)Project a 6×6 Kelvin-Mandel matrix (in a frame where e₃ is the TI symmetry axis) onto the major-symmetric TI subspace, returning the 5 Walpole coefficients. See proj_tens for the tensor-level equivalent.
TensND.KM_from_ti_params Function
KM_from_ti_params(ℓ₁, ℓ₂, ℓ₃, ℓ₅, ℓ₆) → SMatrix{6,6}Build a 6×6 Kelvin-Mandel matrix (in the frame where e₃ is the TI axis) from 5 major-symmetric Walpole coefficients — the reciprocal of ti_params_from_KM.
TensND.ortho_params_from_KM Function
ortho_params_from_KM(C::AbstractMatrix) → NTuple{9}Extract the 9 orthotropic parameters (C₁₁,C₂₂,C₃₃,C₁₂,C₁₃,C₂₃,C₄₄,C₅₅,C₆₆) from a 6×6 Kelvin-Mandel matrix in the material frame. See proj_tens for the tensor-level equivalent.
TensND.KM_from_ortho_params Function
KM_from_ortho_params(C₁₁, C₂₂, C₃₃, C₁₂, C₁₃, C₂₃, C₄₄, C₅₅, C₆₆) → SMatrix{6,6}Build a 6×6 Kelvin-Mandel matrix in the material frame from 9 orthotropic parameters — the reciprocal of ortho_params_from_KM.
TensND.cubic_params_from_KM Function
cubic_params_from_KM(C::AbstractMatrix) → (α, β, γ)Extract the three cubic coefficients on (𝕁, 𝔼, 𝕋) from a 6×6 Kelvin-Mandel matrix in the cube frame. See proj_tens for the tensor-level equivalent, and arg_cubic for the engineering constants.
TensND.KM_from_cubic_params Function
KM_from_cubic_params(α, β, γ) → SMatrix{6,6}Build a 6×6 Kelvin-Mandel matrix in the cube frame from the three cubic coefficients — the reciprocal of cubic_params_from_KM.
TensND.best_fit_iso Function
best_fit_iso(t::AbstractTens) -> TensISOOrthogonal (Frobenius) projection of t onto span(𝕁, 𝕂) at order 4, onto span(𝟏) at order 2 — the projection component of proj_tens(Val(:ISO), t).
For a minor-symmetric tensor this coincides with the exact SO(3) average isotropify; for a general one it does not, and the average is the meaningful object.
TensND.best_fit_ti Function
best_fit_ti(t::AbstractTens{4,3}, axis) -> TensTI{4,T,5}
best_fit_ti(t::AbstractTens{2,3}, axis) -> TensTI{2,T,2}Orthogonal (Frobenius) projection of t onto the major-symmetric Walpole span about axis — the projection component of proj_tens(Val(:TI), t, axis).
A projection, not an orientation average
It forces major symmetry, (ℓ₃+ℓ₄)/2, and drops the antisymmetric azimuthal couplings ℓ₇, ℓ₈. On a concentration tensor that content is physical and its loss is silent. Use transverse_isotropify to average, and this only to report parameters.
TensND.best_fit_ortho Function
best_fit_ortho(t::AbstractTens{4,3}, frame) -> TensOrtho
best_fit_ortho(t::AbstractTens{2,3}, frame) -> MatrixOrthogonal (Frobenius) projection of t onto the orthotropic span in the given material frame — the projection component of proj_tens( Val(:ORTHO), t, frame). Same caveat as best_fit_ti: a fit, not an average.
TensND.best_fit_cubic Function
best_fit_cubic(t::AbstractTens{4,3}, frame) -> TensCubic
best_fit_cubic(t::AbstractTens{2,3}, frame) -> TensISO{2,3}Orthogonal (Frobenius) projection of t onto the cubic span with cube axes frame — the projection component of proj_tens(Val(:CUBIC), t, frame).
Unlike best_fit_ti, this loses nothing that a major-symmetric result would have kept: the cubic class is automatically major-symmetric, so there is no antisymmetric content to drop silently. What it does discard is everything outside the class, and proj_tens reports how much.
At order two it returns an isotropic tensor, the two classes coinciding there.
Exact rotation-group averages
A projection is a least-squares fit and drops whatever does not fit; a rotation-group average is exact and lossless on the invariant subspace. The two coincide only when the tensor already belongs to the class. Use the averages inside a computation — on a concentration or contribution tensor, which carries no major symmetry — and the fits only to report parameters.
TensND.isotropify Function
isotropify(A) → TensISOClosest isotropic tensor to A for the Frobenius distance, i.e. its orthogonal projection onto span(𝕁, 𝕂):
ISO(𝕋) = (𝕋 ⊙ 𝕁) 𝕁 + (𝕋 ⊙ 𝕂)/(𝕂 ⊙ 𝕂) 𝕂For an order-2 tensor this reduces to tr(A)/dim · 𝟏.
It does not commute with inversion
isotropify(inv(A)) ≠ inv(isotropify(A)): the Euclidean distance is not invariant under inversion, so projecting a stiffness and projecting the corresponding compliance give different isotropic materials. State which one was projected whenever a result is reported.
See also proj_tens, Isotropic tensors.
isotropify(t::AbstractTens{4,3}) → TensISO{4}Exact average of t over SO(3): α = T_iijj/3, β = (T_ijij − α)/5 → α𝕁 + β𝕂. Valid for minor-symmetric tensors with or without major symmetry (the isotropic subspace is {𝕁, 𝕂} in both cases), which is what distinguishes it from the AbstractArray method above — the latter is a Frobenius projection and the two coincide exactly on minor-symmetric input.
isotropify(t::AbstractTens{2,3}) → TensISO{2}Exact SO(3) average of a 2nd-order tensor: (tr t / 3) 𝟏.
TensND.transverse_isotropify Function
transverse_isotropify(t::AbstractTens{4,3}, n) → TensTI{4,T,8}Exact average of t over all rotations about the axis n ((1/2π)∫ R_φ ⋆ t dφ), for a minor-symmetric t with or without major symmetry. The result lives in the full 8-dimensional axially-invariant space: the non-major-symmetric components (ℓ₃ ≠ ℓ₄) and the antisymmetric azimuthal couplings (ℓ₇, ℓ₈) are preserved — they are dropped by naive symmetric TI projections but present in e.g. averaged strain-concentration tensors.
transverse_isotropify(t::AbstractTens{2,3}, n) → TensTI{2,T,3}Exact azimuthal average of a 2nd-order tensor about n: a·nT + b·nₙ + c·w with b = n̂ᵀ t n̂, a = (tr t − b)/2 and c = (w : t)/2 (w the in-plane rotation generator w·p = n̂ × p). The antisymmetric in-plane part c is preserved (a symmetric TI parametrization would silently drop it).
TensND.mandel66_minor Function
mandel66_minor(arr::AbstractArray{T,4}) → Matrix{T} (6×6)Kelvin-Mandel 6×6 matrix of a 4th-order array, minor-symmetrizing on read (exactly, so tensors that are minor-symmetric up to round-off are cleanly projected). Index convention (11, 22, 33, 23, 13, 12), weights √2.
Unlike KM this makes no assumption of major symmetry and never goes through a Tensors.SymmetricTensor, so it accepts a concentration tensor.
TensND.array_from_mandel66 Function
array_from_mandel66(M::AbstractMatrix{T}) → Array{T,4}Inverse of mandel66_minor: rebuild the (exactly minor-symmetric) 3×3×3×3 array from a 6×6 Kelvin-Mandel matrix.
TensND.ti8_params_from_KM Function
ti8_params_from_KM(M::AbstractMatrix) → NTuple{8}Coefficients (ℓ₁, …, ℓ₈) of the exact azimuthal average about e₃ of a minor-symmetric tensor given by its 6×6 Kelvin-Mandel matrix (Tensors.jl index order 11, 22, 33, 23, 13, 12). Orthogonal projection onto the commutant algebra; TensND Walpole convention (ℓ₃ ↔ the C₃₃₁₁ side, ℓ₄ ↔ the C₁₁₃₃ side).
Two distinct uses, and the difference matters:
as an average, on a matrix that is not axially invariant — this is what
transverse_isotropifycalls;as an exact read-off, on a matrix already known to be axially invariant about
e₃, where the projection is the identity and the eight coefficients are simply recovered. This is the non-major-symmetric counterpart ofti_params_from_KM, which projects onto the 5-coefficient major-symmetric span and would silently discardℓ₃ ≠ ℓ₄,ℓ₇andℓ₈— precisely the content a concentration tensor carries.
Reciprocal: KM_from_ti8_params.
TensND.KM_from_ti8_params Function
KM_from_ti8_params(p::NTuple{8,T}) → Matrix{T} (6×6)Kelvin-Mandel matrix (Tensors order, axis e₃) of the axially-invariant tensor with coefficients (ℓ₁,…,ℓ₈). Inverse of ti8_params_from_KM on the commutant subspace, and the 8-coefficient counterpart of KM_from_ti_params.
TensND.ti_average_mandel66 Function
ti_average_mandel66(M::AbstractMatrix, n) → Matrix (6×6)Exact azimuthal average about n of the minor-symmetric tensor whose 6×6 Kelvin-Mandel matrix (Tensors.jl order) is M, returned as a 6×6 matrix.
TensND.iso_average_mandel66 Function
iso_average_mandel66(M::AbstractMatrix{T}) → (α, β)Exact SO(3) average of the minor-symmetric tensor whose Kelvin-Mandel matrix is M: α = (1/3)Σ_{i,j≤3} M[i,j], β = (tr M − α)/5. Returns the (α, β) coefficients of α𝕁 + β𝕂.