Rename the project to FieldCraft

Crates fieldcraft-*, binaries fieldcraft and fieldcraft-cli, the .fieldcraft file (format tag
fieldcraft/1, manifest fieldcraft.json), FIELDCRAFT_* environment variables, the MCP server
name, egui widget ids (fc_*), the app icon document FieldCraft.icon and its generated family,
docs and licences. No compatibility shim: no files of the earlier name exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-13 23:51:40 -04:00
1 parent 9bc4e1b182
commit d5bd74570f
268 files changed
+1666 -1666

No files matched your search

+2 -2
View File
@@ -28,7 +28,7 @@
//!
//! Node orders are corners first, then mid-side nodes in the model's edge order (the model is the
//! one place they are written down), then face and body nodes, the Abaqus/VTK convention of
//! [`modelcraft_model::ElementKind`]. The functions are the textbook ones (O. C. Zienkiewicz,
//! [`fieldcraft_model::ElementKind`]. The functions are the textbook ones (O. C. Zienkiewicz,
//! R. L. Taylor, J. Z. Zhu, *The Finite Element Method: Its Basis and Fundamentals*, 7th ed.,
//! 2013, ch. 6; T. J. R. Hughes, *The Finite Element Method*, Dover 2000, ch. 3).
//!
@@ -49,7 +49,7 @@
use std::sync::LazyLock;
use modelcraft_model::ElementKind;
use fieldcraft_model::ElementKind;
use crate::dense::positive_inverse;
use crate::jet::Jet;
+2 -2
View File
@@ -39,7 +39,7 @@
//! webs or the two flanges of a box) (W. D. Pilkey, *Analysis and Design of Elastic Beams*,
//! Wiley 2002, ch. 6).
use modelcraft_model::{BeamProfile, Elastic};
use fieldcraft_model::{BeamProfile, Elastic};
use crate::dense::Matrix;
use crate::dof::{DofSet, ElementDofs};
@@ -211,7 +211,7 @@ fn line_axis(xyz: &[[f64; 3]]) -> Result<(Vec3, f64)> {
let [a, b] = xyz else {
return Err(Error::Invalid(format!("a two-node line element has 2 nodes, got {}", xyz.len())));
};
modelcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
fieldcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
let d = sub(*b, *a);
let length = norm(d);
let ex = unit(d).ok_or_else(|| Error::Degenerate("a line element has zero length".into()))?;
+7 -7
View File
@@ -32,7 +32,7 @@
//! discontinuous finite element functions using a least squares method*, IJNME 8(3):461–480,
//! 1974); mid-side nodes take the mean of their edge's corners.
use modelcraft_model::ElementKind;
use fieldcraft_model::ElementKind;
use crate::basis::Basis;
use crate::dense::{Cholesky, Matrix, inverse3, positive_inverse};
@@ -163,10 +163,10 @@ impl Formulation {
}
/// The formulation a model section asks for, if this element type has it.
pub fn from_section(kind: ElementKind, f: &modelcraft_model::Formulation) -> Result<Formulation> {
pub fn from_section(kind: ElementKind, f: &fieldcraft_model::Formulation) -> Result<Formulation> {
let ty = element_type(kind)?;
if f.hybrid {
if f.incompatible_modes || f.integration == modelcraft_model::Integration::Reduced {
if f.incompatible_modes || f.integration == fieldcraft_model::Integration::Reduced {
return Err(Error::NotSupported("hybrid elements with incompatible modes or reduced integration".into()));
}
return crate::hybrid::pressures(kind).map(|pressures| Formulation::Hybrid { pressures }).ok_or_else(|| {
@@ -184,9 +184,9 @@ impl Formulation {
};
}
match f.integration {
modelcraft_model::Integration::Full => Ok(Formulation::Full),
modelcraft_model::Integration::Reduced if ty.reduced.is_some() => Ok(Formulation::Reduced),
modelcraft_model::Integration::Reduced => Err(Error::NotSupported(format!(
fieldcraft_model::Integration::Full => Ok(Formulation::Full),
fieldcraft_model::Integration::Reduced if ty.reduced.is_some() => Ok(Formulation::Reduced),
fieldcraft_model::Integration::Reduced => Err(Error::NotSupported(format!(
"reduced integration of {} elements (hex8 needs hourglass control; use full integration or incompatible modes)",
kind.name()
))),
@@ -254,7 +254,7 @@ impl<'a> Solid<'a> {
if xyz.len() != ty.nodes {
return Err(Error::Invalid(format!("a {} element has {} nodes, got {}", kind.name(), ty.nodes, xyz.len())));
}
modelcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
fieldcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
let ok = match formulation {
Formulation::Full => true,
Formulation::Reduced => ty.reduced.is_some(),
+3 -3
View File
@@ -2,7 +2,7 @@
//!
//! A pressure p on a face gives node i the force fᵢ = −p ∫ Nᵢ n dA, where n dA =
//! (∂x/∂ξ × ∂x/∂η) dξ dη on the face's own, possibly curved, geometry; faces are numbered so this
//! normal points out of the element ([`modelcraft_model::ElementKind::faces`]), and a positive
//! normal points out of the element ([`fieldcraft_model::ElementKind::faces`]), and a positive
//! pressure pushes in. This is the consistent load vector of the virtual work of the surface load
//! (Zienkiewicz, Taylor & Zhu 2013, §6.6), integrated with the model's face rules (exact for a
//! uniform pressure on a flat face).
@@ -10,10 +10,10 @@
use crate::{Error, Result};
/// ∫ Nᵢ n dA for every node of a face (node coordinates in the face's node order): the model's
/// face integration ([`modelcraft_model::nodal::face_normal_weights`]), so pressures and the
/// face integration ([`fieldcraft_model::nodal::face_normal_weights`]), so pressures and the
/// tractions the model distributes use the same rule.
pub fn normal_weights(xyz: &[[f64; 3]]) -> Result<Vec<[f64; 3]>> {
modelcraft_model::nodal::face_normal_weights(xyz).map_err(|e| Error::Invalid(e.to_string()))
fieldcraft_model::nodal::face_normal_weights(xyz).map_err(|e| Error::Invalid(e.to_string()))
}
/// Consistent nodal forces of a uniform pressure (positive pushes into the element).
+4 -4
View File
@@ -14,7 +14,7 @@
//! Faces are integrated on their own (possibly curved) geometry with the model's face rule
//! (the one pressures use), exact for N Nᵀ on flat faces.
use modelcraft_model::ElementKind;
use fieldcraft_model::ElementKind;
use crate::basis::Basis;
use crate::dense::Matrix;
@@ -47,7 +47,7 @@ impl<'a> Conduction<'a> {
if xyz.len() != ty.nodes {
return Err(Error::Invalid(format!("a {} element has {} nodes, got {}", kind.name(), ty.nodes, xyz.len())));
}
modelcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
fieldcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
Ok(Conduction { ty, basis: Basis::of(kind), xyz })
}
@@ -119,11 +119,11 @@ impl<'a> Conduction<'a> {
}
/// Visit the points of a face (node coordinates in the face's node order): shape values and
/// the area weight, with the model's face rule ([`modelcraft_model::nodal::integrate_face`], the
/// the area weight, with the model's face rule ([`fieldcraft_model::nodal::integrate_face`], the
/// one pressures and tractions use).
fn each_face_point(xyz: &[[f64; 3]], mut visit: impl FnMut(&[f64], f64)) -> Result<()> {
let mut collapsed = false;
modelcraft_model::nodal::integrate_face(xyz, |n, nda| {
fieldcraft_model::nodal::integrate_face(xyz, |n, nda| {
let area = (nda[0] * nda[0] + nda[1] * nda[1] + nda[2] * nda[2]).sqrt();
collapsed |= !(area > 0.0 && area.is_finite());
visit(n, area);
+1 -1
View File
@@ -36,7 +36,7 @@
//! symmetric indefinite saddle point that the Cholesky factorization cannot solve; it waits for
//! an indefinite (LDLᵀ) factorization.
use modelcraft_model::ElementKind;
use fieldcraft_model::ElementKind;
use crate::dense::{Cholesky, Matrix};
use crate::element::{Formulation, Solid, ThermalStrain, accumulate_symmetric, b_block};
+2 -2
View File
@@ -1,7 +1,7 @@
//! Finite element kernels shared by ModelCraft's structural solvers.
//! Finite element kernels shared by FieldCraft's structural solvers.
//!
//! Pure functions over element geometry and material constants, with no solver loop and no
//! global state, so the implicit solver (`modelcraft-solver`) and the explicit solver that follows
//! global state, so the implicit solver (`fieldcraft-solver`) and the explicit solver that follows
//! it use the same element technology:
//! - [`dof`]: degree-of-freedom kinds, what elements declare, and the global numbering
//! ([`DofLayout`]) every solver assembles on;
+1 -1
View File
@@ -10,7 +10,7 @@
//! S₁₁ = 1/E₁, S₁₂ = −ν₁₂/E₁, S₁₃ = −ν₁₃/E₁, S₂₃ = −ν₂₃/E₂ (ν_ji/E_j = ν_ij/E_i), and
//! 1/G on the shear diagonal.
use modelcraft_model::Elastic;
use fieldcraft_model::Elastic;
use crate::dense::inverse3;
use crate::{Error, Result};
+8 -8
View File
@@ -39,7 +39,7 @@
//! inside the element (r > 0) for every element with some node off the axis; the solver holds
//! the radial displacement of nodes on the axis at zero, as symmetry requires.
use modelcraft_model::{ElementKind, Extent, SectionKind};
use fieldcraft_model::{ElementKind, Extent, SectionKind};
use crate::basis::Basis;
use crate::dense::{Cholesky, Matrix};
@@ -166,7 +166,7 @@ pub fn plane_type(kind: ElementKind) -> Result<&'static ElementType> {
}
/// The formulation a 2D section asks for, if this element type has it.
pub fn formulation(kind: ElementKind, f: &modelcraft_model::Formulation) -> Result<Formulation> {
pub fn formulation(kind: ElementKind, f: &fieldcraft_model::Formulation) -> Result<Formulation> {
let ty = plane_type(kind)?;
if f.hybrid {
return Err(Error::NotSupported("hybrid (mixed displacement–pressure) 2D elements".into()));
@@ -179,9 +179,9 @@ pub fn formulation(kind: ElementKind, f: &modelcraft_model::Formulation) -> Resu
};
}
match f.integration {
modelcraft_model::Integration::Full => Ok(Formulation::Full),
modelcraft_model::Integration::Reduced if ty.reduced.is_some() => Ok(Formulation::Reduced),
modelcraft_model::Integration::Reduced => Err(Error::NotSupported(format!(
fieldcraft_model::Integration::Full => Ok(Formulation::Full),
fieldcraft_model::Integration::Reduced if ty.reduced.is_some() => Ok(Formulation::Reduced),
fieldcraft_model::Integration::Reduced => Err(Error::NotSupported(format!(
"reduced integration of 2D {} elements (quad4 needs hourglass control; use full integration or incompatible modes)",
kind.name()
))),
@@ -254,7 +254,7 @@ impl<'a> Plane<'a> {
if xyz.len() != ty.nodes {
return Err(Error::Invalid(format!("a {} element has {} nodes, got {}", kind.name(), ty.nodes, xyz.len())));
}
modelcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
fieldcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
if xyz.iter().any(|p| p[2] != 0.0) {
return Err(Error::Invalid(format!("a 2D {} element must lie in the XY plane (z = 0)", kind.name())));
}
@@ -760,7 +760,7 @@ mod tests {
let f = e.body_force_field(|x| [x[0], 0.0, 0.0]).unwrap();
assert!((f.iter().map(|v| v[0]).sum::<f64>() - 2.0 * pi * 7.0 / 3.0).abs() < 1e-12);
// Pressure on the outer edge (x = 2) of the ring pushes inward with p · 2π · 2 · 1.
let fp = modelcraft_model::nodal::edge_pressure(&[sq[1], sq[2]], Theory::Axisymmetric.extent(), 1.0).unwrap();
let fp = fieldcraft_model::nodal::edge_pressure(&[sq[1], sq[2]], Theory::Axisymmetric.extent(), 1.0).unwrap();
assert!((fp.iter().map(|v| v[0]).sum::<f64>() + 4.0 * pi).abs() < 1e-12, "{fp:?}");
}
@@ -783,6 +783,6 @@ mod tests {
let on_axis = sq.map(|p| [p[0] - 1.0, p[1], 0.0]);
let e = Plane::new(ElementKind::Quad4, &on_axis, Formulation::IncompatibleModes, Theory::Axisymmetric).unwrap();
assert!(e.stiffness(&d).is_ok());
assert!(formulation(ElementKind::Quad4, &modelcraft_model::Formulation { hybrid: true, ..Default::default() }).is_err());
assert!(formulation(ElementKind::Quad4, &fieldcraft_model::Formulation { hybrid: true, ..Default::default() }).is_err());
}
}
+2 -2
View File
@@ -36,7 +36,7 @@
//! Stress 6-vectors hold tensor components (xx, yy, zz, xy, xz, yz), strain increments
//! engineering shears.
use modelcraft_model::{Damage, Elastic, Hardening, Plastic};
use fieldcraft_model::{Damage, Elastic, Hardening, Plastic};
use crate::material::von_mises;
use crate::{Error, Result};
@@ -328,7 +328,7 @@ impl J2 {
#[cfg(test)]
mod tests {
use super::*;
use modelcraft_model::HardeningPoint;
use fieldcraft_model::HardeningPoint;
const E: f64 = 200_000.0;
const NU: f64 = 0.3;
+3 -3
View File
@@ -1,6 +1,6 @@
//! Shape functions and their derivatives in natural coordinates.
//!
//! Node order is the model's (Abaqus/VTK, [`modelcraft_model::ElementKind`]):
//! Node order is the model's (Abaqus/VTK, [`fieldcraft_model::ElementKind`]):
//! - tetrahedra on the reference element with corners (0,0,0), (1,0,0), (0,1,0), (0,0,1) and the
//! volume coordinates L₁ = 1 − ξ − η − ζ, L₂ = ξ, L₃ = η, L₄ = ζ; tet10 mid-side nodes on edges
//! 1-2, 2-3, 3-1, 1-4, 2-4, 3-4 with N = 4 Lₐ L_b, corners N = L(2L − 1);
@@ -9,7 +9,7 @@
//! N = ⅛(1 + ξξᵢ)(1 + ηηᵢ)(1 + ζζᵢ)(ξξᵢ + ηηᵢ + ζζᵢ − 2), a mid-side node with ξᵢ = 0
//! N = ¼(1 − ξ²)(1 + ηηᵢ)(1 + ζζᵢ), and likewise), mid-side nodes 9–12 on the bottom edges,
//! 13–16 on the top edges, 17–20 on the vertical edges (mid-side positions come from
//! [`modelcraft_model::ElementKind::edges`], the one place the node order is written down);
//! [`fieldcraft_model::ElementKind::edges`], the one place the node order is written down);
//! - the six-node wedge (pentahedron) as the product of the linear triangle {ξ, η ≥ 0,
//! ξ + η ≤ 1} (area coordinates L₁ = 1 − ξ − η, L₂ = ξ, L₃ = η) and the linear interval
//! ζ ∈ [−1, 1]: nodes 1–3 at ζ = −1 counter-clockwise seen from above, nodes 4–6 above them,
@@ -19,7 +19,7 @@
//! Element Method: Its Basis and Fundamentals*, 7th ed., Butterworth-Heinemann 2013, ch. 6;
//! T. J. R. Hughes, *The Finite Element Method*, Dover 2000, ch. 3).
use modelcraft_model::ElementKind;
use fieldcraft_model::ElementKind;
/// Most nodes an element family has (hex27, [`crate::basis::Basis::Hex27`]).
pub const MAX_NODES: usize = 27;
+2 -2
View File
@@ -57,7 +57,7 @@
//! the nodes (triangle, constant membrane and bending fields). Transverse shear stresses are
//! reported at the mid-surface as the peak of the parabolic distribution, 3/2 Q/t.
use modelcraft_model::{Elastic, ElementKind};
use fieldcraft_model::{Elastic, ElementKind};
use crate::dense::Matrix;
use crate::dof::{DofSet, ElementDofs};
@@ -213,7 +213,7 @@ impl Shell {
if xyz.len() != nodes {
return Err(Error::Invalid(format!("a {} element has {nodes} nodes, got {}", kind.name(), xyz.len())));
}
modelcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
fieldcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
let degenerate = || Error::Degenerate(format!("a {} shell element is degenerate (zero area, inverted or not convex)", kind.name()));
let p = |i: usize| xyz.get(i).copied().unwrap_or_default();
let (normal, along) = if nodes == 3 {
+2 -2
View File
@@ -1,7 +1,7 @@
//! Element-level checks: rigid-body modes, constant-strain reproduction (the single-element form
//! of the patch test), mass and load totals, extrapolation.
use modelcraft_model::ElementKind;
use fieldcraft_model::ElementKind;
use crate::dense::{Cholesky, Matrix};
use crate::element::{Formulation, Solid};
@@ -203,7 +203,7 @@ fn bad_input_is_an_error() {
#[test]
fn section_formulations() {
use modelcraft_model::{Formulation as F, Integration};
use fieldcraft_model::{Formulation as F, Integration};
let full = F::default();
let reduced = F { integration: Integration::Reduced, ..F::default() };
let im = F { incompatible_modes: true, ..F::default() };
+1 -1
View File
@@ -24,7 +24,7 @@
//! stiffness and mass of a straight member, and the usual full integration of a curved one.
//! Material: the Young's modulus of an isotropic material; the truss knows no other constant.
use modelcraft_model::{Elastic, ElementKind};
use fieldcraft_model::{Elastic, ElementKind};
use crate::basis::Basis;
use crate::dense::Matrix;
+4 -4
View File
@@ -28,7 +28,7 @@
//! element frequency satisfies ω² ≤ k λ_max(Σ b bᵀ) / (V min m_I). The bound is exact for a
//! cube (its dilatation mode) and conservative for distorted elements.
use modelcraft_model::Elastic;
use fieldcraft_model::Elastic;
use crate::dense::inverse3;
use crate::material::Elasticity;
@@ -58,7 +58,7 @@ fn degenerate(what: &str) -> Error {
impl UniformStrain<8> {
/// The uniform-strain hexahedron of corners `xyz` (the model's hex8 node order).
pub fn hex8(xyz: &[[f64; 3]; 8]) -> Result<Hex8> {
modelcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
fieldcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
let mut b = [[0.0; 3]; 8];
let mut nodal_volume = [0.0; 8];
let mut volume = 0.0;
@@ -171,7 +171,7 @@ impl UniformStrain<8> {
impl UniformStrain<4> {
/// The linear tetrahedron of corners `xyz` (the model's tet4 node order).
pub fn tet4(xyz: &[[f64; 3]; 4]) -> Result<Tet4> {
modelcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
fieldcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
let [x0, x1, x2, x3] = *xyz;
let e = |x: [f64; 3]| [x[0] - x0[0], x[1] - x0[1], x[2] - x0[2]];
// J[a][c] = ∂x_c/∂ξ_a: the edges from corner 1.
@@ -330,7 +330,7 @@ mod tests {
}
assert!((h.nodal_volume.iter().sum::<f64>() - h.volume).abs() < 1e-14);
// The volume of the distorted hexahedron agrees with the fem element's.
let solid = crate::Solid::new(modelcraft_model::ElementKind::Hex8, &hex, crate::Formulation::Full).unwrap();
let solid = crate::Solid::new(fieldcraft_model::ElementKind::Hex8, &hex, crate::Formulation::Full).unwrap();
assert!((solid.volume().unwrap() - h.volume).abs() < 1e-14);
let mut inverted = hex;
inverted.swap(0, 1);