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 (
+
+
+
+ );
+}
+
+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')}:
+
+
+
+ -
+
{t('bvp.notebook_equation')}
+
+
+ y″ + ({input.p})y′ + ({input.q})y = {input.f}
+
+
+ -
+
{t('bvp.notebook_differences')}
+
+
+
+
{t('bvp.notebook_substitution')}
+
+
+
+ -
+
{t('bvp.notebook_boundary')}
+
+
{t('bvp.left')}
+
+
+
{t('bvp.notebook_left_after')}
+
{t('bvp.right')}
+
+
+
{t('bvp.notebook_right_after')}
+
+
+
+
+
+ -
+
+
{t('bvp.notebook_system')}
+
+ {hasDirichletBoundaries
+ ? t('bvp.notebook_system_reduced_hint')
+ : t('bvp.notebook_system_hint')}
+
+
+
+ formatSystemEquationTex(row, knownBoundaries)).join('\\\\')}\\end{aligned}\\right.`} />
+
+
+ -
+
{t('bvp.notebook_sweep')}
+
+
+
+
+
{t('bvp.notebook_sweep_hint')}
+
+
+
+
+
+ {copyError &&
{copyError}
}
+
{t('bvp.notebook_values')}: {nodes.length}
+
+
+ )}
+
+
+ {!result ? {t('bvp.empty')}
: <>
+
+
+
+
+
{tableHeaders.map((head) => | {head} | )}
{nodes.map((row) => | {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)}
+
+ | xₖ | yₖ |
{nodes.map((row) => | {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),
+ };
+}