/*
* Copyright (c) 2020 Joseph Rabinoff
* All rights reserved
*
* This file is part of linalg.js.
*
* linalg.js is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* linalg.js is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with linalg.js. If not, see <https://www.gnu.org/licenses/>.
*/
'use strict';
/** @module vector
*
* @file
* Implements a Vector class used for doing vector arithmetic.
*/
import { range } from "./util.js";
import Matrix from "./matrix.js";
/**
* @summary
* Class representing a vector.
*
* @desc
* A vector is a sequence of numbers of a fixed length, called the "length" of
* the vector.
*
* Note that `Vector` uses the `Array` constructor, so `new Vector(3)` will
* return an "empty" vector of length 3. Instead use the convenience routine
* {@link Vector.create}.
*
* @example
* Vector.create(1, 2, 3).toString(1); // "[1.0 2.0 3.0]"
*
* @extends Array
*/
class Vector extends Array {
/**
* @summary
* Create a Vector with the given entries.
*
* @desc
* This is an alias for `Vector.of`.
*
* @example {@lang javascript}
* Vector.create(1).toString(1); // "[1.0]"
* Vector.create(1, 2).toString(1); // "[1.0 2.0]"
*
* @param {...number} entries - The entries of the resulting Vector.
* @return {Vector} The vector with the given entries.
*/
static create(...entries) {
return Vector.from(entries);
}
/**
* @summary
* Create a Vector with `n` entries equal to `c`.
*
* @example {@lang javascript}
* Vector.constant(3, 1).toString(1); // "[1.0 1.0 1.0]"
*
* @param {integer} n - The size of the resulting Vector.
* @param {number} c - The value of the entries.
* @return {Vector} The vector `[c, c, ..., c]`.
*/
static constant(n, c) {
return Vector.from(range(n), () => c);
}
/**
* @summary
* Create a Vector with `n` entries equal to zero.
*
* @example {@lang javascript}
* Vector.zero(3).toString(1); // "[0.0 0.0 0.0]"
*
* @param {integer} n - The size of the resulting Vector.
* @return {Vector} The zero vector of size `n`.
*/
static zero(n) {
return Vector.constant(n, 0);
}
/**
* @summary
* Create a unit coordinate vector.
*
* @desc
* This is the Vector with all entries equal to 0, except the `i`th equal to
* 1.
*
* @example {@lang javascript}
* Vector.e(1, 3).toString(1); // "[0.0 1.0 0.0]"
*
* @param {integer} i - The nonzero entry.
* @param {integer} n - The size of the resulting Vector.
* @param {number} [λ=1] - The nonzero entry is set to this.
* @return {Vector} The `i`th unit coordinate vector in `R^n`.
*/
static e(i, n, λ=1) {
return Vector.from(range(n), j => j === i ? λ : 0);
}
/**
* @summary
* Check if an iterable of vectors is linearly independent.
*
* @desc
* This means that the vector equation
* `c1 v1 + c2 v2 + ... + cn vn = 0`
* has only the solution `c1 = c2 = ... = cn = 0`. Equivalently, the matrix
* with columns `v1, v2, ..., vn` has full column rank.
*
* @example {@lang javascript}
* Vector.isLinearlyIndependent(
* [Vector.create(1, 0, 0),
* Vector.create(0, 1, 0),
* Vector.create(0, 0, 1)]); // true
* Vector.isLinearlyIndependent(
* [Vector.create(1, 0, 0),
* Vector.create(0, 1, 0),
* Vector.create(1, 1, 0)]); // false
*
* @param {Vector[]} vecs - The vectors to check.
* @param {number} [ε=1e-10] - Entries smaller than this value are taken
* to be zero for the purposes of pivoting.
* @return {boolean} True if the vectors are linearly independent.
* @throws Will throw an error if the vectors do not have the same length.
*/
static isLinearlyIndependent(vecs, ε=1e-10) {
let M = Matrix.from(vecs);
// Tall matrices never have linearly independent rows.
if(M.m > M.n) return false;
return M.rank(ε) == vecs.length;
}
/**
* @summary
* Check if an iterable of vectors is linearly dependent.
*
* @desc
* This is an alias for `!Vector.isLinearlyIndependent(vecs)`.
*
* @example {@lang javascript}
* Vector.isLinearlyDependent(
* [Vector.create(1, 0, 0),
* Vector.create(0, 1, 0),
* Vector.create(0, 0, 1)]); // false
* Vector.isLinearlyDependent(
* [Vector.create(1, 0, 0),
* Vector.create(0, 1, 0),
* Vector.create(1, 1, 0)]); // true
*
* @param {Vector[]} vecs - The vectors to check.
* @param {number} [ε=1e-10] - Entries smaller than this value are taken
* to be zero for the purposes of pivoting.
* @return {boolean} True if the vectors are linearly dependent.
* @throws Will throw an error if the vectors do not have the same length.
*/
static isLinearlyDependent(vecs, ε=1e-10) {
return !Vector.isLinearlyIndependent(vecs, ε);
}
/**
* @summary
* Return a linearly independent subset.
*
* @desc
* This returns an Array containing a maximal linearly independent subset of
* vectors from `vecs`.
*
* @example {@lang javascript}
* let v1 = Vector.create(1, 0, 0);
* let v2 = Vector.create(0, 1, 0);
* let v3 = Vector.create(1, 1, 0);
* Vector.linearlyIndependentSubset([v1, v2, v3]); // [v1, v2]
*
* @param {Vector[]} vecs - The vectors to use.
* @param {number} [ε=1e-10] - Entries smaller than this value are taken
* to be zero for the purposes of pivoting.
* @return {Vector[]} A linearly independent subset of `vecs`.
* @throws Will throw an error if the vectors do not have the same length.
*/
static linearlyIndependentSubset(vecs, ε=1e-10) {
return Array.from(Matrix.from(vecs).transpose.pivots(ε),
([,j]) => vecs[j]);
}
/**
* @summary
* Compute a linear combination of vectors.
*
* @desc
* The linear combination of the vectors `v1, v2, ..., vn` with coefficients
* `c1, c2, ..., cn` is the vector `c1 v1 + c2 v2 + ... + cn vn`.
*
* @example {@lang javascript}
* let v1 = Vector.create(1, 0, 0);
* let v2 = Vector.create(0, 1, 0);
* let v3 = Vector.create(1, 1, 0);
* Vector.linearCombination([1, 2, 3], [v1, v2, v3]).toString(1);
* // "[4.0 5.0 0.0]"
*
* @param {number[]} coeffs - A non-empty array of coefficients.
* @param {Vector[]} vecs - A non-empty array of vectors, of the same length
* as `coeffs`.
* @return The sum of the vectors scaled by the coefficients.
* @throws Will throw an error if the vectors do not have the same length,
* or if `coeffs` is empty.
*/
static linearCombination(coeffs, vecs) {
return coeffs.reduce(
(a, c, i) => a.add(vecs[i].clone().scale(c)),
Vector.zero(vecs[0].length));
}
/**
* @summary
* The square of the geometric length of the vector.
*
* @desc
* This is the sum of the squares of the entries, which is the dot product
* of `this` with itself.
*
* @example {@lang javascript}
* Vector.create(3, 4).sizesq; // 25
*
* @type {number}
*/
get sizesq() {
return this.dot(this);
}
/**
* @summary
* The geometric length of the vector.
*
* @desc
* This is the square root of `this.sizesq`.
*
* @example {@lang javascript}
* Vector.create(3, 4).size; // 5
*
* @type {number}
*/
get size() {
return Math.sqrt(this.sizesq);
}
/**
* @summary
* Check if this vector is equal to `other`.
*
* @desc
* Two vectors are equal if they have the same number of entries, and all
* entries are equal.
*
* @example {@lang javascript}
* let v = Vector.create(0.01, -0.01, 0);
* let w = Vector.zero(3);
* v.equals(w); // false
* v.equals(w, 0.05); // true
* w.equals(Vector.zero(2)); // false
*
* @param {Vector} other - The vector to compare.
* @param {number} [ε=0] - Entries will test as equal if they are within `ε`
* of each other. This is provided in order to account for rounding
* errors.
* @return {boolean} True if the vectors are equal.
*/
equals(other, ε=0) {
if(this.length !== other.length)
return false;
if(ε === 0)
return this.every((v, i) => v === other[i]);
return this.every((v, i) => Math.abs(v - other[i]) <= ε);
}
/**
* @summary
* Create a new Vector with the same entries.
*
* @return {Vector} The new vector.
*/
clone() {
return this.slice();
}
/**
* @summary
* Return a string representation of the vector.
*
* @example {@lang javascript}
* Vector.create(1, 2, 3).toString(2); // "[1.00 2.00 3.00]"
*
* @param {integer} [precision=4] - The number of decimal places to include.
* @return {string} A string representation of the vector.
*/
toString(precision=4) {
const strings = Array.from(this, v => v.toFixed(precision));
return "[" + strings.join(' ') + "]";
}
/**
* @summary
* Return a LaTeX representation of the vector.
*
* @example {@lang javascript}
* Vector.create(1, 2, 3).toLaTeX(2);
* // "\begin{bmatrix} 1.00 \\ 2.00 \\ 3.00 \end{bmatrix}"
*
* @param {integer} [precision=4] - The number of decimal places to include.
* @param {Object} [opts={}] - Options.
* @param {string} [opts.env="bmatrix"] - The matrix environment to use.
* @param {boolean} [opts.row=false] - Print as a row vector instead of a
* column vector.
* @return {string} A string representation of the vector.
*/
toLaTeX(precision=4, {env="bmatrix", row=false}={}) {
const strings = Array.from(this, v => v.toFixed(precision));
return `\\begin{${env}} `
+ strings.join(row ? ' & ' : ' \\\\ ')
+ ` \\end{${env}}`;
}
/**
* @summary
* Decide if a vector is zero.
*
* @desc
* This is functionally equivalent to
* `this.equals(Vector.zero(this.length), ε)`.
*
* @param {number} [ε=0] - Entries smaller than this in absolute value will
* be considered zero.
* @return {boolean} True if the vector has all zero entries.
*/
isZero(ε=0) {
return this.every(x => Math.abs(x) <= ε);
}
/**
* @summary
* Scale the vector by the reciprocal of its length.
*
* @desc
* This modifies the vector in-place to have [size]{@link Vector#size} 1.
*
* @example {@lang javascript}
* let v = Vector.create(3, 4);
* v.normalize();
* v.toString(2); // "[0.60 0.80]"
*
* @return {Vector} `this`
* @throws Will throw an error if `this` is the zero vector.
*/
normalize() {
const s = 1/this.size;
if(!isFinite(s))
throw new Error("Tried to normalize the zero vector");
return this.scale(s);
}
/**
* @summary
* Set multiple components of a Vector.
*
* @desc
* This modifies the vector by setting the components to the passed values.
*
* @example {@lang javascript}
* let v = Vector.zero(2);
* v.set(3, 4);
* v.toString(1); // "[3.0, 4.0]"
*
* @param {...number} entries - The new entries of the vector.
* @return {Vector} `this`
* @throws Will throw an error if the number of passed entries is
* incorrect.
*/
set(...entries) {
if(entries.length != this.length)
throw new Error("Tried to set an incorrect number of entries");
this.splice(0, this.length, ...entries);
return this;
}
/**
* @summary
* Add a Vector in-place.
*
* @desc
* This modifies the vector in-place by adding the entries of `other`.
*
* @example {@lang javascript}
* let v = Vector.create(1, 2), w = Vector.create(3, 4);
* v.add(w);
* v.toString(1); // "[4.0 6.0]"
*
* @param {Vector} other - The vector to add.
* @param {number} [factor=1] - Add `factor` times `other` instead of just
* adding `other`.
* @param {integer} [start=0] - Only add the entries `start...this.length`.
* Provided for optimizations when the entries of `other` before `start`
* are known to be zero.
* @return {Vector} `this`
* @throws Will throw an error if the vectors have different lengths.
*/
add(other, factor=1, start=0) {
if(this.length !== other.length)
throw new Error(
'Tried to add vectors of different lengths');
for(let i = start; i < this.length; ++i)
this[i] += factor*other[i];
return this;
}
/**
* @summary
* Subtract a Vector in-place.
*
* @desc
* This modifies the vector in-place by subtracting the entries of `other`.
*
* @example {@lang javascript}
* let v = Vector.create(1, 2), w = Vector.create(3, 4);
* v.sub(w);
* v.toString(1); // "[-2.0 -2.0]"
*
* @param {Vector} other - The vector to subtract.
* @param {integer} [start=0] - Only subtract the entries
* `start...this.length`. Provided for optimizations when the entries of
* `other` before `start` are known to be zero.
* @return {Vector} `this`
* @throws Will throw an error if the vectors have different lengths.
*/
sub(other, start=0) {
return this.add(other, -1, start);
}
/**
* @summary
* Multiply a Vector by a scalar in-place.
*
* @desc
* This modifies the vector in-place by multiplying all entries by `c`.
*
* @example {@lang javascript}
* let v = Vector.create(1, 2);
* v.scale(2);
* v.toString(1); // "[2.0 4.0]"
*
* @param {number} c - The scaling factor.
* @param {integer} [start=0] - Only scale the entries
* `start...this.length`. Provided for optimizations when the entries
* before `start` are known to be zero.
* @return {Vector} `this`
*/
scale(c, start=0) {
for(let i = start; i < this.length; ++i)
this[i] *= c;
return this;
}
/**
* @summary
* Compute the dot product with another vector.
*
* @desc
* This is the sum of the pairwise products of the entries of `this` and
* `other`.
*
* @example {@lang javascript}
* let v = Vector.create(1, 2), w = Vector.create(3, 4);
* v.dot(w); // 1*3 + 2*4
*
* @param {Vector} other - The vector to dot.
* @return {number} The dot product.
* @throws Will throw an error if the vectors have different lengths.
*/
dot(other) {
if(this.length !== other.length)
throw new Error(
'Tried to take the dot product of vectors of different lengths');
return this.reduce((a, v, i) => a + v * other[i], 0);
}
};
export default Vector;