forked from 1b-agent/fieldcraft-upstream
Implicit solver: reference points, couplings, moments, wedge6 (M4) (#26)
* fem: wedge6 element (triangle × interval, 3 × 2 points), patch and cantilever tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * solver: reference points, kinematic and distributing couplings, moments by constraint elimination Coupled reference points get six degrees of freedom; kinematic couplings are rigid links, distributing couplings least-squares interpolation constraints. Every constraint row and prescribed value is eliminated master-slave (Abel-Shephard pivot choice), so the reduced matrix stays SPD; no coupling gives the old elimination bit for bit. Verified: rigid motion of a coupled block, clamping through a point, distributing tension vs pressure, bending and tube torsion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * engine: reference point results in solve.static and solve.probe Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * docs: couplings, moments and wedge6 in the solver docs, literature and roadmap Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * solver: simplify constraint elimination and reference point plumbing Rows combine by one sort; T_f transposed once; K and M reduced in one pass on a shared pattern; prescribed motion computed once; reference point loads and BCs go through Resolved like node ones (Mesh::label_dofs); one dof-direction rule; StepResult::locate for probes; dead Csr::restrict removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * solver: refuse oversized reductions before allocating them A held distributing coupling point makes a dense (3N)^2 block; estimate it from the constraint rows and refuse it against the factorization's memory budget with a clear message, before anything is built. Other reductions with dependents count their entries first (stopping at the budget) and reserve fallibly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm --------- Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
18 files changed
+1502
-199
No files matched your search
+131
-53
@@ -1,8 +1,14 @@
|
||||
//! From the resolved model to the discrete system: the solid elements with their materials and
|
||||
//! formulations, the degrees of freedom, prescribed values and the load vector of a step.
|
||||
//! formulations, the coupled reference points, the degrees of freedom, prescribed values and the
|
||||
//! load vector of a step.
|
||||
//!
|
||||
//! Degrees of freedom: three translations per mesh node (nodes by label), then six per coupled
|
||||
//! reference point (points by name): translations along X, Y, Z and rotations about them.
|
||||
|
||||
use modelcraft_fem::element::{Formulation, Solid};
|
||||
use modelcraft_fem::{Elasticity, elasticity, face};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use modelcraft_model::{ElementKind, Load, Model, Resolved, SectionKind, Step, Target};
|
||||
|
||||
use crate::{Error, Result};
|
||||
@@ -26,15 +32,27 @@ impl Elem {
|
||||
}
|
||||
}
|
||||
|
||||
/// A reference point some coupling ties to the mesh.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Point {
|
||||
pub name: String,
|
||||
pub label: u32,
|
||||
pub xyz: [f64; 3],
|
||||
}
|
||||
|
||||
/// The analysed mesh: the nodes that carry degrees of freedom (those of the solid elements),
|
||||
/// sorted by label, and the elements sorted by label.
|
||||
/// sorted by label, the elements sorted by label, and the coupled reference points by name.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Mesh {
|
||||
pub labels: Vec<u32>,
|
||||
pub xyz: Vec<[f64; 3]>,
|
||||
pub elements: Vec<Elem>,
|
||||
pub points: Vec<Point>,
|
||||
}
|
||||
|
||||
/// The directions of a node's degrees of freedom, 0–5.
|
||||
const DIRECTIONS: [&str; 6] = ["X", "Y", "Z", "rotation about X", "rotation about Y", "rotation about Z"];
|
||||
|
||||
impl Mesh {
|
||||
pub fn node(&self, label: u32) -> Option<usize> {
|
||||
self.labels.binary_search(&label).ok()
|
||||
@@ -44,17 +62,52 @@ impl Mesh {
|
||||
self.elements.binary_search_by_key(&label, |e| e.label).ok().and_then(|i| self.elements.get(i))
|
||||
}
|
||||
|
||||
pub fn dofs(&self) -> usize {
|
||||
pub fn point(&self, name: &str) -> Option<usize> {
|
||||
self.points.binary_search_by(|p| p.name.as_str().cmp(name)).ok()
|
||||
}
|
||||
|
||||
/// The translational degrees of freedom of the mesh nodes.
|
||||
pub fn mesh_dofs(&self) -> usize {
|
||||
3 * self.labels.len()
|
||||
}
|
||||
|
||||
/// Every degree of freedom: the mesh's, then six per reference point.
|
||||
pub fn dofs(&self) -> usize {
|
||||
self.mesh_dofs() + 6 * self.points.len()
|
||||
}
|
||||
|
||||
/// Degree of freedom `c` (0–5) of reference point `k`.
|
||||
pub fn point_dof(&self, k: usize, c: usize) -> usize {
|
||||
self.mesh_dofs() + 6 * k + c
|
||||
}
|
||||
|
||||
/// The direction of a degree of freedom: 0–2 translations along X, Y, Z, 3–5 rotations.
|
||||
pub fn direction(&self, dof: usize) -> usize {
|
||||
if dof < self.mesh_dofs() { dof % 3 } else { (dof - self.mesh_dofs()) % 6 }
|
||||
}
|
||||
|
||||
/// The first degree of freedom and their count (3 or 6) of a node or coupled reference
|
||||
/// point label.
|
||||
pub fn label_dofs(&self, label: u32) -> Option<(usize, usize)> {
|
||||
match self.node(label) {
|
||||
Some(n) => Some((3 * n, 3)),
|
||||
None => self.points.iter().position(|p| p.label == label).map(|k| (self.point_dof(k, 0), 6)),
|
||||
}
|
||||
}
|
||||
|
||||
/// What a degree of freedom is, in the user's terms.
|
||||
pub fn describe_dof(&self, dof: usize) -> String {
|
||||
let direction = DIRECTIONS.get(self.direction(dof)).copied().unwrap_or("?");
|
||||
if dof < self.mesh_dofs() {
|
||||
format!("node {} in {direction}", self.labels.get(dof / 3).copied().unwrap_or(0))
|
||||
} else {
|
||||
let name = self.points.get((dof - self.mesh_dofs()) / 6).map_or("?", |p| p.name.as_str());
|
||||
format!("reference point `{name}` in {direction}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Build it from the model and its resolved view, refusing what the solver cannot represent.
|
||||
pub fn new(model: &Model, resolved: &Resolved, max_dofs: usize) -> Result<Mesh> {
|
||||
if let Some(name) = model.couplings().keys().next() {
|
||||
return Err(Error::NotSupported(format!(
|
||||
"couplings (coupling `{name}`); the solver handles solid elements without reference points so far"
|
||||
)));
|
||||
}
|
||||
let mut elements = Vec::new();
|
||||
for (name, section) in model.sections() {
|
||||
if !matches!(section.kind, SectionKind::Solid {}) {
|
||||
@@ -80,14 +133,23 @@ impl Mesh {
|
||||
let mut labels: Vec<u32> = elements.iter().flat_map(|e| e.2.iter().copied()).collect();
|
||||
labels.sort_unstable();
|
||||
labels.dedup();
|
||||
if 3 * labels.len() > max_dofs {
|
||||
return Err(Error::Limit(format!("the model has {} degrees of freedom; the limit is {max_dofs}", 3 * labels.len())));
|
||||
let coupled: BTreeSet<&str> = model.couplings().values().map(|c| c.reference_point.as_str()).collect();
|
||||
let points: Vec<Point> = coupled
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
let p = model.reference_point(name).ok_or_else(|| Error::Invalid(format!("reference point `{name}` is missing")))?;
|
||||
Ok(Point { name: name.to_string(), label: p.label, xyz: p.xyz })
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
let dofs = 3 * labels.len() + 6 * points.len();
|
||||
if dofs > max_dofs {
|
||||
return Err(Error::Limit(format!("the model has {dofs} degrees of freedom; the limit is {max_dofs}")));
|
||||
}
|
||||
let xyz: Vec<[f64; 3]> = labels
|
||||
.iter()
|
||||
.map(|l| model.node(*l).map(|n| n.xyz).ok_or_else(|| Error::Invalid(format!("node {l} is missing"))))
|
||||
.collect::<Result<_>>()?;
|
||||
let mut mesh = Mesh { labels, xyz, elements: Vec::with_capacity(elements.len()) };
|
||||
let mut mesh = Mesh { labels, xyz, elements: Vec::with_capacity(elements.len()), points };
|
||||
for (label, kind, nodes, formulation, d, density) in elements {
|
||||
let idx: Vec<usize> = nodes.iter().filter_map(|n| mesh.node(*n)).collect();
|
||||
let exyz: Vec<[f64; 3]> = idx.iter().filter_map(|i| mesh.xyz.get(*i).copied()).collect();
|
||||
@@ -98,24 +160,43 @@ impl Mesh {
|
||||
}
|
||||
|
||||
/// The prescribed value of every degree of freedom (`None` = free) under a step's boundary
|
||||
/// conditions. Rotational degrees of freedom do not exist on solid nodes and are ignored.
|
||||
pub fn prescribed(model: &Model, resolved: &Resolved, mesh: &Mesh, step: &Step, values: bool) -> Result<Vec<Option<f64>>> {
|
||||
/// conditions; `values: false` holds them at zero (frequency steps). Rotations of solid nodes do
|
||||
/// not exist and are ignored, like the directions of a reference point no coupling ties
|
||||
/// (`active`); a boundary condition on a reference point that no coupling ties to the mesh holds
|
||||
/// nothing and is reported in `warnings`.
|
||||
pub fn prescribed(
|
||||
model: &Model,
|
||||
resolved: &Resolved,
|
||||
mesh: &Mesh,
|
||||
step: &Step,
|
||||
values: bool,
|
||||
active: &[bool],
|
||||
warnings: &mut Vec<String>,
|
||||
) -> Result<Vec<Option<f64>>> {
|
||||
let mut out: Vec<Option<f64>> = vec![None; mesh.dofs()];
|
||||
for name in &step.boundary_conditions {
|
||||
let bc = model.boundary_condition(name).ok_or_else(|| Error::Invalid(format!("boundary condition `{name}` is missing")))?;
|
||||
if let Target::ReferencePoint(rp) = bc.target() {
|
||||
return Err(Error::NotSupported(format!("boundary conditions on reference points (`{name}` on `{rp}`)")));
|
||||
if let Target::ReferencePoint(rp) = bc.target()
|
||||
&& mesh.point(rp).is_none()
|
||||
{
|
||||
warnings
|
||||
.push(format!("boundary condition `{name}` acts on reference point `{rp}`, which no coupling ties to the mesh; it holds nothing"));
|
||||
continue;
|
||||
}
|
||||
for &label in resolved.target_nodes(bc.target()) {
|
||||
let Some(node) = mesh.node(label) else { continue };
|
||||
for dof in 1..=3u8 {
|
||||
for (first, count) in resolved.target_nodes(bc.target()).iter().filter_map(|l| mesh.label_dofs(*l)) {
|
||||
for dof in 1..=count as u8 {
|
||||
let Some(v) = bc.value(dof) else { continue };
|
||||
let index = first + usize::from(dof - 1);
|
||||
if !active.get(index).copied().unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
let v = if values { v } else { 0.0 };
|
||||
let slot = out.get_mut(3 * node + usize::from(dof - 1)).ok_or_else(|| Error::Internal("dof index".into()))?;
|
||||
let slot = out.get_mut(index).ok_or_else(|| Error::Internal("dof index".into()))?;
|
||||
match *slot {
|
||||
Some(old) if old != v => {
|
||||
return Err(Error::Invalid(format!(
|
||||
"node {label} direction {dof} is prescribed both {old} and {v} (boundary condition `{name}` and another)"
|
||||
"{} is prescribed both {old} and {v} (boundary condition `{name}` and another)",
|
||||
mesh.describe_dof(index)
|
||||
)));
|
||||
}
|
||||
_ => *slot = Some(v),
|
||||
@@ -126,32 +207,47 @@ pub fn prescribed(model: &Model, resolved: &Resolved, mesh: &Mesh, step: &Step,
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn add(f: &mut [f64], node: usize, v: [f64; 3]) {
|
||||
fn add(f: &mut [f64], first: usize, v: [f64; 3]) {
|
||||
for (k, c) in v.iter().enumerate() {
|
||||
if let Some(x) = f.get_mut(3 * node + k) {
|
||||
if let Some(x) = f.get_mut(first + k) {
|
||||
*x += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The load vector of a static step: nodal forces and tractions as the model distributes them,
|
||||
/// consistent pressure loads, gravity and body forces.
|
||||
pub fn loads(model: &Model, resolved: &Resolved, mesh: &Mesh, step: &Step) -> Result<Vec<f64>> {
|
||||
/// forces and moments on coupled reference points, consistent pressure loads, gravity and body
|
||||
/// forces.
|
||||
pub fn loads(model: &Model, resolved: &Resolved, mesh: &Mesh, step: &Step, active: &[bool]) -> Result<Vec<f64>> {
|
||||
let mut f = vec![0.0; mesh.dofs()];
|
||||
for name in &step.loads {
|
||||
let load = model.load(name).ok_or_else(|| Error::Invalid(format!("load `{name}` is missing")))?;
|
||||
match load {
|
||||
Load::Force { target: Target::ReferencePoint(rp), .. } => {
|
||||
return Err(Error::NotSupported(format!("loads on reference points (`{name}` on `{rp}`)")));
|
||||
}
|
||||
Load::Force { .. } | Load::Traction { .. } => {
|
||||
Load::Force { .. } | Load::Moment { .. } | Load::Traction { .. } => {
|
||||
if let Some(rp) = load.reference_point()
|
||||
&& mesh.point(rp).is_none()
|
||||
{
|
||||
return Err(Error::Invalid(format!("load `{name}` acts on reference point `{rp}`, which no coupling ties to the mesh")));
|
||||
}
|
||||
// Moments act on the rotations, which only reference points have.
|
||||
let offset = if matches!(load, Load::Moment { .. }) { 3 } else { 0 };
|
||||
for (label, v) in resolved.nodal_loads.get(name).map(Vec::as_slice).unwrap_or_default() {
|
||||
let node = mesh.node(*label).ok_or_else(|| Error::Invalid(format!("load `{name}`: node {label} belongs to no solid element")))?;
|
||||
add(&mut f, node, *v);
|
||||
let (first, count) =
|
||||
mesh.label_dofs(*label).ok_or_else(|| Error::Invalid(format!("load `{name}`: node {label} belongs to no solid element")))?;
|
||||
if offset + 3 > count {
|
||||
return Err(Error::Invalid(format!("moment `{name}`: solid nodes have no rotations; apply it to a coupled reference point")));
|
||||
}
|
||||
for (c, x) in v.iter().enumerate() {
|
||||
let dof = first + offset + c;
|
||||
if *x != 0.0 && !active.get(dof).copied().unwrap_or(false) {
|
||||
return Err(Error::Invalid(format!(
|
||||
"load `{name}`: {} is not coupled to the mesh (its coupling leaves that direction free)",
|
||||
mesh.describe_dof(dof)
|
||||
)));
|
||||
}
|
||||
}
|
||||
add(&mut f, first + offset, *v);
|
||||
}
|
||||
}
|
||||
Load::Moment { .. } => {
|
||||
return Err(Error::NotSupported(format!("moments (`{name}`): solid nodes have no rotations; a coupled reference point is needed")));
|
||||
}
|
||||
Load::Pressure { set, magnitude } => {
|
||||
let faces = resolved.sets.get(set).map(|s| s.faces.as_slice()).unwrap_or_default();
|
||||
@@ -166,7 +262,7 @@ pub fn loads(model: &Model, resolved: &Resolved, mesh: &Mesh, step: &Step) -> Re
|
||||
let xyz: Vec<[f64; 3]> = idx.iter().filter_map(|i| mesh.xyz.get(*i).copied()).collect();
|
||||
let forces = face::pressure(&xyz, *magnitude).map_err(|e| Error::element(fc.element, e))?;
|
||||
for (i, v) in idx.iter().zip(forces) {
|
||||
add(&mut f, *i, v);
|
||||
add(&mut f, 3 * i, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,25 +290,7 @@ pub fn loads(model: &Model, resolved: &Resolved, mesh: &Mesh, step: &Step) -> Re
|
||||
fn body(f: &mut [f64], e: &Elem, b: [f64; 3]) -> Result<()> {
|
||||
let forces = e.solid()?.body_force(b).map_err(|err| Error::element(e.label, err))?;
|
||||
for (node, v) in e.nodes.iter().zip(forces) {
|
||||
add(f, *node, v);
|
||||
add(f, 3 * node, v);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Equation numbers of the free degrees of freedom, following a node order.
|
||||
pub fn equations(order: &[usize], prescribed: &[Option<f64>]) -> (Vec<Option<usize>>, usize) {
|
||||
let mut map = vec![None; prescribed.len()];
|
||||
let mut next = 0;
|
||||
for &node in order {
|
||||
for c in 0..3 {
|
||||
let dof = 3 * node + c;
|
||||
if prescribed.get(dof).is_some_and(Option::is_none) {
|
||||
if let Some(m) = map.get_mut(dof) {
|
||||
*m = Some(next);
|
||||
}
|
||||
next += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
(map, next)
|
||||
}
|
||||
Reference in new issue
Block a user