Skip to content

Bases, variance and the metric

The one notion TensND has that a Cartesian-only tensor library does not: a tensor carries a basis, which need not be orthonormal, and a variance saying how each index transforms. This tutorial builds an oblique basis, moves components between variances, and checks the identities of Bases and variance — symbolically and numerically.

The definitions in one place: for a basis the dual basis satisfies   , and the two metrics are

julia
using TensND
using LinearAlgebra
using SymPy
using Tensors

An oblique basis

The constructor takes the new basis vectors as columns expressed in the canonical basis. Nothing here is orthogonal or normalized.

julia
= Basis(Sym[1 0 0; 0 1 0; 0 1 1])

The four stored matrices. vecbasis(ℬ, :cov) holds the , vecbasis(ℬ, :cont) the dual vectors .

julia
vecbasis(ℬ, :cov)

julia
vecbasis(ℬ, :cont)

The covariant metric and its inverse :

julia
metric(ℬ, :cov)

julia
metric(ℬ, :cont)

The defining identities

  and   :

julia
tsimplify(metric(ℬ, :cont) * metric(ℬ, :cov))

julia
tsimplify(transpose(vecbasis(ℬ, :cont)) * vecbasis(ℬ, :cov))

The basis is neither orthogonal nor orthonormal, which is exactly why variance will matter below:

julia
isorthogonal(ℬ), isorthonormal(ℬ)
(false, false)

Covariant and contravariant components of a vector

The same vector, two sets of components. TensND stores a tensor together with its basis and variance, and components converts.

julia
V = Tens(Tensor{1, 3}(i -> symbols("v$i", real = true)))

Contravariant components (the coefficients on ):

julia
components(V, ℬ, (:cont,))

Covariant components (the projections on ):

julia
components(V, ℬ, (:cov,))

They are related by the metric,  :

julia
tsimplify(metric(ℬ, :cov) * components(V, ℬ, (:cont,)) - components(V, ℬ, (:cov,)))

Order 2: four variance combinations

An order- tensor has one choice per index, so an order-2 tensor has four.

julia
T = Tens(Tensor{2, 3}((i, j) -> symbols("t$i$j", real = true)))

julia
components(T, ℬ, (:cov, :cov))

julia
tfactor(tsimplify(components(T, ℬ, (:cont, :cov))))

The cheapest sanity check on a basis

Store the covariant metric as a twice-covariant tensor and raise one index. Since  , the answer must be the identity — whatever the basis.

julia
G = Tens(metric(ℬ, :cov), ℬ, (:cov, :cov))
components(G, (:cont, :cov))

Normalization removes the scaling, not the obliquity

Dividing each vector by its norm gives unit vectors, so the metric acquires a unit diagonal — but the off-diagonal terms, which measure the angles between the vectors, survive. Variance still matters.

julia
ℬ̄ = normalize(ℬ)
metric(ℬ̄, :cov)

julia
isorthogonal(ℬ̄), isorthonormal(ℬ̄)
(false, false)

Where variance becomes invisible

On an orthonormal basis   , so the two sets of components coincide and the distinction collapses. This is the case for CanonicalBasis and RotatedBasis, and it is why tensors on those bases carry no variance tuple at all.

julia
θ, ϕ, ψ = symbols("θ ϕ ψ", real = true)
ℬʳ = Basis(θ, ϕ, ψ)
typeof(ℬʳ)
RotatedBasis{3, Sym{PyCall.PyObject}}
julia
isorthonormal(ℬʳ), tsimplify(metric(ℬʳ, :cov))
(true, Sym{PyCall.PyObject}[1 0 0; 0 1 0; 0 0 1])

The same vector expressed on a rotated basis: covariant and contravariant components are equal.

julia
W = Tens(Tensor{1, 3}(i -> symbols("w$i", real = true)))
tsimplify(components(W, ℬʳ, (:cov,)) - components(W, ℬʳ, (:cont,)))

Numerically, on an orthogonal (scaled) basis

An OrthogonalBasis is an orthonormal basis with a scaling factor per direction — the situation of the natural basis of a curvilinear coordinate system, where the are the Lamé coefficients (see Curvilinear differential calculus). Its metric is diagonal,  .

julia
ℬ₀ = Basis(0.3, 0.7, 0.0)          # orthonormal, rotated
χ = (2.0, 3.0, 0.5)                 # scaling factors
ℬχ = Basis(ℬ₀, χ)
typeof(ℬχ)
OrthogonalBasis{3, Float64}
julia
round.(metric(ℬχ, :cov), digits = 12)
3×3 LinearAlgebra.Diagonal{Float64, Vector{Float64}}:
 4.0   ⋅    ⋅ 
  ⋅   9.0   ⋅ 
  ⋅    ⋅   0.25

Diagonal, with entries :

julia
round.(diag(metric(ℬχ, :cov)) .- [χ[i]^2 for i in 1:3], digits = 14)
3-element Vector{Float64}:
 0.0
 0.0
 0.0

and the dual vectors are  :

julia
round.(
    vecbasis(ℬχ, :cont) .- vecbasis(ℬχ, :cov) * diagm([1 / χ[i]^2 for i in 1:3]),
    digits = 14,
)
3×3 Matrix{Float64}:
 0.0  -0.0  0.0
 0.0   0.0  0.0
 0.0   0.0  0.0

Summary

Basis typemetricvariance matters?
CanonicalBasisno
RotatedBasisno
OrthogonalBasisyes, diagonally
Basisfullyes

The constructor always returns the most specific type that applies, so the cost of a component conversion is decided by the basis, not by the caller.


This page was generated using Literate.jl.