# 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 `