export function ftcs1d(p: FtcsParameters, budget = REFERENCE_FTCS_BUDGET): Computation<FtcsFrames> {
const { n, frames, stepsPerFrame, D, dx, dt, profile } = p;
if (!integer(n, 3) || !integer(frames, 1) || !integer(stepsPerFrame, 1))
return invalid(
["n", "frames", "stepsPerFrame"],
"Use at least three cells and positive whole-number frame and step counts.",
);
if (![D, dx, dt].every(Number.isFinite))
return {
kind: "refused",
refusal: makeRefusal("nonfinite-input", { parameterIds: ["D", "dx", "dt"] }),
};
if (D < 0 || dx <= 0 || dt <= 0 || ![0, 1, 2].includes(profile))
return invalid(
["D", "dx", "dt", "profile"],
"Use nonnegative D, positive dx and dt, and profile 0, 1, or 2.",
);
const r = D === 0 ? 0 : (D * dt) / (dx * dx);
if (!Number.isFinite(r) || (D > 0 && r === 0))
return numerical("The stability ratio is outside binary64 range; the grid was not advanced.");
if (r > 0.5) {
const dtMax = (dx * dx) / (2 * D);
const dxMin = Math.sqrt(2 * D * dt);
const dMax = (dx * dx) / (2 * dt);
if (![dtMax, dxMin, dMax].every((v) => Number.isFinite(v) && v > 0))
return numerical("The dimensional stability repairs are outside binary64 range.");
const dtRepair = stableRepair(dtMax, false, (v) => (D * v) / (dx * dx));
const dxRepair = stableRepair(dxMin, true, (v) => (D * dt) / (v * v));
const dRepair = stableRepair(dMax, false, (v) => (v * dt) / (dx * dx));
if (dtRepair === null || dxRepair === null || dRepair === null)
return numerical(
"No representable repair was found near the dimensional stability boundary.",
);
return {
kind: "refused",
refusal: makeRefusal(
"ftcs-unstable",
{ parameterIds: ["dt", "dx", "D"] },
{
details: { ratio: r, limit: 0.5, dtMax },
rankedRepairs: [
{
label: "Reduce the time step to the explicit scheme's limit.",
action: { parameterId: "dt", value: dtRepair },
},
{
label: "Use a coarser spatial grid.",
action: { parameterId: "dx", value: dxRepair },
},
{
label: "Choose a smaller diffusivity; this changes the physical setup.",
action: { parameterId: "D", value: dRepair },
},
],
},
),
};
}
const stepCount = (frames - 1) * stepsPerFrame;
const limited = budgetCheck(n * stepCount, 8 * n * (frames + 2), budget);
if (limited) return limited;
const elapsedTime = stepCount * dt;
if (!Number.isFinite(elapsedTime) || (stepCount > 0 && elapsedTime === 0))
return numerical("Elapsed model time is outside binary64 range.");
const field = new Float64Array(n);
if (profile === 0) field[Math.floor(n / 2)] = 1 / dx;
else if (profile === 1) field.fill(1, 0, Math.floor(n / 2));
else {
field[Math.floor(n / 4)] = 0.5 / dx;
field[Math.floor((3 * n) / 4)] = 0.5 / dx;
}
if (!field.every(Number.isFinite))
return numerical("The initial cell density is outside binary64 range.");
const derivative = new Float64Array(n);
const values = new Float64Array(n * frames);
values.set(field);
for (let frame = 1; frame < frames; frame++) {
if (!advanceInPlace(field, derivative, r, stepsPerFrame))
return numerical("A cell became nonfinite or negative; no partial frames are returned.");
values.set(field, frame * n);
}
return {
kind: "accepted",
data: Object.freeze({
values,
shape: Object.freeze([frames, n] as const),
stepCount,
elapsedTime,
stabilityRatio: r,
boundary: "zero-flux",
ownerId: "diffusion.ftcs1d",
}),
};
}