Skip to content

Symbolic and numeric ​

TensND is generic in its element type. The same code paths run on Float64, on ForwardDiff.Dual, on SymPy's Sym and on Symbolics' Num, which is what lets a derivation be done symbolically and the result evaluated — or differentiated — numerically without rewriting anything.

The four scalar worlds ​

Element typeComes fromUse for
Float64 (and any Real)Basenumerical work
ForwardDiff.DualForwardDiffderivatives and sensitivities
SymSymPysymbolic derivation, dsolve, solve
NumSymbolicsnative Julia CAS, code generation
julia
using TensND, SymPy

k, μ = symbols("k μ", positive = true)
ℂ = TensISO{3}(3k, 2μ)
get_data(inv(ℂ))
(1/(3*k), 1/(2*μ))

The same with Symbolics:

julia
using TensND, Symbolics

Symbolics.@variables kn μn
get_data(inv(TensISO{3}(3kn, 2μn)))
(1 / (3kn), 1 / (2μn))

What is generic, and what is not ​

tensor algebrastructured typesprojection, fixed axisprojection, optimizedsymbolic operatorsnumerical operators
Float64✓✓✓✓—✓
ForwardDiff.Dual✓✓✓——✓
Sym✓✓✓—✓—
Num✓✓✓partial✓—

The one genuine restriction is the orientation search: it is a numerical optimization, so it needs a numeric element type. Projection onto a class with a given axis or frame is closed-form and works symbolically.

Symbolic helpers ​

Six functions apply a symbolic operation elementwise to a tensor, an array or a scalar, and are no-ops on numeric types — so generic code can call them unconditionally:

FunctionDoesSymPySymbolics
tsimplifysimplify✓✓
tfactorfactorize✓—
tsubssubstitute✓✓
tdiffdifferentiate✓✓
ttrigsimptrigonometric simplification✓—
texpand_trigtrigonometric expansion✓—
julia
tsimplify(3.0), tsimplify(k + k)
(3.0, 2*k)

Being no-ops on Float64 is the point: a function written with tsimplify sprinkled through it runs unchanged, and at full speed, on numeric input.

Differentiating ​

ForwardDiff composes with the tensor algebra, the structured types and the projections:

julia
using TensND, ForwardDiff, LinearAlgebra

n = [0.0, 0.0, 1.0]
f(α) = get_data(proj_tens(:TI, get_array(tens_TI(10.0, 3.0, 2.5, 12.0, 2.0, [sin(α), 0.0, cos(α)])), n)[1])[1]

ad = ForwardDiff.derivative(f, 0.3)
fd = (f(0.3 + 1.0e-6) - f(0.3 - 1.0e-6)) / 2.0e-6
(ad, fd)
(-5.32346083364259, -5.323460833928095)

and with the numerical differential operators, in both directions — with respect to the evaluation point and with respect to a parameter carried by the field. See Numerical coordinate systems.

Interoperability with Tensors.jl ​

TensND stores its data in Tensors.jl containers whenever the shape allows, so the two compose directly and index symmetries are preserved:

julia
using TensND, Tensors, LinearAlgebra

st = SymmetricTensor{2, 3}((i, j) -> Float64(i + j))
t = Tens(st)
typeof(get_array(t))
Tensors.SymmetricTensor{2, 3, Float64, 6}
julia
C4 = SymmetricTensor{4, 3}((i, j, k, l) -> Float64(i + j + k + l))
norm(get_array(inv_KM(KM(Tens(C4)))) - get_array(Tens(C4)))
2.5121479338940403e-15

Performance notes ​

Symbolic element types are orders of magnitude slower than numeric ones, and expression size — not operation count — dominates. Three practical consequences:

  • simplify early and often. An unsimplified intermediate propagates into every later expression. This is what the rules / tmp_var / to_coords machinery of CoorSystemSym exists for.

  • derive symbolically, evaluate numerically. Obtain the closed form once, then substitute with tsubs or generate a numeric function from it.

  • prefer a structured type over a dense array whenever the physics allows — the measured gains are one to three orders of magnitude, see Performance of the structured types.

For pointwise numerical work, CoorSystemNum avoids the symbolic layer entirely and costs a constant amount per point.