mirror of https://github.com/aziis98/ro-vis
initial commit
commit
4f4f9537bc
@ -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
|
||||
```
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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…
Reference in New Issue