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

+5 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "modelcraft-explicit"
description = "ModelCraft's explicit dynamics solver: central-difference time integration with lumped mass, one-point hexahedra with hourglass control, nodal-pressure tetrahedra, bulk viscosity and energy accounting"
name = "fieldcraft-explicit"
description = "FieldCraft's explicit dynamics solver: central-difference time integration with lumped mass, one-point hexahedra with hourglass control, nodal-pressure tetrahedra, bulk viscosity and energy accounting"
version.workspace = true
edition.workspace = true
license.workspace = true
@@ -9,8 +9,8 @@ license.workspace = true
workspace = true
[dependencies]
modelcraft-model = { workspace = true }
modelcraft-fem = { workspace = true }
modelcraft-parallel = { workspace = true }
fieldcraft-model = { workspace = true }
fieldcraft-fem = { workspace = true }
fieldcraft-parallel = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true }
+6 -6
View File
@@ -3,7 +3,7 @@
//! - **Trusses** (rod2, rod3), elastic, with lumped (HRZ) mass. With nonlinear geometry a rod2
//! is corotational: its strain is (l − L)/L from its current length l and its force acts along
//! its current axis (Belytschko et al. 2014, §2.4 and §4.5); a rod3 then is not supported yet.
//! Without it the internal force is K u with the kernel's stiffness ([`modelcraft_fem::truss`]).
//! Without it the internal force is K u with the kernel's stiffness ([`fieldcraft_fem::truss`]).
//! The stress is the uniaxial tensor along the axis (the mean of the integration points'), and
//! the critical increment comes from the kernel's frequency bound (exact for rod2: L/c).
//! - **Point masses** add to the lumped mass of their node, per direction.
@@ -21,10 +21,10 @@
//! §6.3), which is 2/D for a dashpot alone. Rotary inertia and rotational springs need
//! rotational degrees of freedom, which this solver does not have yet.
use modelcraft_fem::DofKind;
use modelcraft_fem::dense::Matrix;
use modelcraft_fem::truss::{Truss, uniaxial};
use modelcraft_model::{ElementKind, Resolved, ResolvedDirection};
use fieldcraft_fem::DofKind;
use fieldcraft_fem::dense::Matrix;
use fieldcraft_fem::truss::{Truss, uniaxial};
use fieldcraft_model::{ElementKind, Resolved, ResolvedDirection};
use crate::system::NODE_DOFS;
use crate::{Error, Result};
@@ -92,7 +92,7 @@ impl Rod {
/// The element's degrees of freedom, node by node.
pub fn dofs(&self) -> impl Iterator<Item = usize> + '_ {
self.nodes.iter().flat_map(|n| modelcraft_fem::truss::DOFS.node.iter().filter_map(DofKind::direction).map(|d| dof(*n, d)))
self.nodes.iter().flat_map(|n| fieldcraft_fem::truss::DOFS.node.iter().filter_map(DofKind::direction).map(|d| dof(*n, d)))
}
/// Add its internal force into `force` and return the element's mean stress and strain
+13 -13
View File
@@ -14,9 +14,9 @@
//! strain and the stiffness-form hourglass forces) is rotated with the increment's rotation —
//! the Cayley transform of the spin increment (Jaumann rate, Hughes & Winget 1980) or the
//! increment of the polar rotation (Green–Naghdi rate, D. P. Flanagan, L. M. Taylor, CMAME
//! 62(3):305–320, 1987), see [`modelcraft_fem::rotation`] — then the constitutive increment is
//! 62(3):305–320, 1987), see [`fieldcraft_fem::rotation`] — then the constitutive increment is
//! added: hypoelastic, or J2 radial return with Johnson–Cook damage
//! ([`modelcraft_fem::plasticity`]). Orthotropic elasticity is updated in the polar frame (its
//! ([`fieldcraft_fem::plasticity`]). Orthotropic elasticity is updated in the polar frame (its
//! axes turn with the material) whichever rate the step names.
//!
//! **One-point hexahedron** (D. P. Flanagan, T. Belytschko, IJNME 17(5):679–706, 1981): the
@@ -44,7 +44,7 @@
//! Δt = L / (Q + √(Q² + c²)), Q = C_l c + C_q L |ε̇ᵥ| (Wilkins 1980).
//!
//! **Critical increment.** Each element's stable length L = 2c/ω_max comes from the bound on its
//! highest frequency in its current shape ([`modelcraft_fem::uniform`], with the element's lumped
//! highest frequency in its current shape ([`fieldcraft_fem::uniform`], with the element's lumped
//! masses, plus the hourglass stiffness when it is used), so without bulk viscosity its critical
//! increment is 2/ω_max (Belytschko et al. 2014, §6.3). The elastic moduli bound the tangent
//! moduli of plastic flow, so the bound holds for the inelastic materials too.
@@ -54,11 +54,11 @@
//! reaches its material's erosion plastic strain is removed: it carries no stress from then on,
//! its nodes keep their mass and momentum, and its internal energy at that moment is reported.
use modelcraft_fem::material::stress;
use modelcraft_fem::plasticity::{Increment, PointState};
use modelcraft_fem::rotation::{self, IDENTITY, Mat3, Polar};
use modelcraft_fem::uniform::{Hex8, Tet4, UniformStrain};
use modelcraft_model::{BulkViscosity, HourglassKind, StressRate};
use fieldcraft_fem::material::stress;
use fieldcraft_fem::plasticity::{Increment, PointState};
use fieldcraft_fem::rotation::{self, IDENTITY, Mat3, Polar};
use fieldcraft_fem::uniform::{Hex8, Tet4, UniformStrain};
use fieldcraft_model::{BulkViscosity, HourglassKind, StressRate};
use crate::system::{Material, NODE_DOFS, System};
use crate::{Error, Result};
@@ -330,7 +330,7 @@ fn current(mat: &Material, rest: f64, volume: f64) -> (f64, f64) {
}
/// The bound ω² ≤ k λ_max(Σ b bᵀ) / (V m_min) on an element's highest frequency
/// ([`modelcraft_fem::uniform`]), with the lumped masses of the undeformed element and
/// ([`fieldcraft_fem::uniform`]), with the lumped masses of the undeformed element and
/// `gradient_norm` = λ_max(Σ b bᵀ).
pub fn frequency_bound<const N: usize>(k: &UniformStrain<N>, gradient_norm: f64, modulus: f64, mass: &[f64; N]) -> f64 {
let lightest = mass.iter().copied().fold(f64::INFINITY, f64::min);
@@ -353,7 +353,7 @@ pub fn hex_scales(
mass: &[f64; 8],
mat: &Material,
density: f64,
hourglass: modelcraft_model::Hourglass,
hourglass: fieldcraft_model::Hourglass,
) -> HexScales {
let norm = k.gradient_norm();
// h: the edge of the cube with the element's volume and gradient norm.
@@ -439,7 +439,7 @@ fn run<T: Send>(
) -> Result<()> {
let mut all = pieces::<T>(states, count, &index)?;
for round in all.chunks_mut(ROUND) {
modelcraft_parallel::for_each_mut(round, |_, piece| {
fieldcraft_parallel::for_each_mut(round, |_, piece| {
let Piece { items, base, states, out } = piece;
*out = items
.clone()
@@ -476,7 +476,7 @@ fn hexahedron(sys: &System, h: &crate::system::Hex, m: &Motion, es: &mut Element
if es.failed.is_some() {
return Ok(Outcome::Skipped);
}
let hourglass = modelcraft_model::Hourglass { kind: sys.hourglass, coefficient: sys.hourglass_coefficient };
let hourglass = fieldcraft_model::Hourglass { kind: sys.hourglass, coefficient: sys.hourglass_coefficient };
let e = Nodal::gather(sys, &h.nodes, m);
let x_now = e.positions(0.0);
let (mid, now) = if sys.nonlinear {
@@ -607,7 +607,7 @@ pub fn internal_forces(sys: &System, m: &Motion, state: &mut State, ws: &mut Wor
// Pass 1: each tetrahedron's increment, and each node's mean volumetric increment.
let increments = {
let st: &State = state;
modelcraft_parallel::map_grained(tets.len(), BLOCK, |k| match tets.get(k) {
fieldcraft_parallel::map_grained(tets.len(), BLOCK, |k| match tets.get(k) {
Some(t) => tetrahedron_increment(sys, t, m, st),
None => Ok(None),
})
+1 -1
View File
@@ -20,7 +20,7 @@
//! The run starts from u = 0 and the initial velocities with the half increment
//! v^{½} = v^0 + ½Δt^{½} a^0.
use modelcraft_model::{ExplicitDynamic, Model, OutputVariable, Procedure, Step};
use fieldcraft_model::{ExplicitDynamic, Model, OutputVariable, Procedure, Step};
use crate::elements::{Motion, State, Workspace, internal_forces};
use crate::result::{ControllingElement, Energies, ErodedElement, ExplicitResult, Frame, History};
+6 -6
View File
@@ -1,4 +1,4 @@
//! ModelCraft's explicit dynamics solver: transient response of a model's explicit dynamic step
//! FieldCraft's explicit dynamics solver: transient response of a model's explicit dynamic step
//! by central-difference time integration.
//!
//! The method is the explicit central-difference scheme of T. Belytschko, W. K. Liu, B. Moran,
@@ -24,7 +24,7 @@
//! docs/solvers/literature.md, Parts F and J). Without it the gradients stay those of the
//! undeformed mesh (small displacements).
//!
//! Elements are updated on threads ([`modelcraft_parallel`]), each owning its state, and their
//! Elements are updated on threads ([`fieldcraft_parallel`]), each owning its state, and their
//! nodal forces are added in label order as a sequential loop adds them, with no parallel
//! reductions, so the same model gives the same numbers bit for bit on any number of threads;
//! sizes, increments and output are capped by [`Limits`].
@@ -46,14 +46,14 @@ pub use result::{ControllingElement, Energies, ErodedElement, ExplicitResult, Fr
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Model(#[from] modelcraft_model::Error),
Model(#[from] fieldcraft_model::Error),
#[error("invalid input: {0}")]
Invalid(String),
#[error("not supported yet: {0}")]
NotSupported(String),
/// An element kernel failed (inverted geometry, …).
#[error("element {label}: {source}")]
Element { label: u32, source: modelcraft_fem::Error },
Element { label: u32, source: fieldcraft_fem::Error },
#[error("limit reached: {0}")]
Limit(String),
/// The solution stopped being finite.
@@ -68,7 +68,7 @@ pub type Result<T> = std::result::Result<T, Error>;
impl Error {
/// A kernel error, naming the element.
pub fn element(label: u32, source: modelcraft_fem::Error) -> Error {
pub fn element(label: u32, source: fieldcraft_fem::Error) -> Error {
Error::Element { label, source }
}
}
@@ -86,7 +86,7 @@ pub struct Limits {
impl Limits {
pub const MAX_DOFS: usize = 6_000_000;
pub const MAX_INCREMENTS: u64 = modelcraft_model::steps::MAX_INCREMENTS;
pub const MAX_INCREMENTS: u64 = fieldcraft_model::steps::MAX_INCREMENTS;
pub const MAX_OUTPUT_VALUES: usize = 50_000_000;
}
+10 -10
View File
@@ -3,11 +3,11 @@
//! ([`crate::discrete`]), prescribed velocities, rigid walls, initial velocities and the load
//! vector of an explicit dynamic step.
use modelcraft_fem::material::Elasticity;
use modelcraft_fem::plasticity::J2;
use modelcraft_fem::uniform::{Hex8, Tet4, modulus_bound};
use modelcraft_fem::{DofKind, DofLayout, DofSet, face};
use modelcraft_model::{
use fieldcraft_fem::material::Elasticity;
use fieldcraft_fem::plasticity::J2;
use fieldcraft_fem::uniform::{Hex8, Tet4, modulus_bound};
use fieldcraft_fem::{DofKind, DofLayout, DofSet, face};
use fieldcraft_model::{
Elastic, ElementKind, ExplicitDynamic, HourglassKind, Integration, Load, Model, Resolved, SectionKind, Step, StressRate, Target,
};
@@ -30,7 +30,7 @@ pub struct Material {
/// Dilatational wave speed √(M/ρ), with M the largest normal stiffness (λ + 2μ when
/// isotropic).
pub wave_speed: f64,
/// The bound of [`modelcraft_fem::uniform::modulus_bound`].
/// The bound of [`fieldcraft_fem::uniform::modulus_bound`].
pub modulus: f64,
/// The largest normal stiffness M (λ + 2μ when isotropic): c = √(M/ρ).
pub stiffest: f64,
@@ -143,7 +143,7 @@ fn material(model: &Model, name: &str) -> Result<Material> {
.density
.filter(|d| *d > 0.0 && d.is_finite())
.ok_or_else(|| invalid(format!("material `{name}` has no density, and explicit dynamics needs one")))?;
let d = modelcraft_fem::elasticity(elastic).map_err(|e| invalid(format!("material `{name}`: {e}")))?;
let d = fieldcraft_fem::elasticity(elastic).map_err(|e| invalid(format!("material `{name}`: {e}")))?;
let stiffest = d[0][0].max(d[1][1]).max(d[2][2]);
// The model keeps plasticity, damage and erosion to isotropic materials.
let j2 = match *elastic {
@@ -171,7 +171,7 @@ fn material(model: &Model, name: &str) -> Result<Material> {
}
/// The section's elements of a kind the explicit solver has, or why not.
fn check_formulation(kind: ElementKind, section_name: &str, f: &modelcraft_model::Formulation) -> Result<()> {
fn check_formulation(kind: ElementKind, section_name: &str, f: &fieldcraft_model::Formulation) -> Result<()> {
let refuse = |why: &str| Err(Error::NotSupported(format!("section `{section_name}`: {why}")));
if f.hybrid {
return refuse("hybrid elements in explicit dynamics");
@@ -327,7 +327,7 @@ impl System {
)));
}
let young =
modelcraft_fem::truss::young_modulus(source.elastic.as_ref().ok_or_else(|| Error::Internal("truss material".into()))?)
fieldcraft_fem::truss::young_modulus(source.elastic.as_ref().ok_or_else(|| Error::Internal("truss material".into()))?)
.map_err(|e| invalid(format!("material `{}` of element {label}: {e}", mat.name)))?;
let x: Vec<[f64; 3]> = idx.iter().map(|i| sys.xyz.get(*i).copied().unwrap_or_default()).collect();
let rod = Rod::new(label, index, kind, idx.clone(), &x, mi, crate::discrete::RodSection { young, density: rho, area })?;
@@ -518,7 +518,7 @@ impl System {
self.each_element(chosen, |nodes, weights, rho| {
for (node, w) in nodes.iter().zip(weights) {
let x = xyz.get(*node).copied().unwrap_or_default();
let a = modelcraft_model::centrifugal_acceleration(*angular_velocity, *point, *axis, x);
let a = fieldcraft_model::centrifugal_acceleration(*angular_velocity, *point, *axis, x);
add_at(&mut f, *node, &a.map(|c| rho * w * c));
}
});
+5 -5
View File
@@ -13,10 +13,10 @@
//!
//! Run with `--nocapture` to see the errors.
use modelcraft_explicit::elements::{Motion, State, Workspace, internal_forces};
use modelcraft_explicit::system::System;
use modelcraft_explicit::{ExplicitResult, Failure, Limits, solve_explicit};
use modelcraft_model::*;
use fieldcraft_explicit::elements::{Motion, State, Workspace, internal_forces};
use fieldcraft_explicit::system::System;
use fieldcraft_explicit::{ExplicitResult, Failure, Limits, solve_explicit};
use fieldcraft_model::*;
/// A block [0, size] of `n` cells: one hex8 per cell, or six tet4 (Kuhn's split).
fn block(n: [usize; 3], size: [f64; 3], kind: ElementKind) -> Model {
@@ -179,7 +179,7 @@ impl Driver {
self.try_go(to, dt).unwrap()
}
fn try_go(&mut self, to: impl Fn([f64; 3]) -> [f64; 3], dt: f64) -> modelcraft_explicit::Result<[f64; 6]> {
fn try_go(&mut self, to: impl Fn([f64; 3]) -> [f64; 3], dt: f64) -> fieldcraft_explicit::Result<[f64; 6]> {
let mut du = vec![0.0; self.u.len()];
for (i, x0) in self.sys.xyz.iter().enumerate() {
let x = to(*x0);
+3 -3
View File
@@ -9,9 +9,9 @@
use std::f64::consts::PI;
use modelcraft_explicit::{ExplicitResult, Limits, solve_explicit};
use modelcraft_fem::truss::axial;
use modelcraft_model::*;
use fieldcraft_explicit::{ExplicitResult, Limits, solve_explicit};
use fieldcraft_fem::truss::axial;
use fieldcraft_model::*;
fn model(step: ExplicitDynamic) -> Model {
let mut m = Model::default();
+3 -3
View File
@@ -2,8 +2,8 @@
//! a free bar by α ΔT L without stress; a bar held at both ends carries σ = −E α ΔT, and with
//! perfectly plastic J2 the stress stops at the yield stress.
use modelcraft_explicit::{Limits, solve_explicit};
use modelcraft_model::*;
use fieldcraft_explicit::{Limits, solve_explicit};
use fieldcraft_model::*;
const YOUNG: f64 = 1.0e4;
const ALPHA: f64 = 1.0e-4;
@@ -115,7 +115,7 @@ fn a_clamped_bar_carries_minus_e_alpha_dt_up_to_yield() {
let r = solve_explicit(&bar(kind, true, Some(plastic)), None, &Limits::default()).unwrap();
let last = r.frames.last().unwrap();
for (s, ep) in last.stress.iter().zip(&last.plastic_strain) {
let mises = modelcraft_fem::von_mises(s);
let mises = fieldcraft_fem::von_mises(s);
assert!((mises - 0.5 * sigma.abs()).abs() < 0.02 * sigma.abs(), "{kind:?}: {s:?}");
assert!(*ep > 0.0, "{kind:?}: plastic strain {ep}");
}
+2 -2
View File
@@ -5,8 +5,8 @@
//! The bar has Poisson's ratio 0, so a uniaxial stress wave travels at the rod speed c = √(E/ρ)
//! without lateral inertia effects (d'Alembert's solution of the one-dimensional wave equation).
use modelcraft_explicit::{ExplicitResult, Limits, solve_explicit};
use modelcraft_model::*;
use fieldcraft_explicit::{ExplicitResult, Limits, solve_explicit};
use fieldcraft_model::*;
const YOUNG: f64 = 1.0e4;
const DENSITY: f64 = 1.0;