back to a working state and some graphics

main
parent 4f4f9537bc
commit 8b29e8cab6

@ -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 }) => {

@ -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<Rational>): 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<Rational>
b: Vector<Rational>
c: Vector<Rational>
B: number[]
x?: Vector<Rational>
}) => {
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 <canvas ref={render} />
}
export const PrimaleStep = ({ input, step }: { input: ProblemInput; step: Step }) => {
const { A, b, c } = input
const rows = []
const canvasOptions: Parameters<typeof PrimaleCanvas>[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(
<div class="row">
@ -81,8 +182,9 @@ export const PrimaleStep = ({ input, step }: { input: ProblemInput; step: Step }
</div>
)
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(
<div class="row">
@ -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(
<div class="row">
@ -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(
<div class="row">
@ -136,10 +238,11 @@ export const PrimaleStep = ({ input, step }: { input: ProblemInput; step: Step }
</div>
)
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(
<div class="row">
@ -154,26 +257,66 @@ export const PrimaleStep = ({ input, step }: { input: ProblemInput; step: Step }
</div>
)
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(
<div class="row">
<Katex
formula={[
//
`A_N \\xi`,
String.raw`${matrixToLatex(A_N)} ${vectorToLatex(xi)}`,
String.raw`${vectorToLatex(A_N__xi)}`,
].join(' = ')}
[`N = \\{1, \\dots, m\\} \\setminus B = ${indexSetToLatex(N)}`],
[
`A_N \\xi`,
String.raw`${matrixToLatex(A_N)} ${vectorToLatex(xi)}`,
String.raw`${vectorToLatex(A_N__xi)}`,
].join(' = '),
].join(' \\qquad ')}
/>
</div>
)
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(
<div class="row">
<Katex
formula={[
`\\bar\\lambda`,
`\\min_i \\left\\{ \\frac{b_i - A_i \\bar{x}}{A_i \\xi} \\; \\middle| \\; i \\in N, A_i \\xi > 0 \\right\\}`,
`${lambda}`,
].join(' = ')}
/>
</div>
)
rows.push(
<div class="row">
<Katex
formula={[
`k`,
`\\argmin_i \\left\\{ \\bar\\lambda_i \\; \\middle| \\; i \\in N, A_i \\xi > 0 \\right\\}`,
`${k + 1}`,
].join(' = ')}
/>
</div>
)
rows.push(
<div class="row">
<Katex
formula={[
`\\implies B'`,
`B \\setminus \\{${h + 1}\\} \\cup \\{${k + 1}\\}`,
`${indexSetToLatex([...step.B.filter(i => i !== h), k].toSorted())}`,
].join(' = ')}
/>
</div>
)
} else {
rows.push(
<div class="row">
@ -188,7 +331,9 @@ export const PrimaleStep = ({ input, step }: { input: ProblemInput; step: Step }
return (
<div class="step">
<div class="algebraic-step">{rows}</div>
<div class="geometric-step"></div>
<div class="geometric-step">
<PrimaleCanvas {...canvasOptions} />
</div>
</div>
)
}

@ -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<Rational> {
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)}`)
}

@ -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}`
}
}

@ -11,7 +11,7 @@ A = 1 0
0 1
2 1
-1 0
-1 0;
0 -1;
b = 4
7

@ -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<Rational>
@ -10,52 +12,64 @@ export type ProblemInput = {
B: number[]
}
export function parseSafeProblemInput(source: string): { result: ProblemInput } | { error: string } {
export function parseSafeProblemInput(source: string): Result<ProblemInput> {
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),
},
}
}

@ -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 {

Loading…
Cancel
Save