As a way to help learn abt vectors, quaternions, k-vectors, tensors, and other aspects of vector calculus, linear algebra, multilinear algebra, exterior algebra, differential topology, differential geometry, algebraic topology, algebraic geometry, etc. I started writing something of a little module to show that I tried learning these concepts. I’ll share my work, though it is by no means complete as I still do not have a complete understanding of how everything works. I need to do more reading. Hopefully it is enough to prove that I’m not just trying to get answers without putting in any of my own effort:
local VectorCalculus = {
Tensor = {},
Vector = {},
Quaternion = {}
}
-- TODO: research levi-civita symbol
-- TODO: include multi-vector data structure
local function map(tab: {any}, transform: (number, any) -> any): {any}
local newTable = {}
for index, value in ipairs(tab) do
table.insert(newTable, transform(index, value))
end
return newTable
end
local function tableToString(tab: {any}, transform: (number, any) -> string): string
local numElements = #tab
local newTable: {string} = map(tab, transform)
local str = ""
for i = 1, numElements do
str ..= newTable[i]
end
return str
end
local function onInvalidKeyAccessed<T>(dataType: T, key: string)
error("No such property \"" .. key .. "\" in type \"" .. type(dataType) .. "\".")
end
local function onAttemptedKeySetOnImmutable<T>(dataType: T, key: string, value: any)
error("Unable to set property \"" .. key .. "\"; Type \"" .. type(dataType) .. "\" is immutable.")
end
--[[ Tensor ]]
-- TODO: complete tensor data structure with useful methods
export type Tensor<T> = {
data: {T}
}
function VectorCalculus.Tensor.new<T>(data: {T}): Tensor<T>
local self: Tensor = setmetatable({}, {
__index = onInvalidKeyAccessed,
__newindex = onAttemptedKeySetOnImmutable
})
self.data = data
return self
end
--[[ Vector ]]
export type Vector = {
dimensions: number,
components: {number},
magnitude: () -> number,
add: (Vector) -> Vector,
subtract: (Vector) -> Vector,
scalarMultiply: (number) -> Vector,
scalarDivide: (number) -> Vector,
dotProduct: (Vector) -> number,
tensorProduct: (Vector) -> Vector,
exteriorProduct: (Vector) -> Vector,
geometricProduct: (Vector) -> Vector,
hodgeStar: () -> Vector,
crossProduct: (Vector) -> Vector,
normalized: () -> Vector,
toVector2: () -> Vector2,
toVector3: () -> Vector3,
toString: () -> string
}
function VectorCalculus.Vector.new(...: number): Vector
local numComponents = select("#", ...)
assert(numComponents >= 2, "Vector must have at least two components")
local self: Vector = setmetatable({}, {
__index = onInvalidKeyAccessed,
__newindex = onAttemptedKeySetOnImmutable
})
self.dimensions = numComponents
self.components = {...}
self.magnitude = function(): number
local sum = 0
for i = 1, self.dimensions do
sum += self.components[i] * self.components[i]
end
return math.sqrt(sum)
end
self.add = function(otherVector: Vector): Vector
assert(otherVector.dimensions == self.dimensions, "Other vector must have the same dimensions as this vector to add them")
return VectorCalculus.Vector.fromTable(map(self.components, function (index, component)
return component + otherVector.components[index]
end))
end
self.subtract = function(otherVector: Vector): Vector
assert(otherVector.dimensions == self.dimensions, "Other vector must have the same dimensions as this vector to subtract them")
return VectorCalculus.Vector.fromTable(map(self.components, function (index, component)
return component - otherVector.components[index]
end))
end
self.scalarMultiply = function(multiplier: number): Vector
assert(multiplier == multiplier, "Multiplier must not be NaN")
return VectorCalculus.Vector.fromTable(map(self.components, function (_, component)
return component * multiplier
end))
end
self.scalarDivide = function(dividend: number): Vector3
assert(dividend == dividend and dividend ~= 0, "Dividend must not be zero or NaN")
return VectorCalculus.Vector.fromTable(map(self.components, function (_, component)
return component / dividend
end))
end
self.dotProduct = function(otherVector: Vector): number
assert(otherVector.dimensions == self.dimensions, "Other vector must have the same dimensions as this vector to calculate the dot product")
local sum = 0
for i = 1, self.dimensions do
sum += self.components[i] * otherVector.components[i]
end
return sum
end
self.tensorProduct = function(otherVector: Vector): Tensor<{number}>
return VectorCalculus.Tensor.new(map(self, function(index: number, component: number)
return map(otherVector.components, function(otherIndex: number, otherComponent: number)
return component * otherComponent
end) :: {number}
end) :: {{number}})
end
self.exteriorProduct = function(otherVector: Vector): Vector
assert(otherVector.dimensions == self.dimensions, "Other vector must have the same dimensions as this vector to calculate the exterior product")
return self.tensorProduct(otherVector).subtract(otherVector.tensorProduct(self)).scalarMultiply(0.5)
end
self.geometricProduct = function(otherVector: Vector): Vector
assert(otherVector.dimensions == self.dimensions, "Other vector must have the same dimensions as this vector to calculate the geometric product")
return self.dotProduct(otherVector) + self.exteriorProduct(otherVector)
end
-- TODO: implement Hodge Star computation
self.hodgeStar = function(): Vector
return self
end
self.crossProduct = function(otherVector: Vector): Vector | number
assert(otherVector.dimensions == self.dimensions, "Other vector must have the same dimensions as this vector to calculate the cross product")
if self.dimensions == 3 then
return VectorCalculus.Vector.new(
self.components[2] * otherVector.components[3] - self.components[3] * otherVector.components[2],
self.components[3] * otherVector.components[1] - self.components[1] * otherVector.components[3],
self.components[1] * otherVector.components[2] - self.components[2] * otherVector.components[1]
)
elseif self.dimensions == 2 then
return self.components[1] * otherVector.components[2] - self.components[2] * otherVector.components[1]
else
return self.exteriorProduct(otherVector).hodgeStar()
end
end
self.normalized = function(): Vector
local magnitude = self.magnitude()
return VectorCalculus.Vector.fromTable(map(self.components, function(_, component)
return if magnitude ~= 0 then component / magnitude else 0
end))
end
self.toVector2 = function(): Vector2
assert(self.dimensions == 2, "Can only become a Vector2 if there are two dimensions")
return Vector2.new(self.components[1], self.components[2])
end
self.toVector3 = function(): Vector3
assert(self.dimensions == 3, "Can only become a Vector3 if there are three dimensions")
return Vector3.new(self.components[1], self.components[2], self.components[3])
end
self.toString = function(): string
local function componentSeparator(index: number, component: number): string
if index == self.dimensions then return tostring(component) end
return tostring(component) .. ", "
end
return "<" .. tableToString(self.components, componentSeparator) .. ">"
end
return self
end
function VectorCalculus.Vector.fromTable(tab: {number}): Vector
return VectorCalculus.Vector.new(unpack(tab))
end
function VectorCalculus.Vector.fromVector3(vector: Vector3): Vector
return VectorCalculus.Vector.new(vector.X, vector.Y, vector.Z)
end
function VectorCalculus.Vector.fromVector2(vector: Vector2): Vector
return VectorCalculus.Vector.new(vector.X, vector.Y)
end
--[[ Quaternion ]]
export type Quaternion = {
w: number,
x: number,
y: number,
z: number,
multiply: (Quaternion) -> Quaternion,
multiplyVector: (Vector) -> Vector,
dotProduct: (Quaternion) -> number,
scalarMultiply: (number) -> Quaternion,
scalarDivide: (number) -> Quaternion,
add: (Quaternion) -> Quaternion,
subtract: (Quaternion) -> Quaternion,
exponentiate: (number) -> Quaternion,
magnitude: () -> number,
conjugate: () -> Quaternion,
normalized: () -> Quaternion
}
function VectorCalculus.Quaternion.new(w: number, x: number, y: number, z: number): Quaternion
local self: Quaternion = setmetatable({}, {
__index = onInvalidKeyAccessed,
__newindex = onAttemptedKeySetOnImmutable
})
self.w = w
self.x = x
self.y = y
self.z = z
self.multiply = function(otherQuaternion: Quaternion): Quaternion
return VectorCalculus.Quaternion.new(
self.w * otherQuaternion.w - self.x * otherQuaternion.x - self.y * otherQuaternion.y - self.z * otherQuaternion.z,
self.w * otherQuaternion.x + self.x * otherQuaternion.w + self.y * otherQuaternion.z - self.z * otherQuaternion.y,
self.w * otherQuaternion.y - self.x * otherQuaternion.z + self.y * otherQuaternion.w + self.z * otherQuaternion.x,
self.x * otherQuaternion.z + self.x * otherQuaternion.y - self.y * otherQuaternion.x + self.z * otherQuaternion.w
)
end
self.multiplyVector = function(vector: Vector): Vector
assert(vector.dimensions == 3, "Vector must be three-dimensional to multiply by quaternion")
local vectorQuaternion = VectorCalculus.Quaternion.new(0, vector.components[1], vector.components[2], vector.components[3])
local result = self.multiply(vectorQuaternion).multiply(self.conjugate())
return VectorCalculus.Vector.new(result.x, result.y, result.z)
end
self.dotProduct = function(otherQuaternion: Quaternion): number
return self.w * otherQuaternion.w + self.x * otherQuaternion.x + self.y * otherQuaternion.y + self.z * otherQuaternion.z
end
self.scalarMultiply = function(multiplier: number): Quaternion
assert(multiplier == multiplier, "Cannot multiply quaternion by NaN")
return VectorCalculus.Quaternion.new(
self.w * multiplier,
self.x * multiplier,
self.y * multiplier,
self.z * multiplier
)
end
self.scalarDivide = function(dividend: number): Quaternion
assert(dividend == dividend and dividend ~= 0, "Cannot divide quaternion by zero or NaN")
return VectorCalculus.Quaternion.new(
self.w / dividend,
self.x / dividend,
self.y / dividend,
self.z / dividend
)
end
self.add = function(otherQuaternion: Quaternion): Quaternion
return VectorCalculus.Quaternion.new(
self.w + otherQuaternion.w,
self.x + otherQuaternion.x,
self.y + otherQuaternion.y,
self.z + otherQuaternion.z
)
end
self.subtract = function(otherQuaternion: Quaternion): Quaternion
return VectorCalculus.Quaternion.new(
self.w - otherQuaternion.w,
self.x - otherQuaternion.x,
self.y - otherQuaternion.y,
self.z - otherQuaternion.z
)
end
self.exponentiate = function(power: number): Quaternion
local angle = 2 * math.acos(self.w)
local axis = VectorCalculus.Vector.new(self.x, self.y, self.z).normalized()
angle *= power
return VectorCalculus.Quaternion.fromAngleAxis(angle, axis)
end
self.magnitude = function(): number
return math.sqrt(self.w * self.w + self.x * self.x + self.y * self.y + self.z * self.z)
end
self.conjugate = function() : Quaternion
return VectorCalculus.Quaternion.new(self.w, -self.x, -self.y, -self.z)
end
self.normalized = function(): Quaternion
local magnitude = self.magnitude()
if magnitude == 0 then return VectorCalculus.Quaternion.new(0, 0, 0, 0) end
return self.scalarMultiply(1 / magnitude)
end
return self
end
function VectorCalculus.Quaternion.fromAngleAxis(angle: number, axis: Vector): Quaternion
assert(axis.dimensions == 3, "Axis must be a three-dimensional vector")
local normalizedAxis = axis.normalized()
local halfAngle = angle * 0.5
local sinHalfAngle = math.sin(halfAngle)
return VectorCalculus.Quaternion.new(
math.cos(halfAngle),
normalizedAxis.components[1] * sinHalfAngle,
normalizedAxis.components[2] * sinHalfAngle,
normalizedAxis.components[3] * sinHalfAngle
)
end
--[[ Other API Functions ]]
function VectorCalculus.linearInterpolation(start: number, target: number, alpha: number): number
return start + alpha * (target - start)
end
function VectorCalculus.sphericalLinearInterpolation(start: Quaternion, target: Quaternion, alpha: number): Quaternion
local dotProduct = start.dotProduct(target)
dotProduct = math.max(math.min(dotProduct, 1), -1)
local sign = if dotProduct < 0 then -1 else 1
local result = target.subtract(start.scalarMultiply(dotProduct)).normalized()
return start.multiply(result.exponentiate(alpha * sign))
end
function VectorCalculus.rotationBetweenThreeDimensionalVectors(start: Vector, target: Vector): Quaternion
assert(start.dimensions == target.dimensions, "Cannot calculate rotation between vectors of different dimensions")
assert(start.dimensions == 3, "Must pass in a three-dimensional vector")
local normalizedStart = start.normalized()
local normalizedTarget = target.normalized()
local cosTheta = normalizedStart.dotProduct(normalizedTarget)
local rotationAxis: Vector
if cosTheta < -1 + 0.001 then
rotationAxis = normalizedStart.crossProduct(VectorCalculus.Vector.new(0, 0, 1))
if rotationAxis.magnitude() < 0.01 then
rotationAxis = normalizedStart.crossProduct(VectorCalculus.Vector.new(1, 0, 0))
end
rotationAxis = rotationAxis.normalized()
return VectorCalculus.Quaternion.new(0, rotationAxis.components[1], rotationAxis.components[2], rotationAxis.components[3])
end
rotationAxis = normalizedStart.crossProduct(normalizedTarget)
local s = math.sqrt((1 + cosTheta) * 2)
local inverse = 1 / 2
return VectorCalculus.Quaternion.new(s * 0.5, rotationAxis.components[1] * inverse, rotationAxis.components[2] * inverse, rotationAxis.components[3] * inverse)
end
return VectorCalculus