forked from 1b-agent/fieldcraft-upstream
solver: a node in several ties gets one constraint; ties both ways are an error (#389)
A node on the secondary surfaces of two ties (a chip's underside and its sides, a cut face and an outer face) got one constraint per tie. When the main nodes of one follow the other's main surface, the second constraint is nearly the first again, and eliminating it divided by round-off: the Pi 5 heat model put 21.9 W more in than its 8 W. Such a node now follows the main faces of all its ties together (balanced to 5e-11 of the power), a node following a node that follows it back (a tie each way on one interface) is an error naming both ties, and a tie row that still nearly repeats the constraints before it is an error naming them. mesh.cells ties one way by a fixed order of the cells (hex main to tet) and makes one tie per tied cell, tie_<cell>, replacing the ties an earlier run made, so its own ties never hold a node twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
755576de8a
commit
c6eeac35c1
4 files changed
+421
-79
No files matched your search
@@ -64,13 +64,37 @@ use crate::{Error, Result};
|
||||
pub struct Row<'m> {
|
||||
pub terms: Vec<(usize, f64)>,
|
||||
pub pivot: usize,
|
||||
/// The name of the coupling it comes from (for messages).
|
||||
/// The name of the coupling or tie it comes from (for messages).
|
||||
pub coupling: &'m str,
|
||||
/// What made it.
|
||||
pub source: Source,
|
||||
}
|
||||
|
||||
/// What a constraint row comes from.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Source {
|
||||
Coupling,
|
||||
/// A tie: its secondary node follows a weighted average of main nodes.
|
||||
Tie,
|
||||
}
|
||||
|
||||
impl Source {
|
||||
fn what(self) -> &'static str {
|
||||
match self {
|
||||
Source::Coupling => "coupling",
|
||||
Source::Tie => "tie",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A coefficient below this fraction of a row's largest one is round-off, not a constraint.
|
||||
const NEGLIGIBLE: f64 = 1e-10;
|
||||
|
||||
/// A tie row whose free coefficients, once the constraints before it are substituted, are all
|
||||
/// below this fraction of the coefficients that cancelled to give them nearly repeats those
|
||||
/// constraints: eliminating it would divide by round-off (#389), so it is a conflict.
|
||||
const NEARLY_REPEATED: f64 = 1e-4;
|
||||
|
||||
fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||||
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
|
||||
}
|
||||
@@ -133,14 +157,14 @@ pub fn coupling_rows<'m>(model: &'m Model, resolved: &Resolved, mesh: &Mesh) ->
|
||||
if r[a] != 0.0 {
|
||||
terms.push((rp(3 + b)?, r[a]));
|
||||
}
|
||||
rows.push(Row { terms, pivot, coupling: name });
|
||||
rows.push(Row { terms, pivot, coupling: name, source: Source::Coupling });
|
||||
}
|
||||
// Shell and beam nodes also turn with the point: θ_s = θ_r.
|
||||
if rotations(n) {
|
||||
for k in coupled(3..6) {
|
||||
let pivot =
|
||||
DofKind::from_direction(k).ok_or_else(|| Error::Internal("direction".into())).and_then(|kind| mesh.dof(n, kind))?;
|
||||
rows.push(Row { terms: vec![(pivot, 1.0), (rp(k)?, -1.0)], pivot, coupling: name });
|
||||
rows.push(Row { terms: vec![(pivot, 1.0), (rp(k)?, -1.0)], pivot, coupling: name, source: Source::Coupling });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,7 +216,7 @@ pub fn coupling_rows<'m>(model: &'m Model, resolved: &Resolved, mesh: &Mesh) ->
|
||||
}
|
||||
}
|
||||
}
|
||||
rows.push(Row { terms, pivot, coupling: name });
|
||||
rows.push(Row { terms, pivot, coupling: name, source: Source::Coupling });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -417,6 +441,8 @@ impl Reduction {
|
||||
// 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();
|
||||
// The row each expression came from (for messages).
|
||||
let mut made_by: Vec<(Source, &str)> = 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()));
|
||||
@@ -432,6 +458,7 @@ impl Reduction {
|
||||
Some(State::Inactive) | None => return Err(Error::Internal("a constraint on a missing degree of freedom".into())),
|
||||
}
|
||||
}
|
||||
let gross: f64 = expanded.iter().map(|t| t.1.abs()).sum();
|
||||
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);
|
||||
@@ -439,6 +466,30 @@ impl Reduction {
|
||||
Some(b) if b.1.abs() >= t.1.abs() => Some(b),
|
||||
_ => Some(t),
|
||||
});
|
||||
// A tie whose secondary node is held already: what is left of the row must be a
|
||||
// constraint of its own, not the difference of two near copies of one.
|
||||
if row.source == Source::Tie
|
||||
&& intended.is_none()
|
||||
&& let Some(&(_, c)) = largest
|
||||
&& c.abs() < NEARLY_REPEATED * gross
|
||||
{
|
||||
let mut others: Vec<String> = row
|
||||
.terms
|
||||
.iter()
|
||||
.filter_map(|t| match state.get(t.0) {
|
||||
Some(State::Dependent(e)) => made_by.get(*e).map(|(k, n)| format!("{} `{n}`", k.what())),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|o| *o != format!("tie `{}`", row.coupling))
|
||||
.collect();
|
||||
others.sort_unstable();
|
||||
others.dedup();
|
||||
return Err(Error::Invalid(format!(
|
||||
"tie `{}` conflicts with {}: they hold the same nodes almost alike (a node secondary in two ties, or two ties on one interface), which over-constrains the model; keep one of them there",
|
||||
row.coupling,
|
||||
if others.is_empty() { "the constraints before it".to_string() } else { others.join(", ") }
|
||||
)));
|
||||
}
|
||||
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);
|
||||
@@ -474,6 +525,7 @@ impl Reduction {
|
||||
users.entry(dof).or_default().push(index);
|
||||
}
|
||||
exprs.push(expr);
|
||||
made_by.push((row.source, row.coupling));
|
||||
if let Some(s) = state.get_mut(pivot) {
|
||||
*s = State::Dependent(index);
|
||||
}
|
||||
|
||||
+88
-30
@@ -21,6 +21,15 @@
|
||||
//! flat faces exactly (the tie patch test), because the dual mortar conditions are satisfied by
|
||||
//! the linear field on both sides.
|
||||
//!
|
||||
//! **Nodes in several ties.** A node on the secondary surfaces of several ties (two secondary
|
||||
//! surfaces meeting at an edge: a partition's cut faces and a body's outer faces, a chip's
|
||||
//! underside and its sides) gets one constraint, over the main faces of all of them together, as
|
||||
//! if they were one tie. Two constraints, one per tie, would nearly repeat each other, and
|
||||
//! eliminating the second divides by round-off: the solution is then silently wrong (#389).
|
||||
//! Two ties holding one interface both ways (a node following a node that follows it) are an
|
||||
//! error naming them, and so are constraints that still nearly repeat each other (a tie and a
|
||||
//! coupling on one node, longer loops; [`crate::constraints`]).
|
||||
//!
|
||||
//! **Pairing.** Each secondary face takes the main faces whose boxes, grown by the position
|
||||
//! tolerance, overlap its own, and that it overlaps along its normal with a mean gap within the
|
||||
//! tolerance (by default half the thickness of the main element). A secondary node with no main
|
||||
@@ -43,7 +52,7 @@ use fieldcraft_fem::contact::{Aabb, Grid, bounds};
|
||||
use fieldcraft_fem::mortar::integrate;
|
||||
use fieldcraft_model::{Model, Resolved};
|
||||
|
||||
use crate::constraints::Row;
|
||||
use crate::constraints::{Row, Source};
|
||||
use crate::contact::faces;
|
||||
use crate::system::Mesh;
|
||||
use crate::{Error, Result};
|
||||
@@ -122,10 +131,10 @@ pub(crate) struct TieNodes<'a, F> {
|
||||
pub faces: F,
|
||||
}
|
||||
|
||||
/// The constraint rows of every tie, one per secondary node and kind in `kinds` (ties by name,
|
||||
/// secondary nodes by slot, kinds in order); `dof(slot, kind)` numbers the degrees of freedom.
|
||||
/// The same mortar weights tie displacements (the translations) and temperatures (one scalar
|
||||
/// per node). Warnings name the secondary nodes left free.
|
||||
/// The constraint rows of the ties, one per secondary node (of one tie or several) and kind in
|
||||
/// `kinds` (secondary nodes by slot, kinds in order); `dof(slot, kind)` numbers the degrees of
|
||||
/// freedom. The same mortar weights tie displacements (the translations) and temperatures (one
|
||||
/// scalar per node). Warnings name the secondary nodes left free and those several ties share.
|
||||
pub(crate) fn rows_of<'m, F>(
|
||||
model: &'m Model,
|
||||
nodes: &TieNodes<'_, F>,
|
||||
@@ -136,7 +145,9 @@ pub(crate) fn rows_of<'m, F>(
|
||||
where
|
||||
F: Fn(&str, &str) -> Result<Vec<TieFace>>,
|
||||
{
|
||||
let mut out = Vec::new();
|
||||
// Per secondary node (slot): the ties it is a secondary node of, and its main nodes'
|
||||
// weights M_AJ summed over them.
|
||||
let mut held: BTreeMap<usize, (Vec<&'m str>, BTreeMap<usize, f64>)> = BTreeMap::new();
|
||||
let corners = |f: &TieFace| -> Vec<Vec3> { f.nodes.iter().map(|n| nodes.xyz.get(*n).copied().unwrap_or_default()).collect() };
|
||||
let label = |n: usize| nodes.labels.get(n).copied().unwrap_or(0);
|
||||
for (name, tie) in model.ties() {
|
||||
@@ -147,15 +158,16 @@ where
|
||||
let reach = |f: &TieFace| tie.position_tolerance.unwrap_or(0.5 * f.thickness);
|
||||
let boxes: Vec<Aabb> = main.iter().zip(&main_x).map(|(f, x)| bounds(x, reach(f))).collect();
|
||||
let grid = Grid::new(&boxes, GRID_CELLS_PER_FACE * boxes.len() + 64);
|
||||
// Per secondary node (slot), its main nodes' weights M_AJ, in face order.
|
||||
let mut weights: BTreeMap<usize, BTreeMap<usize, f64>> = BTreeMap::new();
|
||||
let mut candidates = Vec::new();
|
||||
for f in &secondary {
|
||||
let xs = corners(f);
|
||||
let first = f.nodes.first().map_or(0, |n| label(*n));
|
||||
let coefficients = dual(&xs).ok_or_else(|| Error::Invalid(format!("{owner}: a secondary face at node {first} has no area")))?;
|
||||
for n in &f.nodes {
|
||||
weights.entry(*n).or_default();
|
||||
let (ties, _) = held.entry(*n).or_default();
|
||||
if ties.last() != Some(&name.as_str()) {
|
||||
ties.push(name.as_str());
|
||||
}
|
||||
}
|
||||
grid.candidates_in(&bounds(&xs, 0.0), &mut candidates);
|
||||
for &c in &candidates {
|
||||
@@ -179,37 +191,83 @@ where
|
||||
continue;
|
||||
}
|
||||
for (a, s) in f.nodes.iter().enumerate() {
|
||||
let row = weights.entry(*s).or_default();
|
||||
let (_, row) = held.entry(*s).or_default();
|
||||
for (j, mn) in m.nodes.iter().enumerate() {
|
||||
*row.entry(*mn).or_insert(0.0) += local[a][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut free = Vec::new();
|
||||
for (node, row) in &weights {
|
||||
let total: f64 = row.values().sum();
|
||||
if !(total.abs() > 0.0 && total.is_finite()) {
|
||||
free.push(label(*node));
|
||||
continue;
|
||||
}
|
||||
let terms: Vec<(usize, f64)> = row.iter().map(|(j, w)| (*j, w / total)).filter(|(_, w)| w.abs() > NEGLIGIBLE).collect();
|
||||
for &kind in kinds {
|
||||
let pivot = dof(*node, kind)?;
|
||||
let mut row = vec![(pivot, 1.0)];
|
||||
for (j, w) in &terms {
|
||||
row.push((dof(*j, kind)?, -w));
|
||||
}
|
||||
out.push(Row { terms: row, pivot, coupling: name.as_str() });
|
||||
}
|
||||
// One constraint per secondary node, over the main faces of all its ties together: two ties
|
||||
// holding one node (secondary surfaces meeting at an edge) would otherwise give it two
|
||||
// constraints that nearly repeat each other (#389).
|
||||
let mut free: BTreeMap<&str, Vec<u32>> = BTreeMap::new();
|
||||
let mut shared: BTreeMap<Vec<&str>, Vec<u32>> = BTreeMap::new();
|
||||
// Per held node: its ties and the main nodes it follows, with their weights.
|
||||
let mut follows: BTreeMap<usize, (&Vec<&'m str>, Vec<(usize, f64)>)> = BTreeMap::new();
|
||||
for (node, (ties, row)) in &held {
|
||||
let Some(&name) = ties.first() else { continue };
|
||||
let total: f64 = row.values().sum();
|
||||
if !(total.abs() > 0.0 && total.is_finite()) {
|
||||
free.entry(name).or_default().push(label(*node));
|
||||
continue;
|
||||
}
|
||||
if ties.len() > 1 {
|
||||
shared.entry(ties.clone()).or_default().push(label(*node));
|
||||
}
|
||||
let terms: Vec<(usize, f64)> = row.iter().map(|(j, w)| (*j, w / total)).filter(|(_, w)| w.abs() > NEGLIGIBLE).collect();
|
||||
follows.insert(*node, (ties, terms));
|
||||
}
|
||||
// A node may not follow a node that follows it (a tie each way on one interface): the two
|
||||
// constraints would hold both twice.
|
||||
for (node, (ties, terms)) in &follows {
|
||||
for (j, _) in terms {
|
||||
let Some((back, theirs)) = follows.get(j) else { continue };
|
||||
if theirs.iter().any(|(k, _)| k == node) {
|
||||
let names = |t: &Vec<&str>| t.iter().map(|n| format!("tie `{n}`")).collect::<Vec<_>>().join(" and ");
|
||||
let at = nodes.xyz.get(*node).copied().unwrap_or_default();
|
||||
return Err(Error::Invalid(format!(
|
||||
"{} and {} hold one interface both ways: node {} at ({:.6}, {:.6}, {:.6}) follows node {}, which follows it back, over-constraining both; tie each interface one way only",
|
||||
names(ties),
|
||||
names(back),
|
||||
label(*node),
|
||||
at[0],
|
||||
at[1],
|
||||
at[2],
|
||||
label(*j)
|
||||
)));
|
||||
}
|
||||
}
|
||||
if let Some(first) = free.first() {
|
||||
warnings.push(format!(
|
||||
"{owner}: {} secondary node(s) have no main face within the position tolerance and stay free (node {first} first)",
|
||||
free.len()
|
||||
));
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for (node, (ties, terms)) in &follows {
|
||||
let Some(&name) = ties.first() else { continue };
|
||||
for &kind in kinds {
|
||||
let pivot = dof(*node, kind)?;
|
||||
let mut row = vec![(pivot, 1.0)];
|
||||
for (j, w) in terms {
|
||||
row.push((dof(*j, kind)?, -w));
|
||||
}
|
||||
out.push(Row { terms: row, pivot, coupling: name, source: Source::Tie });
|
||||
}
|
||||
}
|
||||
for (name, list) in &free {
|
||||
warnings.push(format!(
|
||||
"tie `{name}`: {} secondary node(s) have no main face within the position tolerance and stay free (node {} first)",
|
||||
list.len(),
|
||||
list.first().copied().unwrap_or(0)
|
||||
));
|
||||
}
|
||||
for (ties, list) in &shared {
|
||||
let names: Vec<String> = ties.iter().map(|t| format!("`{t}`")).collect();
|
||||
warnings.push(format!(
|
||||
"ties {}: {} node(s) are secondary nodes of each, and follow their main surfaces together (node {} first)",
|
||||
names.join(", "),
|
||||
list.len(),
|
||||
list.first().copied().unwrap_or(0)
|
||||
));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user