npm: possibly marvellous

    algebra

    1.0.1 • Public • Published

    algebra

    means completeness and balancing, from the Arabic word الجبر

    NPM version Badge size Build Status JavaScript Style Guide Change log

    Algebra OnQuaternionsAndOctonions

    Table Of Contents

    Features

    Installation

    With npm do

    npm install algebra

    or use a CDN adding this to your HTML page

    <script src="https://unpkg.com/algebra/dist/algebra.min.js"></script>

    Quick start

    This is a 60 seconds tutorial to get your hands dirty with algebra.

    NOTA BENE Imagine all code examples below as written in some REPL where expected output is documented as a comment.

    All code in the examples below should be contained into a single file, like test/quickStart.js.

    First of all, import algebra package.

    const algebra = require('algebra')

    Scalars

    Use the Real numbers as scalars.

    const R = algebra.Real

    Every operator is implemented both as a static function and as an object method.

    Static operators return raw data, while class methods return object instances.

    Use static addition operator to add three numbers.

    R.add(1, 2) // 3

    Create two real number objects: x = 2, y = -2

    // x will be overwritten, see below
    let x = new R(2)
    const y = new R(-2)

    The value r is the result of x multiplied by y.

    // 2 * (-2) = -4
    const r = x.mul(y)
     
    // Scalar { data: -4 }
     
    // x and y are not changed
    x.data // 2
    y.data // -2

    Raw numbers are coerced, operators can be chained when it makes sense. Of course you can reassign x, for example, x value will be 0.1: x -> x + 3 -> x * 2 -> x ^-1

    // ((2 + 3) * 2)^(-1) = 0.1
    = x.add(3).mul(2).inv()
     
    // Scalar { data: 0.1 }

    Comparison operators equal and notEqual are available, but they cannot be chained.

    x.equal(0.1) // true
    x.notEqual(Math.PI) // true

    You can also play with Complexes.

    const C = algebra.Complex
     
    let z1 = new C([1, 2])
    const z2 = new C([3, 4])
     
    z1 = z1.mul(z2)
     
    z1 // Scalar { data: [-5, 10] }
     
    z1 = z1.conj().mul([2, 0])
     
    z1.data // [-10, -20]

    Vectors

    Create vector space of dimension 2 over Reals.

    const R2 = algebra.VectorSpace(R)(2)

    Create two vectors and add them.

    let v1 = new R2([0, 1])
    const v2 = new R2([1, -2])
     
    // v1 -> v1 + v2 -> [0, 1] + [1, -2] = [1, -1]
    v1 = v1.add(v2)
     
    v1 // Vector { data: [1, -1] }

    Matrices

    Create space of matrices 3 x 2 over Reals.

    const R3x2 = algebra.MatrixSpace(R)(3, 2)

    Create a matrix.

    //       | 1 1 |
    //  m1 = | 0 1 |
    //       | 1 0 |
    //
    const m1 = new R3x2([1, 1,
                         0, 1,
                         1, 0])

    Multiply m1 by v1, the result is a vector v3 with dimension 3. In fact we are multiplying a 3 x 2 matrix by a 2 dimensional vector, but v1 is traited as a column vector so it is like a 2 x 1 matrix.

    Then, following the row by column multiplication law we have

    //  3 x 2  by  2 x 1  which gives a   3 x 1
    //      ↑      ↑
    //      +------+----→  by removing the middle indices.
    //
    //                   | 1 1 |
    //    v3 = m1 * v1 = | 0 1 | * [1 , -1] = [0, -1, 1]
    //                   | 1 0 |
     
    const v3 = m1.mul(v1)
     
    v3.data // [0, -1, 1]

    Let's try with two square matrices 2 x 2.

    const R2x2 = algebra.MatrixSpace(R)(2, 2)
     
    let m2 = new R2x2([1, 0,
                       0, 2])
     
    const m3 = new R2x2([0, -1,
                         1,  0])
     
    m2 = m2.mul(m3)
     
    m2 // Matrix { data: [0, -1, 2, 0] }

    Since m2 is a square matrix we can calculate its determinant.

    m2.determinant // Scalar { data: 2 }

    API

    About operators

    All operators can be implemented as static methods and as object methods. In both cases, operands are coerced to raw data. As an example, consider addition of vectors in a Cartesian Plane.

    const R2 = algebra.R2
     
    const vector1 = new R2([1, 2])
    const vector2 = new R2([3, 4])

    The following static methods, give the same result: [4, 6].

    R2.addition(vector1, [3, 4])
    R2.addition([1, 2], vector2)
    R2.addition(vector1, vector2)

    The following object methods, give the same result: a vector instance with data [4, 6].

    const vector3 = vector1.addition([3, 4])
    const vector4 = vector1.addition(vector2)
     
    R2.equal(vector3, vector4) // true

    Operators can be chained when it makes sense.

    vector1.addition(vector1).equality([2, 4]) // true

    Objects are immutable

    vector1.data // still [1, 2]

    CompositionAlgebra

    A composition algebra is one of ℝ, ℂ, ℍ, O: Real, Complex, Quaternion, Octonion. A generic function is provided to iterate the Cayley-Dickson construction over any field.

    CompositionAlgebra(field[, num])

    • num can be 1, 2, 4 or 8

    Let's use for example the algebra.Boole which implements Boolean Algebra by exporting an object with all the stuff needed by algebra-ring npm package.

    const CompositionAlgebra = algebra.CompositionAlgebra
     
    const Boole = algebra.Boole
     
    const Bit = CompositionAlgebra(Boole)
     
    Bit.contains(false) // true
    Bit.contains(4) // false
     
    const bit = new Bit(true)
    Bit.addition(false).data // true

    Not so exciting, let's build something more interesting. Let's pass a second parameter, that is used to build a Composition algebra over the given field. It is something experimental also for me, right now I am writing this but I still do not know how it will behave. My idea (idea feliz) is that

    A byte is an octonion of bits

    Maybe we can discover some new byte operator, taken from octonion rich algebra structure. Create an octonion algebra over the binary field, a.k.a Z2 and create the eight units.

    // n must be a power of two
    const Byte = CompositionAlgebra(Boole, 8)
     
    // Use a single char const for better indentation.
    const t = true
    const f = false
     
    const byte1 = new Byte([t, f, f, f, f, f, f, f])
    const byte2 = new Byte([f, t, f, f, f, f, f, f])
    const byte3 = new Byte([f, f, t, f, f, f, f, f])
    const byte4 = new Byte([f, f, f, t, f, f, f, f])
    const byte5 = new Byte([f, f, f, f, t, f, f, f])
    const byte6 = new Byte([f, f, f, f, f, t, f, f])
    const byte7 = new Byte([f, f, f, f, f, f, t, f])
    const byte8 = new Byte([f, f, f, f, f, f, f, t])

    The first one corresponds to one, while the rest are immaginary units. Every imaginary unit multiplied by itself gives -1, but since the underlying field is homomorphic to Z2, -1 corresponds to 1.

    byte1.mul(byte1).data // [t, f, f, f, f, f, f, f]
    byte2.mul(byte2).data // [t, f, f, f, f, f, f, f]
    byte3.mul(byte3).data // [t, f, f, f, f, f, f, f]
    byte4.mul(byte4).data // [t, f, f, f, f, f, f, f]
    byte5.mul(byte5).data // [t, f, f, f, f, f, f, f]
    byte6.mul(byte6).data // [t, f, f, f, f, f, f, f]
    byte7.mul(byte7).data // [t, f, f, f, f, f, f, f]
    byte8.mul(byte8).data // [t, f, f, f, f, f, f, f]

    You can play around with this structure.

    const max = byte1.add(byte2).add(byte3).add(byte4)
                     .add(byte5).add(byte6).add(byte7).add(byte8)
     
    max.data // [t, t, t, t, t, t, t, t]

    Scalar

    The scalars are the building blocks, they are the elements you can use to create vectors and matrices. They are the underneath set enriched with a ring structure which consists of two binary operators that generalize the arithmetic operations of addition and multiplication. A ring that has the commutativity property is called abelian (in honour to Abel) or also a field.

    Ok, let's make a simple example. Real numbers, with common addition and multiplication are a scalar field.

    The good new is that you can create any scalar field as long as you provide a set with two internal operations and related neutral elements that satisfy the ring axioms.

    We are going to create a scalar field using BigInt elements to implement something similar to a Rational Number. The idea is to use a couple of numbers, the first one is the numerator and the second one the denominator.

    Arguments we need are the same as algebra-ring. Let's start by unities; every element is a couple of numbers, the numerator and the denominator, hence unitites are:

    • zero: [ BigInt(0), BigInt(1) ]
    • one: [ BigInt(1), BigInt(1) ]

    We need a function that computes the Great Common Divisor.

    function greatCommonDivisor (a, b) {
      if (=== BigInt(0)) {
        return a
      } else {
        return greatCommonDivisor(b, a % b)
      }
    }

    So now we can normalize a rational number, by removing the common divisors of numerator and denominator.

    function normalizeRational ([numerator, denominator]) {
      const divisor = greatCommonDivisor(numerator, denominator)
     
      return [numerator / divisor, denominator / divisor]
    }
    const Rational = algebra.Scalar({
      zero: [BigInt(0), BigInt(1)],
      one: [BigInt(1), BigInt(1)],
      equality: ([n1, d1], [n2, d2]) => (n1 * d2 === n2 * d1),
      contains: ([n, d]) => (typeof n === 'bigint' && typeof d === 'bigint'),
      addition: ([n1, d1], [n2, d2]) => normalizeRational([n1 * d2 + n2 * d1, d1 * d2]),
      negation: ([n, d]) => ([-n, d]),
      multiplication: ([n1, d1], [n2, d2]) => normalizeRational([n1 * n2, d1 * d2]),
      inversion: ([n, d]) => ([d, n])
    })

    So far so good, algebra dependencies will do some checks under the hood and will complain if something looks wrong.

    Let's create few rational numbers.

    const half = new Rational([BigInt(1), BigInt(2)])
    const two = new Rational([BigInt(2), BigInt(1)])

    Scalar.one

    Is the neutral element for multiplication operator.

    Rational.one // [1n, 1n]

    Scalar.zero

    Is the neutral element for addition operator.

    Rational.zero // [0n, 1n]

    scalar.data

    The data attribute holds the raw data underneath our scalar instance.

    half.data // [1n, 2n]

    Scalar.contains(scalar)

    Checks a given argument is contained in the scalar field that was defined.

    Rational.contains(half) // true
    Rational.contains([1n, 2n]) // true

    scalar1.belongsTo(Scalar)

    This is a class method that checks a scalar instance is contained in the given scalar field.

    half.belongsTo(Rational) // true

    Scalar.equality(scalar1, scalar2)

    Is a static method

    Rational.equality(half, [BigInt(5), BigInt(10)])

    scalar1.equals(scalar2)

    half.equals([BigInt(2), BigInt(4)])

    Scalar.disequality(scalar1, scalar2)

    Rational.disequality(half, two) // true

    scalar1.disequality(scalar2)

    half.disequality(two) // true

    Scalar.addition(scalar1, scalar2)

    Rational.addition(half, two) // [5n , 2n]

    scalar1.addition(scalar2)

    half.addition(two) // Scalar { data: [5n, 2n] }

    Scalar.subtraction(scalar1, scalar2)

    Rational.subtraction(two, half) // [3n , 2n]

    scalar1.subtraction(scalar2)

    two.multiplication(half) // Scalar { data: [1n, 1n] }

    Scalar.multiplication(scalar1, scalar2)

    Rational.multiplication(half, two) // [1n, 1n]

    scalar1.multiplication(scalar2)

    half.multiplication(two) // Scalar { data: [1n, 1n] }

    Scalar.division(scalar1, scalar2)

    Rational.division(two, half) // [1n, 4n]

    scalar1.division(scalar2)

    half.division(two) // Scalar { data: [1n, 4n] }

    Scalar.negation(scalar)

    Rational.negation(two) // [-2n, 1n]

    scalar.negation()

    two.negation() // Scalar { data: [-2n, 1n] }

    Scalar.inversion(scalar)

    Rational.inversion(two) // [1n, 2n]

    scalar.inversion()

    two.inversion() // Scalar { data: [1n, 2n] }

    Real

    Inherits everything from Scalar. Implements algebra of real numbers.

    const Real = algebra.Real
     
    Real.addition(1, 2) // 3
     
    const pi = new Real(Math.PI)
    const twoPi = pi.mul(2)
     
    Real.subtraction(twoPi, 2 * Math.PI) // 0

    Complex

    Inherits everything from Scalar.

    It is said the Gauss brain is uncommonly big and folded, much more than the Einstein brain (both are conserved and studied). Gauss was one of the biggest mathematicians and discovered many important results in many mathematic areas. One of its biggest intuitions, in my opinion, was to realize that the Complex number field is geometrically a plane. The Complex numbers are an extension on the Real numbers, they have a real part and an imaginary part. The imaginary numbers, as named by Descartes later, were discovered by italian mathematicians Cardano, Bombelli among others as a trick to solve third order equations.

    Complex numbers are a goldmine for mathematics, they are incredibly rich of deepest beauty: just as a divulgative example, take a look to the Mandelbrot set, but please trust me, this is nothing compared to the divine nature of Complex numbers.

    Mandelbrot Set{:.responsive}

    The first thing I noticed when I started to study the Complex numbers is conjugation. Every Complex number has its conjugate, that is its simmetric counterparte respect to the Real numbers line.

    const Complex = algebra.Complex
     
    const complex1 = new Complex([1, 2])
     
    complex1.conjugation() // Complex { data: [1, -2] }

    Quaternion

    Inherits everything from Scalar.

    Quaternions are not commutative, usually if you invert the operands in a multiplication you get the same number in absolute value but with the sign inverted.

    const Quaternion = algebra.Quaternion
     
    const j = new Quaternion([0, 1, 0, 0])
    const k = new Quaternion([0, 0, 1, 0])
     
    // j * k = - k * j
    j.mul(k).equal(k.mul(j).neg()) // true

    Octonion

    Inherits everything from Scalar.

    Octonions are not associative, this is getting hard: a * (b * c) could be equal to the negation of (a * b) * c.

    const Octonion = algebra.Octonion
     
    const a = new Octonion([0, 1, 0, 0, 0, 0, 0, 0])
    const b = new Octonion([0, 0, 0, 0, 0, 1, 0, 0])
    const c = new Octonion([0, 0, 0, 1, 0, 0, 0, 0])
     
    // a * ( b * c )
    const abc1 = a.mul(b.mul(c)) // Octonion { data: [0, 0, 0, 0, 0, 0, 0, -1] }
     
    // (a * b) * c
    const abc2 = a.mul(b).mul(c) // Octonion { data: [0, 0, 0, 0, 0, 0, 0, 1] }
     
    Octonion.equality(Octonion.negation(abc1), abc2)

    Common spaces

    R

    The real line.

    It is in alias of Real.

    const R = algebra.R

    R2

    The Cartesian Plane.

    const R2 = algebra.R2

    It is in alias of VectorSpace(Real)(2).

    R3

    The real space.

    const R3 = algebra.R3

    It is in alias of VectorSpace(Real)(3).

    R2x2

    Real square matrices of rank 2.

    const R2x2 = algebra.R2x2

    It is in alias of MatrixSpace(Real)(2).

    C

    The complex numbers.

    It is in alias of Complex.

    const C = algebra.C

    C2x2

    Complex square matrices of rank 2.

    const C2x2 = algebra.C2x2

    It is in alias of MatrixSpace(Complex)(2).

    H

    Usually it is used the H in honour of Sir Hamilton.

    It is in alias of Quaternion.

    const H = algebra.H

    Vector

    A Vector extends the concept of number, since it is defined as a tuple of numbers. For example, the Cartesian plane is a set where every point has two coordinates, the famous (x, y) that is in fact a vector of dimension 2. A Scalar itself can be identified with a vector of dimension 1.

    We have already seen an implementation of the plain: R2.

    If you want to find the position of an airplain, you need latitute, longitude but also altitude, hence three coordinates. That is a 3-ple, a tuple with three numbers, a vector of dimension 3.

    An implementation of the vector space of dimension 3 over reals is given by R3.

    VectorSpace(Scalar)(dimension)

    Vector dimension

    Strictly speaking, dimension of a Vector is the number of its elements.

    Vector.dimension

    It is a static class attribute.

    R2.dimension // 2
    R3.dimension // 3
    vector.dimension

    It is also defined as a static instance attribute.

    const vector = new R2([1, 1])
     
    vector.dimension // 2

    Vector norm

    The norm, at the end, is the square of the vector length: the good old Pythagorean theorem. It is usually defined as the sum of the squares of the coordinates. Anyway, it must be a function that, given an element, returns a positive real number. For example in Complex numbers it is defined as the multiplication of an element and its conjugate: it works as a well defined norm. It is a really important property since it shapes a metric space. In the Euclidean topology it gives us the common sense of space, but it is also important in other spaces, like a functional space. In fact a norm gives us a distance defined as its square root, thus it defines a metric space and hence a topology: a lot of good stuff.

    Vector.norm()

    Is a static operator that returns the square of the lenght of the vector.

    R2.norm([3, 4]).data // 25

    vector.norm

    This implements a static attribute that returns the square of the length of the vector instance.

    const vector = new R2([1, 2])
     
    vector.norm.data // 5

    Vector addition

    Vector.addition(vector1, vector2)
    R2.addition([2, 1], [1, 2]) // [3, 3]
    vector1.addition(vector2)
    const vector1 = new R2([2, 1])
    const vector2 = new R2([2, 2])
     
    const vector3 = vector1.addition(vector2)
     
    vector3 // Vector { data: [4, 3] }

    Vector cross product

    It is defined only in dimension three. See Cross product on wikipedia.

    Vector.crossProduct(vector1, vector2)
    R3.crossProduct([3, -3, 1], [4, 9, 2]) // [-15, 2, 39]
    vector1.crossProduct(vector2)
    const vector1 = new R3([3, -3, 1])
    const vector2 = new R3([4, 9, 2])
     
    const vector3 = vector1.crossProduct(vector2)
     
    vector3 // Vector { data: [-15, 2, 39] }

    Matrix

    MatrixSpace(Scalar)(numRows[, numCols])

    Matrix.isSquare

    Matrix.numCols

    Matrix.numRows

    Matrix multiplication

    Matrix.multiplication(matrix1, matrix2)
    matrix1.multiplication(matrix2)

    Matrix inversion

    It is defined only for square matrices which determinant is not zero.

    Matrix.inversion(matrix)
    matrix.inversion

    Matrix determinant

    It is defined only for square matrices.

    Matrix.determinant(matrix)
    matrix.determinant

    Matrix adjoint

    Matrix.adjoint(matrix1)
    matrix.adjoint

    License

    MIT

    Install

    npm i algebra

    DownloadsWeekly Downloads

    79

    Version

    1.0.1

    License

    MIT

    Unpacked Size

    186 kB

    Total Files

    31

    Last publish

    Collaborators

    • fibo