mirror of https://github.com/aziis98/ro-vis
back to a working state and some graphics
parent
4f4f9537bc
commit
8b29e8cab6
@ -0,0 +1,47 @@
|
||||
import { Matrix } from './matrix'
|
||||
import { parse } from './parser'
|
||||
import { Rational } from './rationals'
|
||||
|
||||
// Example usage
|
||||
const source = `
|
||||
c' = 500 200;
|
||||
|
||||
A = 1 0
|
||||
0 1
|
||||
2 1
|
||||
-1 0
|
||||
-1 0;
|
||||
|
||||
b = 4
|
||||
7
|
||||
9
|
||||
0
|
||||
0;
|
||||
|
||||
B = 1/2 3;
|
||||
`
|
||||
|
||||
const env = parse(source)
|
||||
Object.entries(env).forEach(([name, { value }]) => {
|
||||
console.log(name, JSON.stringify(value, null, 2))
|
||||
})
|
||||
|
||||
const A = Matrix.of([
|
||||
[Rational.of(1), Rational.of(0), Rational.of(0)],
|
||||
[Rational.of(0), Rational.of(1), Rational.of(1)],
|
||||
[Rational.of(2), Rational.of(1), Rational.of(1)],
|
||||
[Rational.of(-1), Rational.of(0), Rational.of(0)],
|
||||
[Rational.of(-1), Rational.of(0), Rational.of(0)],
|
||||
])
|
||||
|
||||
console.log(A.toString())
|
||||
|
||||
const A_B = A.slice({ rows: [1, 3], cols: [0, 2] })
|
||||
|
||||
console.log(A_B.toString())
|
||||
|
||||
const A_B_inverse = A_B.inverse2x2()
|
||||
|
||||
console.log(A_B_inverse.toString())
|
||||
|
||||
console.log(A_B.forwardRowIndices, A_B.forwardColIndices)
|
||||
@ -0,0 +1,83 @@
|
||||
const MAX_LINE_SIZE = 50
|
||||
|
||||
/**
|
||||
* Draw a semi-plane delimited by the equation `a1 x + a2 y <= b`
|
||||
*/
|
||||
export function drawSemiplane(
|
||||
g: CanvasRenderingContext2D,
|
||||
a1: number,
|
||||
a2: number,
|
||||
b: number,
|
||||
{
|
||||
gradientAccent,
|
||||
gradientTransparent,
|
||||
lineColor,
|
||||
lineWidth,
|
||||
}: { gradientAccent?: string; gradientTransparent?: string; lineColor?: string; lineWidth?: number } = {}
|
||||
) {
|
||||
gradientAccent ??= '#ffa90066'
|
||||
gradientTransparent ??= '#ffa90000'
|
||||
lineColor ??= '#9c6700'
|
||||
lineWidth ??= 2
|
||||
|
||||
// The gradient is perpendicular to the line, first generate a point on the line
|
||||
let [p1, p2] = [0, 0]
|
||||
if (a2 === 0) {
|
||||
p1 = b / a1
|
||||
p2 = 0
|
||||
} else {
|
||||
p1 = 0
|
||||
p2 = b / a2
|
||||
}
|
||||
|
||||
const normalize = Math.sqrt(a1 ** 2 + a2 ** 2) * 1.5
|
||||
|
||||
const gradient = g.createLinearGradient(p1, p2, p1 - a1 / normalize, p2 - a2 / normalize)
|
||||
gradient.addColorStop(0, gradientAccent)
|
||||
gradient.addColorStop(1, gradientTransparent)
|
||||
g.fillStyle = gradient
|
||||
|
||||
// g.fillStyle = 'rgba(0, 0, 0, 0.1)'
|
||||
|
||||
g.beginPath()
|
||||
if (a2 === 0) {
|
||||
if (a1 > 0) {
|
||||
g.moveTo(b / a1, -MAX_LINE_SIZE)
|
||||
g.lineTo(-MAX_LINE_SIZE, -MAX_LINE_SIZE)
|
||||
g.lineTo(-MAX_LINE_SIZE, MAX_LINE_SIZE)
|
||||
g.lineTo(b / a1, MAX_LINE_SIZE)
|
||||
} else {
|
||||
g.moveTo(b / a1, -MAX_LINE_SIZE)
|
||||
g.lineTo(MAX_LINE_SIZE, -MAX_LINE_SIZE)
|
||||
g.lineTo(MAX_LINE_SIZE, MAX_LINE_SIZE)
|
||||
g.lineTo(b / a1, MAX_LINE_SIZE)
|
||||
}
|
||||
} else {
|
||||
if (a2 > 0) {
|
||||
g.moveTo(-MAX_LINE_SIZE, (b - a1 * -MAX_LINE_SIZE) / a2)
|
||||
g.lineTo(-MAX_LINE_SIZE, MAX_LINE_SIZE)
|
||||
g.lineTo(-MAX_LINE_SIZE, -MAX_LINE_SIZE)
|
||||
g.lineTo(MAX_LINE_SIZE, (b - a1 * MAX_LINE_SIZE) / a2)
|
||||
} else {
|
||||
g.moveTo(MAX_LINE_SIZE, (b - a1 * MAX_LINE_SIZE) / a2)
|
||||
g.lineTo(MAX_LINE_SIZE, -MAX_LINE_SIZE)
|
||||
g.lineTo(MAX_LINE_SIZE, MAX_LINE_SIZE)
|
||||
g.lineTo(-MAX_LINE_SIZE, (b - a1 * -MAX_LINE_SIZE) / a2)
|
||||
}
|
||||
}
|
||||
g.fill()
|
||||
|
||||
// Draw the line
|
||||
g.strokeStyle = lineColor
|
||||
g.lineWidth = (lineWidth * 10) / g.canvas.width
|
||||
|
||||
g.beginPath()
|
||||
if (a2 === 0) {
|
||||
g.moveTo(b / a1, -MAX_LINE_SIZE)
|
||||
g.lineTo(b / a1, MAX_LINE_SIZE)
|
||||
} else {
|
||||
g.moveTo(-MAX_LINE_SIZE, (b - a1 * -MAX_LINE_SIZE) / a2)
|
||||
g.lineTo(MAX_LINE_SIZE, (b - a1 * MAX_LINE_SIZE) / a2)
|
||||
}
|
||||
g.stroke()
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
import { Matrix } from './matrix'
|
||||
import { Rational } from './rationals'
|
||||
import { Vector } from './vector'
|
||||
|
||||
export const rationalToLatex = (r: Rational) => (r.den === 1 ? r.num.toString() : `${r.num} / ${r.den}`)
|
||||
|
||||
export const matrixToLatex = (matrix: Matrix<Rational>) =>
|
||||
`\\begin{bmatrix} ${matrix
|
||||
.getData()
|
||||
.map(row => row.map(r => rationalToLatex(r)).join(' & '))
|
||||
.join(' \\\\ ')} \\end{bmatrix}`
|
||||
|
||||
export const vectorToLatex = (vector: Vector<Rational>) =>
|
||||
vector
|
||||
? `\\begin{bmatrix} ${vector
|
||||
.getData()
|
||||
.map(r => rationalToLatex(r))
|
||||
.join(' \\\\ ')} \\end{bmatrix}`
|
||||
: ''
|
||||
|
||||
export const rowVectorToLatex = (vector: Vector<Rational>) =>
|
||||
vector
|
||||
? `\\begin{bmatrix} ${vector
|
||||
.getData()
|
||||
.map(r => rationalToLatex(r))
|
||||
.join(' & ')} \\end{bmatrix}`
|
||||
: ''
|
||||
|
||||
export const indexSetToLatex = (indices: number[]) => `\\{${indices.map(i => (i + 1).toString()).join(', ')}\\}`
|
||||
@ -0,0 +1,42 @@
|
||||
/**
|
||||
* A range of integers from `start` inclusive to `end` exclusive.
|
||||
*/
|
||||
export function range(start: number, end: number): number[] {
|
||||
return Array.from({ length: end - start }, (_, i) => i + start)
|
||||
}
|
||||
|
||||
export const gcd = (a: number, b: number): number => {
|
||||
if (b === 0) {
|
||||
return a
|
||||
}
|
||||
|
||||
return gcd(b, a % b)
|
||||
}
|
||||
|
||||
export type Field<T> = {
|
||||
zero: T
|
||||
one: T
|
||||
}
|
||||
|
||||
export type FieldValue<T> = {
|
||||
baseField: Field<T>
|
||||
|
||||
isZero(): boolean
|
||||
isOne(): boolean
|
||||
|
||||
scale(k: number): T
|
||||
|
||||
add(other: T): T
|
||||
sub(other: T): T
|
||||
mul(other: T): T
|
||||
div(other: T): T
|
||||
|
||||
inverse(): T
|
||||
neg(): T
|
||||
|
||||
eq(other: T): boolean
|
||||
lt(other: T): boolean
|
||||
gt(other: T): boolean
|
||||
leq(other: T): boolean
|
||||
geq(other: T): boolean
|
||||
}
|
||||
@ -0,0 +1,165 @@
|
||||
import { Field, FieldValue, range } from './math'
|
||||
import { Vector } from './vector'
|
||||
|
||||
export type MatrixShape = { rows: number; cols: number }
|
||||
|
||||
export abstract class Matrix<T extends FieldValue<T>> {
|
||||
abstract baseField: Field<T>
|
||||
|
||||
abstract rows: number
|
||||
abstract cols: number
|
||||
|
||||
abstract at(i: number, j: number): T
|
||||
|
||||
getData(): T[][] {
|
||||
return range(0, this.rows).map(i => range(0, this.cols).map(j => this.at(i, j)))
|
||||
}
|
||||
|
||||
apply(vector: Vector<T>): Vector<T> {
|
||||
if (this.cols !== vector.size) {
|
||||
throw new Error('Matrix and vector dimensions do not match')
|
||||
}
|
||||
|
||||
return Vector.of(
|
||||
this.getData().map(row => row.reduce((acc, v, j) => acc.add(v.mul(vector.at(j))), this.baseField.zero))
|
||||
)
|
||||
}
|
||||
|
||||
inverse2x2(): Matrix<T> {
|
||||
if (this.rows !== 2 || this.cols !== 2) {
|
||||
throw new Error('Matrix is not 2x2')
|
||||
}
|
||||
|
||||
const a = this.at(0, 0)
|
||||
const b = this.at(0, 1)
|
||||
const c = this.at(1, 0)
|
||||
const d = this.at(1, 1)
|
||||
|
||||
const det = a.mul(d).sub(b.mul(c))
|
||||
if (det.isZero()) {
|
||||
throw new Error('Matrix is singular')
|
||||
}
|
||||
|
||||
return new MatrixDense(
|
||||
2,
|
||||
2,
|
||||
[
|
||||
[d, b.neg()],
|
||||
[c.neg(), a],
|
||||
].map(row => row.map(r => r.div(det)))
|
||||
)
|
||||
}
|
||||
|
||||
slice({ rows, cols }: { rows?: number[]; cols?: number[] }): MatrixView<T> {
|
||||
rows ??= range(0, this.rows)
|
||||
cols ??= range(0, this.cols)
|
||||
|
||||
return new MatrixView(this, rows, cols)
|
||||
}
|
||||
|
||||
rowAt(i: number): Vector<T> {
|
||||
return new MatrixRow(this, i)
|
||||
}
|
||||
|
||||
transpose(): Matrix<T> {
|
||||
return new MatrixTransposed(this)
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `Matrix of ${this.rows}x${this.cols} [${this.getData()
|
||||
.map(row => `[${row.join(', ')}]`)
|
||||
.join(', ')}]`
|
||||
}
|
||||
|
||||
static of<T extends FieldValue<T>>(data: T[][]): Matrix<T> {
|
||||
const rows = data.length
|
||||
const cols = data[0].length
|
||||
|
||||
return new MatrixDense(rows, cols, data)
|
||||
}
|
||||
}
|
||||
|
||||
class MatrixDense<T extends FieldValue<T>> extends Matrix<T> {
|
||||
constructor(public rows: number, public cols: number, public data: T[][]) {
|
||||
super()
|
||||
|
||||
if (data.length !== rows) {
|
||||
throw new Error('Invalid number of rows')
|
||||
}
|
||||
if (data.some(row => row.length !== cols)) {
|
||||
throw new Error('Invalid number of columns')
|
||||
}
|
||||
}
|
||||
|
||||
get baseField() {
|
||||
return this.data[0][0].baseField
|
||||
}
|
||||
|
||||
at(i: number, j: number): T {
|
||||
return this.data[i][j]
|
||||
}
|
||||
}
|
||||
|
||||
class MatrixView<T extends FieldValue<T>> extends Matrix<T> {
|
||||
public forwardRowIndices: number[] = []
|
||||
public forwardColIndices: number[] = []
|
||||
|
||||
constructor(public parent: Matrix<T>, public rowIndices: number[], public colIndices: number[]) {
|
||||
super()
|
||||
|
||||
rowIndices.forEach((i, j) => (this.forwardRowIndices[i] = j))
|
||||
colIndices.forEach((i, j) => (this.forwardColIndices[i] = j))
|
||||
}
|
||||
|
||||
get baseField() {
|
||||
return this.parent.baseField
|
||||
}
|
||||
|
||||
get rows() {
|
||||
return this.rowIndices.length
|
||||
}
|
||||
|
||||
get cols() {
|
||||
return this.colIndices.length
|
||||
}
|
||||
|
||||
at(i: number, j: number): T {
|
||||
return this.parent.at(this.rowIndices[i], this.colIndices[j])
|
||||
}
|
||||
}
|
||||
|
||||
class MatrixTransposed<T extends FieldValue<T>> extends Matrix<T> {
|
||||
constructor(public parent: Matrix<T>) {
|
||||
super()
|
||||
}
|
||||
|
||||
get baseField() {
|
||||
return this.parent.baseField
|
||||
}
|
||||
|
||||
get rows() {
|
||||
return this.parent.cols
|
||||
}
|
||||
|
||||
get cols() {
|
||||
return this.parent.rows
|
||||
}
|
||||
|
||||
at(i: number, j: number): T {
|
||||
return this.parent.at(j, i)
|
||||
}
|
||||
}
|
||||
|
||||
class MatrixRow<T extends FieldValue<T>> extends Vector<T> {
|
||||
constructor(public parent: Matrix<T>, public row: number) {
|
||||
super()
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this.parent.cols
|
||||
}
|
||||
|
||||
at(i: number): T {
|
||||
return this.parent.at(this.row, i)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,211 @@
|
||||
import { Matrix } from './matrix'
|
||||
import { Rational } from './rationals'
|
||||
import { Result } from './util'
|
||||
import { Vector } from './vector'
|
||||
|
||||
export type Value = { rank: 0; value: Rational } | { rank: 1; value: Rational[] } | { rank: 2; value: Rational[][] }
|
||||
|
||||
export function asScalar(v: Value): Rational {
|
||||
if (v.rank === 0) {
|
||||
return v.value
|
||||
}
|
||||
|
||||
throw new Error(`Expected scalar, got ${JSON.stringify(v)}`)
|
||||
}
|
||||
|
||||
export function asVector(v: Value): Vector<Rational> {
|
||||
if (v.rank === 1) {
|
||||
return Vector.of(v.value)
|
||||
}
|
||||
|
||||
if (v.rank === 2 && v.value.every(row => row.length === 1)) {
|
||||
return Vector.of(v.value.map(row => row[0]))
|
||||
}
|
||||
|
||||
if (v.rank === 2 && v.value.length === 1) {
|
||||
return Vector.of(v.value[0])
|
||||
}
|
||||
|
||||
throw new Error(`Expected column vector, got ${JSON.stringify(v)}`)
|
||||
}
|
||||
|
||||
export function asMatrix(v: Value): Matrix<Rational> {
|
||||
if (v.rank === 0) {
|
||||
return Matrix.of([[v.value]])
|
||||
}
|
||||
|
||||
if (v.rank === 1) {
|
||||
return Matrix.of(v.value.map(vv => [vv]))
|
||||
}
|
||||
|
||||
if (v.rank === 2) {
|
||||
return Matrix.of(v.value)
|
||||
}
|
||||
|
||||
throw new Error(`Expected matrix, got ${JSON.stringify(v)}`)
|
||||
}
|
||||
|
||||
function transposeValue(v: Value): Value {
|
||||
if (v.rank === 0) {
|
||||
return v
|
||||
}
|
||||
|
||||
if (v.rank === 1) {
|
||||
return { rank: 2, value: v.value.map(r => [r]) }
|
||||
}
|
||||
|
||||
if (v.rank === 2) {
|
||||
return { rank: 2, value: asMatrix(v).transpose().getData() }
|
||||
}
|
||||
|
||||
throw new Error(`Cannot transpose value: ${JSON.stringify(v)}`)
|
||||
}
|
||||
|
||||
enum TokenType {
|
||||
Identifier,
|
||||
Transpose,
|
||||
Equals,
|
||||
Number,
|
||||
Divide,
|
||||
Semicolon,
|
||||
Newline,
|
||||
}
|
||||
|
||||
interface Token {
|
||||
type: TokenType
|
||||
value: string
|
||||
}
|
||||
|
||||
function tokenize(source: string): Token[] {
|
||||
const tokens: Token[] = []
|
||||
const patterns: [RegExp, TokenType][] = [
|
||||
[/^[a-zA-Z]+/, TokenType.Identifier],
|
||||
[/^'/, TokenType.Transpose],
|
||||
[/^=/, TokenType.Equals],
|
||||
[/^-?\d+/, TokenType.Number],
|
||||
[/^\//, TokenType.Divide],
|
||||
[/^;/, TokenType.Semicolon],
|
||||
[/^\n/, TokenType.Newline],
|
||||
]
|
||||
|
||||
let remaining = source
|
||||
while (remaining.trimStart().length > 0) {
|
||||
remaining = remaining.replace(/^[\t ]+/, '')
|
||||
|
||||
let matched = false
|
||||
for (const [pattern, type] of patterns) {
|
||||
const match = remaining.match(pattern)
|
||||
if (match) {
|
||||
tokens.push({ type, value: match[0] })
|
||||
remaining = remaining.slice(match[0].length)
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
throw new Error(`Unexpected token: "${remaining}"`)
|
||||
}
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
export function parse(source: string): Record<string, Value> {
|
||||
const tokens = tokenize(source)
|
||||
// console.log(tokens)
|
||||
|
||||
const result: Record<string, Value> = {}
|
||||
let i = 0
|
||||
|
||||
function parseValue(): Value {
|
||||
const rows: Rational[][] = []
|
||||
let currentRow: Rational[] = []
|
||||
|
||||
while (i < tokens.length) {
|
||||
const token = tokens[i]
|
||||
|
||||
if (token.type === TokenType.Number) {
|
||||
const n = Number(token.value)
|
||||
|
||||
if (tokens[i + 1]?.type === TokenType.Divide) {
|
||||
i++
|
||||
|
||||
if (tokens[i + 1]?.type !== TokenType.Number) {
|
||||
throw new Error('Expected denominator after "/"')
|
||||
}
|
||||
|
||||
i++
|
||||
currentRow.push(Rational.of(n, Number(tokens[i].value)))
|
||||
|
||||
i++
|
||||
} else {
|
||||
i++
|
||||
currentRow.push(Rational.of(n, 1))
|
||||
}
|
||||
} else if (token.type === TokenType.Newline) {
|
||||
if (currentRow.length > 0) {
|
||||
rows.push(currentRow)
|
||||
currentRow = []
|
||||
}
|
||||
i++
|
||||
} else if (token.type === TokenType.Semicolon) {
|
||||
if (currentRow.length > 0) {
|
||||
rows.push(currentRow)
|
||||
}
|
||||
i++
|
||||
break
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (rows.length === 1) {
|
||||
return { rank: 1, value: rows[0] }
|
||||
}
|
||||
|
||||
return { rank: 2, value: rows }
|
||||
}
|
||||
|
||||
while (i < tokens.length) {
|
||||
while (tokens[i].type === TokenType.Newline) {
|
||||
i++
|
||||
}
|
||||
|
||||
const token = tokens[i]
|
||||
|
||||
if (token.type === TokenType.Identifier) {
|
||||
const identifier = token.value
|
||||
i++
|
||||
|
||||
let transpose = false
|
||||
if (tokens[i]?.type === TokenType.Transpose) {
|
||||
transpose = true
|
||||
i++
|
||||
}
|
||||
|
||||
if (tokens[i]?.type === TokenType.Equals) {
|
||||
i++
|
||||
result[identifier] = parseValue()
|
||||
|
||||
if (transpose) {
|
||||
result[identifier] = transposeValue(result[identifier])
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Expected '=' after identifier '${identifier}'`)
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unexpected token: "${token.value.replace('\n', '\\n')}"`)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function parseSafe(source: string): Result<Record<string, Value>> {
|
||||
try {
|
||||
return { result: parse(source) }
|
||||
} catch (e) {
|
||||
return { error: e!.toString() }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,117 @@
|
||||
import { Field, FieldValue, gcd } from './math'
|
||||
|
||||
export class Rational implements FieldValue<Rational> {
|
||||
private constructor(public num: number, public den: number) {
|
||||
if (den === 0) {
|
||||
throw new Error('Division by zero')
|
||||
}
|
||||
if ((num | 0) !== num || (den | 0) !== den) {
|
||||
throw new Error('Expected integer')
|
||||
}
|
||||
|
||||
this.#simplify()
|
||||
}
|
||||
|
||||
get baseField() {
|
||||
return RationalField
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.den === 1 ? this.num.toString() : `${this.num} / ${this.den}`
|
||||
}
|
||||
|
||||
#simplify() {
|
||||
if (this.den === 0) {
|
||||
throw new Error('Division by zero')
|
||||
}
|
||||
if (this.num === 0) {
|
||||
this.den = 1
|
||||
}
|
||||
if (this.den < 0) {
|
||||
this.num = -this.num
|
||||
this.den = -this.den
|
||||
}
|
||||
|
||||
const g = Math.abs(gcd(this.num, this.den))
|
||||
|
||||
this.num /= g
|
||||
this.den /= g
|
||||
}
|
||||
|
||||
isZero() {
|
||||
return this.num === 0
|
||||
}
|
||||
|
||||
isOne() {
|
||||
return this.num === this.den
|
||||
}
|
||||
|
||||
isInteger() {
|
||||
return this.den === 1
|
||||
}
|
||||
|
||||
toNumber() {
|
||||
return this.num / this.den
|
||||
}
|
||||
|
||||
add(b: Rational) {
|
||||
return new Rational(this.num * b.den + b.num * this.den, this.den * b.den)
|
||||
}
|
||||
|
||||
sub(b: Rational) {
|
||||
return new Rational(this.num * b.den - b.num * this.den, this.den * b.den)
|
||||
}
|
||||
|
||||
mul(b: Rational) {
|
||||
return new Rational(this.num * b.num, this.den * b.den)
|
||||
}
|
||||
|
||||
div(b: Rational) {
|
||||
return new Rational(this.num * b.den, this.den * b.num)
|
||||
}
|
||||
|
||||
inverse() {
|
||||
if (this.num === 0) {
|
||||
throw new Error('Division by zero')
|
||||
}
|
||||
|
||||
return new Rational(this.den, this.num)
|
||||
}
|
||||
|
||||
neg() {
|
||||
return new Rational(-this.num, this.den)
|
||||
}
|
||||
|
||||
scale(k: number) {
|
||||
return new Rational(this.num * k, this.den)
|
||||
}
|
||||
|
||||
eq(b: Rational) {
|
||||
return this.num * b.den === b.num * this.den
|
||||
}
|
||||
|
||||
lt(b: Rational) {
|
||||
return this.num * b.den < b.num * this.den
|
||||
}
|
||||
|
||||
gt(b: Rational) {
|
||||
return this.num * b.den > b.num * this.den
|
||||
}
|
||||
|
||||
leq(b: Rational) {
|
||||
return this.num * b.den <= b.num * this.den
|
||||
}
|
||||
|
||||
geq(b: Rational) {
|
||||
return this.num * b.den >= b.num * this.den
|
||||
}
|
||||
|
||||
static of(num: number, den: number = 1) {
|
||||
return new Rational(num, den)
|
||||
}
|
||||
}
|
||||
|
||||
export const RationalField: Field<Rational> = {
|
||||
zero: Rational.of(0),
|
||||
one: Rational.of(1),
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
export type Result<T> = { result: T } | { error: string }
|
||||
|
||||
export const tryBlock = <T>(fn: () => T): Result<T> => {
|
||||
try {
|
||||
return { result: fn() }
|
||||
} catch (e) {
|
||||
return { error: e!.toString() }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
import { Field, FieldValue, range } from './math'
|
||||
|
||||
export abstract class Vector<T extends FieldValue<T>> {
|
||||
abstract size: number
|
||||
abstract at(i: number): T
|
||||
|
||||
slice(indices: number[]): Vector<T> {
|
||||
return new VectorDense(indices.map(i => this.at(i)))
|
||||
}
|
||||
|
||||
dot(vector: Vector<T>): T {
|
||||
if (this.size !== vector.size) {
|
||||
throw new Error('Vector dimensions do not match')
|
||||
}
|
||||
|
||||
return this.getData()
|
||||
.map((v, i) => v.mul(vector.at(i)))
|
||||
.reduce((a, b) => a.add(b))
|
||||
}
|
||||
|
||||
getData(): T[] {
|
||||
return range(0, this.size).map(i => this.at(i))
|
||||
}
|
||||
|
||||
neg(): Vector<T> {
|
||||
return new VectorDense(this.getData().map(v => v.neg()))
|
||||
}
|
||||
|
||||
with(indices: number[], vector: Vector<T>): Vector<T> {
|
||||
if (indices.length !== vector.size) {
|
||||
throw new Error('Vector dimensions do not match')
|
||||
}
|
||||
|
||||
return new VectorDense(
|
||||
range(0, this.size).map(i => (indices.includes(i) ? vector.at(indices.indexOf(i)) : this.at(i)))
|
||||
)
|
||||
}
|
||||
|
||||
static of<T extends FieldValue<T>>(data: T[]): Vector<T> {
|
||||
return new VectorDense(data)
|
||||
}
|
||||
|
||||
static oneHot<T extends FieldValue<T>>(baseField: Field<T>, size: number, index: number): Vector<T> {
|
||||
return new OneHotVector(size, index, baseField)
|
||||
}
|
||||
|
||||
static zero<T extends FieldValue<T>>(baseField: Field<T>, size: number): Vector<T> {
|
||||
return Vector.of(range(0, size).map(() => baseField.zero))
|
||||
}
|
||||
}
|
||||
|
||||
class VectorDense<T extends FieldValue<T>> extends Vector<T> {
|
||||
constructor(public data: T[]) {
|
||||
super()
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this.data.length
|
||||
}
|
||||
|
||||
at(i: number): T {
|
||||
return this.data[i]
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `Vector of [${this.data.join(', ')}]`
|
||||
}
|
||||
}
|
||||
|
||||
class OneHotVector<T extends FieldValue<T>> extends Vector<T> {
|
||||
constructor(public size: number, public index: number, public baseField: Field<T>) {
|
||||
super()
|
||||
}
|
||||
|
||||
at(i: number): T {
|
||||
return i === this.index ? this.baseField.one : this.baseField.zero
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `One-hot vector of size ${this.size} with 1 at index ${this.index}`
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue