initial commit

main
commit 4f4f9537bc

175
.gitignore vendored

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

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

Binary file not shown.

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="./src/style.css" />
<title>Ricerca Operativa / Programmazione Lineare</title>
</head>
<body>
<script type="module" src="./src/main.tsx"></script>
</body>
</html>

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

@ -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 (
<Katex
formula={[
'\\begin{cases} \\max c^t x \\\\ Ax \\leq b \\end{cases}',
`A = ${matrixToLatex(A)}`,
`b = ${vectorToLatex(b)}`,
`c^t = ${rowVectorToLatex(c)}`,
`B = \\{${B.map(r => (r + 1).toString()).join(', ')}\\}`,
].join(' \\qquad ')}
/>
)
}

@ -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 <span dangerouslySetInnerHTML={{ __html: html }} />
}

@ -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<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] : []))
}
export const Primale = ({ input }: { input: ProblemInput }) => {
const steps: Step[] = [{ B: input.B }]
return (
<div class="steps">
{steps.map(step => (
<PrimaleStep input={input} step={step} />
))}
</div>
)
}
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(
<div class="row">
<Katex
formula={[
String.raw`A_B = ${matrixToLatex(A_B)}`,
String.raw`b_B = ${vectorToLatex(b_B)}`,
String.raw`c^t = ${rowVectorToLatex(c)}`,
].join(' \\qquad ')}
/>
</div>
)
const x = A_B_inverse.apply(b_B)
rows.push(
<div class="row">
<Katex
formula={[
String.raw`\bar{x}`,
String.raw`A_B^{-1} b_B`,
String.raw`${matrixToLatex(A_B)}^{-1} ${vectorToLatex(b_B)}`,
String.raw`${matrixToLatex(A_B_inverse)} ${vectorToLatex(b_B)}`,
String.raw`${vectorToLatex(x)}`,
].join(' = ')}
/>
</div>
)
const y_B = A_B_inverse.transpose().apply(c)
rows.push(
<div class="row">
<Katex
formula={[
String.raw`\bar{y}_B^t`,
String.raw`c_B^t A_B^{-1}`,
String.raw`${rowVectorToLatex(c)} ${matrixToLatex(A_B_inverse)}`,
String.raw`${rowVectorToLatex(y_B)}`,
].join(' = ')}
/>
</div>
)
const y_Zero = Array.from({ length: A.rows }, () => ({ num: 0, den: 1 }))
const y = Vec.with(y_Zero, step.B, y_B)
rows.push(
<div class="row">
<Katex formula={String.raw`\implies \bar{y}^t = ${rowVectorToLatex(y)}`} />
</div>
)
const I_x = activeIndices(input, x)
rows.push(
<div class="row">
<Katex
formula={[
String.raw`I(\bar{x})`,
String.raw`\{ i \in \{1, \dots, m\} \mid A_i \bar{x}_i = b_i \}`,
indexSetToLatex(I_x),
].join(' = ')}
/>
</div>
)
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))
rows.push(
<div class="row">
<p>
La soluzione primale è <strong>{isDegenerate ? 'degenere' : 'non degenere'}</strong>.
</p>
<p>
La soluzione duale è <strong>{isDualAdmissible ? 'ammissibile' : 'non ammissibile'}</strong> e{' '}
<strong>{isDualDegenerate ? 'degenere' : 'non degenere'}</strong>.
</p>
</div>
)
if (!isDualAdmissible) {
const h = Math.min(...y.flatMap((y, i) => (Rationals.lt(y, Rationals.zero) ? [i] : [])))
rows.push(
<div class="row">
<Katex
formula={[
//
String.raw`h`,
String.raw`\min \{ i \in B \mid \bar{y}_i < 0 \}`,
String.raw`${h + 1}`,
].join(' = ')}
/>
</div>
)
const e_h = Vec.slice(Vec.oneHot(A.length, h), step.B)
console.log(e_h)
const xi = Vec.neg(Mat.apply(A_B_inverse, e_h))
rows.push(
<div class="row">
<Katex
formula={[
//
`\\xi`,
String.raw`-A_B^{-1} u_{B(h)}`,
String.raw`${vectorToLatex(xi)}`,
].join(' = ')}
/>
</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 A_N__xi = Mat.apply(A_N, 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(' = ')}
/>
</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))
} else {
rows.push(
<div class="row">
<p>
La soluzione duale è <strong>illimitata</strong>.
</p>
</div>
)
}
}
return (
<div class="step">
<div class="algebraic-step">{rows}</div>
<div class="geometric-step"></div>
</div>
)
}

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

@ -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<T> {
abstract ops: Ops<T>
abstract rowIndices: number[]
abstract colIndices: number[]
abstract at(i: number, j: number): T
withValues(values: T[][]): Matrix<T> {
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<T>): Vector<T> {
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<T> {
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<T> {
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<T> {
return new MatrixTransposed(this)
}
getRow(i: number): RowVector<T> {
return new RowVector(this, i)
}
getRows(indices: number[] = this.rowIndices): RowVector<T>[] {
return indices.map(i => new RowVector(this, i))
}
getColumn(j: number): ColumnVector<T> {
return new ColumnVector(this, j)
}
getColumns(indices: number[] = this.colIndices): ColumnVector<T>[] {
return indices.map(j => new ColumnVector(this, j))
}
static ofRationals(rows: number, cols: number, data: Rational[][]): Matrix<Rational> {
return Matrix.of(Rationals, rows, cols, data)
}
static of<T>(ops: Ops<T>, rows: number, cols: number, data: T[][]): Matrix<T> {
return new MatrixDense(ops, range(0, rows), range(0, cols), data)
}
}
class MatrixDense<T> extends Matrix<T> {
constructor(public ops: Ops<T>, 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<T> extends Matrix<T> {
constructor(public parent: Matrix<T>) {
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<T> extends Matrix<T> {
private reverseRows: number[] = []
private reverseCols: number[] = []
constructor(public parent: Matrix<T>, 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<T> extends Vector<T> {
constructor(public parent: Matrix<T>, 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<T> extends Vector<T> {
constructor(public parent: Matrix<T>, 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)
}
}

@ -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())

@ -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<Rational> {
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<Rational> {
// 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<string, Value> {
const tokens = tokenize(source)
// console.log(tokens)
const result: Record<string, Value> = {}
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<string, Value> } | { 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 })

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

@ -0,0 +1,146 @@
import { Ops, range } from './math'
import { Rationals, Rational } from './rationals'
export abstract class Vector<T> {
abstract ops: Ops<T>
abstract indices: number[]
abstract at(i: number): T
withValues(values: T[]): Vector<T> {
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<T> {
return new SubVector(this, indices)
}
dot(vector: Vector<T>): 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<Rational> {
return Vector.of(Rationals, data)
}
static of<T>(ops: Ops<T>, data: T[]): Vector<T> {
return VectorDense.of(ops, data)
}
static oneHot<T>(ops: Ops<T>, size: number, index: number): Vector<T> {
return new OneHotVector(ops, size, index)
}
}
class VectorDense<T> extends Vector<T> {
constructor(public ops: Ops<T>, public indices: number[], public data: T[]) {
super()
}
at(i: number): T {
return this.data[this.indices[i]]
}
withValues(values: T[]): Vector<T> {
if (values.length !== this.data.length) {
throw new Error('Invalid number of values')
}
return new VectorDense(this.ops, this.indices, values)
}
static of<T>(ops: Ops<T>, data: T[]): Vector<T> {
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<T> extends Vector<T> {
private backwardIndices: number[] = []
constructor(public parent: Vector<T>, 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<T> {
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<T> extends Vector<T> {
#size: number
constructor(public ops: Ops<T>, 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<T> {
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}`
}
}

@ -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 (
<>
<h1>Ricerca Operativa / Programmazione Lineare</h1>
<p>
Questo sito è un progetto per il corso di Ricerca Operativa dell'Università di Pisa per visualizzare
automaticamente tutti i passaggi dell'<a href="#">algoritmo del simplesso primale e duale</a>.
</p>
<h2>Visualizzazione</h2>
<p>I dati del problema vanno inseriti nel seguente campo di testo nel formato:</p>
<textarea
value={problemInput}
onInput={e => setProblemInput(e.currentTarget.value)}
rows={Math.max(problemInput.split('\n').length, 5)}
cols={100}
></textarea>
<h2>Problema di Input</h2>
{'result' in problemValuesResult ? (
<DisplayProblemInput problemInput={problemValuesResult.result} />
) : (
<p>
<code>{problemValuesResult.error}</code>
</p>
)}
<h2>Svolgimento</h2>
{'result' in problemValuesResult && <Primale input={problemValuesResult.result} />}
<h2>Debug</h2>
<pre>
<code>{JSON.stringify(problemValuesResult, null, 4)}</code>
</pre>
</>
)
}
render(<App />, document.body)

@ -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<Rational>
b: Vector<Rational>
c: Vector<Rational>
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() }
}
}

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

4
src/vite.d.ts vendored

@ -0,0 +1,4 @@
declare module '*.css' {
const classes: { readonly [key: string]: string }
export default classes
}

@ -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"]
}

@ -0,0 +1,10 @@
{
"files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}

@ -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"]
}

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import preact from '@preact/preset-vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [preact()],
})
Loading…
Cancel
Save