# FEAScript
> FEAScript is a finite element simulation library written in JavaScript (ES modules). It runs in the browser and in Node.js. This file is a compact, exact reference of the FEAScript 0.4.0 API for AI assistants. Use only the functions, options and string values listed here: anything not listed is not supported.
Full documentation: https://docs.feascript.com
Source code: https://github.com/FEAScript/FEAScript-core
## Import
```javascript
// Browser, as an ES module (no install)
import { FEAScriptModel } from "https://core.feascript.com/dist/feascript.esm.js";
// Node.js: npm install feascript, and set "type": "module" in package.json
import { FEAScriptModel } from "feascript";
```
```html
```
Exports: `FEAScriptModel`, `FEAScriptWorker`, `FEAScriptError`, `importGmshMesh`, `generateTriangleMesh`, `generateQuadrilateralMesh`, `logSystem`, `version` (a string constant, e.g. `"0.4.0"`), `plotSolution`, `plotInterpolatedSolution`, `plotSolutionVtk`, `plotInterpolatedSolutionVtk`, `createColorScale`, `createContourLineOptions`, `transformSolverOutputToVtkData`, `transformSolverOutputToVTP`, `transformSolverOutputToMLBuffers`.
## Workflow
Every simulation follows the same steps:
```javascript
// 1. Create the model, select the physics and the solver
const model = new FEAScriptModel();
model.setModelConfig("heatConduction", {
solver: { method: "lusolve" }, // Optional, "lusolve" is the default
coefficientFunctions: { thermalConductivity: 1, heatSource: 0 },
});
// 2. Define the domain and how to mesh it
model.setGeometry({ rectangle: [4, 2] }); // 0 ≤ x ≤ 4, 0 ≤ y ≤ 2
model.setMeshConfig({ elements: "quadrilaterals", elementOrder: "quadratic", divisions: [8, 4] });
// 3. Apply the boundary conditions, keyed by the boundary names
model.addBoundaryCondition("bottom", { type: "constantTemperature", temperature: 200 });
model.addBoundaryCondition("left", { type: "symmetry" });
model.addBoundaryCondition("top", { type: "convection", h: 1, ambientTemperature: 20 });
model.addBoundaryCondition("right", { type: "constantTemperature", temperature: 200 });
// 4. Solve
const result = model.solve();
// result.solutionVector: Float64Array, one value per node for the scalar models
// result.nodesCoordinates: { nodesXCoordinates: number[], nodesYCoordinates: number[] }
// result.fields: { name: Float64Array } per node, only for creeping flow and the beam (see below)
// result.info: the run report (see below)
```
`solve()` is synchronous. It accepts optional `{ maxIterations, tolerance }`, which take precedence over the same options of `setModelConfig` for that solve.
### Run report (`result.info`)
`solve()`, `solveAsync()` and `FEAScriptWorker.solve()` return `result.info`, with the same properties for every model and solver method:
| Property | Meaning |
| ------------ | --------------------------------------------------------------------------------------------------------- |
| `method` | The linear solver method used, e.g. `"lusolve"` |
| `converged` | Always `true`: a solve that does not converge throws `"NOT_CONVERGED"` instead of returning |
| `iterations` | Iterations of `"jacobi"` or `"jacobi-gpu"`, summed over Newton–Raphson; `null` for `"lusolve"` and `"frontal"` |
| `residual` | ‖R − J u‖₂ / ‖R‖₂ of the linear system, in 64-bit floats; `null` for `"frontal"` and for nonlinear models |
| `newton` | `{ iterations, increment }` for the nonlinear models (general form with `nonlinear: true`, front propagation); `null` otherwise |
| `solveTime` | Time of the whole solve in milliseconds, from the mesh to the solution |
```javascript
const { solutionVector, info } = model.solve();
console.log(`${info.method}: ${info.iterations ?? "direct"} iterations, residual ${info.residual}, ${info.solveTime} ms`);
```
### TypeScript
The npm package ships TypeScript declarations (`dist/types/`, named by `types` in `package.json`), so TypeScript projects are type-checked and editors complete model names, solver methods, geometry and mesh settings, and boundary condition types with their parameters. The input and result types are exported for `import type`:
```typescript
import { FEAScriptModel } from "feascript";
import type { Geometry, MeshConfig, BoundaryCondition, SolveResult, RunReport } from "feascript";
```
## Models
| `setModelConfig` name | Equation | Mesh |
| ---------------------- | ----------------------------------------------------- | ---------------------------------------- |
| `"heatConduction"` | −∇·(k ∇T) = Q | 1D or 2D, linear or quadratic |
| `"generalFormPDE"` | d/dx(A du/dx) + B du/dx + C u = D | 1D only |
| `"frontPropagation"` | Eikonal equation \|∇u\| = 1 (vanishing viscosity) | 1D or 2D |
| `"creepingFlow"` | Stokes flow: −μ∇²u + ∇p = 0, ∇·u = 0 | 2D, quadratic only |
| `"eulerBernoulliBeam"` | d²/dx²(EI d²w/dx²) + c0 w = q | 1D, `elementOrder: "linear"` only |
`setModelConfig(name, options)` options: `coefficientFunctions` (per model, below), `nonlinear` (general form only), `solver: { method, maxIterations, tolerance }` (see Solvers), and top-level `maxIterations` and `tolerance` (Newton–Raphson, or the linear solver when `solver` does not set them). An unknown solver method or option throws `FEAScriptError` `"INVALID_INPUT"` at once; an unknown model name, when `solve()` runs.
### heatConduction
- Coefficients: `coefficientFunctions: { thermalConductivity, heatSource }`. Each is a number or a function `(x) => ...` in 1D, `(x, y) => ...` in 2D. Defaults: `thermalConductivity = 1`, `heatSource = 0`. `coefficientFunctions` can be omitted.
- Boundary conditions:
- `{ type: "constantTemperature", temperature }`: Dirichlet, T = temperature.
- `{ type: "convection", h, ambientTemperature }`: Robin, −k ∂T/∂n = h (T − T∞). `h` and `ambientTemperature` are numbers.
- `{ type: "symmetry" }`: zero heat flux (also the condition of a boundary without one).
- Give at least one boundary a `"constantTemperature"` or `"convection"` condition. With zero flux on every boundary the temperature is not determined: the solve does not throw, but returns meaningless values.
- Solvers: `"lusolve"`, `"jacobi"`, `"frontal"`, and `"jacobi-gpu"` (with `await model.solveAsync()` only).
- Result: one temperature per node in `solutionVector`.
### generalFormPDE (1D only)
- Coefficients: `coefficientFunctions: { A, B, C, D }`. All four are required and must be **functions** of `x`, even when constant: `A: (x) => 1`, never `A: 1`.
- Nonlinear form: add `nonlinear: true`, make `D` a function `(x, u) => ...`, and add `dDdu: (x, u) => ...` (∂D/∂u, required). It is solved with Newton–Raphson.
- Boundary conditions: `{ type: "constantValue", value }` (Dirichlet), `{ type: "zeroGradient" }` (A du/dx = 0). Boundaries: `"left"` (x = 0) and `"right"` (x = length).
- Solvers: `"lusolve"`, `"jacobi"`, `"frontal"` (linear and nonlinear form).
- Result: one value per node in `solutionVector`.
```javascript
model.setModelConfig("generalFormPDE", {
nonlinear: true,
coefficientFunctions: {
A: (x) => 1,
B: (x) => 0,
C: (x) => 0,
D: (x, u) => u ** 2,
dDdu: (x, u) => 2 * u,
},
});
model.setGeometry({ line: 10 });
model.setMeshConfig({ elementOrder: "linear", divisions: 20 });
model.addBoundaryCondition("left", { type: "constantValue", value: 1 });
model.addBoundaryCondition("right", { type: "zeroGradient" });
const result = model.solve({ maxIterations: 100, tolerance: 1e-5 });
```
### frontPropagation
- No coefficient functions: the front speed is 1 everywhere. The solution u is the arrival time of the front, which equals the distance from the initial front.
- Boundary conditions: `{ type: "constantValue", value }` marks a boundary where the front is at time `value` (use `0` for the initial front); `{ type: "zeroGradient" }`.
- It is solved with Newton–Raphson in 6 steps that activate the eikonal term gradually. A step that does not converge throws `FEAScriptError` `"NOT_CONVERGED"`; refine the mesh.
- Solvers: `"lusolve"`, `"jacobi"`, `"frontal"`.
- Result: one value per node in `solutionVector`.
### creepingFlow (2D, quadratic only)
- Coefficients: `coefficientFunctions: { viscosity }`, the dynamic viscosity μ, a positive number or a function `(x, y) => ...`. Default `viscosity = 1`; `coefficientFunctions` can be omitted.
- Mesh: `elementOrder: "quadratic"` (Taylor–Hood Q2–Q1 on quadrilaterals, P2–P1 on triangles).
- Boundary conditions: `{ type: "constantVelocity", u, v }` (Dirichlet; `u: 0, v: 0` for a no-slip wall), `{ type: "stressFree" }` (natural outflow). If no boundary is `"stressFree"`, the pressure is set to 0 at the first pressure node, so mark open boundaries explicitly with `"stressFree"`.
- Solvers: `"lusolve"` only.
- Result: `result.fields` = `{ velocityMagnitude, u, v, p }`, each a Float64Array with one value per mesh node (the pressure, solved at the corner nodes, is interpolated linearly to the other nodes). Do not slice `solutionVector` (raw layout `[u_1..u_N, v_1..v_N, p_1..p_Np]`).
```javascript
const result = model.solve();
plotSolution(model, result, "contour", "velocityMagnitudeCanvas"); // Default field: velocityMagnitude
plotSolution(model, result, "contour", "uVelocityCanvas", { field: "u" });
plotSolution(model, result, "contour", "pressureCanvas", { field: "p" });
```
### eulerBernoulliBeam (1D)
- Coefficients: `coefficientFunctions: { EI, c0, q }`. All are **functions** of `x`. `EI` is required; `c0` (elastic foundation) and `q` (distributed load) default to `(x) => 0`.
- Mesh: `setGeometry({ line: length })` and `elementOrder: "linear"` (cubic Hermite interpolation is applied internally).
- Boundary conditions are keyed by the **1-based node number** as a string, not by a boundary name: with `divisions: n` on a line of length L, node `"j"` is at x = (j − 1)·L/n, from `"1"` at x = 0 to `"n+1"` at x = L. Call `setModelConfig` before adding them. Each node takes one condition object or a **list** of them:
- `{ type: "fixed" }`: w = 0 and θ = 0
- `{ type: "pinned" }` or `{ type: "deflection", deflection }`: prescribed deflection (default 0)
- `{ type: "rotationFixed" }` or `{ type: "rotation", rotation }`: prescribed rotation (default 0)
- `{ type: "force", force }`: point force; `{ type: "moment", moment }`: point moment
- `{ type: "spring", stiffness, referenceDeflection }`: transverse spring (`referenceDeflection` defaults to 0)
- Several conditions at one node: `[{ type: "pinned" }, { type: "moment", moment: 1250 }]`
- Sign convention: w, q and force are positive in the same direction; θ = dw/dx.
- Solvers: `"lusolve"` (`"jacobi"` usually throws `"NOT_CONVERGED"`; `"frontal"` is not supported).
- Result: `result.fields` = `{ w, theta }`, each a Float64Array with one value per node. Do not filter `solutionVector` (raw layout `[w_1, θ_1, w_2, θ_2, ...]`).
```javascript
model.setModelConfig("eulerBernoulliBeam", {
coefficientFunctions: { EI: (x) => 2.0e6, q: (x) => (x <= 5 ? -1000 : 0) },
});
model.setGeometry({ line: 10 });
model.setMeshConfig({ elementOrder: "linear", divisions: 2 }); // Nodes "1", "2", "3" at x = 0, 5, 10
model.addBoundaryCondition("1", { type: "fixed" });
model.addBoundaryCondition("2", [{ type: "pinned" }, { type: "moment", moment: 1250 }]);
model.addBoundaryCondition("3", [{ type: "spring", stiffness: 200 }, { type: "force", force: -2500 }]);
const result = model.solve();
plotSolution(model, result, "line", "deflectionCanvas"); // Default field: w
plotSolution(model, result, "line", "rotationCanvas", { field: "theta" });
const tipDeflection = result.fields.w.at(-1);
```
## Geometry and mesh
A domain is set up in two steps: `setGeometry(geometry)` describes it and names its boundaries, and `setMeshConfig(meshConfig)` says how to mesh it. The mesh is generated at the first solve (or by `model.getMesh()`). A ready mesh is given with `setMesh(mesh)` instead.
### Geometry (`setGeometry`)
| Geometry | Domain | Boundary names |
| --------------------------------- | --------------------------------------- | ------------------------------------------- |
| `{ line: length }` | 0 ≤ x ≤ length (1D) | `"left"`, `"right"` |
| `{ rectangle: [width, height] }` | 0 ≤ x ≤ width, 0 ≤ y ≤ height | `"bottom"`, `"right"`, `"top"`, `"left"` |
| `{ shape, holes, points }` | Any polygon, with polygonal holes | The names given in the shape and the holes |
The shape is a **path**: a `start` point and its boundaries, each running from where the previous one ended to its `to` point. The last boundary may leave out `to`: it then closes the shape back to `start`. Boundaries with the same name form one boundary. A `to` may also be a list of points (a polyline).
```javascript
// An L-shaped domain: a 2 by 2 square without its top right quarter
model.setGeometry({
shape: {
start: [0, 0],
boundaries: [
{ name: "bottom", to: [2, 0] },
{ name: "right", to: [2, 1] },
{ name: "step", to: [1, 1] },
{ name: "step", to: [1, 2] }, // Same name: one boundary of two segments
{ name: "top", to: [0, 2] },
{ name: "left" }, // No "to": closes the shape back to [0, 0]
],
},
});
model.setMeshConfig({ elements: "triangles", elementOrder: "quadratic", size: 0.1 });
```
- A polygon whose edges all have one name is `{ name, points: [[x, y], ...] }`, the usual form of a hole. A plain list of points is named `"boundary"` (a shape) or `"hole"` (a hole).
- `holes`: polygons inside the shape. They may also be added to a rectangle: `{ rectangle: [2, 1], holes: [...] }`.
- `points`: `[[x, y], ...]` inside the domain that must become nodes (triangles only).
- Curves (circles, arcs) are written as polygons with many points:
```javascript
const circle = (cx, cy, r, n) =>
Array.from({ length: n }, (_, i) => [cx + r * Math.cos((2 * Math.PI * i) / n), cy + r * Math.sin((2 * Math.PI * i) / n)]);
model.setGeometry({ rectangle: [2, 1], holes: [{ name: "hole", points: circle(1, 0.5, 0.2, 32) }] });
model.setMeshConfig({ elements: "triangles", elementOrder: "quadratic", size: 0.05 });
```
A polygon whose edges cross, a hole outside the shape or inside another hole, or a point outside the domain throws `FEAScriptError` `"INVALID_INPUT"` when `setGeometry` is called, listing every problem.
### Mesh configuration (`setMeshConfig`)
| Property | Meaning |
| -------------- | ---------------------------------------------------------------------------------------------------------------- |
| `elements` | `"quadrilaterals"` (default; a rectangle or a convex shape of exactly four points, no holes) or `"triangles"` (any shape, with holes). Ignored for a line |
| `elementOrder` | `"linear"` (default) or `"quadratic"` |
| `size` | The length of an element side; for triangles also a function `(x, y) => ...` for a graded mesh |
| `divisions` | Instead of `size`: the number of elements of a line, or `[nx, ny]` of a quadrilateral mesh |
| `minAngle` | Triangles: the smallest element angle in degrees (default 20; keep it at most about 33) |
| `maxNodes` | Triangles: vertex limit (default 100000); passing it throws `"MESH_TOO_FINE"` |
Node order: a quadrilateral mesh is numbered column by column (index = i·ny + j, with ny = nodes per column); a line from left to right; a triangle mesh in generation order. `solutionVector` follows `nodesCoordinates`, so always use the coordinate arrays rather than recomputing indices.
### Ready meshes (`setMesh`)
```javascript
import { generateTriangleMesh, generateQuadrilateralMesh, importGmshMesh } from "feascript";
// Generate a mesh to inspect or reuse it before solving: same geometry forms as setGeometry
const triMesh = generateTriangleMesh({ rectangle: [2, 1] }, { size: 0.1, minAngle: 25, elementOrder: "quadratic" });
const quadMesh = generateQuadrilateralMesh({ rectangle: [4, 2] }, { divisions: [8, 4], elementOrder: "quadratic" });
// Import a Gmsh mesh. Browser: a File (e.g. from )
const gmshMesh = await importGmshMesh(file);
// Node.js: any object with an async text() method
const gmshMesh2 = await importGmshMesh({ text: async () => fs.readFileSync("mesh.msh", "utf8") });
model.setMesh(triMesh); // The geometry and mesh configuration are then not used
```
- Gmsh: `.msh` version 4.1 ASCII (`gmsh model.geo -2 -format msh41 -o model.msh`), 2D only, all quadrilaterals or all triangles, all linear or all quadratic. Element dimension and order are detected from the file.
- The boundaries of a Gmsh mesh are named after its 1D physical groups, e.g. `Physical Line("top") = {3};` gives the boundary `"top"`. Add a `Physical Surface` too, or Gmsh saves no 2D elements.
- The generators return a mesh object: `nodesXCoordinates`, `nodesYCoordinates`, `nodalNumbering` (node numbers of each element, from 1), `boundaryElements` (keyed by name), `meshDimension`, `elementOrder` and `elementShape`. `importGmshMesh` returns the mesh as read from the file (elements in `nodalNumbering.quadElements` or `.triangleElements`), which `setMesh` converts; read the converted mesh with `model.getMesh()` after `setMesh`.
## Boundary condition rules
- The condition is an **object** with a `type` and named parameters: `{ type: "zeroGradient" }`, `{ type: "convection", h: 1, ambientTemperature: 20 }`.
- The key is a boundary name of the geometry or the mesh (the beam: a node number).
- One condition per boundary; adding again replaces it. A boundary without a condition gets the natural condition (zero flux or zero gradient).
- A missing or unknown `type` or parameter throws `FEAScriptError` `"INVALID_INPUT"` when the condition is added. A type that the model does not have, a value that is not a number, or a boundary that is not in the mesh throws `"INVALID_INPUT"` when `solve()` runs, listing every problem at once.
- At a node shared by two boundaries, a Dirichlet condition takes precedence over a Robin or natural one. Between two Dirichlet conditions with different values, the one added last sets the value. In `creepingFlow`, a zero `constantVelocity` (no slip) always wins over a nonzero one, so the corners of a moving lid get zero velocity.
## Solvers
| `solver.method` | Method | Notes |
| --------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `"lusolve"` | Sparse LU (math.js), default | Works for every model |
| `"jacobi"` | Jacobi iteration (CPU) | Needs diagonally dominant matrices (linear elements); defaults `maxIterations: 10000`, `tolerance: 1e-8` on the residual relative to the right-hand side |
| `"frontal"` | Frontal solver (direct) | Heat conduction, general form (linear and nonlinear), front propagation; less memory than `"lusolve"` on large meshes and the faster choice in Firefox |
| `"jacobi-gpu"` | Jacobi on WebGPU, single precision | Heat conduction only, browser with WebGPU, `await model.solveAsync()`; default `tolerance: 1e-4` |
```javascript
model.setModelConfig("heatConduction", { solver: { method: "jacobi", maxIterations: 20000, tolerance: 1e-10 } });
```
- An unsupported model and method combination throws `FEAScriptError` `"UNSUPPORTED_SOLVER"`.
- Newton–Raphson (nonlinear general form, front propagation): defaults `maxIterations: 100`, `tolerance: 1e-4` on the norm of the update, set with the top-level `maxIterations` and `tolerance` of `setModelConfig` or `solve()`.
- `solveAsync(computeEngine, options)` returns a Promise of the same result as `solve()`; it runs only `heatConduction` with `"jacobi-gpu"`. Where WebGPU or a GPU adapter is missing, `"jacobi-gpu"` throws `"UNSUPPORTED_SOLVER"` with the reason, so offer `"lusolve"` or `"frontal"` as the fallback.
## Visualization (browser)
- Plotly: load `plotly.min.js` with a `