Practical Tips

This page collects small practical notes for writing Tensorial code clearly and efficiently.

Use concrete tensor field types

Tensor constructors can infer the tensor order N and the number of independent components L:

julia> @Tensor{Tuple{@Symmetry{3,3}}}SymmetricSecondOrderTensor{3, T, 6} where T (alias for Tensor{Tuple{Symmetry{Tuple{3, 3}}}, T, 2, 6} where T)

When a tensor type appears as a struct field, include all type parameters so the field type is concrete:

julia> isconcretetype(Tensor{Tuple{@Symmetry{3,3}}, Float64})false
julia> isconcretetype(Tensor{Tuple{@Symmetry{3,3}}, Float64, 2, 6})true

For example, prefer the fully specified field type in code that stores tensor values:

struct MaterialState{T}
    σ::Tensor{Tuple{@Symmetry{3,3}}, T, 2, 6} # same as SymmetricSecondOrderTensor{3,T,6}
end

For fixed-size aliases, the same rule applies. SymmetricSecondOrderTensor{3,T} is convenient in method signatures, but a concrete field type also needs the component-count parameter L, as in SymmetricSecondOrderTensor{3,T,6}. For the Julia background, see Avoid fields with abstract type in the Julia manual.

Put known symmetry in the type

If a value is symmetric, represent that in the tensor type. The tensor still indexes and displays like the full tensor, but only the independent components are stored:

julia> A = @Mat [1.0 2.0 3.0; 2.0 4.0 5.0; 3.0 5.0 6.0]3×3 Tensor{Tuple{3, 3}, Float64, 2, 9}:
 1.0  2.0  3.0
 2.0  4.0  5.0
 3.0  5.0  6.0
julia> S = SymmetricSecondOrderTensor{3}(A)3×3 SymmetricSecondOrderTensor{3, Float64, 6}: 1.0 2.0 3.0 2.0 4.0 5.0 3.0 5.0 6.0
julia> Tuple(S)(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)

This also tells later operations which tensor space to use:

julia> gradient(identity, S) isa SymmetricFourthOrderTensor{3}true

Use symmetric when you want to compute the symmetric part of a general second-order tensor. For the full notation, see Tensor Types and Spaces.

Specify @einsum result types when needed

@einsum infers free indices, but it cannot prove every symmetry of the result. If you know the result belongs to a symmetric tensor space, give that space explicitly:

julia> A = rand(Mat{3,3});
julia> S1 = @einsum A[k,i] * A[k,j]3×3 Tensor{Tuple{3, 3}, Float64, 2, 9}: 1.4203 0.875685 1.58267 0.875685 1.19535 1.39938 1.58267 1.39938 2.13089
julia> S1 isa SymmetricSecondOrderTensor{3}false
julia> S2 = @einsum SymmetricSecondOrderTensor{3} A[k,i] * A[k,j]3×3 SymmetricSecondOrderTensor{3, Float64, 6}: 1.4203 0.875685 1.58267 0.875685 1.19535 1.39938 1.58267 1.39938 2.13089
julia> S2 isa SymmetricSecondOrderTensor{3}true
julia> S1 ≈ S2true

Tensorial uses the annotated type as the result space. Use it only when the formula really has that symmetry.

Flatten at boundaries

Tensorial code usually reads best when tensor values stay as tensors. Use flatview, tovoigt, tomandel, and the corresponding inverse conversions at boundaries, such as solver interfaces, file formats, or code that specifically expects vectors and matrices:

julia> σ = SymmetricSecondOrderTensor{3}((2.0, 0.4, 0.2, 1.2, 0.1, 0.9))3×3 SymmetricSecondOrderTensor{3, Float64, 6}:
 2.0  0.4  0.2
 0.4  1.2  0.1
 0.2  0.1  0.9
julia> v = tovoigt(σ)6-element StaticArraysCore.SVector{6, Float64} with indices SOneTo(6): 2.0 1.2 0.9 0.1 0.2 0.4
julia> fromvoigt(SymmetricSecondOrderTensor{3}, v) ≈ σtrue

For symmetric tensor blocks inside direct sums, flatview uses Mandel scaling. See Voigt Form for the conversion rules.

Type inference when unpacking

unpack(x) returns all blocks of a direct-sum value as a tuple. The return type is concrete because the full block layout is known. The @code_warntype excerpts below omit the long inferred-code body and keep the parts relevant to type inference. In each excerpt, focus on the Body::... line: it shows the return type Julia inferred for the call.

For unpack(x), Julia knows the complete block layout and infers the concrete tuple type:

julia> x = pack(σ, 0.2)
2-element DirectSumVector with storage Float64:
 Space(Symmetry(3, 3),)
 Space()

julia> @code_warntype unpack(x)
MethodInstance for unpack(::DirectSumVector{...})
Arguments
  #self#::Core.Const(Tensorial.unpack)
  A::DirectSumVector{...}
Body::Tuple{SymmetricSecondOrderTensor{3, Float64, 6}, Float64}

For indexed access, the method receives the block index as an Int. Since the blocks can have different types, Julia infers a small Union:

julia> @code_warntype unpack(x, 1)
MethodInstance for unpack(::DirectSumVector{...}, ::Int64)
Arguments
  #self#::Core.Const(Tensorial.unpack)
  A::DirectSumVector{...}
  i::Int64
Body::Union{Float64, SymmetricSecondOrderTensor{3, Float64, 6}}

If the selected block is written as a constant inside a function, Julia can propagate that constant and infer the concrete block type:

julia> first_block(x) = unpack(x, 1);

julia> @code_warntype first_block(x)
MethodInstance for first_block(::DirectSumVector{...})
Arguments
  #self#::Core.Const(first_block)
  x::DirectSumVector{...}
Body::SymmetricSecondOrderTensor{3, Float64, 6}

Use indexed unpack for inspection or when the selected block is known to the compiler. Otherwise, prefer unpack(x) and destructure the returned tuple. For the full output, see the @code_warntype documentation.