From 4f4f9537bc829b6e8f585c05832f709b7c66575c Mon Sep 17 00:00:00 2001 From: Antonio De Lucreziis Date: Sun, 19 Jan 2025 18:10:37 +0100 Subject: [PATCH] initial commit --- .gitignore | 175 +++++++++++++++++++++++++ README.md | 27 ++++ bun.lockb | Bin 0 -> 51916 bytes index.html | 14 ++ package.json | 22 ++++ src/DisplayProblemInput.tsx | 19 +++ src/Katex.tsx | 19 +++ src/Primale.tsx | 194 ++++++++++++++++++++++++++++ src/lib/latex.ts | 29 +++++ src/lib/math.ts | 42 ++++++ src/lib/matrix.ts | 249 ++++++++++++++++++++++++++++++++++++ src/lib/matrix_test.ts | 28 ++++ src/lib/parser.ts | 245 +++++++++++++++++++++++++++++++++++ src/lib/rationals.ts | 99 ++++++++++++++ src/lib/vector.ts | 146 +++++++++++++++++++++ src/main.tsx | 66 ++++++++++ src/parser-problem.tsx | 61 +++++++++ src/style.css | 93 ++++++++++++++ src/vite.d.ts | 4 + tsconfig.app.json | 31 +++++ tsconfig.json | 10 ++ tsconfig.node.json | 24 ++++ vite.config.ts | 7 + 23 files changed, 1604 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 bun.lockb create mode 100644 index.html create mode 100644 package.json create mode 100644 src/DisplayProblemInput.tsx create mode 100644 src/Katex.tsx create mode 100644 src/Primale.tsx create mode 100644 src/lib/latex.ts create mode 100644 src/lib/math.ts create mode 100644 src/lib/matrix.ts create mode 100644 src/lib/matrix_test.ts create mode 100644 src/lib/parser.ts create mode 100644 src/lib/rationals.ts create mode 100644 src/lib/vector.ts create mode 100644 src/main.tsx create mode 100644 src/parser-problem.tsx create mode 100644 src/style.css create mode 100644 src/vite.d.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b1ee42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,175 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..34f87eb --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# math-canvas-v2 + +## Usage + +### Setup + +To install the dependencies, use the following command. + +```bash +bun install +``` + +### Development + +To start the ViteJS development server, use the following command. + +```bash +bun dev +``` + +### Build + +Use this command to build the project and serve the files from the `dist/` directory. + +```bash +bun run build +``` diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..23a36c55388797efb0ddeb669fa55abe69efcb25 GIT binary patch literal 51916 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 + + + + + + + + Ricerca Operativa / Programmazione Lineare + + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..2f9ce6a --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "math-canvas-v2", + "module": "index.ts", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "devDependencies": { + "@preact/preset-vite": "^2.9.3", + "@types/bun": "latest", + "@types/katex": "^0.16.7", + "vite": "^6.0.6" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "katex": "^0.16.20", + "preact": "^10.25.4" + } +} diff --git a/src/DisplayProblemInput.tsx b/src/DisplayProblemInput.tsx new file mode 100644 index 0000000..07cc1f5 --- /dev/null +++ b/src/DisplayProblemInput.tsx @@ -0,0 +1,19 @@ +import { Katex } from './Katex' +import { matrixToLatex, rowVectorToLatex, vectorToLatex } from './lib/latex' +import { ProblemInput } from './parser-problem' + +export const DisplayProblemInput = ({ problemInput }: { problemInput: ProblemInput }) => { + const { A, b, c, B } = problemInput + + return ( + (r + 1).toString()).join(', ')}\\}`, + ].join(' \\qquad ')} + /> + ) +} diff --git a/src/Katex.tsx b/src/Katex.tsx new file mode 100644 index 0000000..32c99ee --- /dev/null +++ b/src/Katex.tsx @@ -0,0 +1,19 @@ +import katex from 'katex' +import 'katex/dist/katex.css' + +type KatexProps = { + formula: string + + displayMode?: boolean +} + +export const Katex = ({ formula, displayMode }: KatexProps) => { + displayMode ??= true + + const html = katex.renderToString(formula, { + throwOnError: false, + displayMode, + }) + + return +} diff --git a/src/Primale.tsx b/src/Primale.tsx new file mode 100644 index 0000000..97584ce --- /dev/null +++ b/src/Primale.tsx @@ -0,0 +1,194 @@ +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 { ProblemInput } from './parser-problem' + +type Step = { + B: number[] +} + +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] : [])) +} + +export const Primale = ({ input }: { input: ProblemInput }) => { + const steps: Step[] = [{ B: input.B }] + + return ( +
+ {steps.map(step => ( + + ))} +
+ ) +} + +export const PrimaleStep = ({ input, step }: { input: ProblemInput; step: Step }) => { + const { A, b, c } = input + const rows = [] + + const A_B = A.slice({ rows: step.B }) + const A_B_inverse = A_B.inverse2x2() + const b_B = b.slice(step.B) + + rows.push( +
+ +
+ ) + + const x = A_B_inverse.apply(b_B) + + rows.push( +
+ +
+ ) + + const y_Zero = Array.from({ length: A.rows }, () => ({ num: 0, den: 1 })) + const y = Vec.with(y_Zero, step.B, y_B) + + rows.push( +
+ +
+ ) + + const I_x = activeIndices(input, x) + + rows.push( +
+ . +

+

+ La soluzione duale è {isDualAdmissible ? 'ammissibile' : 'non ammissibile'} e{' '} + {isDualDegenerate ? 'degenere' : 'non degenere'}. +

+
+ ) + + if (!isDualAdmissible) { + const h = Math.min(...y.flatMap((y, i) => (Rationals.lt(y, Rationals.zero) ? [i] : []))) + + rows.push( +
+ +
+ ) + + const N = Array.from({ length: A.length }, (_, i) => i).filter(i => !step.B.includes(i)) + const A_N = Mat.slice(A, { rows: N }) + + const A_N__xi = Mat.apply(A_N, xi) + + rows.push( +
+ (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/math.ts b/src/lib/math.ts new file mode 100644 index 0000000..4e304ec --- /dev/null +++ b/src/lib/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 Ops = { + name: string + + zero: T + one: T + + isZero(a: T): boolean + isOne(a: T): boolean + + sum(a: T, b: T): T + sub(a: T, b: T): T + mul(a: T, b: T): T + div(a: T, b: T): T + + inverse(a: T): T + neg(a: T): T + + scale(a: T, k: number): T + + eq(a: T, b: T): boolean + lt(a: T, b: T): boolean + gt(a: T, b: T): boolean + leq(a: T, b: T): boolean + geq(a: T, b: T): boolean + + toString(v: T): string +} diff --git a/src/lib/matrix.ts b/src/lib/matrix.ts new file mode 100644 index 0000000..da742fc --- /dev/null +++ b/src/lib/matrix.ts @@ -0,0 +1,249 @@ +import { Ops, range } from './math' +import { Rational, Rationals } from './rationals' +import { Vector } from './vector' + +export type MatrixShape = { rows: number; cols: number } + +export abstract class Matrix { + abstract ops: Ops + + abstract rowIndices: number[] + abstract colIndices: number[] + + abstract at(i: number, j: number): T + + withValues(values: T[][]): Matrix { + if (values.length !== this.shape.rows) { + throw new Error('Invalid number of rows') + } + if (values.some(row => row.length !== this.shape.cols)) { + throw new Error('Invalid number of columns') + } + + return new MatrixDense(this.ops, this.rowIndices, this.colIndices, values) + } + + get rootShape(): MatrixShape { + return { + rows: this.rowIndices.length, + cols: this.colIndices.length, + } + } + + get shape(): MatrixShape { + return this.rootShape + } + + getData(): T[][] { + return range(0, this.shape.rows).map(i => range(0, this.shape.cols).map(j => this.at(i, j))) + } + + apply(vector: Vector): Vector { + if (this.shape.cols !== vector.size) { + throw new Error('Matrix and vector dimensions do not match') + } + + return vector.withValues(this.getRows().map(row => row.dot(vector))) + } + + inverse2x2(): Matrix { + if (this.shape.rows !== 2 || this.shape.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 = this.ops.sub(this.ops.mul(a, d), this.ops.mul(b, c)) + if (this.ops.isZero(det)) { + throw new Error('Matrix is singular') + } + + // return new MatrixDense( + // this.ops, + // 2, + // 2, + // [ + // [d, this.ops.neg(b)], + // [this.ops.neg(c), a], + // ].map(row => row.map(r => this.ops.div(r, det))) + // ) + + return this.withValues( + [ + [d, this.ops.neg(b)], + [this.ops.neg(c), a], + ].map(row => row.map(r => this.ops.div(r, det))) + ) + } + + slice({ rows, cols }: { rows?: number[]; cols?: number[] }): Matrix { + rows ??= range(0, this.rootShape.rows).filter(i => this.rowIndices.includes(i)) + cols ??= range(0, this.rootShape.cols).filter(j => this.colIndices.includes(j)) + + return new MatrixView(this, rows, cols) + } + + transpose(): Matrix { + return new MatrixTransposed(this) + } + + getRow(i: number): RowVector { + return new RowVector(this, i) + } + + getRows(indices: number[] = this.rowIndices): RowVector[] { + return indices.map(i => new RowVector(this, i)) + } + + getColumn(j: number): ColumnVector { + return new ColumnVector(this, j) + } + + getColumns(indices: number[] = this.colIndices): ColumnVector[] { + return indices.map(j => new ColumnVector(this, j)) + } + + static ofRationals(rows: number, cols: number, data: Rational[][]): Matrix { + return Matrix.of(Rationals, rows, cols, data) + } + + static of(ops: Ops, rows: number, cols: number, data: T[][]): Matrix { + return new MatrixDense(ops, range(0, rows), range(0, cols), data) + } +} + +class MatrixDense extends Matrix { + constructor(public ops: Ops, public rowIndices: number[], public colIndices: number[], public data: T[][]) { + super() + + if (data.length !== rowIndices.length) { + throw new Error('Invalid number of rows') + } + if (data.some(row => row.length !== colIndices.length)) { + throw new Error('Invalid number of columns') + } + } + + at(i: number, j: number): T { + console.log('MatrixDense at', i, j) + + return this.data[i][j] + } + + toString() { + return `Matrix over ${this.ops.name} ${this.shape.rows} x ${this.shape.cols} of [${this.data + .map(row => `[${row.map(r => this.ops.toString(r)).join(', ')}]`) + .join(', ')}])` + } +} + +class MatrixTransposed extends Matrix { + constructor(public parent: Matrix) { + super() + } + + get ops() { + return this.parent.ops + } + + get rowIndices() { + return this.parent.colIndices + } + + get colIndices() { + return this.parent.rowIndices + } + + at(i: number, j: number): T { + return this.parent.at(j, i) + } + + toString() { + return `Transpose of (${this.parent.toString()})` + } +} + +class MatrixView extends Matrix { + private reverseRows: number[] = [] + private reverseCols: number[] = [] + + constructor(public parent: Matrix, public rowIndices: number[], public colIndices: number[]) { + super() + + if (rowIndices.some(i => i >= parent.shape.rows)) { + throw new Error('Invalid row index') + } + if (colIndices.some(j => j >= parent.shape.cols)) { + throw new Error('Invalid column index') + } + + rowIndices.forEach((rowIndex, i) => (this.reverseRows[rowIndex] = i)) + colIndices.forEach((colIndex, j) => (this.reverseCols[colIndex] = j)) + } + + get ops() { + return this.parent.ops + } + + at(i: number, j: number): T { + console.log('MatrixView at', i, j) + + return this.parent.at(this.rowIndices[i], this.colIndices[j]) + } + + toString() { + return `View of (${this.parent.toString()}) with {${this.rowIndices.join(', ')}} x {${this.colIndices.join( + ', ' + )}} of [${range(0, this.rowIndices.length) + .map( + i => + `[${range(0, this.colIndices.length) + .map(j => this.ops.toString(this.at(i, j))) + .join(', ')}]` + ) + .join(', ')}]` + } +} + +class ColumnVector extends Vector { + constructor(public parent: Matrix, public colIndex: number) { + super() + } + + get indices() { + return this.parent.rowIndices + } + + get ops() { + return this.parent.ops + } + + at(i: number): T { + console.log('ColumnVector at', i) + + return this.parent.at(i, this.colIndex) + } +} + +class RowVector extends Vector { + constructor(public parent: Matrix, public rowIndex: number) { + super() + } + + get indices() { + return this.parent.colIndices + } + + get ops() { + return this.parent.ops + } + + at(i: number): T { + console.log('RowVector at', i, this.parent.colIndices) + + return this.parent.at(this.rowIndex, i) + } +} diff --git a/src/lib/matrix_test.ts b/src/lib/matrix_test.ts new file mode 100644 index 0000000..e3e7422 --- /dev/null +++ b/src/lib/matrix_test.ts @@ -0,0 +1,28 @@ +import { Matrix } from './matrix' +import { Rationals } from './rationals' +import { Vector } from './vector' + +const A = Matrix.ofRationals(4, 3, [ + [Rationals.of(1), Rationals.of(2), Rationals.of(3)], + [Rationals.of(4), Rationals.of(5), Rationals.of(6)], + [Rationals.of(7), Rationals.of(8), Rationals.of(9)], + [Rationals.of(10), Rationals.of(11), Rationals.of(12)], +]) + +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()) + +const b = Vector.ofRationals([Rationals.of(1), Rationals.of(0)]) + +console.log(b.toString()) + +const x = A_B.apply(b) + +console.log(x.toString()) diff --git a/src/lib/parser.ts b/src/lib/parser.ts new file mode 100644 index 0000000..3ef9940 --- /dev/null +++ b/src/lib/parser.ts @@ -0,0 +1,245 @@ +import { Matrix } from './matrix' +import { isRational, Rational } from './rationals' +import { Vector } from './vector' + +export type Value = Rational | Rational[] | Rational[][] + +export function asScalar(v: Value): Rational { + if (isRational(v)) { + return v + } + + throw new Error(`Expected scalar, got ${JSON.stringify(v)}`) +} + +export function asVector(v: Value): Vector { + if (isRational(v)) { + return Vector.ofRationals([v]) + } + + if (Array.isArray(v) && v.every(vv => isRational(vv))) { + 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.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)}`) +} + +export function asMatrix(v: Value): Matrix { + // scalar + if (isRational(v)) { + return Matrix.ofRationals(1, 1, [[v]]) + } + + // vector + if (Array.isArray(v) && v.every(vv => isRational(vv))) { + return Matrix.ofRationals( + v.length, + 1, + v.map(vv => [vv]) + ) + } + + // matrix + if (Array.isArray(v) && v.every(vv => Array.isArray(vv) && vv.every(vvv => isRational(vvv)))) { + return Matrix.ofRationals(v.length, v[0].length, v) + } + + throw new Error(`Expected matrix, got ${JSON.stringify(v)}`) +} + +function transposeValue(v: Value): Value { + // scalar + if (isRational(v)) { + return v + } + + // vector + if (Array.isArray(v) && v.every(vv => isRational(vv))) { + return v.map(vv => [vv]) + } + + // matrix + if (Array.isArray(v) && v.every(vv => Array.isArray(vv) && vv.every(vvv => isRational(vvv)))) { + return 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 values: 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({ num: n, den: Number(tokens[i].value) }) + + i++ + } else { + i++ + currentRow.push({ num: n, den: 1 }) + } + } else if (token.type === TokenType.Newline) { + if (currentRow.length > 0) { + values.push(currentRow) + currentRow = [] + } + i++ + } else if (token.type === TokenType.Semicolon) { + if (currentRow.length > 0) { + values.push(currentRow) + } + i++ + break + } else { + break + } + } + + if (values.length === 1) { + return values[0].length === 1 ? values[0][0] : values[0] + } + + return values + } + + 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 } | { error: string } { + try { + return { result: parse(source) } + } catch (e) { + return { error: e!.toString() } + } +} + +// 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; +` + +console.dir(parse(source), { depth: null }) diff --git a/src/lib/rationals.ts b/src/lib/rationals.ts new file mode 100644 index 0000000..f703ffb --- /dev/null +++ b/src/lib/rationals.ts @@ -0,0 +1,99 @@ +import { gcd } from './math' + +export type Rational = { num: number; den: number } + +export function isRational(v: any): v is Rational { + return typeof v === 'object' && 'num' in v && 'den' in v +} + +export const Rationals = { + name: 'Rationals', + + zero: { num: 0, den: 1 }, + one: { num: 1, den: 1 }, + + isZero: (r: Rational) => r.num === 0, + isOne: (r: Rational) => r.num === r.den, + + of: (num: number, den: number = 1) => { + if (den === 0) { + throw new Error('Division by zero') + } + if ((num | 0) !== num || (den | 0) !== den) { + throw new Error('Expected integer') + } + + return { num, den } + }, + + toString: (r: Rational) => (r.den === 1 ? r.num.toString() : `${r.num} / ${r.den}`), + + simplify: (r: Rational) => { + if (r.den === 0) { + throw new Error('Division by zero') + } + if (r.num === 0) { + return Rationals.zero + } + if (r.den < 0) { + r = { num: -r.num, den: -r.den } + } + + const g = Math.abs(gcd(r.num, r.den)) + + return { + num: r.num / g, + den: r.den / g, + } + }, + + sum: (a: Rational, b: Rational) => + Rationals.simplify({ + num: a.num * b.den + b.num * a.den, + den: a.den * b.den, + }), + sub: (a: Rational, b: Rational) => + Rationals.simplify({ + num: a.num * b.den - b.num * a.den, + den: a.den * b.den, + }), + mul: (a: Rational, b: Rational) => + Rationals.simplify({ + num: a.num * b.num, + den: a.den * b.den, + }), + div: (a: Rational, b: Rational) => + Rationals.simplify({ + num: a.num * b.den, + den: a.den * b.num, + }), + + inverse: (a: Rational) => { + if (a.num === 0) { + throw new Error('Division by zero') + } + + return Rationals.simplify({ + num: a.den, + den: a.num, + }) + }, + neg: (a: Rational) => { + return Rationals.simplify({ + num: -a.num, + den: a.den, + }) + }, + + scale: (a: Rational, k: number) => + Rationals.simplify({ + num: a.num * k, + den: a.den, + }), + + eq: (a: Rational, b: Rational) => a.num * b.den === b.num * a.den, + lt: (a: Rational, b: Rational) => a.num * b.den < b.num * a.den, + gt: (a: Rational, b: Rational) => a.num * b.den > b.num * a.den, + leq: (a: Rational, b: Rational) => a.num * b.den <= b.num * a.den, + geq: (a: Rational, b: Rational) => a.num * b.den >= b.num * a.den, +} diff --git a/src/lib/vector.ts b/src/lib/vector.ts new file mode 100644 index 0000000..7b6617d --- /dev/null +++ b/src/lib/vector.ts @@ -0,0 +1,146 @@ +import { Ops, range } from './math' +import { Rationals, Rational } from './rationals' + +export abstract class Vector { + abstract ops: Ops + + abstract indices: number[] + abstract at(i: number): T + + withValues(values: T[]): Vector { + if (values.length !== this.indices.length) { + throw new Error('Invalid number of values') + } + + return new VectorDense(this.ops, this.indices, values) + } + + get size(): number { + return this.indices.length + } + + slice(indices: number[]): Vector { + return new SubVector(this, indices) + } + + dot(vector: Vector): T { + if (this.size !== vector.size) { + throw new Error('Vector dimensions do not match') + } + + console.log('this.indices', this.indices) + console.log('vector.indices', vector.indices) + + return this.indices.map((_, ii) => this.ops.mul(this.at(ii), vector.at(ii))).reduce(this.ops.sum, this.ops.zero) + } + + getData(): T[] { + return this.indices.map(i => this.at(i)) + } + + getIndexedData(): [number, T][] { + return this.indices.map(i => [i, this.at(i)]) + } + + static ofRationals(data: Rational[]): Vector { + return Vector.of(Rationals, data) + } + + static of(ops: Ops, data: T[]): Vector { + return VectorDense.of(ops, data) + } + + static oneHot(ops: Ops, size: number, index: number): Vector { + return new OneHotVector(ops, size, index) + } +} + +class VectorDense extends Vector { + constructor(public ops: Ops, public indices: number[], public data: T[]) { + super() + } + + at(i: number): T { + return this.data[this.indices[i]] + } + + withValues(values: T[]): Vector { + if (values.length !== this.data.length) { + throw new Error('Invalid number of values') + } + + return new VectorDense(this.ops, this.indices, values) + } + + static of(ops: Ops, data: T[]): Vector { + return new VectorDense(ops, range(0, data.length), data) + } + + toString() { + return `Vector over ${this.ops.name} of [${this.data.map(r => this.ops.toString(r)).join(', ')}]` + } +} + +class SubVector extends Vector { + private backwardIndices: number[] = [] + + constructor(public parent: Vector, public indices: number[]) { + super() + this.indices.forEach((index, i) => (this.backwardIndices[index] = i)) + } + + get ops() { + return this.parent.ops + } + + at(i: number): T { + return this.parent.at(this.indices[i]) + } + + withValues(values: T[]): Vector { + if (values.length !== this.indices.length) { + throw new Error('Invalid number of values') + } + + return new VectorDense(this.ops, this.indices, values) + } + + toString() { + return `SubVector of (${this.parent.toString()}) with {${this.indices.join(', ')}} of [${this.indices + .map(i => this.ops.toString(this.at(this.backwardIndices[i]))) + .join(', ')}]` + } +} + +class OneHotVector extends Vector { + #size: number + + constructor(public ops: Ops, size: number, public index: number) { + super() + this.#size = size + } + + get size(): number { + return this.#size + } + + get indices(): number[] { + return range(0, this.size) + } + + at(i: number): T { + return i === this.index ? this.ops.one : this.ops.zero + } + + withValues(values: T[]): Vector { + if (values.length !== this.size) { + throw new Error('Invalid number of values') + } + + return new VectorDense(this.ops, this.indices, values) + } + + 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 new file mode 100644 index 0000000..94a8465 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,66 @@ +import { render } from 'preact' +import { useState } from 'preact/hooks' +import { parseSafeProblemInput } from './parser-problem' +import { DisplayProblemInput } from './DisplayProblemInput' +import { Primale } from './Primale' + +const INITIAL_PROBLEM_INPUT = ` +c' = 500 200; + +A = 1 0 + 0 1 + 2 1 + -1 0 + -1 0; + +b = 4 + 7 + 9 + 0 + 0; + +B = 2 3; +`.trim() + +const App = () => { + const [problemInput, setProblemInput] = useState(INITIAL_PROBLEM_INPUT) + + const problemValuesResult = parseSafeProblemInput(problemInput) + + return ( + <> +

Ricerca Operativa / Programmazione Lineare

+

+ Questo sito è un progetto per il corso di Ricerca Operativa dell'Università di Pisa per visualizzare + automaticamente tutti i passaggi dell'algoritmo del simplesso primale e duale. +

+

Visualizzazione

+

I dati del problema vanno inseriti nel seguente campo di testo nel formato:

+ +

Problema di Input

+ {'result' in problemValuesResult ? ( + + ) : ( +

+ {problemValuesResult.error} +

+ )} + +

Svolgimento

+ + {'result' in problemValuesResult && } + +

Debug

+
+                {JSON.stringify(problemValuesResult, null, 4)}
+            
+ + ) +} + +render(, document.body) diff --git a/src/parser-problem.tsx b/src/parser-problem.tsx new file mode 100644 index 0000000..499b0dd --- /dev/null +++ b/src/parser-problem.tsx @@ -0,0 +1,61 @@ +import { Matrix, Vector } from './lib/matvec' +import { asMatrix, asVector, parseSafe } from './lib/parser' +import { isRational, Rational } from './lib/rationals' + +export type ProblemInput = { + A: Matrix + b: Vector + c: Vector + + B: number[] +} + +export function parseSafeProblemInput(source: string): { result: ProblemInput } | { error: string } { + const parseResult = parseSafe(source) + if ('error' in parseResult) { + return parseResult + } + + const { + result: { A, b, c, B }, + } = parseResult + + if (!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) { + return { error: 'Manca il vettore b' } + } + if (!c) { + return { error: 'Manca il vettore c' } + } + if (!B) { + return { error: 'Manca la base iniziale B' } + } + if (!Array.isArray(B)) { + return { error: 'B deve essere un vettore' } + } + if (B.length !== 2) { + return { error: 'B deve contenere esattamente due elementi' } + } + 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)}` } + } + + try { + return { + result: { + A: asMatrix(A), + b: asVector(b), + c: asVector(c), + + B: (B as Rational[]).map(i => i.num - 1), + }, + } + } catch (e) { + return { error: e!.toString() } + } +} diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..11b755d --- /dev/null +++ b/src/style.css @@ -0,0 +1,93 @@ +@layer base, components, utilities; + +@layer base { + *, + *::before, + *::after { + font-family: inherit; + margin: 0; + box-sizing: border-box; + } + + html, + body { + height: 100%; + min-height: 100%; + } + + img { + display: block; + } + + h1, + h2, + h3, + h4 { + font-weight: 300; + margin: 0; + color: #222; + } + + a { + color: royalblue; + text-decoration: underline dotted; + + &:hover { + text-decoration: underline; + } + } + + textarea { + resize: vertical; + + font-family: 'JetBrains Mono', monospace; + font-weight: 400; + color: #333; + } + + pre, + code { + font-family: 'JetBrains Mono', monospace; + font-weight: 400; + color: #333; + } + + strong { + font-weight: 700; + color: #444; + } +} + +@layer components { + body { + font-family: 'Open Sans', sans-serif; + font-size: 16px; + line-height: 1.75; + + color: #333; + + padding: 3rem 1rem; + + > * { + display: block; + max-width: 900px; + margin: 0.5rem auto; + } + } +} + +@layer utilities { + .v-stack { + display: grid; + gap: 0.5rem; + align-content: start; + justify-items: start; + } + + .h-stack { + display: grid; + gap: 0.5rem; + grid-auto-flow: column; + align-items: center; + } +} diff --git a/src/vite.d.ts b/src/vite.d.ts new file mode 100644 index 0000000..bb3f848 --- /dev/null +++ b/src/vite.d.ts @@ -0,0 +1,4 @@ +declare module '*.css' { + const classes: { readonly [key: string]: string } + export default classes +} diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..9ed4f59 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "paths": { + "react": ["./node_modules/preact/compat/"], + "react-dom": ["./node_modules/preact/compat/"] + }, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "preact", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0d11e84 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "files": [], + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + } +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..ff0dc7d --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..b995c32 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import preact from '@preact/preset-vite' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [preact()], +})