diff --git a/app/(workspace)/methods/bvp/page.tsx b/app/(workspace)/methods/bvp/page.tsx new file mode 100644 index 0000000..449240d --- /dev/null +++ b/app/(workspace)/methods/bvp/page.tsx @@ -0,0 +1,31 @@ +import type { Metadata } from 'next'; +import { BvpFiniteDifferenceMethod } from '@/components/bvp-finite-difference-method'; +import { MethodJsonLd } from '@/components/seo/json-ld'; +import { siteConfig } from '@/lib/config'; + +export const metadata: Metadata = { + title: 'Boundary Value Problem Solver | Finite Difference & Thomas Algorithm', + description: 'Solve second-order linear boundary value problems step by step using finite differences and the Thomas tridiagonal algorithm.', + keywords: ['boundary value problem solver', 'finite difference method', 'Thomas algorithm', 'two-point boundary conditions', 'ODE BVP solver'], + alternates: { canonical: '/methods/bvp' }, + openGraph: { + title: 'Finite Difference BVP Solver | Thomas Algorithm', + description: 'Discretize and solve second-order linear boundary value problems, inspect Thomas coefficients, residuals, and the solution graph.', + url: `${siteConfig.url}/methods/bvp`, + }, +}; + +export default function BvpPage() { + return ( + <> + + + + ); +} diff --git a/components/app-shell.tsx b/components/app-shell.tsx index 927f891..b7afef4 100644 --- a/components/app-shell.tsx +++ b/components/app-shell.tsx @@ -70,7 +70,7 @@ export function AppShell({ }, ]; - // 2. Numerical methods (methods with isNew: true automatically pin to top of methods section) + // 2. Numerical methods (new methods appear at the end of the section) const methodItems: NavItem[] = [ { href: '/methods/bisection', @@ -130,11 +130,17 @@ export function AppShell({ icon: Activity, isNew: true, }, + { + href: '/methods/bvp', + label: t('nav.bvp'), + icon: Activity, + isNew: true, + }, ]; const sortedMethods = [...methodItems].sort((a, b) => { if (Boolean(a.isNew) !== Boolean(b.isNew)) { - return a.isNew ? -1 : 1; + return a.isNew ? 1 : -1; } return 0; }); @@ -175,7 +181,9 @@ export function AppShell({ ? t('nav.newtonInterpolation') : pathname.startsWith('/methods/newton') ? t('nav.newton') - : pathname.startsWith('/methods/sweep') + : pathname.startsWith('/methods/bvp') + ? t('nav.bvp') + : pathname.startsWith('/methods/sweep') ? t('nav.sweep') : pathname.startsWith('/methods/lagrange') ? t('nav.lagrange') diff --git a/components/bvp-finite-difference-method.tsx b/components/bvp-finite-difference-method.tsx new file mode 100644 index 0000000..301e99d --- /dev/null +++ b/components/bvp-finite-difference-method.tsx @@ -0,0 +1,410 @@ +'use client'; + +import { useMemo, useState, type FormEvent } from 'react'; +import { Activity, Calculator, Check, Copy, Info } from 'lucide-react'; +import katex from 'katex'; +import { useI18n } from '@/components/i18n-provider'; +import { PsHero, PsMap, PsNote, PsPanel, PsPanelHead, PsStrip } from '@/components/ui/print-stream'; +import { solveBvp, type BvpInput, type BvpResult } from '@/lib/bvp-finite-difference'; +import { DEFAULT_BVP_INTERVALS, parseBvpProblem } from '@/lib/parse-bvp-problem'; + +const INITIAL: BvpInput = { + p: '0', q: '0', f: '0', a: '0', b: '1', h: String(1 / DEFAULT_BVP_INTERVALS), intervals: String(DEFAULT_BVP_INTERVALS), + m0: '1', m1: '0', m2: '0', g0: '1', g1: '0', g2: '1', +}; +const EMPTY_PROBLEM = { equation: '', leftBoundary: '', rightBoundary: '' }; +const EXAMPLE_PROBLEM = { + equation: "y'' - 2(1 + x^2)y = 0", + leftBoundary: 'y(0) = 0', + rightBoundary: 'y(0.8) = 3.5', +}; +const fmt = (value: number) => Number(value.toFixed(6)).toString(); +type KnownBoundaries = { left: number; right: number; lastInteriorIndex: number }; + +function getSystemEquation( + row: BvpResult['rows'][number], + knownBoundaries?: KnownBoundaries, +) { + const index = row.index - 1; + const leftCoefficient = knownBoundaries && row.index === 2 ? 0 : row.a; + const rightCoefficient = knownBoundaries && row.index === knownBoundaries.lastInteriorIndex + ? 0 + : row.c; + const rhs = (knownBoundaries && row.index === 2 + ? row.d - row.a * knownBoundaries.left + : row.d) - (knownBoundaries && row.index === knownBoundaries.lastInteriorIndex + ? row.c * knownBoundaries.right + : 0); + const terms = [ + { coefficient: leftCoefficient, variable: index - 1 }, + { coefficient: row.b, variable: index }, + { coefficient: rightCoefficient, variable: index + 1 }, + ].filter(({ coefficient }) => Math.abs(coefficient) > 1e-12); + return { terms, rhs }; +} + +function formatSystemEquation(row: BvpResult['rows'][number], knownBoundaries?: KnownBoundaries): string { + const { terms, rhs } = getSystemEquation(row, knownBoundaries); + + const left = terms.map(({ coefficient, variable }, termIndex) => { + const sign = coefficient < 0 ? '−' : '+'; + const magnitude = fmt(Math.abs(coefficient)); + const term = Math.abs(Math.abs(coefficient) - 1) < 1e-12 + ? `y${variable}` + : `${magnitude}·y${variable}`; + return `${termIndex === 0 ? (coefficient < 0 ? '− ' : '') : ` ${sign} `}${term}`; + }).join(''); + + return `${left} = ${fmt(rhs)}`; +} + +function formatSystemEquationTex(row: BvpResult['rows'][number], knownBoundaries?: KnownBoundaries): string { + const { terms, rhs } = getSystemEquation(row, knownBoundaries); + const left = terms.map(({ coefficient, variable }, termIndex) => { + const sign = coefficient < 0 ? '-' : '+'; + const magnitude = Math.abs(coefficient); + const term = Math.abs(magnitude - 1) < 1e-12 + ? `y_{${variable}}` + : `${fmt(magnitude)}\\,y_{${variable}}`; + return `${termIndex === 0 ? (coefficient < 0 ? '-' : '') : sign}${term}`; + }).join(''); + return `${left}&=${fmt(rhs)}`; +} + +function MathDisplay({ tex }: { tex: string }) { + const markup = useMemo( + () => katex.renderToString(tex, { throwOnError: false, displayMode: true }), + [tex], + ); + return ( +
+ ); +} + +function SolutionPlot({ result }: { result: BvpResult }) { + const width = 720; + const height = 240; + const pad = 28; + const ys = result.rows.map((row) => row.y); + const min = Math.min(...ys); + const max = Math.max(...ys); + const span = max - min || 1; + const points = result.rows.map((row, index) => { + const x = pad + (index / Math.max(result.rows.length - 1, 1)) * (width - 2 * pad); + const y = height - pad - ((row.y - min) / span) * (height - 2 * pad); + return `${x},${y}`; + }).join(' '); + return ( +
+ + + + + {result.rows.map((row, index) => { + const x = pad + (index / Math.max(result.rows.length - 1, 1)) * (width - 2 * pad); + const y = height - pad - ((row.y - min) / span) * (height - 2 * pad); + return ; + })} + x = {fmt(result.rows[0].x)} + x = {fmt(result.rows[result.rows.length - 1].x)} + y ∈ [{fmt(min)}, {fmt(max)}] + +
+ ); +} + +export function BvpFiniteDifferenceMethod() { + const { t } = useI18n(); + const [input, setInput] = useState(INITIAL); + const [problemFields, setProblemFields] = useState(EMPTY_PROBLEM); + const [result, setResult] = useState(null); + const [error, setError] = useState(''); + const [parsedNotice, setParsedNotice] = useState(''); + const [copiedNotebook, setCopiedNotebook] = useState(false); + const [copyError, setCopyError] = useState(''); + const updateProblemField = (key: keyof typeof EMPTY_PROBLEM, value: string) => { + setProblemFields((current) => ({ ...current, [key]: value })); + setError(''); + setParsedNotice(''); + setCopiedNotebook(false); + setCopyError(''); + }; + const update = (key: keyof BvpInput, value: string) => { + setInput((current) => { + const next = { ...current, [key]: value }; + if (key === 'a' || key === 'b') { + const step = (Number(next.b) - Number(next.a)) / DEFAULT_BVP_INTERVALS; + if (Number.isFinite(step) && step > 0) { + next.intervals = String(DEFAULT_BVP_INTERVALS); + next.h = String(step); + } + } + return next; + }); + setResult(null); + setError(''); + setParsedNotice(''); + setCopiedNotebook(false); + setCopyError(''); + }; + const calculate = (event: FormEvent) => { + event.preventDefault(); + try { + setResult(solveBvp(input)); + setError(''); + setCopiedNotebook(false); + setCopyError(''); + } catch (cause) { + setResult(null); + setError(cause instanceof Error ? cause.message : t('bvp.error')); + } + }; + const parseProblem = (fields = problemFields) => { + try { + const text = [fields.equation, fields.leftBoundary, fields.rightBoundary] + .filter(Boolean) + .join('\n'); + const parsed = parseBvpProblem(text, input); + setInput(parsed); + setResult(null); + setError(''); + setParsedNotice(t('bvp.parsed_summary', { + h: fmt((Number(parsed.b) - Number(parsed.a)) / Number(parsed.intervals)), + a: parsed.a, b: parsed.b, n: parsed.intervals, + })); + } catch (cause) { + setResult(null); + setParsedNotice(''); + setError(cause instanceof Error ? cause.message : t('bvp.parse_error')); + } + }; + const applyExample = () => { + setProblemFields(EXAMPLE_PROBLEM); + setResult(null); + setError(''); + setParsedNotice(''); + parseProblem(EXAMPLE_PROBLEM); + }; + const fields: Array<{ key: keyof BvpInput; label: string; step?: string }> = [ + { key: 'p', label: 'P(x)' }, { key: 'q', label: 'Q(x)' }, { key: 'f', label: 'f(x)' }, + { key: 'a', label: 'a =', step: 'any' }, { key: 'b', label: 'b =', step: 'any' }, + { key: 'm0', label: 'm₀' }, { key: 'm1', label: 'm₁' }, { key: 'm2', label: 'm₂' }, + { key: 'g0', label: 'g₀' }, { key: 'g1', label: 'g₁' }, { key: 'g2', label: 'g₂' }, + ]; + const nodes = result?.rows ?? []; + const backwards = result?.backward ?? []; + const tableHeaders = useMemo(() => ['k', 'xₖ', 'aₖ', 'bₖ', 'cₖ', 'dₖ', 'pivotₖ', 'uₖ', 'vₖ', 'yₖ', 'rₖ'], []); + const hasDirichletBoundaries = Number(input.m1) === 0 && Number(input.g1) === 0; + const knownBoundaries = hasDirichletBoundaries && nodes.length > 2 + ? { + left: Number(input.m2) / Number(input.m0), + right: Number(input.g2) / Number(input.g0), + lastInteriorIndex: nodes.length - 1, + } + : undefined; + const systemRows = hasDirichletBoundaries ? nodes.slice(1, -1) : nodes; + const copyNotebook = async () => { + if (!result) return; + const notebook = [ + t('bvp.notebook_title'), + '', + `1. ${t('bvp.notebook_grid')}: xᵢ = a + ih, a = ${input.a}, b = ${input.b}, h = ${fmt(result.h)}, n = ${input.intervals}.`, + `2. ${t('bvp.notebook_equation')}: y″ + (${input.p})y′ + (${input.q})y = ${input.f}.`, + `3. ${t('bvp.notebook_differences')}: y′ᵢ ≈ (yᵢ₊₁ − yᵢ₋₁)/(2h), y″ᵢ ≈ (yᵢ₋₁ − 2yᵢ + yᵢ₊₁)/h².`, + `4. ${t('bvp.notebook_boundary')}: (${input.m0} − ${input.m1}/h)y₀ + (${input.m1}/h)y₁ = ${input.m2}; −(${input.g1}/h)yₙ₋₁ + (${input.g0} + ${input.g1}/h)yₙ = ${input.g2}.`, + `5. ${t('bvp.notebook_interior')}: (1 − hP(xᵢ)/2)yᵢ₋₁ + (−2 + h²Q(xᵢ))yᵢ + (1 + hP(xᵢ)/2)yᵢ₊₁ = h²f(xᵢ).`, + `6. ${t('bvp.notebook_system')}:`, + ...systemRows.map((row, index) => ` ${index + 1}) x = ${fmt(row.x)}: ${formatSystemEquation(row, knownBoundaries)}`), + `7. ${t('bvp.notebook_sweep')}: u₁ = −c₁/b₁, v₁ = d₁/b₁; uₖ = −cₖ/(aₖuₖ₋₁ + bₖ), vₖ = (dₖ − aₖvₖ₋₁)/(aₖuₖ₋₁ + bₖ); yₙ = vₙ, yₖ = uₖyₖ₊₁ + vₖ.`, + '', + t('bvp.notebook_values'), + ...result.rows.map((row) => ` k=${row.index}: x=${fmt(row.x)}, pivot=${fmt(row.pivot)}, u=${fmt(row.u)}, v=${fmt(row.v)}, y=${fmt(row.y)}`), + ].join('\n'); + + try { + await navigator.clipboard.writeText(notebook); + setCopiedNotebook(true); + setCopyError(''); + } catch { + setCopiedNotebook(false); + setCopyError(t('bvp.copy_error')); + } + }; + return ( +
+ + y″ + P(x)y′ + Q(x)y = f(x)} conditionSub={t('bvp.boundaries')} /> + +
+ +
+ {result && ( + + +

{t('bvp.notebook_intro')}

+
+ {t('bvp.notebook_grid')}: + +
+
    +
  1. +

    {t('bvp.notebook_equation')}

    + +

    + y″ + ({input.p})y′ + ({input.q})y = {input.f} +

    +
  2. +
  3. +

    {t('bvp.notebook_differences')}

    +
    + + +

    {t('bvp.notebook_substitution')}

    + +
    +
  4. +
  5. +

    {t('bvp.notebook_boundary')}

    +
    +

    {t('bvp.left')}

    + + +

    {t('bvp.notebook_left_after')}

    +

    {t('bvp.right')}

    + + +

    {t('bvp.notebook_right_after')}

    +
    + +
    +
    +
  6. +
  7. +
    +

    {t('bvp.notebook_system')}

    + + {hasDirichletBoundaries + ? t('bvp.notebook_system_reduced_hint') + : t('bvp.notebook_system_hint')} + +
    +
    + formatSystemEquationTex(row, knownBoundaries)).join('\\\\')}\\end{aligned}\\right.`} /> +
    +
  8. +
  9. +

    {t('bvp.notebook_sweep')}

    +
    + + + +

    {t('bvp.notebook_sweep_hint')}

    +
    +
  10. +
+
+ + {copyError &&

{copyError}

} + {t('bvp.notebook_values')}: {nodes.length} +
+
+ )} + + + {!result ?

{t('bvp.empty')}

: <> +
+ +
+
+ {tableHeaders.map((head) => )}{nodes.map((row) => )}
{head}
{row.index}{fmt(row.x)}{fmt(row.a)}{fmt(row.b)}{fmt(row.c)}{fmt(row.d)}{fmt(row.pivot)}{fmt(row.u)}{fmt(row.v)}{fmt(row.y)}{row.residual.toExponential(2)}
+
+ } +
+ {result && <> + + +

yₖ = uₖ · yₖ₊₁ + vₖ,   yₙ₊₁ = vₙ₊₁

+
{backwards.map((step) =>
{step.nextY === null ? `y${step.index} = v${step.index}` : `y${step.index} = u${step.index} · ${fmt(step.nextY)} + v${step.index}`} = {fmt(step.y)}
)}
+
+ + +
{t('bvp.max_residual')}: {result.maxResidual.toExponential(3)}
+ +
{nodes.map((row) => )}
xₖyₖ
{fmt(row.x)}{fmt(row.y)}
+
+ } +
+
+
+ ); +} diff --git a/components/dashboard/methods-hub.tsx b/components/dashboard/methods-hub.tsx index 9fa1458..1f057b2 100644 --- a/components/dashboard/methods-hub.tsx +++ b/components/dashboard/methods-hub.tsx @@ -72,6 +72,19 @@ const METHODS: MethodDef[] = [ icon: ArrowRightLeft, accentClass: 'text-violet-600 dark:text-violet-400 bg-violet-500/10 border-violet-500/25', }, + { + id: 'bvp', + code: '12 / ODE', + category: 'odes', + categoryKey: 'dashboard.cat_odes', + titleKey: 'nav.bvp', + descKey: 'dashboard.desc_bvp', + href: '/methods/bvp', + formulaTex: "y'' + P(x)y' + Q(x)y = f(x)", + orderTex: 'O(n)', + icon: Activity, + accentClass: 'text-amber-600 dark:text-amber-400 bg-amber-500/10 border-amber-500/25', + }, { id: 'lagrange', code: '04 / INT', diff --git a/lib/bvp-finite-difference.ts b/lib/bvp-finite-difference.ts new file mode 100644 index 0000000..24b1b73 --- /dev/null +++ b/lib/bvp-finite-difference.ts @@ -0,0 +1,80 @@ +import { compileMath } from '@/lib/math'; + +export interface BvpInput { + p: string; q: string; f: string; a: string; b: string; h: string; intervals: string; + m0: string; m1: string; m2: string; g0: string; g1: string; g2: string; +} +export interface BvpRow { + index: number; x: number; a: number; b: number; c: number; d: number; + pivot: number; u: number; v: number; y: number; residual: number; +} +export interface BvpResult { + h: number; rows: BvpRow[]; + backward: Array<{ index: number; nextY: number | null; y: number }>; + maxResidual: number; +} + +function number(value: string, label: string): number { + const parsed = Number(value); + if (!value.trim() || !Number.isFinite(parsed)) throw new Error(`${label}: finite number required`); + return parsed; +} + +export function solveBvp(input: BvpInput): BvpResult { + const left = number(input.a, 'a'); + const right = number(input.b, 'b'); + const h = number(input.h, 'h'); + const intervals = number(input.intervals, 'n'); + const m0 = number(input.m0, 'm₀'); const m1 = number(input.m1, 'm₁'); const m2 = number(input.m2, 'm₂'); + const g0 = number(input.g0, 'g₀'); const g1 = number(input.g1, 'g₁'); const g2 = number(input.g2, 'g₂'); + if (right <= left) throw new Error('b must be greater than a'); + if (!Number.isInteger(intervals) || intervals < 2 || intervals > 80) throw new Error('n must be an integer from 2 to 80'); + if (h <= 0 || Math.abs((right - left) / intervals - h) > Math.max(1, Math.abs(h)) * 1e-8) { + throw new Error('h must divide the interval into an integer number of steps (2–80).'); + } + if (m0 === 0 && m1 === 0) throw new Error('Left boundary condition cannot be zero'); + if (g0 === 0 && g1 === 0) throw new Error('Right boundary condition cannot be zero'); + const functions = [input.p, input.q, input.f].map((expression) => { + const compiled = compileMath(expression); + if (!compiled) throw new Error(`Invalid expression: ${expression}`); + return (x: number) => { + const value = Number(compiled.evaluate({ x })); + if (!Number.isFinite(value)) throw new Error(`Expression is not finite at x=${x}`); + return value; + }; + }); + const [P, Q, F] = functions; + const coeffs = Array.from({ length: intervals + 1 }, (_, i) => { + if (i === 0) return { a: 0, b: m0 - m1 / h, c: m1 / h, d: m2 }; + if (i === intervals) return { a: -g1 / h, b: g0 + g1 / h, c: 0, d: g2 }; + const x = left + i * h; const p = P(x); + return { a: 1 - h * p / 2, b: -2 + h * h * Q(x), c: 1 + h * p / 2, d: h * h * F(x) }; + }); + const tracedRows: BvpRow[] = []; + coeffs.forEach((row, i) => { + const previous = tracedRows[i - 1]; + const pivot = previous ? row.a * previous.u + row.b : row.b; + const scale = previous ? Math.abs(row.a * previous.u) + Math.abs(row.b) : Math.abs(row.b); + if (!Number.isFinite(pivot) || Math.abs(pivot) <= Math.max(scale, 1) * Number.EPSILON * 64) { + throw new Error(`Thomas pivot is zero or unstable at row ${i + 1}`); + } + const u = -row.c / pivot; + const v = (row.d - (previous ? row.a * previous.v : 0)) / pivot; + if (![u, v].every(Number.isFinite)) throw new Error(`Non-finite Thomas coefficient at row ${i + 1}`); + tracedRows.push({ ...row, index: i + 1, x: left + i * h, pivot, u, v, y: 0, residual: 0 }); + }); + const ys = new Array(tracedRows.length); + ys[ys.length - 1] = tracedRows[tracedRows.length - 1].v; + const backward: BvpResult['backward'] = [{ index: ys.length, nextY: null, y: ys[ys.length - 1] }]; + for (let i = ys.length - 2; i >= 0; i -= 1) { + ys[i] = tracedRows[i].u * ys[i + 1] + tracedRows[i].v; + if (!Number.isFinite(ys[i])) throw new Error(`Non-finite solution at node ${i + 1}`); + backward.push({ index: i + 1, nextY: ys[i + 1], y: ys[i] }); + } + const rows = tracedRows.map((row, i) => ({ + ...row, + y: ys[i], + residual: row.d - (row.a * (ys[i - 1] ?? 0) + row.b * ys[i] + row.c * (ys[i + 1] ?? 0)), + })); + return { h, rows, backward, maxResidual: Math.max(...rows.map((row) => Math.abs(row.residual))) }; +} diff --git a/lib/i18n/locales/en.json b/lib/i18n/locales/en.json index cd9e269..be3470e 100644 --- a/lib/i18n/locales/en.json +++ b/lib/i18n/locales/en.json @@ -18,7 +18,8 @@ "rungeKutta": "Runge–Kutta 4th Order", "learn": "Uzbek learning guides", "newtonInterpolation": "Newton Interpolation", - "adamsPredictor": "Adams Predictor Method" + "adamsPredictor": "Adams Predictor Method", + "bvp": "Finite Difference BVP" }, "about": { "strip": "DOC-002 / ABOUT", @@ -113,7 +114,8 @@ "cat_linear": "Linear systems", "desc_rungeKutta": "Solve a first-order initial value problem with the classical fourth-order Runge–Kutta method.", "desc_newtonInterpolation": "Build the Newton polynomial from divided differences, compare finite differences, and evaluate P(x*) step by step.", - "desc_adamsPredictor": "Solve an initial value problem with RK4 startup steps and the third-order Adams predictor formula." + "desc_adamsPredictor": "Solve an initial value problem with RK4 startup steps and the third-order Adams predictor formula.", + "desc_bvp": "Solve second-order linear boundary value problems with finite differences and a step-by-step Thomas algorithm trace." }, "actions": { "tools_badge": "EXPORT / TOOLS", @@ -846,5 +848,67 @@ "notebook_collapse": "Collapse (key steps)", "notebook_final_ans": "Final Answer", "notebook_ans_desc": "Summary of solution across the interval" + }, + "bvp": { + "eyebrow": "Boundary value problems / 012", + "title": "Finite Difference", + "title_highlight": "BVP Solver", + "lead": "Solve a second-order linear boundary value problem and inspect its discretization, Thomas sweep, residuals, and numerical curve.", + "boundaries": "m₀y(a) + m₁y′(a) = m₂; g₀y(b) + g₁y′(b) = g₂", + "map_eyebrow": "Solution workflow", + "map_title": "Finite differences + Thomas algorithm", + "map_1": "Discretize", + "map_1_desc": "Build the tridiagonal equations at boundary and interior nodes.", + "map_2": "Forward sweep", + "map_2_desc": "Compute the Thomas coefficients uₖ and vₖ row by row.", + "map_3": "Back substitute", + "map_3_desc": "Recover y values, then inspect the residual and graph.", + "input_eyebrow": "Inputs / Boundary conditions", + "input_title": "Problem parameters", + "expression_hint": "Enter Math.js expressions for P(x), Q(x), and f(x). With n=4 intervals, h=(b−a)/4 is calculated automatically.", + "left": "Left boundary", + "right": "Right boundary", + "calculate": "Solve BVP", + "error": "Unable to solve the boundary value problem.", + "note": "Central differences are used for interior derivatives; one-sided differences approximate endpoint derivatives.", + "matrix_eyebrow": "01 / DISCRETIZATION", + "matrix_title": "Tridiagonal system and Thomas coefficients", + "empty": "Set the equation and boundary data, then select Solve BVP.", + "backward_eyebrow": "02 / BACK SUBSTITUTION", + "backward_title": "Recover the solution step by step", + "result_eyebrow": "03 / SOLUTION", + "result_title": "Numerical solution and plot", + "max_residual": "Maximum equation residual", + "example_button": "Load example: y″ − 2(1 + x²)y = 0, y(0)=0, y(0.8)=3.5", + "full_problem_label": "Enter the complete problem", + "full_problem_hint": "Enter the differential equation and each boundary condition in its own field. For n=4, h is calculated automatically from the endpoints.", + "full_problem_placeholder": "h = 0.1\ny″ - x y′ + 2x y = 0.8\ny(1.2) - 0.5 y′(1.2) = 1\ny′(1.5) = 2", + "step_label": "Step size h", + "equation_label": "Differential equation", + "left_boundary_label": "Left boundary condition", + "right_boundary_label": "Right boundary condition", + "parse_button": "Read problem & fill fields", + "parsed_summary": "Parsed: h={h}, interval [{a}, {b}], number of steps n={n}. Review the values below.", + "parse_error": "Could not read the problem text.", + "steps_derived": "n={count} equal intervals; automatically calculated h={h}.", + "derivation_eyebrow": "NOTEBOOK / STEP BY STEP", + "notebook_title": "Full derivation of the finite-difference system", + "notebook_intro": "Follow how the differential equation becomes discrete equations, how the boundary rows are formed, and how the tridiagonal system is assembled.", + "notebook_grid": "Grid and step size", + "notebook_equation": "Step 1. Given differential equation", + "notebook_differences": "Step 2. Replace derivatives with finite differences", + "notebook_substitution": "Substitute the differences into the equation and multiply by h²:", + "notebook_boundary": "Step 3. Add the boundary conditions to the system", + "notebook_left_after": "After substituting into the left boundary row", + "notebook_right_after": "After substituting into the right boundary row", + "notebook_system": "Step 4. Resulting tridiagonal system", + "notebook_system_hint": "All node equations, including boundary conditions", + "notebook_system_reduced_hint": "Boundary values y₀ and yₙ substituted; unknowns are y₁…yₙ₋₁", + "notebook_sweep": "Step 5. Forward sweep and back substitution", + "notebook_sweep_hint": "The pivot, uₖ, and vₖ columns in the table are calculated with these formulas.", + "notebook_values": "Calculated values at the grid nodes", + "copy_button": "Copy for notebook", + "copy_done": "Copied", + "copy_error": "Could not copy the text. Select the equations and copy them manually." } } diff --git a/lib/i18n/locales/ru.json b/lib/i18n/locales/ru.json index 106df1f..67b8ecc 100644 --- a/lib/i18n/locales/ru.json +++ b/lib/i18n/locales/ru.json @@ -18,7 +18,8 @@ "rungeKutta": "Метод Рунге — Кутты 4-го порядка", "learn": "Учебник на узбекском", "newtonInterpolation": "Интерполяция Ньютона", - "adamsPredictor": "Метод предиктора Адамса" + "adamsPredictor": "Метод предиктора Адамса", + "bvp": "Краевая задача (разности)" }, "about": { "strip": "DOC-002 / О ПРОЕКТЕ", @@ -113,7 +114,8 @@ "cat_linear": "Линейные системы", "desc_rungeKutta": "Решение задачи Коши для ОДУ классическим методом Рунге — Кутты четвертого порядка.", "desc_newtonInterpolation": "Постройте многочлен Ньютона по разделённым разностям, сравните конечные разности и пошагово вычислите P(x*).", - "desc_adamsPredictor": "Решите задачу Коши с начальными шагами Рунге — Кутты и формулой предиктора Адамса третьего порядка." + "desc_adamsPredictor": "Решите задачу Коши с начальными шагами Рунге — Кутты и формулой предиктора Адамса третьего порядка.", + "desc_bvp": "Решение линейной краевой задачи второго порядка методом конечных разностей и пошаговой прогонкой Томаса." }, "actions": { "tools_badge": "ЭКСПОРТ / ИНСТРУМЕНТЫ", @@ -846,5 +848,67 @@ "notebook_collapse": "Свернуть (только ключевые)", "notebook_final_ans": "Итоговый ответ", "notebook_ans_desc": "Сводка результатов на расчетном интервале" + }, + "bvp": { + "eyebrow": "Краевые задачи / 012", + "title": "Конечные", + "title_highlight": "разности для КЗ", + "lead": "Решайте линейную краевую задачу второго порядка и изучайте дискретизацию, прогонку Томаса, невязку и график решения.", + "boundaries": "m₀y(a) + m₁y′(a) = m₂; g₀y(b) + g₁y′(b) = g₂", + "map_eyebrow": "Этапы решения", + "map_title": "Конечные разности + алгоритм Томаса", + "map_1": "Дискретизация", + "map_1_desc": "Составление трехдиагональной системы на границах и внутри интервала.", + "map_2": "Прямой ход", + "map_2_desc": "Пошаговый расчет коэффициентов uₖ и vₖ.", + "map_3": "Обратный ход", + "map_3_desc": "Восстановление y, анализ невязки и графика.", + "input_eyebrow": "Ввод / Граничные условия", + "input_title": "Параметры задачи", + "expression_hint": "Введите выражения Math.js для P(x), Q(x) и f(x). При n=4 шаг h=(b−a)/4 рассчитывается автоматически.", + "left": "Левая граница", + "right": "Правая граница", + "calculate": "Решить задачу", + "error": "Не удалось решить краевую задачу.", + "note": "Внутри интервала используются центральные разности; на концах — односторонние.", + "matrix_eyebrow": "01 / ДИСКРЕТИЗАЦИЯ", + "matrix_title": "Трехдиагональная система и коэффициенты прогонки", + "empty": "Задайте уравнение и граничные условия, затем запустите расчет.", + "backward_eyebrow": "02 / ОБРАТНЫЙ ХОД", + "backward_title": "Пошаговое восстановление решения", + "result_eyebrow": "03 / РЕШЕНИЕ", + "result_title": "Численное решение и график", + "max_residual": "Максимальная невязка", + "example_button": "Пример: y″ − 2(1 + x²)y = 0, y(0)=0, y(0.8)=3.5", + "full_problem_label": "Введите задачу целиком", + "full_problem_hint": "Введите дифференциальное уравнение и каждое граничное условие в отдельное поле. При n=4 шаг h рассчитывается автоматически по границам.", + "full_problem_placeholder": "h = 0.1\ny″ - x y′ + 2x y = 0.8\ny(1.2) - 0.5 y′(1.2) = 1\ny′(1.5) = 2", + "step_label": "Шаг h", + "equation_label": "Дифференциальное уравнение", + "left_boundary_label": "Левое граничное условие", + "right_boundary_label": "Правое граничное условие", + "parse_button": "Разобрать задачу и заполнить поля", + "parsed_summary": "Распознано: h={h}, отрезок [{a}, {b}], число шагов n={n}. Проверьте значения ниже.", + "parse_error": "Не удалось разобрать текст задачи.", + "steps_derived": "n={count} равных интервала; автоматически рассчитанный h={h}.", + "derivation_eyebrow": "В ТЕТРАДЬ / ПОШАГОВО", + "notebook_title": "Полный вывод конечно-разностной системы", + "notebook_intro": "Показано, как дифференциальное уравнение превращается в разностные, как составляются граничные строки и собирается трехдиагональная система.", + "notebook_grid": "Сетка и шаг", + "notebook_equation": "Шаг 1. Исходное дифференциальное уравнение", + "notebook_differences": "Шаг 2. Замена производных конечными разностями", + "notebook_substitution": "Подставляем разности в уравнение и умножаем обе части на h²:", + "notebook_boundary": "Шаг 3. Включение граничных условий в систему", + "notebook_left_after": "После подстановки в левую граничную строку", + "notebook_right_after": "После подстановки в правую граничную строку", + "notebook_system": "Шаг 4. Полученная трехдиагональная система", + "notebook_system_hint": "Все уравнения узлов, включая граничные условия", + "notebook_system_reduced_hint": "Граничные значения y₀ и yₙ подставлены; неизвестные — y₁…yₙ₋₁", + "notebook_sweep": "Шаг 5. Прямая прогонка и обратная подстановка", + "notebook_sweep_hint": "Столбцы pivot, uₖ и vₖ в таблице рассчитаны по этим формулам.", + "notebook_values": "Вычисленные значения в узлах сетки", + "copy_button": "Скопировать в тетрадь", + "copy_done": "Скопировано", + "copy_error": "Не удалось скопировать текст. Выделите уравнения и скопируйте вручную." } } diff --git a/lib/i18n/locales/uz.json b/lib/i18n/locales/uz.json index a76b1a8..fbb7042 100644 --- a/lib/i18n/locales/uz.json +++ b/lib/i18n/locales/uz.json @@ -18,7 +18,8 @@ "rungeKutta": "Runge–Kutta 4-tartibli usuli", "learn": "Özbekça qöllanma", "newtonInterpolation": "Nyuton interpolyatsiyasi", - "adamsPredictor": "Adams başorat qiluvçi usuli" + "adamsPredictor": "Adams başorat qiluvçi usuli", + "bvp": "Chegaraviy masala" }, "about": { "strip": "DOC-002 / HAQIDA", @@ -113,7 +114,8 @@ "cat_linear": "Çiziqli sistemalar", "desc_rungeKutta": "Birinçi tartibli Koşi masalasini klassik törtinçi tartibli Runge–Kutta usuli bilan yeçing.", "desc_newtonInterpolation": "Bölingan ayirmalar orqali Nyuton köphadini tuzing, çekli ayirmalarni taqqoslang va P(x*) qiymatini qadam-baqadam hisoblang.", - "desc_adamsPredictor": "Koşi masalasini RK4 boşlanğiç qadamlari va uçinçi tartibli Adams başorat formulasi bilan yeçing." + "desc_adamsPredictor": "Koşi masalasini RK4 boşlanğiç qadamlari va uçinçi tartibli Adams başorat formulasi bilan yeçing.", + "desc_bvp": "Ikkinchi tartibli chiziqli chegaraviy masalani chekli ayirmalar va qadamma-qadam haydash (progonka) usuli bilan yeching." }, "actions": { "tools_badge": "EXPORT / TOOLS", @@ -846,5 +848,67 @@ "notebook_collapse": "Qisqartiriş (asosiy qadamlar)", "notebook_final_ans": "Yakuniy javob", "notebook_ans_desc": "Hisoblangan oraliqdagi natijalar xulosasi" + }, + "bvp": { + "eyebrow": "Chegaraviy masala / 012", + "title": "Chekli ayirmalar", + "title_highlight": "chegaraviy masalasi", + "lead": "Ikkinchi tartibli chiziqli chegaraviy masalani yeching: diskretlash, haydash koeffitsiyentlari, qoldiq va yechim grafigini ko‘ring.", + "boundaries": "m₀y(a) + m₁y′(a) = m₂; g₀y(b) + g₁y′(b) = g₂", + "map_eyebrow": "Yechish bosqichlari", + "map_title": "Chekli ayirmalar + haydash (progonka) usuli", + "map_1": "Diskretlash", + "map_1_desc": "Chegara va ichki nuqtalarda uch diagonalli sistemani tuzish.", + "map_2": "To‘g‘ri yo‘l", + "map_2_desc": "uₖ va vₖ haydash koeffitsiyentlarini ketma-ket hisoblash.", + "map_3": "Teskari yo‘l", + "map_3_desc": "y qiymatlarini topish, qoldiqni va grafikni ko‘rish.", + "input_eyebrow": "Kiritish / Chegaraviy shartlar", + "input_title": "Masala parametrlari", + "expression_hint": "P(x), Q(x), f(x) maydonlariga Math.js ifodalarini kiriting. n=4 bo‘linish uchun h=(b−a)/4 avtomatik hisoblanadi.", + "left": "Chap chegara", + "right": "O‘ng chegara", + "calculate": "Masalani yechish", + "error": "Chegaraviy masalani yechib bo‘lmadi.", + "note": "Ichki nuqtalarda markaziy ayirma, chekka nuqtalarda bir tomonlama ayirma ishlatiladi. Chiziqli sistema haydash (progonka) usulida yechiladi.", + "matrix_eyebrow": "01 / DISKRETLASH", + "matrix_title": "Uch diagonalli sistema va haydash koeffitsiyentlari", + "empty": "Tenglama va chegaraviy shartlarni kiriting, so‘ng yechishni boshlang.", + "backward_eyebrow": "02 / TESKARI YO‘L", + "backward_title": "Yechimni qadamma-qadam tiklash", + "result_eyebrow": "03 / YECHIM", + "result_title": "Sonli yechim va grafik", + "max_residual": "Maksimal qoldiq", + "example_button": "Misolni qo‘yish: y″ − 2(1 + x²)y = 0, y(0)=0, y(0.8)=3.5", + "full_problem_label": "Masalani to‘liq kiriting", + "full_problem_hint": "Differensial tenglama va ikkita chegaraviy shartni alohida maydonga kiriting. n=4 uchun h chegara nuqtalaridan avtomatik hisoblanadi.", + "full_problem_placeholder": "h = 0.1\ny″ - x y′ + 2x y = 0.8\ny(1.2) - 0.5 y′(1.2) = 1\ny′(1.5) = 2", + "step_label": "Qadam h", + "equation_label": "Differensial tenglama", + "left_boundary_label": "Chap chegaraviy shart", + "right_boundary_label": "O‘ng chegaraviy shart", + "parse_button": "Masalani o‘qib, maydonlarni to‘ldirish", + "parsed_summary": "Ajratildi: h={h}, kesma [{a}, {b}], qadamlar soni n={n}. Quyidagi qiymatlarni tekshiring.", + "parse_error": "Masala matnini ajratib bo‘lmadi.", + "steps_derived": "n={count} ta teng bo‘linish; avtomatik h={h}.", + "derivation_eyebrow": "DAFTARGA KO‘CHIRISH / QADAM-BAQADAM", + "notebook_title": "Chekli ayirmalar sistemasining to‘liq chiqarilishi", + "notebook_intro": "Quyida differensial tenglama qanday diskret tenglamalarga aylanishi, chegaraviy satrlar qanday tuzilishi va uch diagonalli sistema qanday hosil bo‘lishi ko‘rsatilgan.", + "notebook_grid": "Tugunlar va qadam", + "notebook_equation": "1-qadam. Berilgan differensial tenglama", + "notebook_differences": "2-qadam. Hosilalarni chekli ayirmalar bilan almashtirish", + "notebook_substitution": "Hosilalar tenglamaga qo‘yilib, tenglik h² ga ko‘paytiriladi:", + "notebook_boundary": "3-qadam. Chegaraviy shartlarni sistemaga kiritish", + "notebook_left_after": "Chap chegara satriga qo‘ysak", + "notebook_right_after": "O‘ng chegara satriga qo‘ysak", + "notebook_system": "4-qadam. Hosil bo‘lgan uch diagonalli sistema", + "notebook_system_hint": "Chegaraviy shartlar bilan birga barcha tugun tenglamalari", + "notebook_system_reduced_hint": "y₀ va yₙ chegaraviy qiymatlari qo‘yildi; noma’lumlar y₁…yₙ₋₁", + "notebook_sweep": "5-qadam. Haydash va orqaga qo‘yish", + "notebook_sweep_hint": "Jadvaldagi pivot, uₖ va vₖ ustunlari shu formulalar bo‘yicha hisoblangan.", + "notebook_values": "Hisoblangan tugun qiymatlari", + "copy_button": "Daftar uchun ko‘chirish", + "copy_done": "Ko‘chirildi", + "copy_error": "Matnni buferga ko‘chirib bo‘lmadi. Tenglamalarni belgilang va qo‘lda nusxa oling." } } diff --git a/lib/parse-bvp-problem.ts b/lib/parse-bvp-problem.ts new file mode 100644 index 0000000..7f46348 --- /dev/null +++ b/lib/parse-bvp-problem.ts @@ -0,0 +1,132 @@ +import type { BvpInput } from '@/lib/bvp-finite-difference'; + +export const DEFAULT_BVP_INTERVALS = 4; + +function decimal(value: string): number { + return Number(value.trim().replace(',', '.')); +} + +function normalizeExpression(value: string): string { + return value.trim().replace(/\s+/g, ' ').replace(/,(?=\d)/g, '.'); +} + +function parseBoundary(line: string): { x: number; y: number; derivative: number; rhs: number } | null { + const [leftRaw, rightRaw, ...extra] = line.split('='); + if (!leftRaw || rightRaw === undefined || extra.length) return null; + const left = leftRaw.replace(/[′’]/g, "'"); + const pattern = /([+-]?\s*(?:(?:\d+(?:[.,]\d+)?)\s*\*?\s*)?)y\s*('?)\s*\(\s*(\d+(?:[.,]\d+)?)\s*\)/g; + let point: number | null = null; + let y = 0; + let derivative = 0; + for (const match of left.matchAll(pattern)) { + const x = decimal(match[3]); + if (!Number.isFinite(x)) throw new Error('Chegara shartidagi nuqta son bo‘lishi kerak.'); + if (point !== null && Math.abs(point - x) > 1e-10) { + throw new Error('Har bir chegaraviy shart bitta x nuqtada berilishi kerak.'); + } + point = x; + const rawCoefficient = match[1].replace(/\s|\*/g, ''); + const coefficient = rawCoefficient === '' || rawCoefficient === '+' ? 1 : rawCoefficient === '-' ? -1 : decimal(rawCoefficient); + if (!Number.isFinite(coefficient)) throw new Error('Chegara shartidagi koeffitsiyentni tushunib bo‘lmadi.'); + if (match[2]) derivative += coefficient; + else y += coefficient; + } + if (point === null) return null; + const remainder = left.replace(pattern, '').replace(/[+\-\s*]/g, ''); + if (remainder) throw new Error(`Chegara shartining ifodasini tushunib bo‘lmadi: ${line}`); + const rhs = decimal(normalizeExpression(rightRaw)); + if (!Number.isFinite(rhs)) throw new Error('Chegara shartining o‘ng tomoni son bo‘lishi kerak.'); + if (y === 0 && derivative === 0) throw new Error('Chegara shartida y yoki y′ koeffitsiyenti bo‘lishi kerak.'); + return { x: point, y, derivative, rhs }; +} + +function splitTerms(expression: string): string[] { + const normalized = expression.replace(/\s+/g, ''); + const terms: string[] = []; + let depth = 0; + let start = 0; + + for (let index = 0; index < normalized.length; index += 1) { + const character = normalized[index]; + if (character === '(') depth += 1; + if (character === ')') depth -= 1; + + if ( + depth === 0 && + index > start && + (character === '+' || character === '-') && + normalized[index - 1]?.toLowerCase() !== 'e' + ) { + terms.push(normalized.slice(start, index)); + start = index; + } + } + + terms.push(normalized.slice(start)); + return terms.filter(Boolean); +} + +function coefficientOf(term: string, marker: string): string { + const markerIndex = term.indexOf(marker); + const raw = term.slice(0, markerIndex).replace(/\*+$/g, ''); + if (raw === '' || raw === '+') return '1'; + if (raw === '-') return '-1'; + return raw.replace(/(\d)([a-zA-Z(])/g, '$1*$2'); +} + +function parseOde(line: string): { p: string; q: string; f: string } { + const normalized = line + .replace(/[′’]/g, "'") + .replace(/[″“”]/g, "''") + .replace(/\s+/g, ' '); + const equation = normalized.split('='); + if (equation.length !== 2 || !equation[0].includes("''")) { + throw new Error('Differensial tenglamani y″ + P(x)y′ + Q(x)y = f(x) ko‘rinishida kiriting.'); + } + const lhs = equation[0].replace(/y\s*''/g, '').trim(); + const pTerms: string[] = []; + const qTerms: string[] = []; + for (const term of splitTerms(lhs)) { + if (term.includes("y'")) pTerms.push(coefficientOf(term, "y'")); + else if (term.includes('y')) qTerms.push(coefficientOf(term, 'y')); + else if (term) throw new Error(`Tenglamadagi qo‘shimcha hadni ajratib bo‘lmadi: ${term}`); + } + const rhs = normalizeExpression(equation[1]); + if (!rhs) throw new Error('Tenglamaning o‘ng tomoni bo‘sh qolmasligi kerak.'); + return { p: pTerms.join(' + ').replace(/\+ -/g, '- ') || '0', q: qTerms.join(' + ').replace(/\+ -/g, '- ') || '0', f: rhs }; +} + +export function parseBvpProblem(text: string, current: BvpInput): BvpInput { + const lines = text.split(/[\n;]/).map((line) => line.trim()).filter(Boolean); + const stepLine = text.match(/\bh\s*=\s*(\d+(?:[.,]\d+)?)/i); + const requestedStep = stepLine ? decimal(stepLine[1]) : null; + if (requestedStep !== null && (!Number.isFinite(requestedStep) || requestedStep <= 0)) { + throw new Error('h musbat son bo‘lishi kerak.'); + } + + const odeLine = lines.find((line) => /y\s*[′’]{2}|y\s*''/.test(line)); + if (!odeLine) throw new Error('Ikkinchi tartibli tenglamani kiriting (masalan: y″ - x y′ + 2x y = 0.8).'); + const ode = parseOde(odeLine); + const boundaries = lines.map((line) => parseBoundary(line)).filter((item) => item !== null); + if (boundaries.length !== 2) throw new Error('Chap va o‘ng chegaraviy shartlarni alohida kiriting.'); + const [first, second] = boundaries.sort((a, b) => a.x - b.x); + if (first.x >= second.x) throw new Error('O‘ng chegara chap chegaradan katta bo‘lishi kerak.'); + const intervalCount = requestedStep === null + ? DEFAULT_BVP_INTERVALS + : (second.x - first.x) / requestedStep; + const roundedCount = Math.round(intervalCount); + if (roundedCount < 2 || roundedCount > 80 || Math.abs(intervalCount - roundedCount) > 1e-8) { + throw new Error('Kesma uzunligi h ga butun marta bo‘linishi kerak (2–80 ta qadam).'); + } + const h = (second.x - first.x) / roundedCount; + return { + ...current, + ...ode, + a: String(first.x), + b: String(second.x), + h: String(h), + intervals: String(roundedCount), + m0: String(first.y), m1: String(first.derivative), m2: String(first.rhs), + g0: String(second.y), g1: String(second.derivative), g2: String(second.rhs), + }; +}