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