commit 4f4f9537bc829b6e8f585c05832f709b7c66575c Author: Antonio De Lucreziis Date: Sun Jan 19 18:10:37 2025 +0100 initial commit 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 0000000..23a36c5 Binary files /dev/null and b/bun.lockb differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..8996609 --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + + + + 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()], +})