forked from 1b-agent/fieldcraft-upstream
Crates fieldcraft-*, binaries fieldcraft and fieldcraft-cli, the .fieldcraft file (format tag fieldcraft/1, manifest fieldcraft.json), FIELDCRAFT_* environment variables, the MCP server name, egui widget ids (fc_*), the app icon document FieldCraft.icon and its generated family, docs and licences. No compatibility shim: no files of the earlier name exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
393 lines
21 KiB
Rust
393 lines
21 KiB
Rust
//! Reference points, couplings and moments against closed-form solutions. Run with
|
||
//! `--nocapture` to see the errors.
|
||
|
||
mod common;
|
||
|
||
use std::f64::consts::PI;
|
||
|
||
use common::*;
|
||
use fieldcraft_model::{BoundaryCondition, CouplingKind, Distribution, ElementKind, Load, Procedure, Symmetry, Target};
|
||
use fieldcraft_solver::{FrequencyOptions, Limits, StaticResult, solve_frequency, solve_static};
|
||
|
||
const ALL: [u8; 6] = [1, 2, 3, 4, 5, 6];
|
||
|
||
fn solve(g: &Grid) -> StaticResult {
|
||
solve_static(&g.model, Some("Step"), &Limits::default()).unwrap()
|
||
}
|
||
|
||
fn at(r: &StaticResult, label: u32) -> usize {
|
||
r.nodes.binary_search(&label).unwrap()
|
||
}
|
||
|
||
fn rp(name: &str) -> Target {
|
||
Target::ReferencePoint(name.into())
|
||
}
|
||
|
||
fn force(target: Target, vector: [f64; 3]) -> Load {
|
||
Load::Force { target, vector, distribution: Distribution::default() }
|
||
}
|
||
|
||
fn moment(target: Target, vector: [f64; 3]) -> Load {
|
||
Load::Moment { target, vector, distribution: Distribution::default() }
|
||
}
|
||
|
||
fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
|
||
}
|
||
|
||
/// A block held only through a reference point coupled to one face, the point given a small
|
||
/// rigid motion: the whole block follows it rigidly, u = u_r + θ × (x − x_r), with no stress.
|
||
/// Kinematic couplings eliminate the face nodes; a distributing coupling with a prescribed point
|
||
/// eliminates face nodes chosen by the constraint itself (the point is dependent).
|
||
#[test]
|
||
fn prescribed_reference_point_moves_the_block_rigidly() {
|
||
let motion = [2e-3, -1e-3, 3e-3, 1e-3, -2e-3, 1.5e-3];
|
||
let xr = [0.0, 0.6, 0.4];
|
||
for (kind, formulation, name) in [
|
||
(ElementKind::Hex8, incompatible(), "hex8 incompatible"),
|
||
(ElementKind::Hex20, full(), "hex20"),
|
||
(ElementKind::Tet10, full(), "tet10"),
|
||
(ElementKind::Wedge6, full(), "wedge6"),
|
||
] {
|
||
for coupling in [CouplingKind::Kinematic, CouplingKind::Distributing] {
|
||
let mut g = grid([3, 2, 2], kind, |q| [3.0 * q[0], 1.2 * q[1], 0.8 * q[2]]);
|
||
g.define(iso(1000.0, 0.3), None, formulation, Procedure::Static {});
|
||
g.couple("RP", xr, coupling, &ALL, |q| near(q[0], 0.0));
|
||
let values = motion.map(Some);
|
||
g.bc("move", BoundaryCondition::Displacement { target: rp("RP"), values });
|
||
let r = solve(&g);
|
||
let (t, w) = ([motion[0], motion[1], motion[2]], [motion[3], motion[4], motion[5]]);
|
||
let mut worst: f64 = 0.0;
|
||
for (label, u) in r.nodes.iter().zip(&r.displacement) {
|
||
let x = g.model.node(*label).unwrap().xyz;
|
||
let rot = cross(w, [x[0] - xr[0], x[1] - xr[1], x[2] - xr[2]]);
|
||
worst = worst.max((0..3).map(|k| (u[k] - t[k] - rot[k]).abs()).fold(0.0, f64::max));
|
||
}
|
||
let stress = r.stress.iter().flatten().fold(0.0f64, |m, s| m.max(s.abs()));
|
||
let p = &r.reference_points[0];
|
||
eprintln!("rigid {name} {coupling:?}: displacement error {worst:.1e}, largest stress {stress:.1e}");
|
||
assert!(worst < 1e-13, "{name} {coupling:?}: {worst}");
|
||
assert!(stress < 1e-9, "{name} {coupling:?}: {stress}");
|
||
assert_eq!(p.displacement, motion);
|
||
assert!(p.reaction.iter().all(|x| x.abs() < 1e-9), "{:?}", p.reaction);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A cantilever clamped through a fixed reference point kinematically coupled to its root face
|
||
/// is the cantilever clamped directly: same displacements and natural frequencies, and the
|
||
/// point's reaction carries the whole load (force and moment about the point).
|
||
#[test]
|
||
fn clamping_through_a_coupled_reference_point_is_clamping() {
|
||
let build = |through_point: bool, procedure: Procedure| {
|
||
let mut g = grid([8, 1, 1], ElementKind::Hex20, |q| [8.0 * q[0], q[1], q[2]]);
|
||
g.define(iso(1000.0, 0.3), Some(1.0), full(), procedure);
|
||
if through_point {
|
||
g.couple("Root", [0.0, 0.5, 0.5], CouplingKind::Kinematic, &[1, 2, 3], |q| near(q[0], 0.0));
|
||
g.bc("clamp", BoundaryCondition::Fixed { target: rp("Root") });
|
||
} else {
|
||
g.fixed("clamp", |q| near(q[0], 0.0));
|
||
}
|
||
g
|
||
};
|
||
let (mut direct, mut coupled) = (build(false, Procedure::Static {}), build(true, Procedure::Static {}));
|
||
for g in [&mut direct, &mut coupled] {
|
||
g.face_set("end", |q| near(q[0], 1.0));
|
||
g.load("shear", Load::Traction { set: "end".into(), vector: [0.0, 0.0, -1.0] });
|
||
}
|
||
let (a, b) = (solve(&direct), solve(&coupled));
|
||
let scale = a.displacement.iter().flatten().fold(0.0f64, |m, v| m.max(v.abs()));
|
||
let diff = a.displacement.iter().flatten().zip(b.displacement.iter().flatten()).fold(0.0f64, |m, (x, y)| m.max((x - y).abs()));
|
||
let p = &b.reference_points[0];
|
||
eprintln!("clamp through a point: displacement difference {:.1e} (relative), reaction {:?}", diff / scale, p.reaction);
|
||
assert!(diff < 1e-10 * scale, "{diff}");
|
||
// The tip load −1 in z at x = 8 (centroid line y = z = 0.5): reaction +1 in z and the moment
|
||
// about the point M_y = −8 (r × F with r = (8, 0, 0), F = (0, 0, 1) → (0, −8, 0)).
|
||
assert!((p.reaction[2] - 1.0).abs() < 1e-9 && p.reaction[0].abs() < 1e-9 && p.reaction[1].abs() < 1e-9, "{:?}", p.reaction);
|
||
assert!((p.reaction[4] + 8.0).abs() < 1e-8 && p.reaction[3].abs() < 1e-8 && p.reaction[5].abs() < 1e-8, "{:?}", p.reaction);
|
||
assert!((b.reaction_total[2] - 1.0).abs() < 1e-9, "{:?}", b.reaction_total);
|
||
|
||
let modes = Procedure::Frequency { modes: 3, min_hz: None, max_hz: None };
|
||
let (direct, coupled) = (build(false, modes.clone()), build(true, modes));
|
||
let f = |g: &Grid| solve_frequency(&g.model, Some("Step"), &FrequencyOptions::default(), &Limits::default()).unwrap();
|
||
let (fa, fb) = (f(&direct), f(&coupled));
|
||
for (x, y) in fa.modes.iter().zip(&fb.modes) {
|
||
assert!((x.frequency - y.frequency).abs() < 1e-9 * x.frequency, "{} vs {}", x.frequency, y.frequency);
|
||
}
|
||
assert_eq!(fb.reference_points.len(), 1);
|
||
assert!(fb.modes.iter().all(|m| m.point_shape[0] == [0.0; 6]), "the fixed point does not move");
|
||
}
|
||
|
||
/// A bar in tension loaded through a distributing coupling at the centroid of its end face
|
||
/// against the same total force as a uniform pressure: away from the end (Saint-Venant) both give
|
||
/// σxx = F/A and u_x = F x / (E A); the coupled load reaches the face as equal nodal forces.
|
||
#[test]
|
||
fn distributing_force_equals_the_equivalent_pressure_away_from_the_end() {
|
||
let (l, b, h, young, nu, total) = (20.0, 1.0, 1.0, 1000.0, 0.3, 4.0);
|
||
let area = b * h;
|
||
let mut errors = Vec::new();
|
||
for (kind, formulation, name) in [(ElementKind::Hex20, full(), "hex20"), (ElementKind::Hex8, incompatible(), "hex8 incompatible")] {
|
||
let mut results = Vec::new();
|
||
for coupled in [false, true] {
|
||
let mut g = grid([20, 2, 2], kind, |q| [l * q[0], b * q[1], h * q[2]]);
|
||
g.define(iso(young, nu), None, formulation, Procedure::Static {});
|
||
g.symmetry("x0", Symmetry::X, |q| near(q[0], 0.0));
|
||
g.symmetry("y0", Symmetry::Y, |q| near(q[1], 0.0));
|
||
g.symmetry("z0", Symmetry::Z, |q| near(q[2], 0.0));
|
||
if coupled {
|
||
// The quarter model's end face: its centroid is at (l, b/2, h/2).
|
||
g.couple("End", [l, b / 2.0, h / 2.0], CouplingKind::Distributing, &ALL, |q| near(q[0], 1.0));
|
||
g.load("pull", force(rp("End"), [total, 0.0, 0.0]));
|
||
} else {
|
||
g.face_set("end", |q| near(q[0], 1.0));
|
||
g.load("pull", Load::Pressure { set: "end".into(), magnitude: -total / area });
|
||
}
|
||
let r = solve(&g);
|
||
// At least eight widths of the quarter (four of the whole bar) from the loaded end.
|
||
let middle: Vec<u32> = g.nodes_where(|q| q[0] > 0.25 && q[0] < 0.6);
|
||
let es = middle.iter().map(|n| (r.stress[at(&r, *n)][0] - total / area).abs() / (total / area)).fold(0.0, f64::max);
|
||
let eu = middle
|
||
.iter()
|
||
.map(|n| {
|
||
let x = g.model.node(*n).unwrap().xyz[0];
|
||
let want = total * x / (young * area);
|
||
(r.displacement[at(&r, *n)][0] - want).abs() / want
|
||
})
|
||
.fold(0.0, f64::max);
|
||
eprintln!("tension {name} {}: mid-length σxx error {:.2e}, u_x error {:.2e}", if coupled { "coupled" } else { "pressure" }, es, eu);
|
||
assert!((r.applied_total[0] - total).abs() < 1e-12 && (r.reaction_total[0] + total).abs() < 1e-9, "{name}");
|
||
errors.push((es, eu));
|
||
results.push(r);
|
||
}
|
||
// The point moves with the face's mean: between the pressure-loaded face's extremes.
|
||
let end = x_range(&results[0]);
|
||
let ux = results[1].reference_points[0].displacement[0];
|
||
assert!(ux > 0.9 * end.0 && ux < 1.1 * end.1, "{ux} {end:?}");
|
||
}
|
||
assert!(errors.iter().all(|(es, eu)| *es < 1e-4 && *eu < 1e-4), "{errors:?}");
|
||
}
|
||
|
||
/// The smallest and largest x displacement.
|
||
fn x_range(r: &StaticResult) -> (f64, f64) {
|
||
r.displacement.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), u| (lo.min(u[0]), hi.max(u[0])))
|
||
}
|
||
|
||
/// Pure bending by a moment on a reference point with a distributing coupling on the end face:
|
||
/// away from the end σxx = M z / I and the curvature κ = M / (E I) (u_z = κ x² / 2 on the axis).
|
||
#[test]
|
||
fn bending_moment_through_a_distributing_coupling() {
|
||
let (l, b, h, young, nu, m) = (10.0, 1.0, 2.0, 1000.0, 0.3, 3.0);
|
||
let i = b * h * h * h / 12.0;
|
||
let mut g = grid([10, 2, 4], ElementKind::Hex20, |q| [l * q[0], b * (q[1] - 0.5), h * (q[2] - 0.5)]);
|
||
g.define(iso(young, nu), None, full(), Procedure::Static {});
|
||
g.symmetry("x0", Symmetry::X, |q| near(q[0], 0.0));
|
||
// The pure-bending field has u_y = −ν κ y z and u_z = κ (x² + ν (z² − y²)) / 2: zero u_y on
|
||
// y = 0 at x = 0, and zero u_z at the origin.
|
||
g.node_set("axis", |q| near(q[0], 0.0) && near(q[1], 0.5) && near(q[2], 0.5));
|
||
g.bc("axis", BoundaryCondition::Displacement { target: Target::Set("axis".into()), values: [None, Some(0.0), Some(0.0), None, None, None] });
|
||
g.node_set("edge", |q| near(q[0], 0.0) && near(q[1], 0.5) && near(q[2], 0.0));
|
||
g.bc("edge", BoundaryCondition::Displacement { target: Target::Set("edge".into()), values: [None, Some(0.0), None, None, None, None] });
|
||
g.couple("End", [l, 0.0, 0.0], CouplingKind::Distributing, &ALL, |q| near(q[0], 1.0));
|
||
g.load("bend", moment(rp("End"), [0.0, m, 0.0]));
|
||
let r = solve(&g);
|
||
let mut es: f64 = 0.0;
|
||
for n in g.nodes_where(|q| q[0] > 0.35 && q[0] < 0.65) {
|
||
let z = g.model.node(n).unwrap().xyz[2];
|
||
es = es.max((r.stress[at(&r, n)][0] - m * z / i).abs() / (m * h / 2.0 / i));
|
||
}
|
||
let kappa = m / (young * i);
|
||
let mid = g.nodes_where(|q| near(q[0], 0.5) && near(q[1], 0.5) && near(q[2], 0.5))[0];
|
||
let x = g.model.node(mid).unwrap().xyz[0];
|
||
// Positive M_y stretches +z: the axis bends toward −z.
|
||
let want = -kappa * x * x / 2.0;
|
||
let eu = (r.displacement[at(&r, mid)][2] - want).abs() / want.abs();
|
||
eprintln!("bending through a distributing coupling: σxx error {es:.2e} (of the peak), mid-span deflection error {eu:.2e}");
|
||
assert!(es < 5e-3 && eu < 5e-3, "{es} {eu}");
|
||
for k in 0..3 {
|
||
assert!(r.applied_total[k].abs() < 1e-12 && r.reaction_total[k].abs() < 1e-9, "a moment adds no net force");
|
||
}
|
||
}
|
||
|
||
/// A tube of hex20 elements in (r, θ, z), the seam at θ = 0 shared.
|
||
fn tube(nr: usize, nt: usize, nz: usize, ri: f64, ro: f64, length: f64) -> Grid {
|
||
let mut g = grid([nr, nt, nz], ElementKind::Hex20, |q| {
|
||
let (r, t) = (ri + (ro - ri) * q[0], 2.0 * PI * q[1]);
|
||
[r * t.cos(), r * t.sin(), length * q[2]]
|
||
});
|
||
// Merge the seam: nodes at θ = 2π take the labels of those at θ = 0.
|
||
let seam: Vec<(u32, u32)> = g
|
||
.param
|
||
.iter()
|
||
.filter(|(_, p)| near(p[1], 1.0))
|
||
.map(|(l, p)| (*l, *g.param.iter().find(|(_, o)| near(o[1], 0.0) && near(o[0], p[0]) && near(o[2], p[2])).unwrap().0))
|
||
.collect();
|
||
let elements: Vec<(u32, fieldcraft_model::Element)> = g.model.elements().iter().map(|(l, e)| (*l, e.clone())).collect();
|
||
for (label, e) in elements {
|
||
let nodes: Vec<u32> = e.nodes.iter().map(|n| seam.iter().find(|(a, _)| a == n).map_or(*n, |(_, b)| *b)).collect();
|
||
g.model.set_element(label, e.kind, nodes).unwrap();
|
||
}
|
||
for (old, _) in &seam {
|
||
g.model.remove_node(*old).unwrap();
|
||
g.param.remove(old);
|
||
}
|
||
g
|
||
}
|
||
|
||
/// Torsion of a tube by a moment on a reference point kinematically coupled to its end face, the
|
||
/// other end clamped: twist φ = T L / (G J), shear stress τ = T r / J, J = π (r_o⁴ − r_i⁴) / 2
|
||
/// (circular sections do not warp, so the clamp and the rigid end are exact).
|
||
#[test]
|
||
fn torsion_of_a_tube_through_a_reference_point() {
|
||
let (ri, ro, length, young, nu, torque) = (0.6f64, 1.0f64, 4.0, 1000.0, 0.3, 5.0);
|
||
let shear = young / (2.0 * (1.0 + nu));
|
||
let j = PI / 2.0 * (ro.powi(4) - ri.powi(4));
|
||
let mut g = tube(2, 16, 4, ri, ro, length);
|
||
g.define(iso(young, nu), None, full(), Procedure::Static {});
|
||
g.fixed("root", |q| near(q[2], 0.0));
|
||
g.couple("Tip", [0.0, 0.0, length], CouplingKind::Kinematic, &ALL, |q| near(q[2], 1.0));
|
||
g.load("twist", moment(rp("Tip"), [0.0, 0.0, torque]));
|
||
let r = solve(&g);
|
||
let phi = r.reference_points[0].displacement[5];
|
||
let want = torque * length / (shear * j);
|
||
let e_phi = (phi - want).abs() / want;
|
||
// Shear stress on the outer surface at mid-length, τ_θz = −sin θ σxz + cos θ σyz.
|
||
let outer = g.nodes_where(|q| near(q[0], 1.0) && near(q[2], 0.5));
|
||
let tau = outer
|
||
.iter()
|
||
.map(|n| {
|
||
let x = g.model.node(*n).unwrap().xyz;
|
||
let t = x[1].atan2(x[0]);
|
||
let s = r.stress[at(&r, *n)];
|
||
-t.sin() * s[4] + t.cos() * s[5]
|
||
})
|
||
.sum::<f64>()
|
||
/ outer.len() as f64;
|
||
let e_tau = (tau - torque * ro / j).abs() / (torque * ro / j);
|
||
eprintln!("torsion: twist {phi:.6e} vs {want:.6e} ({:.3}%), τ {tau:.5} vs {:.5} ({:.3}%)", 100.0 * e_phi, torque * ro / j, 100.0 * e_tau);
|
||
assert!(e_phi < 1e-3 && e_tau < 5e-3, "{e_phi} {e_tau}");
|
||
// The rotation is about the axis only; the reaction moment at the root balances the torque
|
||
// (reaction forces sum to zero).
|
||
let p = &r.reference_points[0];
|
||
assert!(p.displacement[..5].iter().all(|v| v.abs() < 1e-9 * phi), "{:?}", p.displacement);
|
||
assert!(r.reaction_total.iter().all(|v| v.abs() < 1e-9));
|
||
}
|
||
|
||
#[test]
|
||
fn coupled_results_are_deterministic() {
|
||
let build = || {
|
||
let mut g = grid([4, 2, 2], ElementKind::Tet10, |q| [4.0 * q[0], q[1], q[2]]);
|
||
g.define(iso(1000.0, 0.3), None, full(), Procedure::Static {});
|
||
g.couple("Root", [0.0, 0.5, 0.5], CouplingKind::Kinematic, &ALL, |q| near(q[0], 0.0));
|
||
g.bc("clamp", BoundaryCondition::Fixed { target: rp("Root") });
|
||
g.couple("Tip", [4.5, 0.5, 0.5], CouplingKind::Distributing, &ALL, |q| near(q[0], 1.0));
|
||
g.load("f", force(rp("Tip"), [0.0, 1.0, -2.0]));
|
||
g.load("m", moment(rp("Tip"), [0.5, 0.0, 0.0]));
|
||
g
|
||
};
|
||
let (a, b) = (solve(&build()), solve(&build()));
|
||
assert_eq!(a, b);
|
||
// Balance: forces and the reactions at the clamped point.
|
||
for k in 0..3 {
|
||
assert!((a.applied_total[k] + a.reaction_total[k]).abs() < 1e-9, "{:?} {:?}", a.applied_total, a.reaction_total);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn coupling_errors_name_the_problem() {
|
||
let base = || {
|
||
let mut g = grid([2, 1, 1], ElementKind::Hex8, |q| [2.0 * q[0], q[1], q[2]]);
|
||
g.define(iso(1000.0, 0.3), None, full(), Procedure::Static {});
|
||
g.fixed("root", |q| near(q[0], 0.0));
|
||
g
|
||
};
|
||
let err = |g: &Grid| solve_static(&g.model, Some("Step"), &Limits::default()).unwrap_err().to_string();
|
||
|
||
// A moment on a point whose distributing coupling ties translations only.
|
||
let mut g = base();
|
||
g.couple("Tip", [2.0, 0.5, 0.5], CouplingKind::Distributing, &[1, 2, 3], |q| near(q[0], 1.0));
|
||
g.load("m", moment(rp("Tip"), [1.0, 0.0, 0.0]));
|
||
assert!(err(&g).contains("rotation about X is not coupled"), "{}", err(&g));
|
||
|
||
let loose = |g: &mut Grid| g.model.add_reference_point("Loose", fieldcraft_model::ReferencePoint { xyz: [3.0, 0.0, 0.0], label: 0 }).unwrap();
|
||
|
||
// A force on a reference point nothing couples.
|
||
let mut g = base();
|
||
loose(&mut g);
|
||
g.couple("Tip", [2.0, 0.5, 0.5], CouplingKind::Kinematic, &ALL, |q| near(q[0], 1.0));
|
||
g.load("f", force(rp("Loose"), [1.0, 0.0, 0.0]));
|
||
assert!(err(&g).contains("no coupling ties"), "{}", err(&g));
|
||
|
||
// Rotations through nodes on one line.
|
||
let mut g = base();
|
||
g.couple("Edge", [2.0, 0.0, 0.5], CouplingKind::Distributing, &ALL, |q| near(q[0], 1.0) && near(q[1], 0.0));
|
||
g.load("f", force(rp("Edge"), [1.0, 0.0, 0.0]));
|
||
assert!(err(&g).contains("one line"), "{}", err(&g));
|
||
|
||
// Only rotations coupled kinematically.
|
||
let mut g = base();
|
||
g.couple("Tip", [2.0, 0.5, 0.5], CouplingKind::Kinematic, &[4, 5, 6], |q| near(q[0], 1.0));
|
||
assert!(err(&g).contains("only rotations"), "{}", err(&g));
|
||
|
||
// A rigid face whose nodes boundary conditions pull apart.
|
||
let mut g = grid([2, 1, 1], ElementKind::Hex8, |q| [2.0 * q[0], q[1], q[2]]);
|
||
g.define(iso(1000.0, 0.3), None, full(), Procedure::Static {});
|
||
g.couple("Root", [0.0, 0.5, 0.5], CouplingKind::Kinematic, &ALL, |q| near(q[0], 0.0));
|
||
for (name, y, v) in [("a", 0.0, 0.1), ("b", 1.0, 0.0)] {
|
||
g.node_set(name, |q| near(q[0], 0.0) && near(q[1], y) && near(q[2], 0.0));
|
||
g.bc(name, BoundaryCondition::Displacement { target: Target::Set(name.into()), values: [None, Some(v), None, None, None, None] });
|
||
}
|
||
assert!(err(&g).contains("contradicts"), "{}", err(&g));
|
||
|
||
// A free reference point rotation: one coupled node gives the point's rotations no stiffness.
|
||
let mut g = base();
|
||
g.couple("One", [2.5, 0.5, 0.5], CouplingKind::Kinematic, &[1, 2, 3], |q| near(q[0], 1.0) && near(q[1], 1.0) && near(q[2], 1.0));
|
||
g.load("f", force(rp("One"), [1.0, 0.0, 0.0]));
|
||
assert!(err(&g).contains("reference point `One` in rotation"), "{}", err(&g));
|
||
|
||
// A boundary condition on an uncoupled point holds nothing and says so.
|
||
let mut g = base();
|
||
loose(&mut g);
|
||
g.bc("hold", BoundaryCondition::Fixed { target: rp("Loose") });
|
||
g.face_set("end", |q| near(q[0], 1.0));
|
||
g.load("p", Load::Pressure { set: "end".into(), magnitude: 1.0 });
|
||
let r = solve(&g);
|
||
assert!(r.warnings.iter().any(|w| w.contains("holds nothing")), "{:?}", r.warnings);
|
||
|
||
// Hostile geometry: a reference point far away still solves or fails cleanly.
|
||
for kind in [CouplingKind::Kinematic, CouplingKind::Distributing] {
|
||
let mut g = base();
|
||
g.couple("Far", [1e9, -1e9, 1e9], kind, &ALL, |q| near(q[0], 1.0));
|
||
g.load("m", moment(rp("Far"), [1.0, 1.0, 1.0]));
|
||
let _ = solve_static(&g.model, Some("Step"), &Limits::default());
|
||
}
|
||
}
|
||
|
||
/// A distributing coupling on a large face whose reference point is held ties every coupled
|
||
/// node to every other (a dense block of (3N)² entries): refused up front, before the reduced
|
||
/// matrix is built, with the size it would need. Any other reduction that would outgrow the
|
||
/// budget stops while counting, before allocating.
|
||
#[test]
|
||
fn oversized_reductions_are_refused_before_allocating() {
|
||
let mut g = grid([83, 83, 1], ElementKind::Hex8, |q| [83.0 * q[0], 83.0 * q[1], q[2]]);
|
||
g.define(iso(1000.0, 0.3), None, full(), Procedure::Static {});
|
||
g.couple("Base", [41.5, 41.5, 0.0], CouplingKind::Distributing, &ALL, |q| near(q[2], 0.0));
|
||
g.bc("hold", BoundaryCondition::Fixed { target: rp("Base") });
|
||
g.face_set("top", |q| near(q[2], 1.0));
|
||
g.load("p", Load::Pressure { set: "top".into(), magnitude: 1.0 });
|
||
let start = std::time::Instant::now();
|
||
let e = solve_static(&g.model, Some("Step"), &Limits::default()).unwrap_err().to_string();
|
||
eprintln!("refused in {:?}: {e}", start.elapsed());
|
||
assert!(e.contains("couples 7056 nodes") && e.contains("GB") && e.contains("kinematic coupling"), "{e}");
|
||
|
||
let mut g = grid([2, 1, 1], ElementKind::Hex8, |q| [2.0 * q[0], q[1], q[2]]);
|
||
g.define(iso(1000.0, 0.3), None, full(), Procedure::Static {});
|
||
g.couple("Root", [0.0, 0.5, 0.5], CouplingKind::Kinematic, &ALL, |q| near(q[0], 0.0));
|
||
g.bc("clamp", BoundaryCondition::Fixed { target: rp("Root") });
|
||
g.face_set("end", |q| near(q[0], 1.0));
|
||
g.load("p", Load::Pressure { set: "end".into(), magnitude: 1.0 });
|
||
let tight = Limits { max_factor_entries: 4, ..Limits::default() };
|
||
let e = solve_static(&g.model, Some("Step"), &tight).unwrap_err().to_string();
|
||
assert!(e.contains("reduced matrices need more than"), "{e}");
|
||
assert!(solve_static(&g.model, Some("Step"), &Limits::default()).is_ok());
|
||
}
|