From 12c053ea6e3d40b51d4365ea1ae5a34599f72af6 Mon Sep 17 00:00:00 2001 From: nikoscham Date: Mon, 17 Aug 2026 12:46:59 +0300 Subject: [PATCH 1/2] Rename 1D Euler-Bernoulli beam solver example. Remove old test runner. Apply Prettier checks --- .../README.md | 0 .../clampedSpringSupportedBeam1D.js} | 0 tests/run-all-tests.js | 60 ------------------- 3 files changed, 60 deletions(-) rename examples/{Beam1DFEM => eulerBernoulliBeamScript}/README.md (100%) rename examples/{Beam1DFEM/Beam1DEuler_Bernoulli.js => eulerBernoulliBeamScript/clampedSpringSupportedBeam1D/clampedSpringSupportedBeam1D.js} (100%) delete mode 100644 tests/run-all-tests.js diff --git a/examples/Beam1DFEM/README.md b/examples/eulerBernoulliBeamScript/README.md similarity index 100% rename from examples/Beam1DFEM/README.md rename to examples/eulerBernoulliBeamScript/README.md diff --git a/examples/Beam1DFEM/Beam1DEuler_Bernoulli.js b/examples/eulerBernoulliBeamScript/clampedSpringSupportedBeam1D/clampedSpringSupportedBeam1D.js similarity index 100% rename from examples/Beam1DFEM/Beam1DEuler_Bernoulli.js rename to examples/eulerBernoulliBeamScript/clampedSpringSupportedBeam1D/clampedSpringSupportedBeam1D.js diff --git a/tests/run-all-tests.js b/tests/run-all-tests.js deleted file mode 100644 index e1f848d..0000000 --- a/tests/run-all-tests.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Test runner — discovers and executes every *.test.js file under tests/. - * Add a new test file anywhere in this tree and it runs automatically. - * - * Usage: node tests/run-all-tests.js - */ - -import { readdirSync, statSync } from "fs"; -import { join, relative } from "path"; -import { spawnSync } from "child_process"; -import { fileURLToPath } from "url"; -import { basicLog, errorLog, warnLog } from "../src/utilities/logging.js"; - -const __dirname = fileURLToPath(new URL(".", import.meta.url)); - -function collectTestFiles(dir) { - const entries = readdirSync(dir); - const files = []; - for (const entry of entries) { - const fullPath = join(dir, entry); - if (statSync(fullPath).isDirectory()) { - files.push(...collectTestFiles(fullPath)); - } else if (entry.endsWith(".test.js")) { - files.push(fullPath); - } - } - return files; -} - -const testFiles = collectTestFiles(__dirname); - -if (testFiles.length === 0) { - warnLog("No test files found."); - process.exit(0); -} - -basicLog(`Found ${testFiles.length} test file(s).`); -basicLog(""); - -let passed = 0; -let failed = 0; - -for (const file of testFiles) { - const label = relative(__dirname, file); - const result = spawnSync(process.execPath, [file], { stdio: "inherit" }); - if (result.status === 0) { - passed++; - } else { - errorLog(`FAILED: ${label}`); - basicLog(""); - failed++; - } -} - -if (failed > 0) { - errorLog(`${passed} passed, ${failed} failed.`); -} else { - basicLog(`${passed} passed, ${failed} failed.`); -} -process.exit(failed > 0 ? 1 : 0); From 50e8345ecba7e18b26e9ec98fbd0eb18e9e4ea35 Mon Sep 17 00:00:00 2001 From: nikoscham Date: Mon, 17 Aug 2026 12:51:08 +0300 Subject: [PATCH 2/2] Improved formatting in various test files --- CONTRIBUTING.md | 8 ++- examples/eulerBernoulliBeamScript/README.md | 27 +++---- .../clampedSpringSupportedBeam1D.js | 53 ++++---------- package.json | 2 +- src/mesh/meshUtils.js | 2 +- src/models/beamBoundaryConditions.js | 6 +- src/models/eulerBernoulliBeam.js | 5 +- src/visualization/vtkPlot.js | 18 +++-- src/workers/worker.js | 2 +- .../EulerBernoulliBeam/REGRESSION.md | 60 +++++++++------- .../EulerBernoulliBeam/regression.test.js | 67 ++++++++++------- .../HeatConduction1DWall/REGRESSION.md | 19 ++--- .../HeatConduction1DWall/regression.test.js | 33 +++++++-- .../HeatConduction2DFin/REGRESSION.md | 20 +++--- .../HeatConduction2DFin/regression.test.js | 42 ++++++++--- tests/unit/README.md | 10 ++- tests/unit/eulerBernoulliBeam.test.js | 71 +++++++++++++------ tests/unit/jacobiMethod.test.js | 52 +++++++++----- 18 files changed, 303 insertions(+), 194 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 97e5221..6a0fb68 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -100,7 +100,11 @@ External contributors: Before submitting a pull request, test your modifications by running the FEAScript library from a local directory. For example, you can load the library in your HTML file as follows: ```javascript -import { FEAScriptModel, plotSolution, printVersion } from "[USER_DIRECTORY]/FEAScript-core/src/index.js"; +import { + FEAScriptModel, + plotSolution, + printVersion, +} from "[USER_DIRECTORY]/FEAScript-core/src/index.js"; ``` FEAScript can be run on a local server. You **must** start the server from the workspace root directory (the folder that contains both `FEAScript-core/` and `FEAScript-website/`), not from inside either subfolder. The HTML files use relative paths such as `../feascript-website.css` and `../../FEAScript-core/src/index.js` that only resolve correctly from that root. @@ -127,4 +131,4 @@ Testing can be also performed at the Node.js environment. In this case you can a npm test ``` -These tests compare the numerical results against stored reference solutions at selected points. +This command uses the Node.js test runner to discover all test files under `tests/`. The tests compare numerical results against stored reference solutions and verify individual solver and assembler behavior. diff --git a/examples/eulerBernoulliBeamScript/README.md b/examples/eulerBernoulliBeamScript/README.md index 923e53c..ee14e93 100644 --- a/examples/eulerBernoulliBeamScript/README.md +++ b/examples/eulerBernoulliBeamScript/README.md @@ -1,4 +1,4 @@ -FEAScript Beam1DFEM Logo +FEAScript Euler-Bernoulli beam logo # 1D Euler-Bernoulli Beam Examples @@ -7,7 +7,7 @@ This directory contains Node.js examples demonstrating how to use the FEAScript ## Examples -#### 1. Clamped and Spring-Supported Beam (`Beam1DEuler_Bernoulli.js`) +#### 1. Clamped and Spring-Supported Beam (`clampedSpringSupportedBeam1D.js`) Reproduces the "Bending of a Beam" example from J.N. Reddy, _An Introduction to the Finite Element Method_, 3rd ed., McGraw-Hill, 2006 (FEM1D example problems, Chapter 7). A 10 m beam is clamped at @@ -69,17 +69,20 @@ plus a point load): ```javascript model.addBoundaryCondition("1", [["fixed"]]); // w=0, theta=0 (clamped) model.addBoundaryCondition("2", [["pinned"], ["moment", 1250]]); // w=0, plus an applied moment -model.addBoundaryCondition("3", [["spring", 200], ["force", -2500]]); // elastic support, plus a point load +model.addBoundaryCondition("3", [ + ["spring", 200], + ["force", -2500], +]); // elastic support, plus a point load ``` -| Condition type | Kind | Effect | -| ------------------------------------ | ---------------- | ----------------------------------------------------------- | -| `["fixed"]` | Essential | `w = 0` and `theta = 0` (clamped support) | -| `["pinned"]` / `["deflection", v]` | Essential | `w = v` (default `v = 0`; roller/pin support) | -| `["rotationFixed"]` / `["rotation", v]` | Essential | `theta = v` (default `v = 0`) | -| `["force", v]` | Natural | Applies a concentrated transverse force `v` at the node | -| `["moment", v]` | Natural | Applies a concentrated moment `v` at the node | -| `["spring", k, uRef]` | Mixed (Robin) | Transverse elastic support of stiffness `k` about `uRef` (default `uRef = 0`) | +| Condition type | Kind | Effect | +| --------------------------------------- | ------------- | ----------------------------------------------------------------------------- | +| `["fixed"]` | Essential | `w = 0` and `theta = 0` (clamped support) | +| `["pinned"]` / `["deflection", v]` | Essential | `w = v` (default `v = 0`; roller/pin support) | +| `["rotationFixed"]` / `["rotation", v]` | Essential | `theta = v` (default `v = 0`) | +| `["force", v]` | Natural | Applies a concentrated transverse force `v` at the node | +| `["moment", v]` | Natural | Applies a concentrated moment `v` at the node | +| `["spring", k, uRef]` | Mixed (Robin) | Transverse elastic support of stiffness `k` about `uRef` (default `uRef = 0`) | ## Running the Node.js Examples @@ -98,5 +101,5 @@ npm install feascript #### 3. Run the example: ```bash -node Beam1DEuler_Bernoulli.js +node clampedSpringSupportedBeam1D/clampedSpringSupportedBeam1D.js ``` diff --git a/examples/eulerBernoulliBeamScript/clampedSpringSupportedBeam1D/clampedSpringSupportedBeam1D.js b/examples/eulerBernoulliBeamScript/clampedSpringSupportedBeam1D/clampedSpringSupportedBeam1D.js index ef22f55..5c8c390 100644 --- a/examples/eulerBernoulliBeamScript/clampedSpringSupportedBeam1D/clampedSpringSupportedBeam1D.js +++ b/examples/eulerBernoulliBeamScript/clampedSpringSupportedBeam1D/clampedSpringSupportedBeam1D.js @@ -7,33 +7,6 @@ * ════════════════════════════════════════════════════════════════ */ -/** - * Clamped and spring-supported Euler-Bernoulli beam - * - * Reproduces the "Bending of a Beam" example from J.N. Reddy, "An Introduction to - * the Finite Element Method", 3rd ed., McGraw-Hill, 2006 (FEM1D example problems, - * Chapter 7), solved there with the reference FEM1D Fortran program (MODEL=3, - * NTYPE=0, IELEM=0 i.e. Hermite cubic elements). - * - * Geometry: a 10 m beam, clamped at x=0, resting on a roller at midspan (x=5 m), - * and connected to a linear transverse spring at the free end (x=10 m). - * Mesh: 2 Hermite cubic beam elements of 5 m each -> 3 nodes: [1, 2, 3] - * - * 1,000 N/m (on 0 <= x <= 5) 2,500 N (down, at node 3) - * v v v v v v v v v v | - * /////|--------------------|-------------------| ~~~~ spring, k = 1e-4*EI - * ///// 1 (clamped) 2 (roller) 3 (free end, spring) - * |<-------- 5 m ----->|<------- 5 m ------>| - * ^ moment 1,250 N-m applied at node 2 - * - * EI = 2e6 N-m^2 (constant), k_spring = 1e-4 * EI = 200 N/m - * - * Boundary conditions (see beamBoundaryConditions.js for the condition syntax): - * - Node 1 (x=0): fixed -> w=0, theta=0 (clamped support) - * - Node 2 (x=5): pinned + moment -> w=0, applied moment M=1250 N-m - * - Node 3 (x=10): spring + force -> k=200 N/m, applied point load P=-2500 N - */ - // Import Math.js import * as math from "mathjs"; global.math = math; @@ -49,16 +22,12 @@ const model = new FEAScriptModel(); // Select physics/PDE model.setModelConfig("eulerBernoulliBeamScript", { coefficientFunctions: { - EI: (x) => 2.0e6, // Bending stiffness E*I (N-m^2), constant along the beam - // Distributed transverse load: -1,000 N/m over the first (clamped) span only + EI: (x) => 2.0e6, // Bending stiffness q: (x) => (x <= 5 ? -1000 : 0), - // c0 defaults to 0 (no elastic foundation) when omitted }, }); // Define mesh configuration -// elementOrder is 'linear' because that only describes the 2-node beam geometry; -// the field itself is always interpolated with cubic Hermite shape functions internally model.setMeshConfig({ meshDimension: "1D", elementOrder: "linear", @@ -66,10 +35,13 @@ model.setMeshConfig({ maxX: 10, }); -// Define boundary conditions (keyed by 1-based global node number) +// Define boundary conditions model.addBoundaryCondition("1", [["fixed"]]); // Clamped support model.addBoundaryCondition("2", [["pinned"], ["moment", 1250]]); // Roller + applied moment -model.addBoundaryCondition("3", [["spring", 200], ["force", -2500]]); // Spring support + point load +model.addBoundaryCondition("3", [ + ["spring", 200], + ["force", -2500], +]); // Spring support + point load // Set solver method model.setSolverMethod("lusolve"); @@ -77,9 +49,10 @@ model.setSolverMethod("lusolve"); // Solve the problem const { solutionVector } = model.solve(); -// The solution vector is ordered [w_0, theta_0, w_1, theta_1, w_2, theta_2, ...] -// (mathjs' lusolve returns a nested array, so flatten defensively before reading it) -const flatSolution = solutionVector.map((entry) => (Array.isArray(entry) ? entry[0] : entry)); +// Print results +const flatSolution = solutionVector.map((entry) => + Array.isArray(entry) ? entry[0] : entry +); const nodeXCoordinates = [0, 5, 10]; console.log("\nNode | x (m) | Deflection w (m) | Rotation theta (rad)"); @@ -88,8 +61,10 @@ for (let nodeIndex = 0; nodeIndex < nodeXCoordinates.length; nodeIndex++) { const w = flatSolution[2 * nodeIndex]; const theta = flatSolution[2 * nodeIndex + 1]; console.log( - ` ${nodeIndex + 1} | ${nodeXCoordinates[nodeIndex].toFixed(2).padStart(8)} | ${w + ` ${nodeIndex + 1} | ${nodeXCoordinates[nodeIndex] + .toFixed(2) + .padStart(8)} | ${w.toExponential(4).padStart(17)} | ${theta .toExponential(4) - .padStart(17)} | ${theta.toExponential(4).padStart(20)}`, + .padStart(20)}` ); } diff --git a/package.json b/package.json index e4ae545..fe27839 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "build": "rollup -c", "prepare": "npm run build", "prepublishOnly": "npm run build", - "test": "node tests/run-all-tests.js", + "test": "node --test tests", "format": "prettier --write ." }, "repository": { diff --git a/src/mesh/meshUtils.js b/src/mesh/meshUtils.js index 8447e80..3ac6784 100644 --- a/src/mesh/meshUtils.js +++ b/src/mesh/meshUtils.js @@ -238,7 +238,7 @@ export function performIsoparametricMapping2D(params) { /** * Function to test if a point is inside a triangle using barycentric coordinates, - * also returning the natural coordinates (ksi, eta). + * also returning the natural coordinates (ksi, eta) * @param {number} x - X-coordinate of the point * @param {number} y - Y-coordinate of the point * @param {array} vertices - Triangle vertices [[x0,y0],[x1,y1],[x2,y2]] diff --git a/src/models/beamBoundaryConditions.js b/src/models/beamBoundaryConditions.js index d879e61..cb70ff8 100644 --- a/src/models/beamBoundaryConditions.js +++ b/src/models/beamBoundaryConditions.js @@ -121,11 +121,7 @@ export class BeamBoundaryConditions { } else if (conditionType === "rotationFixed" || conditionType === "rotation") { applyDirichlet(rotationDOF, value ?? 0); debugLog(`Node ${nodeKey}: Applied rotation theta=${value ?? 0} (essential BC)`); - } else if ( - conditionType !== "force" && - conditionType !== "moment" && - conditionType !== "spring" - ) { + } else if (conditionType !== "force" && conditionType !== "moment" && conditionType !== "spring") { errorLog(`Unknown beam boundary condition type: "${conditionType}"`); } }); diff --git a/src/models/eulerBernoulliBeam.js b/src/models/eulerBernoulliBeam.js index c061a98..db0b5c0 100644 --- a/src/models/eulerBernoulliBeam.js +++ b/src/models/eulerBernoulliBeam.js @@ -81,7 +81,10 @@ export function assembleEulerBernoulliBeamMat(meshData, boundaryConditions, coef // Cubic Hermite basis functions for the field, with a 4-point Gauss quadrature rule const basisFunctions = new BasisFunctions({ meshDimension: "1D", elementOrder: "hermiteCubic" }); - const numericalIntegration = new NumericalIntegration({ meshDimension: "1D", elementOrder: "hermiteCubic" }); + const numericalIntegration = new NumericalIntegration({ + meshDimension: "1D", + elementOrder: "hermiteCubic", + }); const { gaussPoints, gaussWeights } = numericalIntegration.getGaussPointsAndWeights(); // Matrix assembly diff --git a/src/visualization/vtkPlot.js b/src/visualization/vtkPlot.js index 4ecc8b4..d16a6c9 100644 --- a/src/visualization/vtkPlot.js +++ b/src/visualization/vtkPlot.js @@ -492,7 +492,7 @@ function convertElementNodesToLinearCell(elementNodes) { return [indices[0], indices[6], indices[8], indices[2]]; } - // Generic fallback for polygonal/high-order cells. + // Generic fallback for polygonal/high-order cells return indices.slice(0, Math.min(4, indices.length)); } @@ -546,15 +546,23 @@ function buildVTPString(vtkData) { '', '', " ", - ` `, + ` `, ' ', - ` ${Array.from(vtkData.scalars).join(" ")}`, + ` ${Array.from( + vtkData.scalars, + ).join(" ")}`, " ", " ", - ` ${Array.from(vtkData.points).join(" ")}`, + ` ${Array.from( + vtkData.points, + ).join(" ")}`, " ", ` <${topologyTag}>`, - ` ${connectivity.join(" ")}`, + ` ${connectivity.join( + " ", + )}`, ` ${offsets.join(" ")}`, ` `, " ", diff --git a/src/workers/worker.js b/src/workers/worker.js index 4ad4ecc..3780dd7 100644 --- a/src/workers/worker.js +++ b/src/workers/worker.js @@ -16,7 +16,7 @@ import * as Comlink from "../vendor/comlink.mjs"; export class FEAScriptWorker { /** * Constructor to initialize the FEAScriptWorker class - * Sets up the worker and initializes the workerWrapper. + * Sets up the worker and initializes the workerWrapper */ constructor() { this.worker = null; diff --git a/tests/regression/EulerBernoulliBeam/REGRESSION.md b/tests/regression/EulerBernoulliBeam/REGRESSION.md index f78eed9..3c54d5c 100644 --- a/tests/regression/EulerBernoulliBeam/REGRESSION.md +++ b/tests/regression/EulerBernoulliBeam/REGRESSION.md @@ -6,7 +6,7 @@ This test guards the numerical output of the 1D Euler-Bernoulli beam example aga unintended changes to the beam solver, assembler, or mesh-generation logic. It replicates exactly the problem set up in -[`Beam1DEuler_Bernoulli.js`](../../../examples/Beam1DFEM/Beam1DEuler_Bernoulli.js) — the +[`clampedSpringSupportedBeam1D.js`](../../../examples/eulerBernoulliBeamScript/clampedSpringSupportedBeam1D/clampedSpringSupportedBeam1D.js) — the "Bending of a Beam" example from J.N. Reddy, _An Introduction to the Finite Element Method_, 3rd ed., McGraw-Hill, 2006 (FEM1D example problems, Chapter 7) — and asserts both a set of known-good baseline values and, independently of those baseline numbers, that the resulting @@ -14,27 +14,27 @@ finite element solution satisfies global static equilibrium. ## Problem setup -| Parameter | Value | -| ----------------------------- | -------------------------------------------------------- | -| Domain | 1D beam, 0 – 10 m | -| Mesh | 2 cubic Hermite beam elements of 5 m each (3 nodes) | -| Bending stiffness EI | 2.0 × 10⁶ N·m² (constant) | -| Distributed load | −1,000 N/m over 0 ≤ x ≤ 5 m only | -| Node 1 (x = 0) | Fixed (clamped): w = 0, theta = 0 | -| Node 2 (x = 5) | Pinned (roller): w = 0, plus an applied moment M = 1,250 N·m | -| Node 3 (x = 10) | Transverse spring k = 200 N/m, plus a point load P = −2,500 N | -| Solver | LU decomposition (`lusolve`) | +| Parameter | Value | +| -------------------- | ------------------------------------------------------------- | +| Domain | 1D beam, 0 – 10 m | +| Mesh | 2 cubic Hermite beam elements of 5 m each (3 nodes) | +| Bending stiffness EI | 2.0 × 10⁶ N·m² (constant) | +| Distributed load | −1,000 N/m over 0 ≤ x ≤ 5 m only | +| Node 1 (x = 0) | Fixed (clamped): w = 0, theta = 0 | +| Node 2 (x = 5) | Pinned (roller): w = 0, plus an applied moment M = 1,250 N·m | +| Node 3 (x = 10) | Transverse spring k = 200 N/m, plus a point load P = −2,500 N | +| Solver | LU decomposition (`lusolve`) | ## Expected values -| Quantity | Value | -| ------------------------ | ------------------------- | -| w₁ (deflection, node 1) | 0 m | -| θ₁ (rotation, node 1) | 0 rad | -| w₂ (deflection, node 2) | 0 m | -| θ₂ (rotation, node 2) | −5.6790761806 × 10⁻³ rad | -| w₃ (deflection, node 3) | −8.0144777663 × 10⁻² m | -| θ₃ (rotation, node 3) | −2.1203895209 × 10⁻² rad | +| Quantity | Value | +| ----------------------- | ------------------------ | +| w₁ (deflection, node 1) | 0 m | +| θ₁ (rotation, node 1) | 0 rad | +| w₂ (deflection, node 2) | 0 m | +| θ₂ (rotation, node 2) | −5.6790761806 × 10⁻³ rad | +| w₃ (deflection, node 3) | −8.0144777663 × 10⁻² m | +| θ₃ (rotation, node 3) | −2.1203895209 × 10⁻² rad | Tolerance used in the baseline assertions: `1e-8`. @@ -77,16 +77,24 @@ node tests/regression/EulerBernoulliBeam/regression.test.js The `test` script in `package.json` also runs this file, so `npm test` works too. +A passing run prints a `PASS:` line for each check, followed by a summary line: + +``` +8 passed, 0 failed. +``` + +A failing run prints one or more `FAIL:` lines, ends with the same summary line format, and exits with code 1. + ## After modifying the code -| Situation | Action | -| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | -| Bug fix that should not change results | Run the test — it must still pass. | -| Intentional algorithm change (new integration rule, new element type, etc.) | Re-derive the expected values, update `EXPECTED` in `regression.test.js`, and document the reason here. | -| New boundary condition type | Update both the test and `Beam1DEuler_Bernoulli.js` together. | +| Situation | Action | +| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| Bug fix that should not change results | Run the test — it must still pass. | +| Intentional algorithm change (new integration rule, new element type, etc.) | Re-derive the expected values, update `EXPECTED` in `regression.test.js`, and document the reason here. | +| New boundary condition type | Update both the test and `clampedSpringSupportedBeam1D.js` together. | ## Change log | Date | Change | New expected values | -| ---------- | ---------------------------- | -------------------- | -| 2026-07-17 | Initial regression baseline | See table above | +| ---------- | --------------------------- | ------------------- | +| 2026-07-17 | Initial regression baseline | See table above | diff --git a/tests/regression/EulerBernoulliBeam/regression.test.js b/tests/regression/EulerBernoulliBeam/regression.test.js index a228fdf..2b731aa 100644 --- a/tests/regression/EulerBernoulliBeam/regression.test.js +++ b/tests/regression/EulerBernoulliBeam/regression.test.js @@ -1,15 +1,26 @@ +/** + * ════════════════════════════════════════════════════════════════ + * FEAScript Core Library + * Lightweight Finite Element Simulation in JavaScript + * Version: 0.3.0 (RC) | https://feascript.com + * MIT License © 2023–2026 FEAScript + * ════════════════════════════════════════════════════════════════ + */ + /** * Regression test for the 1D Euler-Bernoulli beam model (EulerBernoulliBeam) * - * Replicates the exact setup from Beam1DEuler_Bernoulli.js — the "Bending of a - * Beam" example from J.N. Reddy, "An Introduction to the Finite Element Method", - * 3rd ed., McGraw-Hill, 2006 (FEM1D example problems, Chapter 7) — and asserts: + * Replicates the exact setup from + * examples/eulerBernoulliBeamScript/clampedSpringSupportedBeam1D/ + * clampedSpringSupportedBeam1D.js — the "Bending of a Beam" example from J.N. + * Reddy, "An Introduction to the Finite Element Method", 3rd ed., McGraw-Hill, + * 2006 (FEM1D example problems, Chapter 7) — and asserts: * 1) the deflection/rotation solution vector against known-good baseline values * 2) global force and moment equilibrium of the resulting FE solution, which * holds regardless of the specific numeric baseline and independently * confirms the assembled system is physically consistent * - * Run: node tests/regression/EulerBernoulliBeam/regression.test.js + * Run: node tests/regression/EulerBernoulliBeam/regression.test.js (or npm test) */ import * as mathjs from "mathjs"; @@ -57,18 +68,26 @@ function runSimulation() { model.addBoundaryCondition("1", [["fixed"]]); model.addBoundaryCondition("2", [["pinned"], ["moment", appliedMoment]]); - model.addBoundaryCondition("3", [["spring", springConstant], ["force", appliedForce]]); + model.addBoundaryCondition("3", [ + ["spring", springConstant], + ["force", appliedForce], + ]); model.setSolverMethod("lusolve"); return model.solve(); } +let passed = 0; +let failed = 0; + function assert(condition, message) { if (!condition) { errorLog(`FAIL: ${message}`); - process.exit(1); + failed++; + } else { + basicLog(`PASS: ${message}`); + passed++; } - basicLog(`PASS: ${message}`); } basicLog(""); @@ -80,29 +99,20 @@ const { solutionVector } = runSimulation(); const flatSolution = solutionVector.map((entry) => (Array.isArray(entry) ? entry[0] : entry)); const [w1, theta1, w2, theta2, w3, theta3] = flatSolution; -// --------------------------------------------------------------------------- -// 1) Baseline values -// --------------------------------------------------------------------------- +basicLog(""); +basicLog("[1] Baseline deflection/rotation values"); + assert(Math.abs(w1 - EXPECTED.w1) < TOLERANCE, `w1: expected ~${EXPECTED.w1}, got ${w1}`); assert(Math.abs(theta1 - EXPECTED.theta1) < TOLERANCE, `theta1: expected ~${EXPECTED.theta1}, got ${theta1}`); assert(Math.abs(w2 - EXPECTED.w2) < TOLERANCE, `w2: expected ~${EXPECTED.w2}, got ${w2}`); -assert( - Math.abs(theta2 - EXPECTED.theta2) < TOLERANCE, - `theta2: expected ${EXPECTED.theta2}, got ${theta2}`, -); +assert(Math.abs(theta2 - EXPECTED.theta2) < TOLERANCE, `theta2: expected ${EXPECTED.theta2}, got ${theta2}`); assert(Math.abs(w3 - EXPECTED.w3) < TOLERANCE, `w3: expected ${EXPECTED.w3}, got ${w3}`); -assert( - Math.abs(theta3 - EXPECTED.theta3) < TOLERANCE, - `theta3: expected ${EXPECTED.theta3}, got ${theta3}`, -); +assert(Math.abs(theta3 - EXPECTED.theta3) < TOLERANCE, `theta3: expected ${EXPECTED.theta3}, got ${theta3}`); + +// Reassemble without boundary conditions to recover reactions for the equilibrium checks +basicLog(""); +basicLog("[2] Global force and moment equilibrium"); -// --------------------------------------------------------------------------- -// 2) Global equilibrium check, independent of the specific baseline values above. -// Recompute the raw (no boundary conditions applied) element assembly and use -// it to recover support reactions: at any DOF, K_raw.u - F_raw equals whatever -// external generalized force (reaction, spring force, or applied load) is -// required to balance that row. -// --------------------------------------------------------------------------- const meshDataForCheck = { nodesXCoordinates: [0, 5, 10], nop: [ @@ -156,4 +166,11 @@ assert( `Global moment equilibrium about x=0 holds (sum=${sumMomentsAboutOrigin})`, ); +basicLog(""); +if (failed > 0) { + errorLog(`${passed} passed, ${failed} failed.`); +} else { + basicLog(`${passed} passed, ${failed} failed.`); +} basicLog("================================"); +if (failed > 0) process.exit(1); diff --git a/tests/regression/HeatConduction1DWall/REGRESSION.md b/tests/regression/HeatConduction1DWall/REGRESSION.md index 6359a5b..0b8b0ba 100644 --- a/tests/regression/HeatConduction1DWall/REGRESSION.md +++ b/tests/regression/HeatConduction1DWall/REGRESSION.md @@ -5,9 +5,9 @@ This test guards the numerical output of the 1D heat-conduction-through-a-wall example against unintended changes to the solver, assembler, or mesh-generation logic. -It replicates exactly the problem set up in -[`HeatConduction1DWall.html`](../../../examples/solidHeatTransferScript/HeatConduction1DWall/HeatConduction1DWall.html) -and asserts a known good value. +It replicates the physical problem set up in +[`heatConduction1DWall.js`](../../../examples/heatConductionScript/heatConduction1DWall/heatConduction1DWall.js), +uses `lusolve` as the regression baseline, and asserts a known good value. ## Problem setup @@ -35,15 +35,16 @@ From the repository root: node tests/regression/HeatConduction1DWall/regression.test.js ``` -A passing run prints: +The `test` script in `package.json` also runs this file, so `npm test` works too. + +A passing run prints a `PASS:` line for each check, followed by a summary line: ``` -PASS: T(x=0) = 10.29412 (expected 10.29412) +PASS: Temperature at node 0: expected 10.29412, got 10.294117647058822 (tolerance 0.0001) +1 passed, 0 failed. ``` -A failing run prints a `FAIL:` message and exits with code 1. - -The `test` script in `package.json` also runs this file, so `npm test` works too. +A failing run prints one or more `FAIL:` lines, ends with the same summary line format, and exits with code 1. ## After modifying the code @@ -51,7 +52,7 @@ The `test` script in `package.json` also runs this file, so `npm test` works too | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | Bug fix that should not change results | Run the test — it must still pass. | | Intentional algorithm change (new element type, new integration rule, etc.) | Re-derive the expected value, update `EXPECTED_T0` in `regression.test.js`, and document the reason here. | -| New boundary condition API | Update both the test and the reference HTML example together. | +| New boundary condition API | Update both the test and the reference JavaScript example together. | | Adding a new solver method | Add a separate assertion block for the new method; keep the `lusolve` block untouched as the baseline. | ## Change log diff --git a/tests/regression/HeatConduction1DWall/regression.test.js b/tests/regression/HeatConduction1DWall/regression.test.js index faf7858..81098e8 100644 --- a/tests/regression/HeatConduction1DWall/regression.test.js +++ b/tests/regression/HeatConduction1DWall/regression.test.js @@ -1,10 +1,19 @@ +/** + * ════════════════════════════════════════════════════════════════ + * FEAScript Core Library + * Lightweight Finite Element Simulation in JavaScript + * Version: 0.3.0 (RC) | https://feascript.com + * MIT License © 2023–2026 FEAScript + * ════════════════════════════════════════════════════════════════ + */ + /** * Regression test for HeatConduction1DWall * - * Replicates the exact setup from HeatConduction1DWall.html and asserts - * that the temperature at node 0 (convection boundary) remains 10.29412. + * Replicates the physical setup from heatConduction1DWall.js using lusolve and + * asserts that the temperature at node 0 (convection boundary) remains 10.29412 * - * Run: node tests/regression/HeatConduction1DWall/regression.test.js + * Run: node tests/regression/HeatConduction1DWall/regression.test.js (or npm test) */ import * as mathjs from "mathjs"; @@ -36,16 +45,22 @@ function runSimulation() { return model.solve(); } +let passed = 0; +let failed = 0; + function assert(condition, message) { if (!condition) { errorLog(`FAIL: ${message}`); - process.exit(1); + failed++; + } else { + basicLog(`PASS: ${message}`); + passed++; } } basicLog(""); basicLog("================================"); -basicLog("Starting test in solid heat transfer 1D wall..."); +basicLog("Starting regression test for solid heat transfer in a 1D wall..."); const { solutionVector } = runSimulation(); // solutionVector from math.lusolve is a nested array: [[T0], [T1], ...] @@ -56,5 +71,11 @@ assert( `Temperature at node 0: expected ${EXPECTED_T0}, got ${T0} (tolerance ${TOLERANCE})`, ); -basicLog(`PASS: T(x=0) = ${T0.toFixed(5)} (expected ${EXPECTED_T0})`); +basicLog(""); +if (failed > 0) { + errorLog(`${passed} passed, ${failed} failed.`); +} else { + basicLog(`${passed} passed, ${failed} failed.`); +} basicLog("================================"); +if (failed > 0) process.exit(1); diff --git a/tests/regression/HeatConduction2DFin/REGRESSION.md b/tests/regression/HeatConduction2DFin/REGRESSION.md index 4c27f0d..a7e2afb 100644 --- a/tests/regression/HeatConduction2DFin/REGRESSION.md +++ b/tests/regression/HeatConduction2DFin/REGRESSION.md @@ -5,9 +5,10 @@ This test guards the numerical output of the 2D heat-conduction-in-a-fin example against unintended changes to the solver, assembler, or mesh-generation logic. -It replicates exactly the problem set up in -[`HeatConduction2DFin.html`](../../../examples/solidHeatTransferScript/HeatConduction2DFin/HeatConduction2DFin.html) -and asserts a known good value at a representative interior point. +It replicates the physical problem set up in +[`heatConduction2DFin.js`](../../../examples/heatConductionScript/heatConduction2DFin/heatConduction2DFin.js), +uses `lusolve` as the regression baseline, and asserts a known good value at a representative +interior point. ## Problem setup @@ -41,15 +42,16 @@ From the repository root: node tests/regression/HeatConduction2DFin/regression.test.js ``` -A passing run prints: +The `test` script in `package.json` also runs this file, so `npm test` works too. + +A passing run prints a `PASS:` line for each check, followed by a summary line: ``` -PASS: T(x=0, y=2) = 81.31873 (expected 81.31873) +PASS: Temperature at (x=0, y=2): expected 81.31873, got 81.31873348502957 (tolerance 0.0001) +2 passed, 0 failed. ``` -A failing run prints a `FAIL:` message and exits with code 1. - -Running `npm test` executes all regression tests, including this one. +A failing run prints one or more `FAIL:` lines, ends with the same summary line format, and exits with code 1. ## After modifying the code @@ -57,7 +59,7 @@ Running `npm test` executes all regression tests, including this one. | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | Bug fix that should not change results | Run the test — it must still pass. | | Intentional algorithm change (new element type, new integration rule, etc.) | Re-derive the expected value, update `EXPECTED_T` in `regression.test.js`, and document the reason here. | -| New boundary condition API | Update both the test and the reference HTML example together. | +| New boundary condition API | Update both the test and the reference JavaScript example together. | | Mesh refinement study | Add a separate assertion block for the refined mesh; keep the current block as the coarse-mesh baseline. | ## Change log diff --git a/tests/regression/HeatConduction2DFin/regression.test.js b/tests/regression/HeatConduction2DFin/regression.test.js index 334d3cb..71ec63d 100644 --- a/tests/regression/HeatConduction2DFin/regression.test.js +++ b/tests/regression/HeatConduction2DFin/regression.test.js @@ -1,20 +1,25 @@ +/** + * ════════════════════════════════════════════════════════════════ + * FEAScript Core Library + * Lightweight Finite Element Simulation in JavaScript + * Version: 0.3.0 (RC) | https://feascript.com + * MIT License © 2023–2026 FEAScript + * ════════════════════════════════════════════════════════════════ + */ + /** * Regression test for HeatConduction2DFin * - * Replicates the exact setup from HeatConduction2DFin.html and asserts - * that the temperature at (x=0, y=2) remains 81.31873. + * Replicates the physical setup from heatConduction2DFin.js using lusolve and + * asserts that the temperature at (x=0, y=2) remains 81.31873 * - * Run: node tests/regression/HeatConduction2DFin/regression.test.js + * Run: node tests/regression/HeatConduction2DFin/regression.test.js (or npm test) */ import * as mathjs from "mathjs"; import { FEAScriptModel } from "../../../src/FEAScript.js"; import { basicLog, errorLog } from "../../../src/utilities/logging.js"; -basicLog(""); -basicLog("================================"); -basicLog("Starting test in solid heat transfer 2D fin..."); - // FEAScript.js references `math` as a global (loaded via CDN in browser). // Set it here before any solve() call. globalThis.math = mathjs; @@ -49,10 +54,20 @@ function runSimulation() { function assert(condition, message) { if (!condition) { errorLog(`FAIL: ${message}`); - process.exit(1); + failed++; + } else { + basicLog(`PASS: ${message}`); + passed++; } } +let passed = 0; +let failed = 0; + +basicLog(""); +basicLog("================================"); +basicLog("Starting regression test for solid heat transfer in a 2D fin..."); + const { solutionVector, nodesCoordinates } = runSimulation(); const { nodesXCoordinates, nodesYCoordinates } = nodesCoordinates; @@ -62,7 +77,7 @@ const nodeIndex = nodesXCoordinates.findIndex( (x, i) => Math.abs(x - EXPECTED_X) < 1e-10 && Math.abs(nodesYCoordinates[i] - EXPECTED_Y) < 1e-10, ); -assert(nodeIndex !== -1, `No node found at (x=${EXPECTED_X}, y=${EXPECTED_Y})`); +assert(nodeIndex !== -1, `Found node at (x=${EXPECTED_X}, y=${EXPECTED_Y})`); // solutionVector from math.lusolve is a nested array: [[T0], [T1], ...] const T = Array.isArray(solutionVector[nodeIndex]) ? solutionVector[nodeIndex][0] : solutionVector[nodeIndex]; @@ -72,6 +87,11 @@ assert( `Temperature at (x=${EXPECTED_X}, y=${EXPECTED_Y}): expected ${EXPECTED_T}, got ${T} (tolerance ${TOLERANCE})`, ); -basicLog(`PASS: T(x=${EXPECTED_X}, y=${EXPECTED_Y}) = ${T.toFixed(5)} (expected ${EXPECTED_T})`); - +basicLog(""); +if (failed > 0) { + errorLog(`${passed} passed, ${failed} failed.`); +} else { + basicLog(`${passed} passed, ${failed} failed.`); +} basicLog("================================"); +if (failed > 0) process.exit(1); diff --git a/tests/unit/README.md b/tests/unit/README.md index dee4e93..f6d2718 100644 --- a/tests/unit/README.md +++ b/tests/unit/README.md @@ -1,8 +1,14 @@ # Unit Tests -This folder will contain unit tests for individual FEAScript modules (solvers, assemblers, utilities, etc.). +This folder contains unit tests for individual FEAScript modules (solvers, assemblers, utilities, etc.). -Each test file should target a single module and be runnable with: +Run all unit and regression tests from the repository root with: + +```bash +npm test +``` + +Each unit test can also be run directly with: ```bash node tests/unit/.js diff --git a/tests/unit/eulerBernoulliBeam.test.js b/tests/unit/eulerBernoulliBeam.test.js index 8b71882..02f471e 100644 --- a/tests/unit/eulerBernoulliBeam.test.js +++ b/tests/unit/eulerBernoulliBeam.test.js @@ -1,3 +1,12 @@ +/** + * ════════════════════════════════════════════════════════════════ + * FEAScript Core Library + * Lightweight Finite Element Simulation in JavaScript + * Version: 0.3.0 (RC) | https://feascript.com + * MIT License © 2023–2026 FEAScript + * ════════════════════════════════════════════════════════════════ + */ + /** * Unit tests for the 1D Euler-Bernoulli beam model (assembleEulerBernoulliBeamMat) * @@ -7,7 +16,7 @@ * - Cantilever beam under a tip point load vs. the classical closed-form * solution (w_tip = P*L^3/(3EI), theta_tip = P*L^2/(2EI)) * - * Run: node tests/unit/eulerBernoulliBeam.test.js + * Run: node tests/unit/eulerBernoulliBeam.test.js (or npm test) */ import * as mathjs from "mathjs"; @@ -59,25 +68,33 @@ function maxAbsDiff(A, B) { return diff; } -// --------------------------------------------------------------------------- -// 1. Single-element stiffness matrix vs. closed-form matrix -// --------------------------------------------------------------------------- basicLog(""); -basicLog("[1] Single-element stiffness matrix vs. closed-form Hermite beam matrix"); +basicLog( + "[1] Single-element stiffness matrix vs. closed-form Hermite beam matrix" +); for (const [EI, L] of [ [2.0e6, 5], [7.5e4, 2.5], ]) { - const meshData = prepareMesh({ meshDimension: "1D", elementOrder: "linear", numElementsX: 1, maxX: L }); - const { jacobianMatrix } = assembleEulerBernoulliBeamMat(meshData, {}, { EI: () => EI }); + const meshData = prepareMesh({ + meshDimension: "1D", + elementOrder: "linear", + numElementsX: 1, + maxX: L, + }); + const { jacobianMatrix } = assembleEulerBernoulliBeamMat( + meshData, + {}, + { EI: () => EI } + ); const diff = maxAbsDiff(jacobianMatrix, closedFormBeamStiffness(EI, L)); - assert(diff < 1e-6, `Element stiffness matches closed-form matrix for EI=${EI}, L=${L} (diff=${diff})`); + assert( + diff < 1e-6, + `Element stiffness matches closed-form matrix for EI=${EI}, L=${L} (diff=${diff})` + ); } -// --------------------------------------------------------------------------- -// 2. Cantilever beam under a tip point load vs. classical closed-form solution -// --------------------------------------------------------------------------- basicLog(""); basicLog("[2] Cantilever beam under a tip point load"); @@ -85,37 +102,49 @@ const L = 4; const EI = 1.0e5; const P = -1000; // Downward tip load -const meshData = prepareMesh({ meshDimension: "1D", elementOrder: "linear", numElementsX: 1, maxX: L }); +const meshData = prepareMesh({ + meshDimension: "1D", + elementOrder: "linear", + numElementsX: 1, + maxX: L, +}); const { jacobianMatrix, residualVector } = assembleEulerBernoulliBeamMat( meshData, { 1: [["fixed"]], 2: [["force", P]] }, - { EI: () => EI }, + { EI: () => EI } +); +const { solutionVector } = solveLinearSystem( + "lusolve", + jacobianMatrix, + residualVector +); +const flatSolution = solutionVector.map((entry) => + Array.isArray(entry) ? entry[0] : entry ); -const { solutionVector } = solveLinearSystem("lusolve", jacobianMatrix, residualVector); -const flatSolution = solutionVector.map((entry) => (Array.isArray(entry) ? entry[0] : entry)); const wExact = (P * L ** 3) / (3 * EI); const thetaExact = (P * L ** 2) / (2 * EI); const tolerance = 1e-9; assert( - Math.abs(flatSolution[0]) < tolerance && Math.abs(flatSolution[1]) < tolerance, - "Clamped end has zero deflection and rotation", + Math.abs(flatSolution[0]) < tolerance && + Math.abs(flatSolution[1]) < tolerance, + "Clamped end has zero deflection and rotation" ); assert( Math.abs(flatSolution[2] - wExact) < tolerance, - `Tip deflection matches closed form (got ${flatSolution[2]}, expected ${wExact})`, + `Tip deflection matches closed form (got ${flatSolution[2]}, expected ${wExact})` ); assert( Math.abs(flatSolution[3] - thetaExact) < tolerance, - `Tip rotation matches closed form (got ${flatSolution[3]}, expected ${thetaExact})`, + `Tip rotation matches closed form (got ${flatSolution[3]}, expected ${thetaExact})` ); -// --------------------------------------------------------------------------- basicLog(""); if (failed > 0) { errorLog(`${passed} passed, ${failed} failed.`); - process.exit(1); } else { basicLog(`${passed} passed, ${failed} failed.`); } +basicLog("================================"); +if (failed > 0) process.exit(1); diff --git a/tests/unit/jacobiMethod.test.js b/tests/unit/jacobiMethod.test.js index 070ce5f..fbb1a1f 100644 --- a/tests/unit/jacobiMethod.test.js +++ b/tests/unit/jacobiMethod.test.js @@ -1,3 +1,12 @@ +/** + * ════════════════════════════════════════════════════════════════ + * FEAScript Core Library + * Lightweight Finite Element Simulation in JavaScript + * Version: 0.3.0 (RC) | https://feascript.com + * MIT License © 2023–2026 FEAScript + * ════════════════════════════════════════════════════════════════ + */ + /** * Unit tests for jacobiSolver * @@ -5,7 +14,7 @@ * - Success case on a diagonally dominant 2x2 system * - Non-convergence when maxIterations is too small * - * Run: node tests/unit/jacobiMethod.test.js + * Run: node tests/unit/jacobiMethod.test.js (or npm test) */ import { jacobiSolver } from "../../src/methods/jacobiSolver.js"; @@ -28,12 +37,9 @@ function assert(condition, message) { } } -// --------------------------------------------------------------------------- -// 1. SUCCESS CASE — diagonally dominant 2x2 system -// +// Diagonally dominant system with exact solution x = 1, y = 1 // 10x + 2y = 12 exact solution: x = 1, y = 1 // 1x + 5y = 6 -// --------------------------------------------------------------------------- basicLog(""); basicLog("[1] Success case"); @@ -54,12 +60,15 @@ assert(result.iterations > 0, "At least one iteration was performed"); assert(result.iterations <= 500, "Converges within configured iteration limit"); const tolerance = 1e-6; -assert(Math.abs(result.solutionVector[0] - 1) < tolerance, `x ~= 1 (got ${result.solutionVector[0]})`); -assert(Math.abs(result.solutionVector[1] - 1) < tolerance, `y ~= 1 (got ${result.solutionVector[1]})`); +assert( + Math.abs(result.solutionVector[0] - 1) < tolerance, + `x ~= 1 (got ${result.solutionVector[0]})` +); +assert( + Math.abs(result.solutionVector[1] - 1) < tolerance, + `y ~= 1 (got ${result.solutionVector[1]})` +); -// --------------------------------------------------------------------------- -// 2. NON-CONVERGENCE CASE — force early stop -// --------------------------------------------------------------------------- basicLog(""); basicLog("[2] Non-convergence case"); @@ -68,17 +77,24 @@ const hardResult = jacobiSolver(A, b, x0, { tolerance: 1e-20, }); -assert(hardResult.converged === false, "Method reports non-convergence with too few iterations"); -assert(hardResult.iterations === 1, "Method reports the configured iteration cap"); -assert(hardResult.solutionVector.length === 2, "Returns a solution vector of expected size"); +assert( + hardResult.converged === false, + "Method reports non-convergence with too few iterations" +); +assert( + hardResult.iterations === 1, + "Method reports the configured iteration cap" +); +assert( + hardResult.solutionVector.length === 2, + "Returns a solution vector of expected size" +); -// --------------------------------------------------------------------------- -// Summary -// --------------------------------------------------------------------------- +basicLog(""); if (failed > 0) { - errorLog(`${passed + failed} assertions — ${passed} passed, ${failed} failed.`); + errorLog(`${passed} passed, ${failed} failed.`); } else { - basicLog(`${passed + failed} assertions — ${passed} passed, ${failed} failed.`); + basicLog(`${passed} passed, ${failed} failed.`); } basicLog("================================"); if (failed > 0) process.exit(1);