All notable changes to FEAScript-core are documented in this file. The format is based on Keep a Changelog, and the project follows Semantic Versioning (while in 0.x, minor versions may contain breaking changes).
0.4.0 - 2026-11-13
-
One way to set up a domain, whatever its mesh:
setGeometry()takes the geometry,{ line: length },{ rectangle: [width, height] }or{ shape, holes, points }, where a polygon is a path{ start, boundaries: [{ name, to }] }(each boundary runs from where the previous one ended toto, a point or a polyline, and the last may leave outtoto close the shape), a polygon{ name, points }with one name, or a plain list of points;setMeshConfig()then takes how to mesh it,{ elements, elementOrder, size, divisions, minAngle, maxNodes }, withelements: "quadrilaterals"(a rectangle or a convex four-sided shape, mapped from a grid) or"triangles"(any shape with holes). Thesizeis the length of an element side, for triangles also a function of position for a graded mesh;divisionsis the number of elements of a line or[nx, ny]of a quadrilateral, for an exact element count. The boundary conditions are keyed by the boundary names:"left"and"right"for a line,"bottom","right","top"and"left"for a rectangle, the names given for a polygon, and the names of the physical groups for a Gmsh mesh -
setMesh(mesh)takes a mesh directly: one imported withimportGmshMesh, or generated withgenerateTriangleMesh(geometry, options)orgenerateQuadrilateralMesh(geometry, options), which are exported and return the same mesh object (nodes, elements, boundary element sides by name, aliases). The geometry checks (a polygon that is not simple, a hole outside the shape or inside another hole, a point outside the domain, a path that does not close) throwFEAScriptError"INVALID_INPUT"listing every problem -
generateTriangleMesh(), a mesh generator for a polygon with holes (#135): a constrained Delaunay triangulation of the polygon (Delaunator and Constrainautor), refined by Ruppert's algorithm with off-centers until every triangle has angles of at leastminAngle(default 20°) and an area of at most that of the equilateral triangle of sidesize, a number or a function of position for a graded mesh. Points inside the domain can be made nodes. Small input angles are handled with concentric shell splitting, so the refinement always stops. Linear or quadratic triangles. A refinement that passesmaxNodesthrowsFEAScriptError"MESH_TOO_FINE" -
generateQuadrilateralMesh(): the structured mesh of 0.3, generalized to any convex four-sided shape by a bilinear map of the grid (a rectangle and the former skewed trapezoid are special cases), with linear or quadratic elements and the same node numbering as before -
FEAScriptWorkerhassetGeometry()andsetMesh(), and itssetMeshConfig()accepts asizefunction -
Benchmark tests for 2D heat conduction from the Code_Aster validation manual: TPLP302 (strip with a Fourier series solution) and TPLP01 (L-shaped domain with a re-entrant corner, on Gmsh triangle and quadrilateral meshes)
-
FEAScriptError, with acode, is exported, and stops a solve that would otherwise return an empty or a wrong result (#125). Itsdetailslist every problem found by one check, so that all the input mistakes are reported at once, like a compiler does. AFEAScriptErrorthrown inFEAScriptWorkerkeeps itscodeanddetails -
Input checks that were missing, so far ignored or left to fail later with a
TypeErroror a wrong result: a misspelled or unset model, a boundary condition on a boundary (or beam node) that is not in the mesh, a missing condition value or one that is not a number (e.g.["constantTemperature"]or["constantTemperature", "5"]), a missing coefficient function (A–Dof the general form PDE,dDduwhen nonlinear,EIof the beam), and a number of elements that is not a positive integer or a length that is not positive, and a coefficient that the model does not use, e.g. a misspelledthermalConductivtythat would leave the default value in place -
A NaN or infinite value in the assembled system or in the solution throws
"NON_FINITE_VALUES". The sparse LU solver took a NaN on the right-hand side for a zero, so a coefficient function that returns NaN somewhere gave a finite but wrong solution -
The frontal solver runs the nonlinear general form PDE (
nonlinear: true) inside Newton–Raphson (#101), as it does front propagation. It stopped with"UNSUPPORTED_SOLVER"before -
Boundary conditions as objects with named parameters (#123), e.g.
addBoundaryCondition("top", { type: "convection", h: 1, ambientTemperature: 20 })instead of["convection", 1, 20]. The parameters of each type:- Heat conduction:
{ type: "constantTemperature", temperature },{ type: "convection", h, ambientTemperature }and{ type: "symmetry" } - General form PDE and front propagation:
{ type: "constantValue", value }and{ type: "zeroGradient" } - Creeping flow:
{ type: "constantVelocity", u, v }and{ type: "stressFree" } - Euler-Bernoulli beam, one object or a list of them per node:
{ type: "fixed" },{ type: "pinned" },{ type: "rotationFixed" },{ type: "deflection", deflection },{ type: "rotation", rotation },{ type: "force", force },{ type: "moment", moment }and{ type: "spring", stiffness, referenceDeflection }. The deflection, the rotation and the reference deflection default to 0
A condition object with no type, an unknown type, or a missing or unknown (e.g. misspelled) parameter throws
FEAScriptError"INVALID_INPUT"when it is added, listing every problem. A value that is not a number is reported with the name of its parameter - Heat conduction:
-
Solver options in
setModelConfig(#122):setModelConfig("heatConduction", { solver: { method: "jacobi", maxIterations: 20000, tolerance: 1e-10 } }). Themethodis"lusolve"(the default),"jacobi","frontal"or"jacobi-gpu"(withsolveAsync()), andmaxIterationsandtoleranceapply to the iterative linear solvers, also inside Newton-Raphson. The top-levelmaxIterationsandtoleranceofsetModelConfigkeep setting Newton-Raphson, and the linear solver whensolverdoes not set them. An unknown method or option, or an iteration limit or tolerance that is not positive, throwsFEAScriptError"INVALID_INPUT". Preconditioners will be added as solver options with the PCG solver (#115, #116) -
version, the version of FEAScript as in its npm package, e.g."0.4.0"(#128) -
A run report,
result.info, fromsolve(),solveAsync()andFEAScriptWorker.solve()(#127):{ method, converged, iterations, residual, newton, solveTime }, with the same properties for every model and solver method.iterationsare those of the linear solver (nullfor the direct solvers),residualis ‖R − J u‖₂ / ‖R‖₂ of the linear system computed in 64-bit floats for every method (nullfor"frontal", which assembles no system matrix, and for nonlinear models),newtonis{ iterations, increment }for the nonlinear models, andsolveTimeis the time of the whole solve in milliseconds -
TypeScript declarations in the npm package (#126), generated from the JSDoc of the source into
dist/types/(npm run build:types, part ofnpm run build) and named bytypesinpackage.json. TypeScript projects are type-checked against the API, and editors complete the model names, solver methods, geometry and mesh settings, boundary condition types with their parameters, and the result with its run report. The types of the inputs and results (Geometry,MeshConfig,BoundaryCondition,SolveResult,RunReport, ...) are exported forimport type. A test type-checks a TypeScript use of the package against the declarations, and checks that they catch common mistakes -
Loading from a classic
<script>tag: the UMD bundle,dist/feascript.umd.js, defines the globalFEAScriptwith every export, e.g.const { FEAScriptModel, plotSolution } = FEAScript;. It reads its own script URL, soFEAScriptWorkerand the WebGPU worker find their files next to the bundle, also when it is loaded from a CDN, instead of next to the page.tests/umd/umd.test.htmlchecks the script-tag build in headless Chromium and Firefox (npm run test:browser, not part ofnpm test)
-
Model names without the
Scriptsuffix (#124):"heatConduction","generalFormPDE","frontPropagation","creepingFlow"and"eulerBernoulliBeam".model.solverConfig, and thesolverConfigof a worker result, hold the new name also when the old one is given -
The example folders are named after the models, e.g.
examples/heatConduction/instead ofexamples/heatConductionScript/. Links to the old folders onmainno longer work; links to a release tag, such astree/0.3.1/examples/heatConductionScript, still do -
A mesh imported with
importGmshMeshis converted to the mesh object once, with its boundaries keyed by the names of the physical groups, and the boundary lines are matched to element sides through a lookup table instead of a search over all elements for each line -
A solve that cannot succeed now throws a
FEAScriptErrorinstead of logging an error and returning a result:"UNSUPPORTED_SOLVER"for a solver method the model does not support, such as the frontal solver for creeping flow or beams, and for anythingsolveAsync()cannot run, and"NOT_CONVERGED"for a Jacobi solve, a front propagation or a nonlinear general form PDE that does not converge, and"SINGULAR_MATRIX"for a singular system, where the frontal solver returned zeros and the LU solver failed with aTypeError. Code that read a partial result after such a failure must now catch the error -
The other input checks also throw a
FEAScriptErrorinstead of logging an error and going on (#125):"INVALID_INPUT"for an unknown boundary condition type (it was ignored), a condition that is not an array, a mesh dimension other than 1D or 2D, and a negative creeping flow viscosity;"UNSUPPORTED_MESH"for a mesh the model or the Gmsh reader cannot use yet (creeping flow without quadratic 2D elements, a beam that is not on a linear 1D mesh, a 2D general form PDE, a Gmsh mesh that is not 2D or that mixes element orders or shapes); and"INTERNAL_ERROR"for a frontal solver step that fails, which returned a wrong solution. A condition on an imported Gmsh boundary with a node pair that is not on an element side throws"INVALID_INPUT", as it would apply to only part of the boundary; such a boundary without a condition only gives a warning. Plots throw"INVALID_INPUT"for an unknown field and for a field requested from a result without named fields (they plotted another field), for a plot type they cannot draw (they drew nothing) and for a plot container that does not exist. A mesh setting that conflicts with an imported Gmsh mesh is a warning instead of an error, as the mesh value is used, and so is a vtk.js plot outside a browser -
Dirichlet conditions (constant temperature, value and velocity, beam supports) are applied symmetrically (#117): the known values move to the right-hand side and the columns of the prescribed nodes are eliminated as well as their rows, so a symmetric system matrix stays symmetric. The solution is the same, and the frontal solver is not affected
-
The Jacobi solver (
"jacobi") stops on the residual relative to the right-hand side, ‖R − J u‖ / ‖R‖, instead of on the size of the update, and its defaulttoleranceis1e-8. The update size could be small while the solution was still about 1e-3 away, so the method reported convergence too early -
The WebGPU Jacobi solver (
"jacobi-gpu") stops on the same relative residual, ‖R − J u‖ / ‖R‖, as"jacobi"(#113), instead of on the largest absolute residual, which depended on the scale of the problem (e.g. on fixed temperatures of 200 °C). Its defaulttolerancestays1e-4: the GPU works in 32-bit floats, where the relative residual stops falling at about 1e-7 to 1e-5, depending on the size of the system -
A WebGPU compute engine reused for systems of different sizes (
solveAsync(computeEngine)) kept running its kernels on the fields of the first system, and returned a wrong solution as converged. The kernels are now compiled again when the system size changes -
The sparse LU solver imports only the math.js functions it uses (
slu,lusolve,flatten,SparseMatrix) instead of the whole library. This makes the bundles indist/about 60% smaller, e.g.feascript.esm.jsgoes from 730 KB to 308 KB -
The system matrix is assembled directly into compressed sparse row (CSR) storage with typed arrays (#120). The sparsity pattern is computed once from the element connectivity, and element contributions are added at fixed positions, instead of growing a
Mapper row. Assembly of the 2D fin on a 320×160 mesh (51,681 nodes) takes about 0.1 s, and the peak memory of a"lusolve"solve falls from 375 MB to 355 MB (one core of an AMD Ryzen 5 9600X).SparseMatrixkeeps its methods; entries outside the pattern are still accepted -
"lusolve"uses a fill-reducing ordering suited to each model (#132). Creeping flow, whose saddle-point matrix has zeros on the diagonal of its pressure rows, is now factorized with an approximate minimum degree ordering of JᵀJ and a pivoting threshold of 0.001, which halves the fill-in of the LU decomposition. The other models keep the ordering of J + Jᵀ with partial pivoting, which is the faster one for them. On one core of an AMD Ryzen 5 9600X, together with the CSR assembly, the lid-driven cavity on a 64×64 mesh (16,641 nodes) solves in 10 s instead of 145 s, with a peak memory of 0.73 GB instead of 1.26 GB. The ordering and the threshold can be passed tomathjsLUSolver(ordering,pivotThreshold)
setMeshConfig({ meshDimension, numElementsX, maxX, numElementsY, maxY, angleLeft, angleRight }): usesetGeometry()with{ line },{ rectangle }or a four-point shape, andsetMeshConfig({ elements, elementOrder, divisions }). The old form keeps working with a warning, and is removed in 0.7.0 at the earliestsetMeshConfig({ parsedMesh }): usesetMesh(mesh)- The numeric boundary keys:
"0"to"3"of the structured meshes (bottom, left, top, right),"0"and"1"of 1D meshes (left, right), and the Gmsh physical tag minus one. They keep working as aliases of the boundary names, with a warning - The model names with the
Scriptsuffix, e.g."heatConductionScript": use"heatConduction"(#124). They keep working with a warning - The array form of boundary conditions, e.g.
["convection", 1, 20], and conditions given as a plain string, e.g."zeroGradient": use the condition objects (#123). They keep working, with a warning that gives the object form of the condition setSolverMethod(method)ofFEAScriptModelandFEAScriptWorker: usesetModelConfig(model, { solver: { method } })(#122). It keeps working with a warning- Model information in the form of 0.3 passed to the plot functions, with the mesh settings (
meshDimension,numElementsX, ...) or an imported mesh (parsedMesh) inmeshConfig: pass the model itself, the information ofFEAScriptWorker.getModelInfo(), or{ solverConfig, mesh }. It keeps working with a warning printVersion: useversion(#128).printVersionis a string, so reading it cannot log a warning; it is marked@deprecatedfor editors, and holds the version as text,"0.4.0", until it is removed
- The
"constantTemp"boundary condition, deprecated in 0.3.0 (#129): use{ type: "constantTemperature", temperature }. It now throwsFEAScriptError"INVALID_INPUT", with a message that names its replacement - The NAFEMS T4 Node.js example: the benchmark is covered by its test
(
tests/verification/benchmark/heatConduction2DNafemsT4) and by the verification pages of the documentation
FEAScriptWorkerstarts its first call as soon as the worker is ready. It checked once a second, so every new worker added up to a second before the first result. A worker that fails to start now reports the failure to the waiting call straight away, instead of after the 50 s timeout"jacobi-gpu"where WebGPU is missing: it checks fornavigator.gpuand a GPU adapter, in the page and in the worker, and throwsFEAScriptError"UNSUPPORTED_SOLVER"with the reason, instead of hanging. A WebGPU worker whose initialisation fails is terminated- Frontal solver memory and run time on medium-sized problems
(#121):
- The front was allocated with the size of the whole system (N × N), and the solver eliminated at most one node per assembled element, so the front kept growing. It now eliminates every fully summed node before assembling the next element. It sizes the front from its maximum width, computed in a prefront pass, and stores it in typed arrays.
- The unused determinant bookkeeping, which cost O(N) per pivot, is removed.
- The elements are assembled in the order that keeps the front narrowest: the solver compares the order of the mesh, sweeps along x and y, and a breadth-first order through neighboring elements, and keeps the one with the smallest maximum front width. On one core of an AMD Ryzen 5 9600X, a 32×320 quadratic strip solves in 0.5 s instead of 16.7 s, with a third of the memory, and the L-shaped Gmsh mesh of the TPLP01 test 2.4× faster. Structured meshes numbered across their short side are unchanged.
- Results on one core of an Intel N100:
- 80×40 quadratic mesh (13k nodes): previously ran out of memory, now solves in 0.9 s.
- 160×80 linear mesh (13k nodes): 31 s → 1.1 s.
- Creeping flow with velocity conditions on every boundary: at a node shared by a moving wall and a fixed wall, the condition applied last won, so one corner of a moving lid leaked fluid through a side wall. With the pressure fixed at one node, this gave a spurious flow near that node. Zero-velocity (no-slip) conditions now take precedence at shared nodes, and the lid-driven cavity is now mirror-symmetric
- Sparse LU (
lusolve) speed (#136). The inner loop of the mathjs sparse LU called mathjs's generic divide, multiply and subtract functions for every entry. The bundles now use plain arithmetic there, with the same solution bit for bit:-
In Node.js and Chromium, every solve after the first in a process, including every Newton-Raphson iteration after the first, ran 2 to 3 times slower than the first, because V8 stopped inlining those functions. Every solve now runs at the speed of the first.
-
In Firefox, every solve was slow. It is now 3 to 12 times faster.
-
On one core of an AMD Ryzen 5 9600X:
Case Before After Lid-driven cavity 64×64, repeated solve in Node.js 26.4 s 8.7 s Lid-driven cavity 64×64, repeated solve in Chromium 21.9 s 7.4 s Lid-driven cavity 64×64, first solve in Firefox 217.6 s 18.1 s Heat conduction, 51,681 nodes, first solve in Firefox 21.0 s 3.9 s Heat conduction, Gmsh quadratic triangles, first solve in Firefox 60.3 s 6.9 s First solves in Node.js and Chromium are unchanged or slightly faster.
-
Loading FEAScript from
src/with mathjs from npm keeps the old speed until mathjs fixes it (josdejong/mathjs#3792)
-
- Contour plot values in quadratic elements with curved edges, such as the elements along the hole of the homepage
fin example (#134).
plotInterpolatedSolutionand the VTK plot located a grid point in a 9-node quadrilateral by inverting the bilinear map of its four corners, and in a 6-node triangle by the barycentric coordinates of its three vertices, then evaluated the quadratic basis functions at that point. Where the edges are curved, the two maps differ, and the plotted values were off by up to 0.25 for a field of range about 10. Quadratic elements are now located by inverting their full isoparametric map with Newton's method; linear elements are unchanged. The solution itself was not affected
0.3.1 - 2026-10-30
- The FEAScript icon at the start of the README showed at full size on npmjs.com, which ignores the
styleattribute; it now has a fixed width and height
0.3.0 - 2026-10-30
- Triangular elements (linear and quadratic), including triangular meshes imported from Gmsh (#97)
- Euler–Bernoulli beam model (
eulerBernoulliBeamScript) with cubic Hermite elements and fixed, pinned, spring, force and moment conditions (#86) - Viscosity in the creeping flow model:
coefficientFunctions: { viscosity }, a number or a function of (x, y) (#102) - Named result fields for models with several unknowns per node:
solve()returnsfields(velocityMagnitude,u,v,pfor creeping flow;w,thetafor the beam), and the plotting functions take a{ field }option (#106) - Nonlinear 1D problems with the Newton–Raphson method in the general form PDE (#90)
- Spatially varying coefficients:
thermalConductivityandheatSourceas functions of position (#92) - 1D front propagation
- Frontal solver for every model that supports it, including the linear general form PDE (#100)
- vtk.js visualization (
plotSolutionVtk,plotInterpolatedSolutionVtk) and VTK/VTP export (#73, #79) FEAScriptWorkertakes the same model options asFEAScriptModel, so every model can run in a web worker (#107)- Gmsh import detects the mesh dimension and element order
- Input validation for solvers and boundary conditions (#83)
- Regression, analytical and benchmark tests (including NAFEMS T4), run with
npm teston Node.js 18 and later - Node.js examples for the NAFEMS T4 benchmark, Couette flow and a cantilever beam
- Sparse matrix format during assembly (#99)
- BLAS routines from stdlib (#81)
- Linear elements use a 2-point Gauss rule per direction (2 × 2 on quadrilaterals) instead of 1 point, so linear quadrilaterals no longer under-integrate the stiffness matrix; results on linear elements change slightly (#105)
- Every solver returns the solution vector as a
Float64Array - The CommonJS build is
dist/feascript.cjs(wasdist/feascript.cjs.js), sorequire("feascript")works (#112) - Source files are reorganized and lose the
Scriptsuffix (#80, #94)
- The
"constantTemp"boundary condition; use"constantTemperature"
setSolverConfig; usesetModelConfig(#73)importGmshQuadTri; useimportGmshMesh(#80)- The private
_creepingFlowMetadataand_eulerBernoulliBeamMetadataproperties; useresult.fields(#106)
- 1D convection and nonzero front values
- Non-contiguous node tags in the Gmsh reader (#96)
0.2.0 - 2026-02-25
- Creeping (Stokes) flow model (
creepingFlowScript) with Taylor–Hood elements (#63) - WebGPU Jacobi solver (#53)
- Arbitrary geometries from Gmsh meshes (#58, #59)
- Skewed structured 2D meshes (
angleLeft,angleRight) (#60) plotInterpolatedSolution
- Solvers consolidated behind a single interface (#56)
setModelConfigreplacessetSolverConfig, which is deprecated
0.1.3 - 2025-10-08
- General form PDE model (
generalFormPDEScript) (#48) - Frontal solver (#40, #43)
- 1D PDE solver (#42)
printVersion(#45)- Automated tests on pull requests (#44)
solidHeatTransferScriptis renamed toheatConductionScript(#47)
- Signs of the advection and reaction terms in the general form PDE
0.1.2 - 2025-07-31
- Front propagation model (
frontPropagationScript) for the eikonal equation (#29) - Solidification front example (#30)
0.1.1 - 2025-05-22
- First build published to npm (#25)
0.1.0 - 2025-05-15
- Initial release: steady heat conduction in 1D and 2D (
solidHeatTransferScript), structured meshes and Gmsh import, Jacobi and LU solvers, Plotly plotting, and a web worker (FEAScriptWorker)