import { renderLatex } from "@ofb/katex"; import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js"; type Eq = [number, number, number]; type Method = "substitution" | "elimination"; type Preset = { id: string; label: string; eq1: Eq; eq2: Eq; methods: Method[]; }; type Step = { title: string; latex: string; }; const PRESETS: Preset[] = [ { id: "add-y", label: "x + y = 5, 2x − y = 1", eq1: [1, 1, 5], eq2: [2, -1, 1], methods: ["elimination", "substitution"], }, { id: "sub-y", label: "y = 2x + 1, x + y = 7", eq1: [-2, 1, 1], eq2: [1, 1, 7], methods: ["substitution", "elimination"], }, { id: "coins", label: "x + y = 8, 5x + 2y = 28", eq1: [1, 1, 8], eq2: [5, 2, 28], methods: ["substitution", "elimination"], }, { id: "parallel", label: "x + y = 5, 2x + 2y = 12 (parallel)", eq1: [1, 1, 5], eq2: [2, 2, 12], methods: ["elimination"], }, ]; function gcd(a: number, b: number): number { a = Math.abs(Math.round(a)); b = Math.abs(Math.round(b)); while (b) { const t = b; b = a % b; a = t; } return a || 1; } function lcm(a: number, b: number): number { return (Math.abs(a) * Math.abs(b)) / gcd(a, b); } function formatTerm(coef: number, variable: string, first: boolean): string { if (coef === 0) return ""; const abs = Math.abs(coef); const sign = coef < 0 ? (first ? "-" : " - ") : first ? "" : " + "; const body = abs === 1 ? variable : `${abs}${variable}`; return `${sign}${body}`; } function formatEqLatex([a, b, c]: Eq): string { const xPart = formatTerm(a, "x", true); const yPart = formatTerm(b, "y", !xPart); const left = (xPart + yPart).trim() || "0"; return `${left} = ${c}`; } function determinant(eq1: Eq, eq2: Eq): number { const [a1, b1] = eq1; const [a2, b2] = eq2; return a1 * b2 - a2 * b1; } function solveSystem(eq1: Eq, eq2: Eq): { x: number; y: number } | null { const det = determinant(eq1, eq2); if (Math.abs(det) < 1e-9) return null; const [a1, b1, c1] = eq1; const [a2, b2, c2] = eq2; const x = (c1 * b2 - c2 * b1) / det; const y = (a1 * c2 - a2 * c1) / det; return { x: round(x), y: round(y) }; } function round(n: number, digits = 4) { const p = 10 ** digits; return Math.round(n * p) / p; } function formatNum(n: number) { if (Number.isInteger(n)) return String(n); return round(n, 2).toString(); } function classifySystem(eq1: Eq, eq2: Eq): "unique" | "parallel" | "coincident" { const det = determinant(eq1, eq2); if (Math.abs(det) > 1e-9) return "unique"; const [a1, b1, c1] = eq1; const [a2, b2, c2] = eq2; const ratio = Math.abs(a2) > 1e-9 ? a1 / a2 : Math.abs(b2) > 1e-9 ? b1 / b2 : 1; const same = Math.abs(a1 - ratio * a2) < 1e-9 && Math.abs(b1 - ratio * b2) < 1e-9 && Math.abs(c1 - ratio * c2) < 1e-9; return same ? "coincident" : "parallel"; } function buildSubstitutionSteps(eq1: Eq, eq2: Eq, solution: { x: number; y: number }): Step[] { const [a1, b1, c1] = eq1; const steps: Step[] = [ { title: "Write the system", latex: `\\begin{cases} ${formatEqLatex(eq1)} \\\\ ${formatEqLatex(eq2)} \\end{cases}`, }, ]; if (b1 === 1 && a1 === 0) { steps.push({ title: "Isolate y from equation (1)", latex: `y = ${formatNum(c1)}`, }); steps.push({ title: "Substitute into equation (2)", latex: `${formatTerm(eq2[0], "x", true)}${formatTerm(1, `(${formatNum(c1)})`, false)} = ${eq2[2]}`, }); } else if (b1 === -2 && a1 === 0 && c1 === 1) { steps.push({ title: "Rewrite equation (1) as y = 2x + 1", latex: "y = 2x + 1", }); steps.push({ title: "Substitute into equation (2)", latex: "x + (2x + 1) = 7", }); steps.push({ title: "Collect like terms", latex: "3x + 1 = 7", }); steps.push({ title: "Solve for x", latex: `3x = 6 \\Rightarrow x = ${formatNum(solution.x)}`, }); steps.push({ title: "Find y", latex: `y = 2(${formatNum(solution.x)}) + 1 = ${formatNum(solution.y)}`, }); } else if (eq2[1] === 1 && eq2[0] === 1) { steps.push({ title: "Express y from equation (2)", latex: `y = ${eq2[2]} - x`, }); steps.push({ title: "Substitute into equation (1)", latex: `${formatTerm(eq1[0], "x", true)}${formatTerm(eq1[1], `(${eq2[2]} - x)`, false)} = ${c1}`, }); const combined = eq1[0] - eq1[1]; const constant = c1 - eq1[1] * eq2[2]; steps.push({ title: "Simplify", latex: `${formatTerm(combined, "x", true)} = ${constant}`, }); steps.push({ title: "Solve for x", latex: `x = ${formatNum(solution.x)}`, }); steps.push({ title: "Find y", latex: `y = ${eq2[2]} - ${formatNum(solution.x)} = ${formatNum(solution.y)}`, }); } else { steps.push({ title: "Isolate one unknown, then substitute", latex: `\\text{Solve one equation for } x \\text{ or } y, \\text{ substitute into the other.}`, }); steps.push({ title: "Solution", latex: `(x, y) = (${formatNum(solution.x)},\\; ${formatNum(solution.y)})`, }); return steps; } steps.push({ title: "Solution", latex: `(x, y) = (${formatNum(solution.x)},\\; ${formatNum(solution.y)})`, }); return steps; } function buildEliminationSteps(eq1: Eq, eq2: Eq, solution: { x: number; y: number }): Step[] { const [a1, b1, c1] = eq1; const [a2, b2, c2] = eq2; const steps: Step[] = [ { title: "Write the system", latex: `\\begin{cases} ${formatEqLatex(eq1)} \\quad (1) \\\\ ${formatEqLatex(eq2)} \\quad (2) \\end{cases}`, }, ]; if (b1 !== 0 && b2 !== 0 && b1 === -b2) { steps.push({ title: "Add (1) and (2) to eliminate y", latex: `${formatTerm(a1 + a2, "x", true)} = ${c1 + c2}`, }); steps.push({ title: "Solve for x", latex: `x = ${formatNum(solution.x)}`, }); steps.push({ title: "Substitute into (1)", latex: `${formatNum(solution.x)} + y = ${c1} \\Rightarrow y = ${formatNum(solution.y)}`, }); } else if (a1 !== 0 && a2 !== 0 && a1 === -a2) { steps.push({ title: "Add (1) and (2) to eliminate x", latex: `${formatTerm(b1 + b2, "y", true)} = ${c1 + c2}`, }); steps.push({ title: "Solve for y", latex: `y = ${formatNum(solution.y)}`, }); steps.push({ title: "Substitute into (1)", latex: `x = ${formatNum(solution.x)}`, }); } else { const mult1 = lcm(b1, b2) / Math.abs(b1); const mult2 = lcm(b1, b2) / Math.abs(b2); const newB1 = b1 * mult1; const newB2 = b2 * mult2; steps.push({ title: "Match coefficients of y", latex: `\\text{Multiply (1) by } ${mult1}, \\text{ (2) by } ${mult2} \\text{ so y terms cancel.}`, }); const op = newB1 + newB2 === 0 ? "+" : "−"; steps.push({ title: `Combine equations (${op})`, latex: `${formatTerm(a1 * mult1 + (op === "+" ? a2 * mult2 : -a2 * mult2), "x", true)} = ${op === "+" ? c1 * mult1 + c2 * mult2 : c1 * mult1 - c2 * mult2}`, }); steps.push({ title: "Solve and back-substitute", latex: `x = ${formatNum(solution.x)},\\quad y = ${formatNum(solution.y)}`, }); } steps.push({ title: "Solution", latex: `(x, y) = (${formatNum(solution.x)},\\; ${formatNum(solution.y)})`, }); return steps; } function buildSteps( eq1: Eq, eq2: Eq, method: Method ): { steps: Step[]; error: string | null } { const kind = classifySystem(eq1, eq2); if (kind === "parallel") { return { steps: [], error: "These lines are parallel — the system has no solution. Elimination would lead to a false statement such as 0 = 7.", }; } if (kind === "coincident") { return { steps: [], error: "The two equations describe the same line — infinitely many solutions. A single (x, y) pair is not enough.", }; } const solution = solveSystem(eq1, eq2); if (!solution) { return { steps: [], error: "Could not solve this system." }; } const steps = method === "substitution" ? buildSubstitutionSteps(eq1, eq2, solution) : buildEliminationSteps(eq1, eq2, solution); return { steps, error: null }; } function SimultaneousSolverWorkbench() { const [presetId, setPresetId] = createSignal(PRESETS[0].id); const [method, setMethod] = createSignal("elimination"); const [stepIndex, setStepIndex] = createSignal(0); const [playing, setPlaying] = createSignal(false); const [speed, setSpeed] = createSignal(1200); const preset = createMemo(() => PRESETS.find((p) => p.id === presetId()) ?? PRESETS[0]); const availableMethods = createMemo(() => preset().methods); const result = createMemo(() => buildSteps(preset().eq1, preset().eq2, method()) ); const steps = createMemo(() => result().steps); const error = createMemo(() => result().error); const current = createMemo(() => steps()[stepIndex()] ?? null); const progress = createMemo(() => steps().length > 0 ? ((stepIndex() + 1) / steps().length) * 100 : 0 ); createEffect(() => { presetId(); method(); setStepIndex(0); setPlaying(false); }); createEffect(() => { if (!playing() || error()) return; const id = window.setInterval(() => { setStepIndex((i) => { if (i >= steps().length - 1) { setPlaying(false); return i; } return i + 1; }); }, speed()); onCleanup(() => window.clearInterval(id)); }); createEffect(() => { if (method() === "substitution" && !availableMethods().includes("substitution")) { setMethod(availableMethods()[0]); } if (method() === "elimination" && !availableMethods().includes("elimination")) { setMethod(availableMethods()[0]); } }); const eqDisplay = createMemo(() => renderLatex( `\\begin{cases} ${formatEqLatex(preset().eq1)} \\\\ ${formatEqLatex(preset().eq2)} \\end{cases}` ) ); return (

Substitution & elimination step-by-step

Step {stepIndex() + 1} of {steps().length} {current() ? `: ${current()!.title}` : ""}

} >

{error()}

System

{current()!.title}

); } export default SimultaneousSolverWorkbench;