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 <[email protected]> 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 <[email protected]> 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 <[email protected]> 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 <[email protected]> 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 <[email protected]> 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 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm --------- Co-authored-by: jyviko <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
18 files changed
+1502
-199
No files matched your search
+113
-70
@@ -7,16 +7,17 @@ use modelcraft_model::{ElementKind, Model, Procedure, Resolved, Step};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::cholesky::{Factor, FactorError, UpperColumns};
|
||||
use crate::constraints::{Budget, Reduction, coupling_rows};
|
||||
use crate::eigen::{Operator, Wanted, lanczos};
|
||||
use crate::ordering::nested_dissection;
|
||||
use crate::sparse::{Csr, NodeGraph, add_block, add_diagonal, add_scalar};
|
||||
use crate::system::{Mesh, equations, loads, prescribed};
|
||||
use crate::system::{Mesh, loads, prescribed};
|
||||
use crate::{Error, Limits, Result};
|
||||
|
||||
/// Size and effort of a solution.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
|
||||
pub struct Stats {
|
||||
/// Degrees of freedom of the mesh (3 per node).
|
||||
/// Degrees of freedom: 3 per mesh node and 6 per coupled reference point.
|
||||
pub dofs: usize,
|
||||
/// Free degrees of freedom solved for.
|
||||
pub equations: usize,
|
||||
@@ -37,6 +38,16 @@ pub struct ElementStress {
|
||||
pub nodal: Vec<[f64; 6]>,
|
||||
}
|
||||
|
||||
/// The motion and reaction of a coupled reference point: translations along X, Y, Z, then
|
||||
/// rotations about them (reactions: forces, then moments). Directions no coupling ties are zero.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct PointResult {
|
||||
pub name: String,
|
||||
pub label: u32,
|
||||
pub displacement: [f64; 6],
|
||||
pub reaction: [f64; 6],
|
||||
}
|
||||
|
||||
/// The result of a static step.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct StaticResult {
|
||||
@@ -51,7 +62,9 @@ pub struct StaticResult {
|
||||
/// Von Mises stress of the averaged nodal stress.
|
||||
pub von_mises: Vec<f64>,
|
||||
pub elements: Vec<ElementStress>,
|
||||
/// Sum of the applied loads and of the reactions.
|
||||
/// The coupled reference points, by name.
|
||||
pub reference_points: Vec<PointResult>,
|
||||
/// Sum of the applied forces and of the reaction forces (moments are not included).
|
||||
pub applied_total: [f64; 3],
|
||||
pub reaction_total: [f64; 3],
|
||||
pub stats: Stats,
|
||||
@@ -71,6 +84,9 @@ pub struct Mode {
|
||||
pub participation: [f64; 3],
|
||||
/// Effective mass Γ² along X, Y, Z.
|
||||
pub effective_mass: [f64; 3],
|
||||
/// The shape at the coupled reference points ([`FrequencyResult::reference_points`] order):
|
||||
/// translations, then rotations.
|
||||
pub point_shape: Vec<[f64; 6]>,
|
||||
}
|
||||
|
||||
/// The result of a frequency step.
|
||||
@@ -78,6 +94,8 @@ pub struct Mode {
|
||||
pub struct FrequencyResult {
|
||||
pub step: String,
|
||||
pub nodes: Vec<u32>,
|
||||
/// The coupled reference points' names and labels.
|
||||
pub reference_points: Vec<(String, u32)>,
|
||||
pub modes: Vec<Mode>,
|
||||
/// Mass of the model.
|
||||
pub total_mass: f64,
|
||||
@@ -156,33 +174,45 @@ fn factor_raw(a: &Csr, limits: &Limits) -> std::result::Result<Factor, FactorErr
|
||||
Factor::new(&UpperColumns::from_symmetric(a), limits.max_factor_entries)
|
||||
}
|
||||
|
||||
/// A factorization failure in the user's terms: the node and direction of a singular pivot.
|
||||
fn factor_error(e: FactorError, mesh: &Mesh, map: &[Option<usize>]) -> Error {
|
||||
/// A factorization failure in the user's terms: the node (or reference point) and direction of
|
||||
/// a singular pivot.
|
||||
fn factor_error(e: FactorError, mesh: &Mesh, reduction: &Reduction) -> Error {
|
||||
match e {
|
||||
FactorError::TooLarge { entries, limit } => {
|
||||
Error::Limit(format!("the factorization needs {entries} entries; the limit is {limit} (refine less or raise the limit)"))
|
||||
}
|
||||
FactorError::NotPositiveDefinite { column } => {
|
||||
let dof = map.iter().position(|m| *m == Some(column)).unwrap_or(0);
|
||||
let label = mesh.labels.get(dof / 3).copied().unwrap_or(0);
|
||||
let axis = ["X", "Y", "Z"].get(dof % 3).copied().unwrap_or("?");
|
||||
let at = reduction.eq_dof.get(column).map_or_else(|| "an unknown degree of freedom".to_string(), |d| mesh.describe_dof(*d));
|
||||
Error::Singular(format!(
|
||||
"the stiffness is singular at node {label} in {axis}: the model can move without straining (missing or insufficient boundary conditions, or a part not connected to the rest)"
|
||||
"the stiffness is singular at {at}: the model can move without straining (missing or insufficient boundary conditions, a part not connected to the rest, or a reference point direction nothing holds)"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spread a free-equation vector over every degree of freedom (`base` holds the prescribed
|
||||
/// values) and group it per node.
|
||||
fn per_node(x: &[f64], map: &[Option<usize>], base: Vec<f64>) -> Vec<[f64; 3]> {
|
||||
let mut full = base;
|
||||
for (dof, m) in map.iter().enumerate() {
|
||||
if let (Some(eq), Some(v)) = (m, full.get_mut(dof)) {
|
||||
*v = x.get(*eq).copied().unwrap_or(0.0);
|
||||
}
|
||||
}
|
||||
full.as_chunks::<3>().0.to_vec()
|
||||
/// The mesh nodes' part of a full vector, per node, and the reference points' part, per point.
|
||||
fn split(full: &[f64], mesh: &Mesh) -> (Vec<[f64; 3]>, Vec<[f64; 6]>) {
|
||||
let (nodes, points) = full.split_at(mesh.mesh_dofs().min(full.len()));
|
||||
(nodes.as_chunks::<3>().0.to_vec(), points.as_chunks::<6>().0.to_vec())
|
||||
}
|
||||
|
||||
/// Constraint rows, prescribed values and their elimination for a step.
|
||||
/// The model, its resolved view, the mesh and the step being solved.
|
||||
struct Setup<'a> {
|
||||
model: &'a Model,
|
||||
resolved: &'a Resolved,
|
||||
mesh: &'a Mesh,
|
||||
step: &'a Step,
|
||||
}
|
||||
|
||||
fn reduction(s: &Setup, values: bool, order: &[usize], budget: Budget, warnings: &mut Vec<String>) -> Result<Reduction> {
|
||||
let rows = coupling_rows(s.model, s.resolved, s.mesh)?;
|
||||
Reduction::new(s.mesh, order, &rows, |active| prescribed(s.model, s.resolved, s.mesh, s.step, values, active, warnings), budget)
|
||||
}
|
||||
|
||||
/// The reduced matrices' memory budget: the factorization's.
|
||||
fn budget(limits: &Limits, graph: &NodeGraph, matrices: usize) -> Budget {
|
||||
Budget { max_bytes: limits.max_factor_entries.saturating_mul(8), pattern_entries: graph.entries(), matrices }
|
||||
}
|
||||
|
||||
/// Solve a linear static step: the step named `step`, or the model's only static step.
|
||||
@@ -190,49 +220,42 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
|
||||
let resolved = model.resolve()?;
|
||||
let step = select_step(model, step, |p| matches!(p, Procedure::Static {}), "static")?;
|
||||
let mesh = Mesh::new(model, &resolved, limits.max_dofs)?;
|
||||
let fixed = prescribed(model, &resolved, &mesh, step, true)?;
|
||||
let f = loads(model, &resolved, &mesh, step)?;
|
||||
let mut warnings: Vec<String> = resolved.warnings.iter().map(ToString::to_string).collect();
|
||||
let graph = NodeGraph::new(mesh.labels.len(), mesh.elements.iter().map(|e| e.nodes.as_slice()));
|
||||
let order = nested_dissection(&mesh.xyz, &graph.adjacency);
|
||||
let setup = Setup { model, resolved: &resolved, mesh: &mesh, step };
|
||||
let red = reduction(&setup, true, &order, budget(limits, &graph, 1), &mut warnings)?;
|
||||
let f = loads(model, &resolved, &mesh, step, &red.active)?;
|
||||
let k = assemble(&mesh, &graph, None)?.k;
|
||||
let (map, neq) = equations(&nested_dissection(&mesh.xyz, &graph.adjacency), &fixed);
|
||||
if neq == 0 {
|
||||
if red.neq() == 0 {
|
||||
return Err(Error::Invalid("every degree of freedom is prescribed; there is nothing to solve".into()));
|
||||
}
|
||||
let chol = factor_raw(&k.restrict(&map, neq), limits).map_err(|e| factor_error(e, &mesh, &map))?;
|
||||
// Right-hand side: f_free − K_fp u_p (K u_p only when something moves).
|
||||
let up: Vec<f64> = fixed.iter().map(|v| v.unwrap_or(0.0)).collect();
|
||||
let mut kup = vec![0.0; mesh.dofs()];
|
||||
if up.iter().any(|v| *v != 0.0) {
|
||||
k.mul(&up, &mut kup);
|
||||
}
|
||||
let mut rhs = vec![0.0; neq];
|
||||
for (dof, m) in map.iter().enumerate() {
|
||||
if let (Some(eq), Some(fv), Some(kv)) = (m, f.get(dof), kup.get(dof))
|
||||
&& let Some(r) = rhs.get_mut(*eq)
|
||||
{
|
||||
*r = fv - kv;
|
||||
}
|
||||
}
|
||||
chol.solve(&mut rhs)?;
|
||||
let displacement = per_node(&rhs, &map, up);
|
||||
if displacement.iter().flatten().any(|x| !x.is_finite()) {
|
||||
let [kff] = red.reduce([&k])?;
|
||||
let chol = factor_raw(&kff, limits).map_err(|e| factor_error(e, &mesh, &red))?;
|
||||
drop(kff);
|
||||
let mut q = red.rhs(&f, &k);
|
||||
chol.solve(&mut q)?;
|
||||
let u = red.expand(&q, true);
|
||||
if u.iter().any(|x| !x.is_finite()) {
|
||||
return Err(Error::Singular("the solution is not finite; the model is ill-conditioned".into()));
|
||||
}
|
||||
// Reactions R = K u − f at the prescribed degrees of freedom (only their rows of K).
|
||||
let mut reaction = vec![[0.0; 3]; mesh.labels.len()];
|
||||
let (displacement, point_motion) = split(&u, &mesh);
|
||||
let reactions = red.reactions(&k, &u, &f);
|
||||
let (reaction, point_reaction) = split(&reactions, &mesh);
|
||||
let (mut applied_total, mut reaction_total) = ([0.0; 3], [0.0; 3]);
|
||||
for (dof, fv) in f.iter().enumerate() {
|
||||
let (node, c) = (dof / 3, dof % 3);
|
||||
applied_total[c] += fv;
|
||||
if fixed.get(dof).is_some_and(Option::is_some) {
|
||||
let (cols, vals) = k.row(dof);
|
||||
let ku: f64 = cols.iter().zip(vals).map(|(j, v)| v * displacement.get(j / 3).map_or(0.0, |u| u[j % 3])).sum();
|
||||
if let Some(rn) = reaction.get_mut(node) {
|
||||
rn[c] = ku - fv;
|
||||
}
|
||||
reaction_total[c] += ku - fv;
|
||||
for (dof, (fv, rv)) in f.iter().zip(&reactions).enumerate() {
|
||||
let d = mesh.direction(dof);
|
||||
if let (Some(a), Some(r)) = (applied_total.get_mut(d), reaction_total.get_mut(d)) {
|
||||
*a += fv;
|
||||
*r += rv;
|
||||
}
|
||||
}
|
||||
let reference_points = mesh
|
||||
.points
|
||||
.iter()
|
||||
.zip(point_motion.iter().zip(&point_reaction))
|
||||
.map(|(p, (d, r))| PointResult { name: p.name.clone(), label: p.label, displacement: *d, reaction: *r })
|
||||
.collect();
|
||||
// Stresses: one extrapolation per element type and rule.
|
||||
let mut extrapolations: Vec<(ElementKind, Rule, Extrapolation)> = Vec::new();
|
||||
let mut elements = Vec::with_capacity(mesh.elements.len());
|
||||
@@ -266,7 +289,6 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
|
||||
}
|
||||
let stress: Vec<[f64; 6]> = sum.iter().zip(&count).map(|(s, c)| s.map(|v| if *c > 0 { v / f64::from(*c) } else { 0.0 })).collect();
|
||||
let von_mises = stress.iter().map(von_mises).collect();
|
||||
let mut warnings: Vec<String> = resolved.warnings.iter().map(ToString::to_string).collect();
|
||||
let scale = applied_total.iter().chain(&reaction_total).fold(0.0f64, |m, v| m.max(v.abs()));
|
||||
let imbalance = (0..3).map(|c| (applied_total[c] + reaction_total[c]).abs()).fold(0.0, f64::max);
|
||||
if scale > 0.0 && imbalance > 1e-6 * scale {
|
||||
@@ -280,9 +302,10 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
|
||||
stress,
|
||||
von_mises,
|
||||
elements,
|
||||
reference_points,
|
||||
applied_total,
|
||||
reaction_total,
|
||||
stats: Stats { dofs: mesh.dofs(), equations: neq, factor_entries: chol.entries(), lanczos_vectors: 0 },
|
||||
stats: Stats { dofs: mesh.dofs(), equations: red.neq(), factor_entries: chol.entries(), lanczos_vectors: 0 },
|
||||
warnings,
|
||||
})
|
||||
}
|
||||
@@ -324,21 +347,22 @@ pub fn solve_frequency(model: &Model, step: Option<&str>, options: &FrequencyOpt
|
||||
return Err(Error::Invalid(format!("the shift must be a finite number, got {s}")));
|
||||
}
|
||||
let mesh = Mesh::new(model, &resolved, limits.max_dofs)?;
|
||||
let fixed = prescribed(model, &resolved, &mesh, step, false)?;
|
||||
let mut warnings: Vec<String> = resolved.warnings.iter().map(ToString::to_string).collect();
|
||||
let graph = NodeGraph::new(mesh.labels.len(), mesh.elements.iter().map(|e| e.nodes.as_slice()));
|
||||
let order = nested_dissection(&mesh.xyz, &graph.adjacency);
|
||||
let setup = Setup { model, resolved: &resolved, mesh: &mesh, step };
|
||||
let red = reduction(&setup, false, &order, budget(limits, &graph, 2), &mut warnings)?;
|
||||
let Assembled { k, m } = assemble(&mesh, &graph, Some(options.mass))?;
|
||||
let m = m.ok_or_else(|| Error::Internal("mass matrix".into()))?;
|
||||
// Every element's mass entries sum to its mass, once per direction.
|
||||
let total_mass = m.val.iter().sum::<f64>() / 3.0;
|
||||
let (map, neq) = equations(&nested_dissection(&mesh.xyz, &graph.adjacency), &fixed);
|
||||
let neq = red.neq();
|
||||
if neq == 0 {
|
||||
return Err(Error::Invalid("every degree of freedom is held; there is nothing to vibrate".into()));
|
||||
}
|
||||
let kff = k.restrict(&map, neq);
|
||||
let mff = m.restrict(&map, neq);
|
||||
let [kff, mff] = red.reduce([&k, &m])?;
|
||||
drop((k, m));
|
||||
let mut warnings: Vec<String> = resolved.warnings.iter().map(ToString::to_string).collect();
|
||||
let shifted = |s: f64| -> Result<Factor> { factor_raw(&kff.add_scaled(-s, &mff)?, limits).map_err(|e| factor_error(e, &mesh, &map)) };
|
||||
let shifted = |s: f64| -> Result<Factor> { factor_raw(&kff.add_scaled(-s, &mff)?, limits).map_err(|e| factor_error(e, &mesh, &red)) };
|
||||
let (shift, chol) = match options.shift {
|
||||
Some(s) => (s, shifted(s)?),
|
||||
None => match factor_raw(&kff, limits) {
|
||||
@@ -353,7 +377,7 @@ pub fn solve_frequency(model: &Model, step: Option<&str>, options: &FrequencyOpt
|
||||
warnings.push("the structure is not fully supported; rigid-body modes appear at (near) zero frequency".into());
|
||||
(s, shifted(s)?)
|
||||
}
|
||||
Err(e) => return Err(factor_error(e, &mesh, &map)),
|
||||
Err(e) => return Err(factor_error(e, &mesh, &red)),
|
||||
},
|
||||
};
|
||||
drop(kff);
|
||||
@@ -369,15 +393,11 @@ pub fn solve_frequency(model: &Model, step: Option<&str>, options: &FrequencyOpt
|
||||
let max_basis = limits.max_lanczos_vectors.min(by_memory).max(BLOCK.min(neq));
|
||||
let op = ShiftInvert { m: &mff, factor: &chol };
|
||||
let pairs = lanczos(&op, shift, wanted, BLOCK, max_basis, TOLERANCE)?;
|
||||
// Participation: Γ_d = φᵀ M r_d with r_d the unit translation along d on the free dofs.
|
||||
// Participation: Γ_d = φᵀ M r_d with r_d the unit translation along d on the free equations
|
||||
// (mesh and reference point translations).
|
||||
let mr: Vec<Vec<f64>> = (0..3)
|
||||
.map(|d| {
|
||||
let mut r = vec![0.0; neq];
|
||||
for (dof, mm) in map.iter().enumerate() {
|
||||
if let Some(x) = mm.filter(|_| dof % 3 == d).and_then(|eq| r.get_mut(eq)) {
|
||||
*x = 1.0;
|
||||
}
|
||||
}
|
||||
let r: Vec<f64> = red.eq_dof.iter().map(|dof| if mesh.direction(*dof) == d { 1.0 } else { 0.0 }).collect();
|
||||
let mut y = vec![0.0; neq];
|
||||
mff.mul(&r, &mut y);
|
||||
y
|
||||
@@ -386,12 +406,14 @@ pub fn solve_frequency(model: &Model, step: Option<&str>, options: &FrequencyOpt
|
||||
let mut out = Vec::with_capacity(pairs.values.len());
|
||||
for (lambda, phi) in pairs.values.iter().zip(&pairs.vectors) {
|
||||
let participation = [0, 1, 2].map(|d| mr.get(d).map_or(0.0, |y| y.iter().zip(phi).map(|(a, b)| a * b).sum()));
|
||||
let (shape, point_shape) = split(&red.expand(phi, false), &mesh);
|
||||
out.push(Mode {
|
||||
eigenvalue: *lambda,
|
||||
frequency: lambda.max(0.0).sqrt() / two_pi,
|
||||
shape: per_node(phi, &map, vec![0.0; mesh.dofs()]),
|
||||
shape,
|
||||
participation,
|
||||
effective_mass: participation.map(|g| g * g),
|
||||
point_shape,
|
||||
});
|
||||
}
|
||||
if out.len() < wanted.count && max_hz.is_none() {
|
||||
@@ -400,6 +422,7 @@ pub fn solve_frequency(model: &Model, step: Option<&str>, options: &FrequencyOpt
|
||||
Ok(FrequencyResult {
|
||||
step: step.name.clone(),
|
||||
nodes: mesh.labels.clone(),
|
||||
reference_points: mesh.points.iter().map(|p| (p.name.clone(), p.label)).collect(),
|
||||
modes: out,
|
||||
total_mass,
|
||||
stats: Stats { dofs: mesh.dofs(), equations: neq, factor_entries: chol.entries(), lanczos_vectors: pairs.basis },
|
||||
@@ -407,6 +430,13 @@ pub fn solve_frequency(model: &Model, step: Option<&str>, options: &FrequencyOpt
|
||||
})
|
||||
}
|
||||
|
||||
/// Where a label's result is in a [`StepResult`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Location {
|
||||
Node(usize),
|
||||
Point(usize),
|
||||
}
|
||||
|
||||
/// The result of one step, as a session keeps it.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum StepResult {
|
||||
@@ -428,4 +458,17 @@ impl StepResult {
|
||||
StepResult::Frequency(r) => &r.nodes,
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a label's result is: a mesh node (index into [`Self::nodes`]) or a coupled
|
||||
/// reference point (index into the result's reference points).
|
||||
pub fn locate(&self, label: u32) -> Option<Location> {
|
||||
if let Ok(i) = self.nodes().binary_search(&label) {
|
||||
return Some(Location::Node(i));
|
||||
}
|
||||
match self {
|
||||
StepResult::Static(r) => r.reference_points.iter().position(|p| p.label == label),
|
||||
StepResult::Frequency(r) => r.reference_points.iter().position(|p| p.1 == label),
|
||||
}
|
||||
.map(Location::Point)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,651 @@
|
||||
//! Couplings of reference points to the mesh, and the elimination of every linear constraint
|
||||
//! (couplings and prescribed values) from the system.
|
||||
//!
|
||||
//! A coupled reference point is a node with six degrees of freedom (three translations, three
|
||||
//! rotations) after the mesh's translational ones. Each coupling becomes linear constraint rows
|
||||
//! Σ cⱼ uⱼ = 0:
|
||||
//!
|
||||
//! - **Kinematic** (a rigid link): every node s of the set follows the reference point r rigidly
|
||||
//! in the coupled translations, u_s = u_r + θ_r × (x_s − x_r) (small rotations; R. D. Cook,
|
||||
//! D. S. Malkus, M. E. Plesha, R. J. Witt, *Concepts and Applications of Finite Element
|
||||
//! Analysis*, 4th ed., Wiley 2002, §9.2). The reference point's rotations take part through the
|
||||
//! lever arms, so a moment on it reaches the face as a couple even when only translations are
|
||||
//! coupled; coupled rotations have nothing to act on at solid nodes, which have none.
|
||||
//! - **Distributing** (an interpolation constraint): the reference point moves with the rigid
|
||||
//! motion that fits the set's nodal translations best in the least-squares sense, with equal
|
||||
//! weights (as the model and the deck writers define it). About the nodes' centroid c, with
|
||||
//! rᵢ = xᵢ − c, the fit is u_c = Σ uᵢ / N and θ = J⁻¹ Σ rᵢ × uᵢ with
|
||||
//! J = Σ (|rᵢ|² I − rᵢ rᵢᵀ); the reference point's motion is θ_r = θ and
|
||||
//! u_r = u_c + θ × (x_r − c). Only the coupled degrees of freedom are tied. By virtual work a
|
||||
//! force or moment on the point is spread over the nodes as the forces whose resultant and
|
||||
//! moment about the point equal it, weighted like the fit, and the region stays free to deform
|
||||
//! (G. H. K. Heirman, W. Desmet, *Interface reduction of flexible bodies for efficient modeling
|
||||
//! of body flexibility in multibody dynamics*, Multibody Syst. Dyn. 24(2):219–234, 2010, on
|
||||
//! interpolation constraints). Nodes on one line cannot carry a rotation about that line: that
|
||||
//! is an error when rotations are coupled, and their mean motion is used otherwise.
|
||||
//!
|
||||
//! **Elimination** (master–slave by transformation, Cook et al. 2002 §9.2; C. A. Felippa,
|
||||
//! *Introduction to Finite Element Methods*, ch. 8, University of Colorado lecture notes): every
|
||||
//! degree of freedom is free, prescribed (a boundary condition), dependent on others, or inactive
|
||||
//! (a reference point direction no coupling ties). Rows are taken one at a time in a fixed order
|
||||
//! (boundary conditions first, then couplings by name, nodes by label, directions in order). A
|
||||
//! row is first rewritten in terms of the current free and prescribed degrees of freedom; one
|
||||
//! free degree of freedom is then chosen to depend on the rest: the row's intended one (the
|
||||
//! coupled node, or the distributing coupling's reference point) while it is free, otherwise the
|
||||
//! largest coefficient, lowest index first (the automatic choice of J. F. Abel, M. S. Shephard,
|
||||
//! *An algorithm for multipoint constraints in finite element analysis*, IJNME 14(3):464–467,
|
||||
//! 1979). A row with no free degree of freedom left is redundant when the prescribed values
|
||||
//! satisfy it and contradictory otherwise. Every dependent degree of freedom ends up as a
|
||||
//! combination of free and prescribed ones, u = T_f q + T_p u_p, so the reduced system is
|
||||
//! T_fᵀ K T_f q = T_fᵀ (f − K T_p u_p) and the reactions at prescribed degrees of freedom are
|
||||
//! T_pᵀ (K u − f): no penalty numbers and no Lagrange multipliers, so the matrix stays symmetric
|
||||
//! positive definite for the Cholesky factorization. With no coupling, T is the identity on the
|
||||
//! free degrees of freedom and the result is the plain elimination of prescribed values, bit for
|
||||
//! bit.
|
||||
//!
|
||||
//! **Memory.** A held distributing coupling point makes one coupled node depend on all the others,
|
||||
//! a dense (3N)² block in the reduced matrix; its size is estimated from the rows and refused
|
||||
//! against the factorization's memory budget before anything is built, and every other reduction
|
||||
//! counts its entries (stopping at the budget) before it allocates them.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use modelcraft_fem::dense::inverse3;
|
||||
use modelcraft_model::{CouplingKind, Model, Resolved};
|
||||
|
||||
use crate::sparse::Csr;
|
||||
use crate::system::Mesh;
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// One linear constraint Σ cⱼ uⱼ = 0 and the degree of freedom meant to be eliminated with it.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Row<'m> {
|
||||
pub terms: Vec<(usize, f64)>,
|
||||
pub pivot: usize,
|
||||
/// The name of the coupling it comes from (for messages).
|
||||
pub coupling: &'m str,
|
||||
}
|
||||
|
||||
/// A coefficient below this fraction of a row's largest one is round-off, not a constraint.
|
||||
const NEGLIGIBLE: f64 = 1e-10;
|
||||
|
||||
fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||||
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
|
||||
}
|
||||
|
||||
/// [a]×, the matrix of a × ·.
|
||||
fn cross_matrix(a: [f64; 3]) -> [[f64; 3]; 3] {
|
||||
[[0.0, -a[2], a[1]], [a[2], 0.0, -a[0]], [-a[1], a[0], 0.0]]
|
||||
}
|
||||
|
||||
fn mat_mul(a: &[[f64; 3]; 3], b: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
|
||||
let mut c = [[0.0; 3]; 3];
|
||||
for i in 0..3 {
|
||||
for j in 0..3 {
|
||||
c[i][j] = (0..3).map(|k| a[i][k] * b[k][j]).sum();
|
||||
}
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
/// The constraint rows of every coupling: couplings by name, nodes by label, directions in order.
|
||||
pub fn coupling_rows<'m>(model: &'m Model, resolved: &Resolved, mesh: &Mesh) -> Result<Vec<Row<'m>>> {
|
||||
let mut rows = Vec::new();
|
||||
for (name, c) in model.couplings() {
|
||||
let point = mesh.point(&c.reference_point).ok_or_else(|| Error::Internal(format!("reference point `{}`", c.reference_point)))?;
|
||||
let xr = mesh.points.get(point).map(|p| p.xyz).ok_or_else(|| Error::Internal("reference point index".into()))?;
|
||||
let rp = mesh.point_dof(point, 0);
|
||||
let set = resolved.sets.get(&c.set).ok_or_else(|| Error::Invalid(format!("coupling `{name}`: set `{}` is not resolved", c.set)))?;
|
||||
if set.nodes.is_empty() {
|
||||
return Err(Error::Invalid(format!("coupling `{name}`: set `{}` has no nodes", c.set)));
|
||||
}
|
||||
let nodes: Vec<usize> = set
|
||||
.nodes
|
||||
.iter()
|
||||
.map(|l| mesh.node(*l).ok_or_else(|| Error::Invalid(format!("coupling `{name}`: node {l} belongs to no solid element"))))
|
||||
.collect::<Result<_>>()?;
|
||||
let xyz = |n: usize| mesh.xyz.get(n).copied().unwrap_or_default();
|
||||
// The coupled directions of the reference point, 0–5.
|
||||
let coupled = |range: std::ops::Range<usize>| range.filter(|k| c.dofs.contains(*k as u8 + 1));
|
||||
match c.kind {
|
||||
CouplingKind::Kinematic => {
|
||||
if coupled(0..3).next().is_none() {
|
||||
return Err(Error::Invalid(format!(
|
||||
"coupling `{name}` couples only rotations, which the nodes of solid elements do not have; couple translations too"
|
||||
)));
|
||||
}
|
||||
for &n in &nodes {
|
||||
let r = sub(xyz(n), xr);
|
||||
for k in coupled(0..3) {
|
||||
// u_s,k − u_r,k − (θ × r)_k = 0, (θ × r)_k = θ_a r_b − θ_b r_a.
|
||||
let (a, b) = ((k + 1) % 3, (k + 2) % 3);
|
||||
let mut terms = vec![(3 * n + k, 1.0), (rp + k, -1.0)];
|
||||
if r[b] != 0.0 {
|
||||
terms.push((rp + 3 + a, -r[b]));
|
||||
}
|
||||
if r[a] != 0.0 {
|
||||
terms.push((rp + 3 + b, r[a]));
|
||||
}
|
||||
rows.push(Row { terms, pivot: 3 * n + k, coupling: name });
|
||||
}
|
||||
}
|
||||
}
|
||||
CouplingKind::Distributing => {
|
||||
let count = nodes.len() as f64;
|
||||
let mut centroid = [0.0; 3];
|
||||
for &n in &nodes {
|
||||
let x = xyz(n);
|
||||
for k in 0..3 {
|
||||
centroid[k] += x[k] / count;
|
||||
}
|
||||
}
|
||||
let arms: Vec<[f64; 3]> = nodes.iter().map(|n| sub(xyz(*n), centroid)).collect();
|
||||
let mut j = [[0.0; 3]; 3];
|
||||
for r in &arms {
|
||||
let rr = r[0] * r[0] + r[1] * r[1] + r[2] * r[2];
|
||||
for a in 0..3 {
|
||||
for b in 0..3 {
|
||||
j[a][b] += if a == b { rr } else { 0.0 } - r[a] * r[b];
|
||||
}
|
||||
}
|
||||
}
|
||||
let trace = j[0][0] + j[1][1] + j[2][2];
|
||||
let inverse = inverse3(&j).filter(|(_, det)| *det > 1e-10 * trace.powi(3) / 27.0).map(|(inv, _)| inv);
|
||||
if inverse.is_none() && coupled(3..6).next().is_some() {
|
||||
return Err(Error::Invalid(format!(
|
||||
"coupling `{name}`: the nodes of set `{}` lie on one line (or at one point), so a distributing coupling cannot carry rotations through them; couple translations only or use a kinematic coupling",
|
||||
c.set
|
||||
)));
|
||||
}
|
||||
// θ = Σ Gᵢ uᵢ with Gᵢ = J⁻¹ [rᵢ]×; u_r = Σ (I/N − [d]× Gᵢ) uᵢ with d = x_r − c.
|
||||
let d = cross_matrix(sub(xr, centroid));
|
||||
let g: Vec<[[f64; 3]; 3]> = arms.iter().map(|r| inverse.map_or([[0.0; 3]; 3], |inv| mat_mul(&inv, &cross_matrix(*r)))).collect();
|
||||
for k in coupled(0..6) {
|
||||
let mut terms = Vec::with_capacity(1 + 3 * nodes.len());
|
||||
terms.push((rp + k, 1.0));
|
||||
for (&n, gi) in nodes.iter().zip(&g) {
|
||||
for comp in 0..3 {
|
||||
let a = if k < 3 {
|
||||
let dg: f64 = (0..3).map(|m| d[k][m] * gi[m][comp]).sum();
|
||||
if k == comp { 1.0 / count - dg } else { -dg }
|
||||
} else {
|
||||
gi[k - 3][comp]
|
||||
};
|
||||
if a != 0.0 {
|
||||
terms.push((3 * n + comp, -a));
|
||||
}
|
||||
}
|
||||
}
|
||||
rows.push(Row { terms, pivot: rp + k, coupling: name });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// The degrees of freedom that exist: every mesh translation, and the reference point
|
||||
/// directions some coupling row involves.
|
||||
fn active_dofs(mesh: &Mesh, rows: &[Row]) -> Vec<bool> {
|
||||
let mut active = vec![false; mesh.dofs()];
|
||||
active.iter_mut().take(mesh.mesh_dofs()).for_each(|a| *a = true);
|
||||
for row in rows {
|
||||
for (dof, c) in &row.terms {
|
||||
if *c != 0.0
|
||||
&& let Some(a) = active.get_mut(*dof)
|
||||
{
|
||||
*a = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
active
|
||||
}
|
||||
|
||||
/// A sparse list of (index, coefficient) per row.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct Terms {
|
||||
ptr: Vec<usize>,
|
||||
idx: Vec<usize>,
|
||||
val: Vec<f64>,
|
||||
}
|
||||
|
||||
impl Terms {
|
||||
fn row(&self, r: usize) -> impl Iterator<Item = (usize, f64)> + '_ {
|
||||
let (a, b) = (self.ptr.get(r).copied().unwrap_or(0), self.ptr.get(r + 1).copied().unwrap_or(0));
|
||||
self.idx.get(a..b).unwrap_or_default().iter().copied().zip(self.val.get(a..b).unwrap_or_default().iter().copied())
|
||||
}
|
||||
|
||||
fn is_empty_row(&self, r: usize) -> bool {
|
||||
self.ptr.get(r) == self.ptr.get(r + 1)
|
||||
}
|
||||
|
||||
fn push_row(&mut self, terms: impl Iterator<Item = (usize, f64)>) {
|
||||
if self.ptr.is_empty() {
|
||||
self.ptr.push(0);
|
||||
}
|
||||
for (i, v) in terms {
|
||||
self.idx.push(i);
|
||||
self.val.push(v);
|
||||
}
|
||||
self.ptr.push(self.idx.len());
|
||||
}
|
||||
|
||||
/// The transpose, with `n` rows.
|
||||
fn transpose(&self, n: usize) -> Terms {
|
||||
let rows = self.ptr.len().saturating_sub(1);
|
||||
let mut count = vec![0usize; n + 1];
|
||||
for i in &self.idx {
|
||||
if let Some(c) = count.get_mut(i + 1) {
|
||||
*c += 1;
|
||||
}
|
||||
}
|
||||
for i in 0..n {
|
||||
count[i + 1] += count[i];
|
||||
}
|
||||
let mut next = count.clone();
|
||||
let mut idx = vec![0; self.idx.len()];
|
||||
let mut val = vec![0.0; self.idx.len()];
|
||||
for r in 0..rows {
|
||||
for (i, v) in self.row(r) {
|
||||
if let Some(slot) = next.get_mut(i)
|
||||
&& let (Some(x), Some(y)) = (idx.get_mut(*slot), val.get_mut(*slot))
|
||||
{
|
||||
*x = r;
|
||||
*y = v;
|
||||
*slot += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Terms { ptr: count, idx, val }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum State {
|
||||
Free,
|
||||
Fixed,
|
||||
Dependent(usize),
|
||||
Inactive,
|
||||
}
|
||||
|
||||
/// Sort terms by degree of freedom (stably, so equal ones add in the order given) and add the
|
||||
/// equal ones, dropping exact zeros.
|
||||
fn combine(mut terms: Vec<(usize, f64)>) -> Vec<(usize, f64)> {
|
||||
terms.sort_by_key(|t| t.0);
|
||||
let mut out: Vec<(usize, f64)> = Vec::with_capacity(terms.len());
|
||||
for (dof, c) in terms {
|
||||
match out.last_mut() {
|
||||
Some(last) if last.0 == dof => last.1 += c,
|
||||
_ => out.push((dof, c)),
|
||||
}
|
||||
}
|
||||
out.retain(|t| t.1 != 0.0);
|
||||
out
|
||||
}
|
||||
|
||||
/// The map from the free equations and prescribed values to every degree of freedom.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Reduction {
|
||||
/// The degree of freedom of each free equation.
|
||||
pub eq_dof: Vec<usize>,
|
||||
/// Whether a degree of freedom exists (mesh translations, and the reference point
|
||||
/// directions some coupling ties).
|
||||
pub active: Vec<bool>,
|
||||
/// T_f: per degree of freedom, (equation, coefficient).
|
||||
free: Terms,
|
||||
/// T_fᵀ: per equation, (degree of freedom, coefficient).
|
||||
columns: Terms,
|
||||
/// T_p: per degree of freedom, (prescribed degree of freedom, coefficient).
|
||||
fixed: Terms,
|
||||
/// T_p u_p, the motion of the prescribed values alone.
|
||||
prescribed_motion: Vec<f64>,
|
||||
/// Whether any degree of freedom depends on others (T is not a plain selection).
|
||||
has_dependents: bool,
|
||||
budget: Budget,
|
||||
}
|
||||
|
||||
/// The memory the reduced matrices may take, checked before they are built: the same budget as
|
||||
/// the factorization's (`Limits::max_factor_entries` doubles of 8 bytes).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Budget {
|
||||
pub max_bytes: usize,
|
||||
/// Entries of the assembled mesh pattern.
|
||||
pub pattern_entries: usize,
|
||||
/// Matrices reduced together (stiffness, and mass for frequency steps).
|
||||
pub matrices: usize,
|
||||
}
|
||||
|
||||
impl Budget {
|
||||
/// Bytes of `entries` reduced entries: one column index and one value per matrix.
|
||||
fn bytes(&self, entries: usize) -> usize {
|
||||
entries.saturating_mul(8 * (1 + self.matrices))
|
||||
}
|
||||
|
||||
fn exceeded(&self, entries: usize) -> bool {
|
||||
self.bytes(entries) > self.max_bytes
|
||||
}
|
||||
}
|
||||
|
||||
/// An empty vector with room for `n` elements, or a limit error when the memory is not there.
|
||||
fn reserved<T>(n: usize) -> Result<Vec<T>> {
|
||||
let mut v = Vec::new();
|
||||
v.try_reserve_exact(n).map_err(|_| Error::Limit(format!("cannot allocate {} for the reduced matrices", gigabytes(n.saturating_mul(8)))))?;
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn gigabytes(bytes: usize) -> String {
|
||||
format!("{:.2} GB", bytes as f64 / 1e9)
|
||||
}
|
||||
|
||||
impl Reduction {
|
||||
/// Eliminate the prescribed values (`prescribed(active)` gives them) and the constraint rows.
|
||||
/// `order` is the mesh nodes' order for numbering the free equations; reference point
|
||||
/// equations follow them.
|
||||
///
|
||||
/// A distributing coupling whose reference point is held makes one of its nodes depend on
|
||||
/// all the others, which couples every coupled degree of freedom with every other: a dense
|
||||
/// (3N)² block in the reduced matrix. Its size is checked against `budget` before anything is
|
||||
/// built.
|
||||
pub fn new(
|
||||
mesh: &Mesh,
|
||||
order: &[usize],
|
||||
rows: &[Row],
|
||||
prescribed: impl FnOnce(&[bool]) -> Result<Vec<Option<f64>>>,
|
||||
budget: Budget,
|
||||
) -> Result<Reduction> {
|
||||
let n = mesh.dofs();
|
||||
let active = active_dofs(mesh, rows);
|
||||
let prescribed = prescribed(&active)?;
|
||||
let mut state: Vec<State> = (0..n)
|
||||
.map(|d| match (active.get(d).copied().unwrap_or(false), prescribed.get(d).copied().flatten()) {
|
||||
(false, _) => State::Inactive,
|
||||
(true, Some(_)) => State::Fixed,
|
||||
(true, None) => State::Free,
|
||||
})
|
||||
.collect();
|
||||
let values: Vec<f64> = (0..n).map(|d| prescribed.get(d).copied().flatten().unwrap_or(0.0)).collect();
|
||||
// Dense blocks: the mesh degrees of freedom of the rows meant to eliminate a held
|
||||
// reference point, one block per coupling.
|
||||
let mut coupled: BTreeMap<&str, Vec<usize>> = BTreeMap::new();
|
||||
for row in rows {
|
||||
if row.pivot >= mesh.mesh_dofs() && state.get(row.pivot) == Some(&State::Fixed) {
|
||||
coupled.entry(row.coupling).or_default().extend(row.terms.iter().map(|t| t.0).filter(|d| *d < mesh.mesh_dofs()));
|
||||
}
|
||||
}
|
||||
let mut blocks: Vec<(&str, usize, usize)> = Vec::new();
|
||||
for (name, mut dofs) in coupled {
|
||||
dofs.sort_unstable();
|
||||
dofs.dedup();
|
||||
let mut nodes: Vec<usize> = dofs.iter().map(|d| d / 3).collect();
|
||||
nodes.dedup();
|
||||
blocks.push((name, nodes.len(), dofs.len().saturating_mul(dofs.len())));
|
||||
}
|
||||
let dense: usize = blocks.iter().fold(0, |a, b| a.saturating_add(b.2));
|
||||
if dense > 0 && budget.exceeded(budget.pattern_entries.saturating_add(dense)) {
|
||||
let (name, nodes, _) = blocks.iter().max_by_key(|b| b.2).copied().unwrap_or(("?", 0, 0));
|
||||
return Err(Error::Limit(format!(
|
||||
"distributing coupling `{name}` couples {nodes} nodes and its reference point is held, which ties every coupled node to every other: the reduced stiffness needs about {} (the limit is {}); split the surface into smaller couplings, hold the surface itself, or use a kinematic coupling",
|
||||
gigabytes(budget.bytes(budget.pattern_entries.saturating_add(dense))),
|
||||
gigabytes(budget.max_bytes)
|
||||
)));
|
||||
}
|
||||
// Dependent expressions in free and prescribed degrees of freedom, and for each free
|
||||
// degree of freedom the expressions that use it (possibly listed twice).
|
||||
let mut exprs: Vec<Vec<(usize, f64)>> = Vec::new();
|
||||
let mut users: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
|
||||
for row in rows {
|
||||
let scale = row.terms.iter().fold(0.0f64, |m, t| m.max(t.1.abs()));
|
||||
if !(scale > 0.0 && scale.is_finite()) {
|
||||
return Err(Error::Invalid(format!("coupling `{}`: a constraint with no finite coefficient", row.coupling)));
|
||||
}
|
||||
// The row in current free and prescribed degrees of freedom.
|
||||
let mut expanded = Vec::with_capacity(row.terms.len());
|
||||
for &(dof, c) in &row.terms {
|
||||
match state.get(dof).copied() {
|
||||
Some(State::Free | State::Fixed) => expanded.push((dof, c)),
|
||||
Some(State::Dependent(e)) => expanded.extend(exprs.get(e).into_iter().flatten().map(|t| (t.0, c * t.1))),
|
||||
Some(State::Inactive) | None => return Err(Error::Internal("a constraint on a missing degree of freedom".into())),
|
||||
}
|
||||
}
|
||||
let terms = combine(expanded);
|
||||
let candidate = |t: &&(usize, f64)| state.get(t.0) == Some(&State::Free) && t.1.abs() > NEGLIGIBLE * scale;
|
||||
let intended = terms.iter().filter(candidate).find(|t| t.0 == row.pivot);
|
||||
let largest = terms.iter().filter(candidate).fold(None, |best: Option<&(usize, f64)>, t| match best {
|
||||
Some(b) if b.1.abs() >= t.1.abs() => Some(b),
|
||||
_ => Some(t),
|
||||
});
|
||||
let Some(&(pivot, cp)) = intended.or(largest) else {
|
||||
// Only prescribed degrees of freedom (or round-off) are left.
|
||||
let value = |t: &(usize, f64)| t.1 * values.get(t.0).copied().unwrap_or(0.0);
|
||||
let residual: f64 = terms.iter().filter(|t| state.get(t.0) == Some(&State::Fixed)).map(value).sum();
|
||||
let size: f64 = terms.iter().map(|t| value(t).abs()).sum();
|
||||
if residual.abs() > 1e-9 * size.max(f64::MIN_POSITIVE) && residual.abs() > 0.0 {
|
||||
return Err(Error::Invalid(format!(
|
||||
"coupling `{}` contradicts the prescribed values (boundary conditions or other couplings hold its nodes in a way it cannot follow)",
|
||||
row.coupling
|
||||
)));
|
||||
}
|
||||
continue;
|
||||
};
|
||||
let expr: Vec<(usize, f64)> = terms.iter().filter(|t| t.0 != pivot).map(|t| (t.0, -t.1 / cp)).collect();
|
||||
let free_terms: Vec<usize> = expr.iter().filter(|t| state.get(t.0) == Some(&State::Free)).map(|t| t.0).collect();
|
||||
// Substitute the pivot in the expressions that use it.
|
||||
if let Some(mut using) = users.remove(&pivot) {
|
||||
using.sort_unstable();
|
||||
using.dedup();
|
||||
for u in using {
|
||||
let Some(e) = exprs.get_mut(u) else { continue };
|
||||
let Ok(at) = e.binary_search_by_key(&pivot, |t| t.0) else { continue };
|
||||
let a = e.remove(at).1;
|
||||
let merged = combine(e.drain(..).chain(expr.iter().map(|t| (t.0, a * t.1))).collect());
|
||||
*e = merged;
|
||||
for &dof in &free_terms {
|
||||
users.entry(dof).or_default().push(u);
|
||||
}
|
||||
}
|
||||
}
|
||||
let index = exprs.len();
|
||||
for &dof in &free_terms {
|
||||
users.entry(dof).or_default().push(index);
|
||||
}
|
||||
exprs.push(expr);
|
||||
if let Some(s) = state.get_mut(pivot) {
|
||||
*s = State::Dependent(index);
|
||||
}
|
||||
}
|
||||
// Equations: free mesh degrees of freedom in node order, then reference point ones.
|
||||
let mut eq = vec![usize::MAX; n];
|
||||
let mut eq_dof = Vec::new();
|
||||
let mesh_order = order.iter().flat_map(|node| (0..3).map(move |c| 3 * node + c));
|
||||
for dof in mesh_order.chain(mesh.mesh_dofs()..n) {
|
||||
if state.get(dof) == Some(&State::Free)
|
||||
&& let Some(e) = eq.get_mut(dof)
|
||||
&& *e == usize::MAX
|
||||
{
|
||||
*e = eq_dof.len();
|
||||
eq_dof.push(dof);
|
||||
}
|
||||
}
|
||||
if state.iter().enumerate().any(|(d, s)| *s == State::Free && eq.get(d) == Some(&usize::MAX)) {
|
||||
return Err(Error::Internal("the node order misses a degree of freedom".into()));
|
||||
}
|
||||
let (mut free, mut fixed) = (Terms::default(), Terms::default());
|
||||
for dof in 0..n {
|
||||
match state.get(dof).copied().unwrap_or(State::Inactive) {
|
||||
State::Free => {
|
||||
free.push_row(std::iter::once((eq.get(dof).copied().unwrap_or(0), 1.0)));
|
||||
fixed.push_row(std::iter::empty());
|
||||
}
|
||||
State::Fixed => {
|
||||
free.push_row(std::iter::empty());
|
||||
fixed.push_row(std::iter::once((dof, 1.0)));
|
||||
}
|
||||
State::Dependent(e) => {
|
||||
let expr = exprs.get(e).map(Vec::as_slice).unwrap_or_default();
|
||||
let state = &state;
|
||||
let on = |want: State| expr.iter().filter(move |t| state.get(t.0) == Some(&want));
|
||||
free.push_row(on(State::Free).map(|t| (eq.get(t.0).copied().unwrap_or(0), t.1)));
|
||||
fixed.push_row(on(State::Fixed).copied());
|
||||
}
|
||||
State::Inactive => {
|
||||
free.push_row(std::iter::empty());
|
||||
fixed.push_row(std::iter::empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
let prescribed_motion = (0..n).map(|d| fixed.row(d).map(|(p, c)| c * values.get(p).copied().unwrap_or(0.0)).sum()).collect();
|
||||
let columns = free.transpose(eq_dof.len());
|
||||
Ok(Reduction { eq_dof, active, free, columns, fixed, prescribed_motion, has_dependents: !exprs.is_empty(), budget })
|
||||
}
|
||||
|
||||
/// Free equations.
|
||||
pub fn neq(&self) -> usize {
|
||||
self.eq_dof.len()
|
||||
}
|
||||
|
||||
/// T_fᵀ A T_f for each matrix, all over the mesh's degrees of freedom and on one pattern
|
||||
/// (reference points carry no stiffness or mass of their own). Row by row with a dense
|
||||
/// accumulator (F. G. Gustavson, *Two fast algorithms for sparse matrices*, ACM TOMS
|
||||
/// 4(3):250–269, 1978), in a fixed order, sharing the symbolic work between the matrices;
|
||||
/// structural zeros are kept, so the results share one pattern too.
|
||||
pub fn reduce<const M: usize>(&self, mats: [&Csr; M]) -> Result<[Csr; M]> {
|
||||
if let Some(first) = mats.first()
|
||||
&& mats.iter().any(|m| m.ptr != first.ptr || m.idx != first.idx)
|
||||
{
|
||||
return Err(Error::Internal("matrices with different patterns".into()));
|
||||
}
|
||||
let pattern = mats.first().ok_or_else(|| Error::Internal("no matrix to reduce".into()))?;
|
||||
let n = self.neq();
|
||||
let mut mark = vec![usize::MAX; n];
|
||||
// A plain selection has at most the mesh pattern's entries; otherwise count them first
|
||||
// (stopping at the budget) so nothing larger than the budget is ever allocated.
|
||||
let entries = if self.has_dependents {
|
||||
let budget = Budget { matrices: M, ..self.budget };
|
||||
let mut count = 0usize;
|
||||
for e in 0..n {
|
||||
for (dof, _) in self.columns.row(e) {
|
||||
for b in pattern.row(dof).0 {
|
||||
for (f, _) in self.free.row(*b) {
|
||||
if let Some(m) = mark.get_mut(f)
|
||||
&& *m != e
|
||||
{
|
||||
*m = e;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if budget.exceeded(count) {
|
||||
return Err(Error::Limit(format!(
|
||||
"the reduced matrices need more than {} (the limit is {}); couplings tie too many nodes together",
|
||||
gigabytes(budget.bytes(count)),
|
||||
gigabytes(budget.max_bytes)
|
||||
)));
|
||||
}
|
||||
}
|
||||
mark.iter_mut().for_each(|m| *m = usize::MAX);
|
||||
count
|
||||
} else {
|
||||
pattern.idx.len()
|
||||
};
|
||||
let mut acc = vec![[0.0; M]; n];
|
||||
let mut touched: Vec<usize> = Vec::new();
|
||||
let mut ptr = Vec::with_capacity(n + 1);
|
||||
ptr.push(0);
|
||||
let mut idx: Vec<usize> = reserved(entries)?;
|
||||
let mut vals: [Vec<f64>; M] = std::array::from_fn(|_| Vec::new());
|
||||
for v in &mut vals {
|
||||
*v = reserved(entries)?;
|
||||
}
|
||||
for e in 0..n {
|
||||
touched.clear();
|
||||
for (dof, ca) in self.columns.row(e) {
|
||||
let (start, end) = (pattern.ptr.get(dof).copied().unwrap_or(0), pattern.ptr.get(dof + 1).copied().unwrap_or(0));
|
||||
for at in start..end {
|
||||
let Some(&b) = pattern.idx.get(at) else { continue };
|
||||
let v: [f64; M] = std::array::from_fn(|i| mats.get(i).and_then(|m| m.val.get(at)).copied().unwrap_or(0.0));
|
||||
for (f, cb) in self.free.row(b) {
|
||||
let (Some(m), Some(x)) = (mark.get_mut(f), acc.get_mut(f)) else { continue };
|
||||
if *m != e {
|
||||
*m = e;
|
||||
*x = [0.0; M];
|
||||
touched.push(f);
|
||||
}
|
||||
for i in 0..M {
|
||||
x[i] += ca * v[i] * cb;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
touched.sort_unstable();
|
||||
for &f in &touched {
|
||||
idx.push(f);
|
||||
let x = acc.get(f).copied().unwrap_or([0.0; M]);
|
||||
for i in 0..M {
|
||||
vals[i].push(x[i]);
|
||||
}
|
||||
}
|
||||
ptr.push(idx.len());
|
||||
}
|
||||
Ok(vals.map(|val| {
|
||||
let mut out = Csr { n, ptr: ptr.clone(), idx: idx.clone(), val };
|
||||
if self.has_dependents {
|
||||
out.mirror_lower();
|
||||
}
|
||||
out
|
||||
}))
|
||||
}
|
||||
|
||||
/// The reduced right-hand side T_fᵀ (f − K T_p u_p); `k` is over the mesh's degrees of freedom.
|
||||
pub fn rhs(&self, f: &[f64], k: &Csr) -> Vec<f64> {
|
||||
let mut ku = Vec::new();
|
||||
if self.prescribed_motion.iter().any(|v| *v != 0.0) {
|
||||
ku = vec![0.0; k.n];
|
||||
k.mul(self.prescribed_motion.get(..k.n).unwrap_or_default(), &mut ku);
|
||||
}
|
||||
let mut rhs = vec![0.0; self.neq()];
|
||||
for (dof, fv) in f.iter().enumerate() {
|
||||
let g = fv - ku.get(dof).copied().unwrap_or(0.0);
|
||||
for (e, c) in self.free.row(dof) {
|
||||
if let Some(r) = rhs.get_mut(e) {
|
||||
*r += c * g;
|
||||
}
|
||||
}
|
||||
}
|
||||
rhs
|
||||
}
|
||||
|
||||
/// Every degree of freedom from the free equations' values `q` (and the prescribed values
|
||||
/// when `with_values`).
|
||||
pub fn expand(&self, q: &[f64], with_values: bool) -> Vec<f64> {
|
||||
(0..self.prescribed_motion.len())
|
||||
.map(|d| {
|
||||
let free: f64 = self.free.row(d).map(|(e, c)| c * q.get(e).copied().unwrap_or(0.0)).sum();
|
||||
match self.prescribed_motion.get(d) {
|
||||
Some(base) if with_values && !self.fixed.is_empty_row(d) => base + free,
|
||||
_ => free,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The reactions at the prescribed degrees of freedom, T_pᵀ (K u − f), over every degree of
|
||||
/// freedom (zero where nothing is prescribed).
|
||||
pub fn reactions(&self, k: &Csr, u: &[f64], f: &[f64]) -> Vec<f64> {
|
||||
let mut out = vec![0.0; self.prescribed_motion.len()];
|
||||
for (dof, fv) in f.iter().enumerate() {
|
||||
if self.fixed.is_empty_row(dof) {
|
||||
continue;
|
||||
}
|
||||
let (cols, vals) = k.row(dof);
|
||||
let ku: f64 = cols.iter().zip(vals).map(|(j, v)| v * u.get(*j).copied().unwrap_or(0.0)).sum();
|
||||
let r = ku - fv;
|
||||
for (p, c) in self.fixed.row(dof) {
|
||||
if let Some(x) = out.get_mut(p) {
|
||||
*x += c * r;
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
@@ -24,13 +24,15 @@
|
||||
|
||||
pub mod analysis;
|
||||
pub mod cholesky;
|
||||
pub mod constraints;
|
||||
pub mod eigen;
|
||||
pub mod ordering;
|
||||
pub mod sparse;
|
||||
pub mod system;
|
||||
|
||||
pub use analysis::{
|
||||
ElementStress, FrequencyOptions, FrequencyResult, MassKind, Mode, StaticResult, Stats, StepResult, solve_frequency, solve_static,
|
||||
ElementStress, FrequencyOptions, FrequencyResult, Location, MassKind, Mode, PointResult, StaticResult, Stats, StepResult, solve_frequency,
|
||||
solve_static,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
|
||||
|
||||
+19
-30
@@ -41,33 +41,23 @@ impl Csr {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The matrix on a subset of rows and columns, renumbered: `map[i]` is the new index of old
|
||||
/// index i, or `None` to drop it. The new rows keep sorted columns.
|
||||
pub fn restrict(&self, map: &[Option<usize>], n: usize) -> Csr {
|
||||
let mut rows: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
|
||||
for (r, m) in map.iter().enumerate() {
|
||||
let Some(nr) = *m else { continue };
|
||||
let (cols, vals) = self.row(r);
|
||||
let Some(out) = rows.get_mut(nr) else { continue };
|
||||
for (c, v) in cols.iter().zip(vals) {
|
||||
if let Some(Some(nc)) = map.get(*c) {
|
||||
out.push((*nc, *v));
|
||||
/// Make the matrix exactly symmetric by copying the lower triangle (what the factorization
|
||||
/// reads) onto the upper one; the pattern must be structurally symmetric.
|
||||
pub fn mirror_lower(&mut self) {
|
||||
for r in 0..self.n {
|
||||
let (start, end) = (self.ptr.get(r).copied().unwrap_or(0), self.ptr.get(r + 1).copied().unwrap_or(0));
|
||||
for at in start..end {
|
||||
let Some(&c) = self.idx.get(at) else { continue };
|
||||
if c <= r {
|
||||
continue;
|
||||
}
|
||||
let (cs, ce) = (self.ptr.get(c).copied().unwrap_or(0), self.ptr.get(c + 1).copied().unwrap_or(0));
|
||||
let mirror = self.idx.get(cs..ce).and_then(|cols| cols.binary_search(&r).ok()).and_then(|i| self.val.get(cs + i)).copied();
|
||||
if let (Some(v), Some(x)) = (mirror, self.val.get_mut(at)) {
|
||||
*x = v;
|
||||
}
|
||||
}
|
||||
out.sort_by_key(|e| e.0);
|
||||
}
|
||||
let mut ptr = Vec::with_capacity(n + 1);
|
||||
ptr.push(0);
|
||||
let mut idx = Vec::new();
|
||||
let mut val = Vec::new();
|
||||
for row in rows {
|
||||
for (c, v) in row {
|
||||
idx.push(c);
|
||||
val.push(v);
|
||||
}
|
||||
ptr.push(idx.len());
|
||||
}
|
||||
Csr { n, ptr, idx, val }
|
||||
}
|
||||
|
||||
/// `self + a·other` on the same pattern.
|
||||
@@ -198,7 +188,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn assemble_restrict_multiply() {
|
||||
fn assemble_mirror_multiply() {
|
||||
// Two "elements" sharing node 1.
|
||||
let g = NodeGraph::new(3, [vec![0usize, 1], vec![1, 2]].into_iter());
|
||||
assert_eq!(g.adjacency, vec![vec![0, 1], vec![0, 1, 2], vec![1, 2]]);
|
||||
@@ -215,11 +205,10 @@ mod tests {
|
||||
let mut y = vec![0.0; 9];
|
||||
k.mul(&x, &mut y);
|
||||
assert_eq!(y, vec![1.0; 3].into_iter().chain([2.0; 3]).chain([1.0; 3]).collect::<Vec<_>>());
|
||||
// Keep the x components only, in reverse order.
|
||||
let map: Vec<Option<usize>> = (0..9).map(|i| (i % 3 == 0).then_some(2 - i / 3)).collect();
|
||||
let r = k.restrict(&map, 3);
|
||||
assert_eq!(r.diagonal(), vec![2.0, 4.0, 2.0]);
|
||||
assert_eq!(r.row(1).0, &[0, 1, 2]);
|
||||
// Mirroring keeps a symmetric matrix as it is.
|
||||
let mut mirrored = k.clone();
|
||||
mirrored.mirror_lower();
|
||||
assert_eq!(mirrored, k);
|
||||
assert!(add_block(&mut k, &[0, 2], &e).is_err(), "nodes 0 and 2 share no element");
|
||||
// Scalar (per component) and diagonal additions land on each component.
|
||||
let mut m = g.zero_matrix();
|
||||
|
||||
+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