diff --git a/src/DisplayProblemInput.tsx b/src/DisplayProblemInput.tsx index 07cc1f5..9cfcbe6 100644 --- a/src/DisplayProblemInput.tsx +++ b/src/DisplayProblemInput.tsx @@ -1,5 +1,5 @@ import { Katex } from './Katex' -import { matrixToLatex, rowVectorToLatex, vectorToLatex } from './lib/latex' +import { matrixToLatex, rowVectorToLatex, vectorToLatex } from './lib-v2/latex' import { ProblemInput } from './parser-problem' export const DisplayProblemInput = ({ problemInput }: { problemInput: ProblemInput }) => { diff --git a/src/Primale.tsx b/src/Primale.tsx index 97584ce..896b044 100644 --- a/src/Primale.tsx +++ b/src/Primale.tsx @@ -1,7 +1,10 @@ import { Katex } from './Katex' -import { indexSetToLatex, matrixToLatex, rowVectorToLatex, vectorToLatex } from './lib/latex' -import { Matrix, Vector } from './lib/matvec' -import { Rationals, Rational } from './lib/rationals' +import { drawSemiplane } from './lib-v2/canvas' +import { indexSetToLatex, matrixToLatex, rowVectorToLatex, vectorToLatex } from './lib-v2/latex' +import { range } from './lib-v2/math' +import { Matrix } from './lib-v2/matrix' +import { Rational, RationalField } from './lib-v2/rationals' +import { Vector } from './lib-v2/vector' import { ProblemInput } from './parser-problem' type Step = { @@ -10,12 +13,9 @@ type Step = { const activeIndices = (input: ProblemInput, x: Vector): number[] => { const { A, b } = input - const A_x = A.apply(x) - console.log(A_x, b) - - return A_x.flatMap((a, i) => (Rationals.eq(a, b[i]) ? [i] : [])) + return A_x.getData().flatMap((a, i) => (a.eq(b.at(i)) ? [i] : [])) } export const Primale = ({ input }: { input: ProblemInput }) => { @@ -30,9 +30,109 @@ export const Primale = ({ input }: { input: ProblemInput }) => { ) } +const PrimaleCanvas = ({ + A, + b, + c, + B, + + x, +}: { + A: Matrix + b: Vector + c: Vector + B: number[] + + x?: Vector +}) => { + const render = ($canvas: HTMLCanvasElement | null) => { + if (!$canvas) { + return + } + + $canvas.width = $canvas.offsetWidth + $canvas.height = $canvas.offsetHeight + + const g = $canvas.getContext('2d') + if (!g) { + throw new Error('Could not get 2d context') + } + + const width = $canvas.width + const height = $canvas.height + + g.clearRect(0, 0, width, height) + g.strokeStyle = '#333' + g.lineWidth = 2 + g.lineCap = 'round' + g.lineJoin = 'round' + + // draw y axis arrow + g.beginPath() + g.moveTo(width / 2, height / 2) + g.lineTo(width / 2, 5) + g.lineTo(width / 2 - 10, 15) + g.moveTo(width / 2, 5) + g.lineTo(width / 2 + 10, 15) + g.stroke() + + // draw x axis arrow + g.beginPath() + g.moveTo(width / 2, height / 2) + g.lineTo(width - 5, height / 2) + g.lineTo(width - 15, height / 2 - 10) + g.moveTo(width - 5, height / 2) + g.lineTo(width - 15, height / 2 + 10) + g.stroke() + + // g.fillStyle = '#333' + // g.font = '16px sans-serif' + // g.textAlign = 'center' + // g.textBaseline = 'middle' + // g.fillText(`A = ${A}`, width / 2, height / 2) + + g.translate(width / 2, height / 2) + g.scale(width / 2, -width / 2) + g.scale(1 / 10, 1 / 10) + + // draw semiplanes + + // draw semiplanes not in B + range(0, A.rows) + .filter(i => !B.includes(i)) + .forEach(i => { + const [a1, a2] = A.rowAt(i).getData() + const b_i = b.at(i) + + drawSemiplane(g, a1.toNumber(), a2.toNumber(), b_i.toNumber()) + }) + + // draw semiplanes in B + B.forEach(i => { + const [a1, a2] = A.rowAt(i).getData() + const b_i = b.at(i) + + drawSemiplane(g, a1.toNumber(), a2.toNumber(), b_i.toNumber(), { lineColor: '#040', lineWidth: 3 }) + }) + + // draw x + if (x) { + const [x1, x2] = x.getData() + g.fillStyle = '#f00' + g.beginPath() + g.ellipse(x1.toNumber(), x2.toNumber(), 0.1, 0.1, 0, 0, 2 * Math.PI) + g.fill() + } + } + + return +} + export const PrimaleStep = ({ input, step }: { input: ProblemInput; step: Step }) => { const { A, b, c } = input + const rows = [] + const canvasOptions: Parameters[0] = { A, b, c, B: step.B } const A_B = A.slice({ rows: step.B }) const A_B_inverse = A_B.inverse2x2() @@ -51,6 +151,7 @@ export const PrimaleStep = ({ input, step }: { input: ProblemInput; step: Step } ) const x = A_B_inverse.apply(b_B) + canvasOptions.x = x rows.push(
@@ -81,8 +182,9 @@ export const PrimaleStep = ({ input, step }: { input: ProblemInput; step: Step }
) - const y_Zero = Array.from({ length: A.rows }, () => ({ num: 0, den: 1 })) - const y = Vec.with(y_Zero, step.B, y_B) + const y_Zero = Vector.zero(RationalField, A.rows) + + const y = y_Zero.with(step.B, y_B) rows.push(
@@ -105,8 +207,8 @@ export const PrimaleStep = ({ input, step }: { input: ProblemInput; step: Step } ) const isDegenerate = I_x.length < A_B.rows - const isDualAdmissible = y_B.every(y => Rationals.geq(y, Rationals.zero)) - const isDualDegenerate = y_B.some(y => Rationals.eq(y, Rationals.zero)) + const isDualAdmissible = y_B.getData().every(y => y.geq(RationalField.zero)) + const isDualDegenerate = y_B.getData().some(y => y.eq(RationalField.zero)) rows.push(
@@ -121,7 +223,7 @@ export const PrimaleStep = ({ input, step }: { input: ProblemInput; step: Step } ) if (!isDualAdmissible) { - const h = Math.min(...y.flatMap((y, i) => (Rationals.lt(y, Rationals.zero) ? [i] : []))) + const h = Math.min(...y.getData().flatMap((y, i) => (y.lt(RationalField.zero) ? [i] : []))) rows.push(
@@ -136,10 +238,11 @@ export const PrimaleStep = ({ input, step }: { input: ProblemInput; step: Step }
) - const e_h = Vec.slice(Vec.oneHot(A.length, h), step.B) + const e_h = Vector.oneHot(RationalField, A.rows, h).slice(step.B) console.log(e_h) - const xi = Vec.neg(Mat.apply(A_B_inverse, e_h)) + // const xi = Vec.neg(Mat.apply(A_B_inverse, e_h)) + const xi = A_B_inverse.apply(e_h).neg() rows.push(
@@ -154,26 +257,66 @@ export const PrimaleStep = ({ input, step }: { input: ProblemInput; step: Step }
) - const N = Array.from({ length: A.length }, (_, i) => i).filter(i => !step.B.includes(i)) - const A_N = Mat.slice(A, { rows: N }) + const N = range(0, A.rows).filter(i => !step.B.includes(i)) + const A_N = A.slice({ rows: N }) - const A_N__xi = Mat.apply(A_N, xi) + const A_N__xi = A_N.apply(xi) rows.push(
) - if (!A_N__xi.every(x => Rationals.leq(x, Rationals.zero))) { - const positiveIndices = Array.from({ length: A.length }, (_, i) => i).filter(i => N.includes(i)) + if (!A_N__xi.getData().every(x => x.leq(RationalField.zero))) { + const positiveIndices = N.filter(i => A_N__xi.at(A_N.forwardRowIndices[i]).gt(RationalField.zero)) + + const [k, lambda] = positiveIndices + .map<[number, Rational]>(i => [i, b.at(i).sub(A.rowAt(i).dot(x)).div(A.rowAt(i).dot(xi))]) + .reduce(([i1, lambda1], [i2, lambda2]) => (lambda2.lt(lambda1) ? [i2, lambda2] : [i1, lambda1])) + + rows.push( +
+ 0 \\right\\}`, + `${lambda}`, + ].join(' = ')} + /> +
+ ) + rows.push( +
+ 0 \\right\\}`, + `${k + 1}`, + ].join(' = ')} + /> +
+ ) + rows.push( +
+ i !== h), k].toSorted())}`, + ].join(' = ')} + /> +
+ ) } else { rows.push(
@@ -188,7 +331,9 @@ export const PrimaleStep = ({ input, step }: { input: ProblemInput; step: Step } return (
{rows}
-
+
+ +
) } diff --git a/src/lib/latex.ts b/src/lib-v1/latex.ts similarity index 100% rename from src/lib/latex.ts rename to src/lib-v1/latex.ts diff --git a/src/lib/math.ts b/src/lib-v1/math.ts similarity index 100% rename from src/lib/math.ts rename to src/lib-v1/math.ts diff --git a/src/lib/matrix.ts b/src/lib-v1/matrix.ts similarity index 100% rename from src/lib/matrix.ts rename to src/lib-v1/matrix.ts diff --git a/src/lib/matrix_test.ts b/src/lib-v1/matrix_test.ts similarity index 100% rename from src/lib/matrix_test.ts rename to src/lib-v1/matrix_test.ts diff --git a/src/lib/parser.ts b/src/lib-v1/parser.ts similarity index 88% rename from src/lib/parser.ts rename to src/lib-v1/parser.ts index 3ef9940..8fe04d5 100644 --- a/src/lib/parser.ts +++ b/src/lib-v1/parser.ts @@ -2,32 +2,36 @@ import { Matrix } from './matrix' import { isRational, Rational } from './rationals' import { Vector } from './vector' -export type Value = Rational | Rational[] | Rational[][] +export type Value = { rank: 0; value: Rational } | { rank: 1; value: Rational[] } | { rank: 2; value: Rational[][] } export function asScalar(v: Value): Rational { - if (isRational(v)) { - return v + if (v.rank === 0) { + return v.value } throw new Error(`Expected scalar, got ${JSON.stringify(v)}`) } export function asVector(v: Value): Vector { - if (isRational(v)) { - return Vector.ofRationals([v]) + if (v.rank === 1) { + return Vector.ofRationals(v.value) } - if (Array.isArray(v) && v.every(vv => isRational(vv))) { - return Vector.ofRationals(v) - } + // if (isRational(v)) { + // return Vector.ofRationals([v]) + // } - if (Array.isArray(v) && v.every(vv => Array.isArray(vv) && vv.length === 1 && isRational(vv[0]))) { - return Vector.ofRationals(v.map(vv => vv[0])) - } + // if (Array.isArray(v) && v.every(vv => isRational(vv))) { + // return Vector.ofRationals(v) + // } - if (Array.isArray(v) && v.length === 1 && Array.isArray(v[0]) && v[0].every(vv => isRational(vv))) { - return Vector.ofRationals(v[0]) - } + // if (Array.isArray(v) && v.every(vv => Array.isArray(vv) && vv.length === 1 && isRational(vv[0]))) { + // return Vector.ofRationals(v.map(vv => vv[0])) + // } + + // if (Array.isArray(v) && v.length === 1 && Array.isArray(v[0]) && v[0].every(vv => isRational(vv))) { + // return Vector.ofRationals(v[0]) + // } throw new Error(`Expected column vector, got ${JSON.stringify(v)}`) } diff --git a/src/lib/rationals.ts b/src/lib-v1/rationals.ts similarity index 100% rename from src/lib/rationals.ts rename to src/lib-v1/rationals.ts diff --git a/src/lib/vector.ts b/src/lib-v1/vector.ts similarity index 100% rename from src/lib/vector.ts rename to src/lib-v1/vector.ts diff --git a/src/lib-v2/_example.ts b/src/lib-v2/_example.ts new file mode 100644 index 0000000..6031f32 --- /dev/null +++ b/src/lib-v2/_example.ts @@ -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) diff --git a/src/lib-v2/canvas/index.ts b/src/lib-v2/canvas/index.ts new file mode 100644 index 0000000..02c916d --- /dev/null +++ b/src/lib-v2/canvas/index.ts @@ -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() +} diff --git a/src/lib-v2/latex.ts b/src/lib-v2/latex.ts new file mode 100644 index 0000000..e433708 --- /dev/null +++ b/src/lib-v2/latex.ts @@ -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) => + `\\begin{bmatrix} ${matrix + .getData() + .map(row => row.map(r => rationalToLatex(r)).join(' & ')) + .join(' \\\\ ')} \\end{bmatrix}` + +export const vectorToLatex = (vector: Vector) => + vector + ? `\\begin{bmatrix} ${vector + .getData() + .map(r => rationalToLatex(r)) + .join(' \\\\ ')} \\end{bmatrix}` + : '' + +export const rowVectorToLatex = (vector: Vector) => + vector + ? `\\begin{bmatrix} ${vector + .getData() + .map(r => rationalToLatex(r)) + .join(' & ')} \\end{bmatrix}` + : '' + +export const indexSetToLatex = (indices: number[]) => `\\{${indices.map(i => (i + 1).toString()).join(', ')}\\}` diff --git a/src/lib-v2/math.ts b/src/lib-v2/math.ts new file mode 100644 index 0000000..baf550a --- /dev/null +++ b/src/lib-v2/math.ts @@ -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 = { + zero: T + one: T +} + +export type FieldValue = { + baseField: Field + + 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 +} diff --git a/src/lib-v2/matrix.ts b/src/lib-v2/matrix.ts new file mode 100644 index 0000000..bcc823b --- /dev/null +++ b/src/lib-v2/matrix.ts @@ -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> { + abstract baseField: Field + + 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): Vector { + 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 { + 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 { + rows ??= range(0, this.rows) + cols ??= range(0, this.cols) + + return new MatrixView(this, rows, cols) + } + + rowAt(i: number): Vector { + return new MatrixRow(this, i) + } + + transpose(): Matrix { + return new MatrixTransposed(this) + } + + toString() { + return `Matrix of ${this.rows}x${this.cols} [${this.getData() + .map(row => `[${row.join(', ')}]`) + .join(', ')}]` + } + + static of>(data: T[][]): Matrix { + const rows = data.length + const cols = data[0].length + + return new MatrixDense(rows, cols, data) + } +} + +class MatrixDense> extends Matrix { + 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> extends Matrix { + public forwardRowIndices: number[] = [] + public forwardColIndices: number[] = [] + + constructor(public parent: Matrix, 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> extends Matrix { + constructor(public parent: Matrix) { + 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> extends Vector { + constructor(public parent: Matrix, public row: number) { + super() + } + + get size() { + return this.parent.cols + } + + at(i: number): T { + return this.parent.at(this.row, i) + } +} diff --git a/src/lib-v2/parser.ts b/src/lib-v2/parser.ts new file mode 100644 index 0000000..3c49b24 --- /dev/null +++ b/src/lib-v2/parser.ts @@ -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 { + 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 { + 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 { + const tokens = tokenize(source) + // console.log(tokens) + + const result: Record = {} + 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> { + try { + return { result: parse(source) } + } catch (e) { + return { error: e!.toString() } + } +} diff --git a/src/lib-v2/rationals.ts b/src/lib-v2/rationals.ts new file mode 100644 index 0000000..7d3fc2b --- /dev/null +++ b/src/lib-v2/rationals.ts @@ -0,0 +1,117 @@ +import { Field, FieldValue, gcd } from './math' + +export class Rational implements FieldValue { + 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 = { + zero: Rational.of(0), + one: Rational.of(1), +} diff --git a/src/lib-v2/util.ts b/src/lib-v2/util.ts new file mode 100644 index 0000000..84d69a2 --- /dev/null +++ b/src/lib-v2/util.ts @@ -0,0 +1,9 @@ +export type Result = { result: T } | { error: string } + +export const tryBlock = (fn: () => T): Result => { + try { + return { result: fn() } + } catch (e) { + return { error: e!.toString() } + } +} diff --git a/src/lib-v2/vector.ts b/src/lib-v2/vector.ts new file mode 100644 index 0000000..223e48e --- /dev/null +++ b/src/lib-v2/vector.ts @@ -0,0 +1,82 @@ +import { Field, FieldValue, range } from './math' + +export abstract class Vector> { + abstract size: number + abstract at(i: number): T + + slice(indices: number[]): Vector { + return new VectorDense(indices.map(i => this.at(i))) + } + + dot(vector: Vector): 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 { + return new VectorDense(this.getData().map(v => v.neg())) + } + + with(indices: number[], vector: Vector): Vector { + 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>(data: T[]): Vector { + return new VectorDense(data) + } + + static oneHot>(baseField: Field, size: number, index: number): Vector { + return new OneHotVector(size, index, baseField) + } + + static zero>(baseField: Field, size: number): Vector { + return Vector.of(range(0, size).map(() => baseField.zero)) + } +} + +class VectorDense> extends Vector { + 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> extends Vector { + constructor(public size: number, public index: number, public baseField: Field) { + 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}` + } +} diff --git a/src/main.tsx b/src/main.tsx index 94a8465..326cceb 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -11,7 +11,7 @@ A = 1 0 0 1 2 1 -1 0 - -1 0; + 0 -1; b = 4 7 diff --git a/src/parser-problem.tsx b/src/parser-problem.tsx index 499b0dd..3c40d8d 100644 --- a/src/parser-problem.tsx +++ b/src/parser-problem.tsx @@ -1,6 +1,8 @@ -import { Matrix, Vector } from './lib/matvec' -import { asMatrix, asVector, parseSafe } from './lib/parser' -import { isRational, Rational } from './lib/rationals' +import { Matrix } from './lib-v2/matrix' +import { asMatrix, asVector, parseSafe } from './lib-v2/parser' +import { Rational } from './lib-v2/rationals' +import { Result, tryBlock } from './lib-v2/util' +import { Vector } from './lib-v2/vector' export type ProblemInput = { A: Matrix @@ -10,52 +12,64 @@ export type ProblemInput = { B: number[] } -export function parseSafeProblemInput(source: string): { result: ProblemInput } | { error: string } { +export function parseSafeProblemInput(source: string): Result { const parseResult = parseSafe(source) if ('error' in parseResult) { return parseResult } + const { result: env } = parseResult - const { - result: { A, b, c, B }, - } = parseResult - - if (!A) { + if (!env.A) { return { error: 'Manca la matrice A' } } - if (!Array.isArray(A) || !A.every(row => Array.isArray(row))) { - return { error: 'A deve essere una matrice' } - } - if (!b) { + if (!env.b) { return { error: 'Manca il vettore b' } } - if (!c) { + if (!env.c) { return { error: 'Manca il vettore c' } } - if (!B) { - return { error: 'Manca la base iniziale B' } + if (!env.B) { + return { error: 'Manca il vettore B' } } - if (!Array.isArray(B)) { - return { error: 'B deve essere un vettore' } + + const A_asMatrixResult = tryBlock(() => asMatrix(env.A)) + if ('error' in A_asMatrixResult) { + return A_asMatrixResult } - if (B.length !== 2) { - return { error: 'B deve contenere esattamente due elementi' } + const { result: A } = A_asMatrixResult + + const b_asVectorResult = tryBlock(() => asVector(env.b)) + if ('error' in b_asVectorResult) { + return b_asVectorResult } - if (B.some(i => !isRational(i) || i.den !== 1 || !(1 <= i.num && i.num <= A.length))) { - return { error: `Gli elementi di B devono essere interi tra 1 e ${A[0].length}: ${JSON.stringify(B)}` } + const { result: b } = b_asVectorResult + + const c_asVectorResult = tryBlock(() => asVector(env.c)) + if ('error' in c_asVectorResult) { + return c_asVectorResult } + const { result: c } = c_asVectorResult - try { - return { - result: { - A: asMatrix(A), - b: asVector(b), - c: asVector(c), + const B = env.B - B: (B as Rational[]).map(i => i.num - 1), - }, + if (B.rank !== 1) { + return { error: 'B deve essere un vettore' } + } + if (B.value.length !== 2) { + return { error: 'B deve contenere esattamente due elementi' } + } + if (B.value.some(v => !v.isInteger() || v.num < 1 || v.num > A.rows)) { + return { + error: `Gli elementi di B devono essere interi tra 1 e ${A.rows}: ${JSON.stringify(B.value)}`, } - } catch (e) { - return { error: e!.toString() } + } + + return { + result: { + A, + b, + c, + B: B.value.map(i => i.num - 1), + }, } } diff --git a/src/style.css b/src/style.css index 11b755d..0a2427a 100644 --- a/src/style.css +++ b/src/style.css @@ -74,6 +74,57 @@ margin: 0.5rem auto; } } + + canvas { + display: block; + width: 100%; + + aspect-ratio: 1 / 1; + + border: 1px solid #ddd; + box-shadow: 0 0 1rem rgba(0, 0, 0, 0.1); + + border-radius: 1rem; + } + + .steps { + display: grid; + grid-auto-flow: row; + justify-items: center; + + border: 1px solid #ddd; + box-shadow: 0 0 1rem rgba(0, 0, 0, 0.1); + + border-radius: 1rem; + + padding: 2rem; + max-width: fit-content; + + > .step { + display: grid; + grid-template-columns: 1fr 1fr; + justify-items: center; + align-items: center; + gap: 0; + + @media (width < 768px) { + grid-template-columns: 1fr; + } + + > .algebraic-step { + /* border: 1px solid #ddd; */ + /* box-shadow: 0 0 1rem rgba(0, 0, 0, 0.1); */ + + border-radius: 1rem; + padding: 0 1rem; + } + + > .geometric-step { + /* padding: 1rem; */ + min-width: 30rem; + } + } + } } @layer utilities {