From 9922075e168b6651405dcf85aa2c360cec59383d Mon Sep 17 00:00:00 2001 From: Antonio De Lucreziis Date: Thu, 23 Jan 2025 20:14:48 +0100 Subject: [PATCH] feat: new diagram per step --- src/Primal.tsx | 409 +++++++++++++------------------- src/lib-v2/canvas/index.ts | 16 +- src/lib-v2/ro/primal-simplex.ts | 5 + src/style.css | 10 +- 4 files changed, 186 insertions(+), 254 deletions(-) diff --git a/src/Primal.tsx b/src/Primal.tsx index 37c195d..7fa5bdf 100644 --- a/src/Primal.tsx +++ b/src/Primal.tsx @@ -4,7 +4,7 @@ import { Katex } from './Katex' import { fillDot, drawSemiplane, drawSimpleArrow, strokeInfiniteLine } from './lib-v2/canvas' import { range } from './lib-v2/math' import { Matrix } from './lib-v2/matrix' -import { Rational } from './lib-v2/rationals' +import { Rational, RationalField } from './lib-v2/rationals' import { ProblemComment, computePrimalSimplexSteps } from './lib-v2/ro/primal-simplex' import { Vector } from './lib-v2/vector' import { MiniMark } from './MiniMark' @@ -31,6 +31,7 @@ const PrimalStep = ({ x, xi, + y_B, comments, }: { @@ -43,6 +44,7 @@ const PrimalStep = ({ B: number[] x?: Vector xi?: Vector + y_B?: Vector comments: ProblemComment[] }) => { @@ -68,7 +70,8 @@ const PrimalStep = ({ )}
- + +
) @@ -90,6 +93,7 @@ export const Primal = ({ input }: { input: ProblemInput }) => { B: input.B, maxIterations: 10, }) + const elapsedTime = performance.now() - timerStart console.log('Computed primal simplex steps in', elapsedTime, 'ms') @@ -116,6 +120,7 @@ export const Primal = ({ input }: { input: ProblemInput }) => { B: step.B, x: step.x, xi: step.xi, + y_B: step.y_B, comments: step.comments, }} @@ -131,7 +136,7 @@ export const Primal = ({ input }: { input: ProblemInput }) => { ) } -type PrimalCanvasProps = { +type CanvasProps = { A: Matrix b: Vector c: Vector @@ -139,10 +144,11 @@ type PrimalCanvasProps = { x?: Vector xi?: Vector + y_B?: Vector } -const PrimalCanvas = ({ A, b, c, B, x, xi }: PrimalCanvasProps) => { - const render = ($canvas: HTMLCanvasElement | null, props: PrimalCanvasProps) => { +const PrimalCanvas = ({ A, b, c, B, x, xi }: CanvasProps) => { + const render = ($canvas: HTMLCanvasElement | null, props: CanvasProps) => { if (!$canvas) { return } @@ -176,42 +182,6 @@ const PrimalCanvas = ({ A, b, c, B, x, xi }: PrimalCanvasProps) => { const [c1, c2] = c.getData() const cLen = Math.sqrt(c1.toNumber() ** 2 + c2.toNumber() ** 2) - // // draw y axis arrow - // g.beginPath() - // g.moveTo(width / 2, height / 2) - // g.lineTo(width / 2, 5) - // g.lineTo(width / 2 - 10, 15) - // g.moveTo(width / 2, 5) - // g.lineTo(width / 2 + 10, 15) - // g.stroke() - - // // draw x axis arrow - // g.beginPath() - // g.moveTo(width / 2, height / 2) - // g.lineTo(width - 5, height / 2) - // g.lineTo(width - 15, height / 2 - 10) - // g.moveTo(width - 5, height / 2) - // g.lineTo(width - 15, height / 2 + 10) - // g.stroke() - - // g.beginPath() - // g.translate(50, height - 50) - // g.rotate(Math.atan2(c2.toNumber(), c1.toNumber())) - // g.moveTo(0, 0) - // g.lineTo(30, 0) - // g.moveTo(30, 0) - // g.lineTo(25, -5) - // g.moveTo(30, 0) - // g.lineTo(25, 5) - // g.stroke() - // g.restore() - - // g.fillStyle = '#333' - // g.font = '16px sans-serif' - // g.textAlign = 'center' - // g.textBaseline = 'middle' - // g.fillText(`A = ${A}`, width / 2, height / 2) - g.save() { g.translate(width / 2, height / 2) @@ -318,212 +288,151 @@ const PrimalCanvas = ({ A, b, c, B, x, xi }: PrimalCanvasProps) => { return render($canvas, { A, b, c, B, x, xi })} /> } -// export const PrimaleStep = ({ input, step }: { input: ProblemInput; step: Step }) => { -// const { A, b, c } = input - -// const rows = [] -// const canvasOptions: Parameters[0] = { A, b, c, B: step.B } - -// const A_B = A.slice({ rows: step.B }) -// const A_B_inverse = A_B.inverse2x2() -// const b_B = b.slice(step.B) - -// rows.push( -//
-// -//
-// ) - -// const x = A_B_inverse.apply(b_B) -// canvasOptions.x = x - -// rows.push( -//
-// -//
-// ) - -// const y_Zero = Vector.zero(RationalField, A.rows) - -// const y = y_Zero.with(step.B, y_B) - -// rows.push( -//
-// -//
-// ) - -// const I_x = activeIndices(input, x) - -// rows.push( -//
-// . -//

-//

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

-//
-// ) - -// if (!isDualAdmissible) { -// const h = Math.min(...y.getData().flatMap((y, i) => (y.lt(RationalField.zero) ? [i] : []))) - -// rows.push( -//
-// -//
-// ) - -// const N = range(0, A.rows).filter(i => !step.B.includes(i)) -// const A_N = A.slice({ rows: N }) - -// const A_N__xi = A_N.apply(xi) - -// rows.push( -//
-// -//
-// ) - -// if (!A_N__xi.getData().every(x => x.leq(RationalField.zero))) { -// const positiveIndices = N.filter(i => A_N__xi.at(A_N.forwardRowIndices[i]).gt(RationalField.zero)) - -// const [k, lambda] = positiveIndices -// .map<[number, Rational]>(i => [i, b.at(i).sub(A.rowAt(i).dot(x)).div(A.rowAt(i).dot(xi))]) -// .reduce(([i1, lambda1], [i2, lambda2]) => (lambda2.lt(lambda1) ? [i2, lambda2] : [i1, lambda1])) - -// rows.push( -//
-// 0 \\right\\}`, -// `${lambda}`, -// ].join(' = ')} -// /> -//
-// ) -// rows.push( -//
-// 0 \\right\\}`, -// `${k + 1}`, -// ].join(' = ')} -// /> -//
-// ) -// rows.push( -//
-// i !== h), k].toSorted())}`, -// ].join(' = ')} -// /> -//
-// ) -// } else { -// rows.push( -//
-//

-// La soluzione duale è illimitata. -//

-//
-// ) -// } -// } - -// return ( -//
-//
{rows}
-//
-// -//
-//
-// ) -// } +const PrimalCanvasCone = ({ A, b, c, B, x, xi, y_B }: CanvasProps) => { + const render = ($canvas: HTMLCanvasElement | null, props: CanvasProps) => { + if (!$canvas) { + return + } + + const { A, c, B, y_B } = props + + $canvas.width = $canvas.offsetWidth * window.devicePixelRatio + $canvas.height = $canvas.offsetHeight * window.devicePixelRatio + + const width = $canvas.width / window.devicePixelRatio + const height = $canvas.height / window.devicePixelRatio + + const g = $canvas.getContext('2d') + if (!g) { + throw new Error('Could not get 2d context') + } + + g.strokeStyle = '#333' + g.lineWidth = 2 + g.lineCap = 'round' + g.lineJoin = 'round' + + g.fillStyle = '#333' + g.font = 'bold 16px sans-serif' + g.textAlign = 'center' + g.textBaseline = 'middle' + + g.scale(window.devicePixelRatio, window.devicePixelRatio) + g.clearRect(0, 0, width, height) + + g.save() + { + g.translate(width / 2, height / 2) + g.scale(width / 2, -width / 2) + g.scale(1 / 5, 1 / 5) + + const [c1, c2] = c.getData() + const cLen = Math.sqrt(c1.toNumber() ** 2 + c2.toNumber() ** 2) + + let directions = [] + + if (y_B) { + const [y1, y2] = y_B.getData() + + if (y1.gt(RationalField.zero)) { + directions.push(A.rowAt(B[0])) + } + if (y1.lt(RationalField.zero)) { + directions.push(A.rowAt(B[0]).neg()) + } + + if (y2.gt(RationalField.zero)) { + directions.push(A.rowAt(B[1])) + } + if (y2.lt(RationalField.zero)) { + directions.push(A.rowAt(B[1]).neg()) + } + + if (directions.length === 1) { + const [[d0x, d0y]] = directions.map(v => v.getData()) + drawSimpleArrow(g, 0, 0, d0x.toNumber() * 3, d0y.toNumber() * 3, 0.2, '#44f') + } + if (directions.length === 2) { + const [[d1x, d1y], [d2x, d2y]] = directions.map(v => v.getData()) + + const d1Len = Math.sqrt(d1x.toNumber() ** 2 + d1y.toNumber() ** 2) + const d2Len = Math.sqrt(d2x.toNumber() ** 2 + d2y.toNumber() ** 2) + + // draw cone + g.fillStyle = '#4444ff18' + g.beginPath() + g.moveTo(0, 0) + g.lineTo((d1x.toNumber() * 100) / d1Len, (d1y.toNumber() * 100) / d1Len) + g.lineTo((d2x.toNumber() * 100) / d2Len, (d2y.toNumber() * 100) / d2Len) + g.fill() + } + } + + B.forEach(i => { + const [a1, a2] = A.rowAt(i).getData() + + const aLen = Math.sqrt(a1.toNumber() ** 2 + a2.toNumber() ** 2) + + g.save() + { + g.strokeStyle = '#b60' + g.lineWidth = 20 / (g.canvas.width / window.devicePixelRatio) + + g.setLineDash([0.2, 0.2]) + g.beginPath() + g.moveTo((a1.toNumber() / aLen) * 10, (a2.toNumber() / aLen) * 10) + g.lineTo(-(a1.toNumber() / aLen) * 10, -(a2.toNumber() / aLen) * 10) + g.stroke() + } + g.restore() + + drawSemiplane(g, a1.toNumber(), a2.toNumber(), 0, { + gradientSize: 2, + }) + }) + + B.forEach(i => { + const [a1, a2] = A.rowAt(i).getData() + + const aLen = Math.sqrt(a1.toNumber() ** 2 + a2.toNumber() ** 2) + + drawSimpleArrow(g, 0, 0, (a1.toNumber() / aLen) * 4, (a2.toNumber() / aLen) * 4, 0.2, '#b60') + }) + + drawSimpleArrow(g, 0, 0, (c1.toNumber() / cLen) * 4, (c2.toNumber() / cLen) * 4, 0.2, 'darkgreen') + } + g.restore() + + // draw A_i labels + + g.save() + { + g.translate(width / 2, height / 2) + + B.forEach(i => { + const [a1, a2] = A.rowAt(i).getData() + const aLen = Math.sqrt(a1.toNumber() ** 2 + a2.toNumber() ** 2) + + g.beginPath() + g.ellipse( + (a1.toNumber() / aLen) * width * 0.45, + -(a2.toNumber() / aLen) * width * 0.45, + 9, + 9, + 0, + 0, + Math.PI * 2 + ) + g.fillStyle = '#b60' + g.fill() + + g.font = 'bold 12px sans-serif' + g.fillStyle = '#fff' + g.fillText(`${i + 1}`, (a1.toNumber() / aLen) * width * 0.45, -(a2.toNumber() / aLen) * width * 0.45) + }) + } + g.restore() + } + + return render($canvas, { A, b, c, B, x, xi, y_B })} /> +} diff --git a/src/lib-v2/canvas/index.ts b/src/lib-v2/canvas/index.ts index 02174cf..1500930 100644 --- a/src/lib-v2/canvas/index.ts +++ b/src/lib-v2/canvas/index.ts @@ -11,14 +11,22 @@ export function drawSemiplane( { gradientAccent, gradientTransparent, + gradientSize, lineColor, lineWidth, - }: { gradientAccent?: string; gradientTransparent?: string; lineColor?: string; lineWidth?: number } = {} + }: { + gradientAccent?: string + gradientTransparent?: string + gradientSize?: number + lineColor?: string + lineWidth?: number + } = {} ) { gradientAccent ??= '#ffa90066' gradientTransparent ??= '#ffa90000' lineColor ??= '#9c6700' lineWidth ??= 2 + gradientSize ??= 1 / 1.25 // The gradient is perpendicular to the line, first generate a point on the line let [p1, p2] = [0, 0] @@ -30,7 +38,7 @@ export function drawSemiplane( p2 = b / a2 } - const normalize = Math.sqrt(a1 ** 2 + a2 ** 2) * 1.25 + const normalize = Math.sqrt(a1 ** 2 + a2 ** 2) / gradientSize const gradient = g.createLinearGradient(p1, p2, p1 - a1 / normalize, p2 - a2 / normalize) gradient.addColorStop(0, gradientAccent) @@ -89,7 +97,8 @@ export function drawSimpleArrow( x2: number, y2: number, size: number, - color: string = '#333' + color: string = '#333', + lineDash: number[] = [] ) { const arrowLength = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) const actualSize = (500 * size) / g.canvas.offsetWidth @@ -97,6 +106,7 @@ export function drawSimpleArrow( g.save() g.strokeStyle = color g.fillStyle = color + g.setLineDash(lineDash) g.beginPath() g.translate(x1, y1) diff --git a/src/lib-v2/ro/primal-simplex.ts b/src/lib-v2/ro/primal-simplex.ts index 2f619c5..d0f5084 100644 --- a/src/lib-v2/ro/primal-simplex.ts +++ b/src/lib-v2/ro/primal-simplex.ts @@ -21,6 +21,7 @@ export type ProblemStep = { x?: Vector xi?: Vector + y_B?: Vector comments: ProblemComment[] } @@ -52,6 +53,7 @@ export function computePrimalSimplexSteps(input: ProblemInput): ProblemOutput { let B = input.B let stepResult_B: number[] | undefined = undefined let stepResult_xi: Vector | undefined = undefined + let stepResult_y_B: Vector | undefined = undefined let status: ProblemStatus | null = null @@ -91,6 +93,7 @@ export function computePrimalSimplexSteps(input: ProblemInput): ProblemOutput { }) const y_B = A_B_inverse.transpose().apply(c) + stepResult_y_B = y_B comments.push({ type: 'formula', @@ -263,6 +266,7 @@ export function computePrimalSimplexSteps(input: ProblemInput): ProblemOutput { B, x, xi: stepResult_xi, + y_B: stepResult_y_B, comments, }) @@ -272,6 +276,7 @@ export function computePrimalSimplexSteps(input: ProblemInput): ProblemOutput { stepResult_B = undefined stepResult_xi = undefined + stepResult_y_B = undefined } } diff --git a/src/style.css b/src/style.css index 62c8338..a70373f 100644 --- a/src/style.css +++ b/src/style.css @@ -209,11 +209,19 @@ > .geometric-step { display: grid; + align-content: start; + justify-items: center; + gap: 2rem; canvas { width: 25rem; height: 25rem; + &.small { + width: 15rem; + height: 15rem; + } + @media (width < 70rem) { width: calc(100vw - 4rem); height: calc(100vw - 4rem); @@ -224,7 +232,7 @@ } @media (width < 70rem) { - padding: 0 1rem; + padding: 0 1rem 1rem; } }