forked from 1b-agent/fieldcraft-upstream
mesh.block: mapped hex, tet and wedge blocks with curved edges; solve.probe at points
A structured mesh of a six-sided block from its corners, with circular and elliptical edges (Gordon-Hall transfinite interpolation), grading, named faces, edges and vertices as geometry tags, and joining to the existing mesh face to face (refused otherwise). solve.probe takes "at" points and reports the nearest node with a result. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
1 parent
15a9ac9157
commit
a3cb6d749b
7 files changed
+1022
-10
No files matched your search
@@ -187,7 +187,14 @@ pub static CATALOGUE: &[Capability] = &[
|
||||
soon("mesh.untangle", "Untangle", Mesh, "Improve", "M6", "Repair inverted elements"),
|
||||
soon("mesh.layers", "Layers", Mesh, "Improve", "M6", "Boundary padding and layers through thin walls"),
|
||||
ready_more("mesh.node.add", "Node", Mesh, "Edit", "Add a node", &["mesh.node.remove"]),
|
||||
ready_more("mesh.element.add", "Element", Mesh, "Edit", "Add an element on existing nodes", &["mesh.element.remove"]),
|
||||
ready_more(
|
||||
"mesh.element.add",
|
||||
"Element",
|
||||
Mesh,
|
||||
"Edit",
|
||||
"Add an element on existing nodes, or a mapped block of them",
|
||||
&["mesh.element.remove", "mesh.block"],
|
||||
),
|
||||
soon("mesh.node.merge", "Merge Nodes", Mesh, "Edit", "M6", "Merge coincident nodes"),
|
||||
soon("mesh.region.remesh", "Remesh Region", Mesh, "Edit", "M6", "Remesh a region in place; split and collapse elements"),
|
||||
soon("mesh.import.orphan", "Orphan Mesh", Mesh, "Edit", "M6", "Import a mesh without geometry"),
|
||||
|
||||
@@ -0,0 +1,412 @@
|
||||
//! `mesh.block`: a structured mesh of one six-sided block from its corners and curved edges
|
||||
//! ([`modelcraft_mesh::block`]), joined to the mesh already there and tagged as a body with named
|
||||
//! faces, edges and vertices, so sets by geometry select it.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use modelcraft_mesh::block::{Block, BlockMesh, Curve, block};
|
||||
use modelcraft_model::{ElementKind, Entity, GeometryRef, MAX_LABEL, Model};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
||||
|
||||
pub const BLOCK: CommandSpec = CommandSpec {
|
||||
id: "mesh.block",
|
||||
label: "Mesh Block",
|
||||
params: r#"{"body": "Plate", "kind": "hex8|hex20|tet4|tet10|wedge6", "corners": [[x, y, z] × 8] (hex8 order: 1-2-3-4 counter-clockwise seen from 5-6-7-8), "divisions": [n12, n14, n15] (elements along 1→2, 1→4, 1→5), "bias": [1, 1, 1] (optional: last over first element size along each direction), "edges": [{"corners": [1, 2], "arc": {"center": [x, y, z]}} | {"corners": [2, 3], "ellipse": {"center": [x, y, z], "axes": [[a], [b]] (perpendicular)}} | {"corners": [5, 6], "name": "tip_edge"}] (optional: curved edges, the shorter arc between the corners, and edge names), "faces": {"1": "bottom", "4": "tip"} (optional: names of block faces, numbered as hex8 faces: 1 = 1-2-3-4, 2 = 5-6-7-8, 3 = 1-2-6-5, 4 = 2-3-7-6, 5 = 3-4-8-7, 6 = 4-1-5-8), "vertices": {"1": "A"} (optional: names of corners), "merge": distance (optional, default 1e-6 of the block's diagonal; 0 never joins)} — adds a mapped mesh of the block (transfinite interpolation of its edges; quadratic nodes on the curves), uses existing nodes within `merge` of new ones so blocks that share a face join (the shared face must have the same elements on both sides, or nothing changes), and tags the elements with the body and element faces, edges and nodes with the named faces, edges and vertices"#,
|
||||
kind: CommandKind::Model,
|
||||
run: block_mesh,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Params {
|
||||
body: String,
|
||||
kind: String,
|
||||
corners: [[f64; 3]; 8],
|
||||
divisions: [usize; 3],
|
||||
#[serde(default = "uniform")]
|
||||
bias: [f64; 3],
|
||||
#[serde(default)]
|
||||
edges: Vec<EdgeParams>,
|
||||
#[serde(default)]
|
||||
faces: BTreeMap<String, String>,
|
||||
#[serde(default)]
|
||||
vertices: BTreeMap<String, String>,
|
||||
merge: Option<f64>,
|
||||
}
|
||||
|
||||
fn uniform() -> [f64; 3] {
|
||||
[1.0; 3]
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EdgeParams {
|
||||
corners: [usize; 2],
|
||||
name: Option<String>,
|
||||
arc: Option<ArcParams>,
|
||||
ellipse: Option<EllipseParams>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ArcParams {
|
||||
center: [f64; 3],
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EllipseParams {
|
||||
center: [f64; 3],
|
||||
axes: [[f64; 3]; 2],
|
||||
}
|
||||
|
||||
/// A 1-based number in `1..=n` given as a JSON object key, 0-based.
|
||||
fn numbered(key: &str, n: usize, what: &str) -> Result<usize> {
|
||||
key.parse::<usize>()
|
||||
.ok()
|
||||
.filter(|k| (1..=n).contains(k))
|
||||
.map(|k| k - 1)
|
||||
.ok_or_else(|| EngineError::Params(format!("{what} `{key}` must be a number from 1 to {n}")))
|
||||
}
|
||||
|
||||
fn block_mesh(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let p: Params = serde_json::from_value(p.clone()).map_err(|e| EngineError::Params(format!("mesh.block: {e}")))?;
|
||||
let kind = ElementKind::from_name(&p.kind).ok_or_else(|| EngineError::Params(format!("unknown element kind `{}`", p.kind)))?;
|
||||
let mut curves = Vec::new();
|
||||
let mut edge_names = Vec::new();
|
||||
for e in &p.edges {
|
||||
let pair = [0, 1].map(|i| e.corners[i].wrapping_sub(1));
|
||||
let index =
|
||||
Block::edge_index(pair).ok_or_else(|| EngineError::Params(format!("corners {:?} are not the two ends of a block edge", e.corners)))?;
|
||||
let curve = match (&e.arc, &e.ellipse) {
|
||||
(Some(a), None) => Some(Curve::Arc { center: a.center }),
|
||||
(None, Some(el)) => Some(Curve::Ellipse { center: el.center, axes: el.axes }),
|
||||
(None, None) => None,
|
||||
(Some(_), Some(_)) => return Err(EngineError::Params(format!("edge {:?} is an arc or an ellipse, not both", e.corners))),
|
||||
};
|
||||
if let Some(c) = curve {
|
||||
curves.push((pair, c));
|
||||
}
|
||||
if let Some(n) = &e.name {
|
||||
edge_names.push((index, n.clone()));
|
||||
}
|
||||
if curve.is_none() && e.name.is_none() {
|
||||
return Err(EngineError::Params(format!("edge {:?} gives neither a curve nor a name", e.corners)));
|
||||
}
|
||||
}
|
||||
let faces: Vec<(usize, String)> = p.faces.iter().map(|(k, n)| Ok((numbered(k, 6, "face")?, n.clone()))).collect::<Result<_>>()?;
|
||||
let vertices: Vec<(usize, String)> = p.vertices.iter().map(|(k, n)| Ok((numbered(k, 8, "vertex")?, n.clone()))).collect::<Result<_>>()?;
|
||||
// Check every name before meshing.
|
||||
let body = GeometryRef::body(p.body.clone())?;
|
||||
let face_refs: Vec<(usize, GeometryRef)> =
|
||||
faces.iter().map(|(k, n)| Ok((*k, GeometryRef::face(p.body.clone(), n.clone())?))).collect::<Result<_>>()?;
|
||||
let edge_refs: Vec<(usize, GeometryRef)> =
|
||||
edge_names.iter().map(|(k, n)| Ok((*k, GeometryRef::new(p.body.clone(), Entity::Edge(n.clone()))?))).collect::<Result<_>>()?;
|
||||
let vertex_refs: Vec<(usize, GeometryRef)> =
|
||||
vertices.iter().map(|(k, n)| Ok((*k, GeometryRef::new(p.body.clone(), Entity::Vertex(n.clone()))?))).collect::<Result<_>>()?;
|
||||
|
||||
let mesh = block(&Block { kind, corners: p.corners, divisions: p.divisions, bias: p.bias, curves })?;
|
||||
let diagonal = {
|
||||
let lo = (0..3).map(|d| p.corners.iter().map(|c| c[d]).fold(f64::INFINITY, f64::min));
|
||||
let hi = (0..3).map(|d| p.corners.iter().map(|c| c[d]).fold(f64::NEG_INFINITY, f64::max));
|
||||
lo.zip(hi).map(|(l, h)| (h - l) * (h - l)).sum::<f64>().sqrt()
|
||||
};
|
||||
let merge = p.merge.unwrap_or(1e-6 * diagonal);
|
||||
if !(merge.is_finite() && merge >= 0.0 && merge <= 1e-2 * diagonal) {
|
||||
return Err(EngineError::Params(format!("`merge` must be from 0 to 1/100 of the block's diagonal ({diagonal})")));
|
||||
}
|
||||
|
||||
let next = |last: Option<u32>| last.map_or(Some(1), |l| l.checked_add(1));
|
||||
let first_node = next(s.model.nodes().keys().next_back().copied());
|
||||
let first_element = next(s.model.elements().keys().next_back().copied());
|
||||
let fits = |first: Option<u32>, count: usize| first.filter(|&f| u64::from(f) + count as u64 <= u64::from(MAX_LABEL) + 1);
|
||||
let (Some(first_node), Some(first_element)) = (fits(first_node, mesh.points.len()), fits(first_element, mesh.elements.len())) else {
|
||||
return Err(EngineError::Params(format!("the block needs labels beyond {MAX_LABEL}")));
|
||||
};
|
||||
|
||||
// Each block point: an existing node within `merge`, or a new label.
|
||||
let existing = joined(&s.model, &mesh.points, merge)?;
|
||||
let mut labels = Vec::with_capacity(mesh.points.len());
|
||||
let mut added = 0u32;
|
||||
for (i, xyz) in mesh.points.iter().enumerate() {
|
||||
match existing.get(i).copied().flatten() {
|
||||
Some(label) => labels.push(label),
|
||||
None => {
|
||||
let label = first_node + added;
|
||||
s.model.set_node(label, *xyz)?;
|
||||
labels.push(label);
|
||||
added += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let merged: BTreeSet<u32> = existing.iter().flatten().copied().collect();
|
||||
let elements: Vec<Vec<u32>> = mesh.elements.iter().map(|e| e.iter().map(|&i| labels.get(i as usize).copied().unwrap_or(0)).collect()).collect();
|
||||
if !merged.is_empty() {
|
||||
check_conforming(&s.model, kind, &mesh, &elements, &merged)?;
|
||||
}
|
||||
|
||||
let body_tag = s.model.geometry_tag(&body)?;
|
||||
for (i, nodes) in elements.into_iter().enumerate() {
|
||||
let label = first_element + i as u32;
|
||||
s.model.set_element(label, kind, nodes)?;
|
||||
s.model.tag_element(label, body_tag)?;
|
||||
}
|
||||
for (k, r) in &face_refs {
|
||||
let t = s.model.geometry_tag(r)?;
|
||||
for &(e, f, bf) in &mesh.faces {
|
||||
if usize::from(bf) == *k {
|
||||
s.model.tag_element_face(first_element + e, f, t)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (k, r) in &edge_refs {
|
||||
let t = s.model.geometry_tag(r)?;
|
||||
for &(e, edge, be) in &mesh.edges {
|
||||
if usize::from(be) == *k {
|
||||
s.model.tag_element_edge(first_element + e, edge, t)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (k, r) in &vertex_refs {
|
||||
let t = s.model.geometry_tag(r)?;
|
||||
let node = mesh.corners.get(*k).and_then(|&i| labels.get(i as usize)).copied().unwrap_or(0);
|
||||
s.model.tag_node(node, t)?;
|
||||
}
|
||||
Ok(json!({
|
||||
"body": p.body,
|
||||
"kind": kind.name(),
|
||||
"elements": mesh.elements.len(),
|
||||
"first_element": first_element,
|
||||
"nodes": added,
|
||||
"first_node": first_node,
|
||||
"joined": merged.len(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// For each block point, the existing node within `merge` of it (the nearest; the lowest label
|
||||
/// on a tie). Two points may not join the same node.
|
||||
fn joined(model: &Model, points: &[[f64; 3]], merge: f64) -> Result<Vec<Option<u32>>> {
|
||||
if merge == 0.0 || model.nodes().is_empty() {
|
||||
return Ok(vec![None; points.len()]);
|
||||
}
|
||||
let cell = |x: f64| (x / merge).floor() as i64;
|
||||
let mut grid: BTreeMap<[i64; 3], Vec<(u32, [f64; 3])>> = BTreeMap::new();
|
||||
for (&label, node) in model.nodes() {
|
||||
grid.entry(node.xyz.map(cell)).or_default().push((label, node.xyz));
|
||||
}
|
||||
let mut out = Vec::with_capacity(points.len());
|
||||
let mut taken: BTreeMap<u32, usize> = BTreeMap::new();
|
||||
for (i, p) in points.iter().enumerate() {
|
||||
let c = p.map(cell);
|
||||
let mut best: Option<(f64, u32)> = None;
|
||||
for dx in -1..=1 {
|
||||
for dy in -1..=1 {
|
||||
for dz in -1..=1 {
|
||||
for &(label, xyz) in grid.get(&[c[0] + dx, c[1] + dy, c[2] + dz]).into_iter().flatten() {
|
||||
let d = ((xyz[0] - p[0]).powi(2) + (xyz[1] - p[1]).powi(2) + (xyz[2] - p[2]).powi(2)).sqrt();
|
||||
if d <= merge && best.is_none_or(|(bd, bl)| d < bd || (d == bd && label < bl)) {
|
||||
best = Some((d, label));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((_, label)) = best
|
||||
&& let Some(other) = taken.insert(label, i)
|
||||
{
|
||||
return Err(EngineError::Params(format!(
|
||||
"block points {other} and {i} both lie within `merge` of node {label}: lower `merge` or refine the block"
|
||||
)));
|
||||
}
|
||||
out.push(best.map(|(_, l)| l));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Faces as sorted node lists.
|
||||
fn key(nodes: impl Iterator<Item = u32>) -> Vec<u32> {
|
||||
let mut k: Vec<u32> = nodes.collect();
|
||||
k.sort_unstable();
|
||||
k
|
||||
}
|
||||
|
||||
/// Where the block joins existing nodes, its boundary faces and the existing element faces on
|
||||
/// those nodes must be the same faces: no hanging nodes, no mixed linear and quadratic sides.
|
||||
/// A block joins along whole block faces and edges: when every corner of a block face (both
|
||||
/// ends of a block edge) lands on an existing node, every node on it must.
|
||||
fn check_conforming(model: &Model, kind: ElementKind, mesh: &BlockMesh, elements: &[Vec<u32>], merged: &BTreeSet<u32>) -> Result<()> {
|
||||
// Labels of the block's corners: the corner point's label in any element that uses it.
|
||||
let mut point_label: BTreeMap<u32, u32> = BTreeMap::new();
|
||||
for (e, points) in mesh.elements.iter().enumerate() {
|
||||
for (k, &pt) in points.iter().enumerate() {
|
||||
if let Some(&l) = elements.get(e).and_then(|n| n.get(k)) {
|
||||
point_label.insert(pt, l);
|
||||
}
|
||||
}
|
||||
}
|
||||
let joined = |pt: u32| point_label.get(&pt).is_some_and(|l| merged.contains(l));
|
||||
let hex_faces = ElementKind::Hex8.faces();
|
||||
let pieces = |list: &[(u32, u8, u8)], which: usize, local: fn(ElementKind, usize) -> Option<&'static [usize]>| -> Vec<u32> {
|
||||
list.iter()
|
||||
.filter(|t| usize::from(t.2) == which)
|
||||
.flat_map(|&(e, k, _)| {
|
||||
let nodes = elements.get(e as usize);
|
||||
local(kind, usize::from(k)).unwrap_or(&[]).iter().filter_map(move |&i| nodes.and_then(|n| n.get(i)).copied())
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
for (bf, corners) in hex_faces.iter().enumerate() {
|
||||
if corners.iter().all(|&c| mesh.corners.get(c).is_some_and(|&pt| joined(pt))) {
|
||||
let nodes = pieces(&mesh.faces, bf, |k, f| k.faces().get(f.checked_sub(1)?).copied());
|
||||
if nodes.iter().any(|n| !merged.contains(n)) {
|
||||
return Err(EngineError::Params(format!(
|
||||
"block face {} lies on existing nodes at its corners but not at every node: the block must meet the mesh face to face (same divisions and element kind)",
|
||||
bf + 1
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (be, [a, b]) in modelcraft_mesh::block::EDGES.iter().enumerate() {
|
||||
if [a, b].iter().all(|&&c| mesh.corners.get(c).is_some_and(|&pt| joined(pt))) {
|
||||
let nodes = pieces(&mesh.edges, be, |k, f| k.edges().get(f.checked_sub(1)?).copied());
|
||||
if nodes.iter().any(|n| !merged.contains(n)) {
|
||||
return Err(EngineError::Params(format!(
|
||||
"the block edge from corner {} to {} ends on existing nodes but not every node on it does: the block must meet the mesh with the same divisions",
|
||||
a + 1,
|
||||
b + 1
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
let new_faces: BTreeSet<Vec<u32>> = mesh
|
||||
.faces
|
||||
.iter()
|
||||
.filter_map(|&(e, f, _)| {
|
||||
let nodes = elements.get(e as usize)?;
|
||||
let local = kind.faces().get(usize::from(f).checked_sub(1)?)?;
|
||||
Some(key(local.iter().filter_map(|&i| nodes.get(i).copied())))
|
||||
})
|
||||
.collect();
|
||||
let mut old_faces = BTreeSet::new();
|
||||
for (label, e) in model.elements() {
|
||||
if !e.nodes.iter().any(|n| merged.contains(n)) {
|
||||
continue;
|
||||
}
|
||||
for local in e.kind.faces() {
|
||||
let face = key(local.iter().filter_map(|&i| e.nodes.get(i).copied()));
|
||||
if face.iter().all(|n| merged.contains(n)) {
|
||||
if !new_faces.contains(&face) {
|
||||
return Err(EngineError::Params(format!(
|
||||
"element {label} has a face on the block that no block element shares: the block must meet the mesh face to face (same divisions and element kind)"
|
||||
)));
|
||||
}
|
||||
old_faces.insert(face);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(f) = new_faces.iter().find(|f| f.iter().all(|n| merged.contains(n)) && !old_faces.contains(*f)) {
|
||||
return Err(EngineError::Params(format!(
|
||||
"the block face on nodes {f:?} lies on existing nodes but matches no element face there: the block must meet the mesh face to face (same divisions and element kind)"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
fn cube(x0: f64) -> serde_json::Value {
|
||||
json!([[x0, 0, 0], [x0 + 1.0, 0, 0], [x0 + 1.0, 1, 0], [x0, 1, 0], [x0, 0, 1], [x0 + 1.0, 0, 1], [x0 + 1.0, 1, 1], [x0, 1, 1]])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocks_join_face_to_face_and_name_their_geometry() {
|
||||
let mut s = Session::default();
|
||||
let r = s
|
||||
.execute(
|
||||
"mesh.block",
|
||||
&json!({"body": "Bar", "kind": "hex20", "corners": cube(0.0), "divisions": [2, 2, 2], "faces": {"6": "root"}, "vertices": {"7": "corner"}, "edges": [{"corners": [5, 8], "name": "top_root"}]}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!((r["elements"].clone(), r["nodes"].clone(), r["joined"].clone()), (json!(8), json!(81), json!(0)));
|
||||
let r = s
|
||||
.execute("mesh.block", &json!({"body": "Bar", "kind": "hex20", "corners": cube(1.0), "divisions": [2, 2, 2], "faces": {"4": "tip"}}))
|
||||
.unwrap();
|
||||
assert_eq!((r["first_element"].clone(), r["nodes"].clone(), r["joined"].clone()), (json!(9), json!(81 - 21), json!(21)));
|
||||
for (set, geometry, nodes) in [
|
||||
("root", json!({"body": "Bar", "face": "root"}), 21),
|
||||
("tip", json!({"body": "Bar", "face": "tip"}), 21),
|
||||
("edge", json!({"body": "Bar", "edge": "top_root"}), 5),
|
||||
("corner", json!({"body": "Bar", "vertex": "corner"}), 1),
|
||||
] {
|
||||
s.execute("model.set.create", &json!({"name": set, "geometry": [geometry]})).unwrap();
|
||||
let r = s.execute("model.set.resolve", &json!({"name": set})).unwrap();
|
||||
assert_eq!(r["nodes"]["count"], nodes, "{set}: {r}");
|
||||
}
|
||||
let q = s.execute("mesh.quality", &json!({})).unwrap();
|
||||
assert_eq!((q["valid"].clone(), q["volume"].as_f64().map(|v| (v - 2.0).abs() < 1e-12)), (json!(16), Some(true)));
|
||||
|
||||
// A third block that meets the second with other divisions or another kind changes nothing.
|
||||
let model = s.model.clone();
|
||||
for bad in [
|
||||
json!({"body": "Bar", "kind": "hex20", "corners": cube(2.0), "divisions": [2, 3, 2]}),
|
||||
json!({"body": "Bar", "kind": "hex20", "corners": cube(2.0), "divisions": [1, 1, 1]}),
|
||||
json!({"body": "Bar", "kind": "hex8", "corners": cube(2.0), "divisions": [4, 4, 4]}),
|
||||
json!({"body": "Bar", "kind": "tet10", "corners": cube(2.0), "divisions": [2, 2, 2]}),
|
||||
] {
|
||||
assert!(s.execute("mesh.block", &bad).is_err(), "{bad}");
|
||||
assert_eq!(s.model, model);
|
||||
}
|
||||
// A finer block first, then a coarser one against it: caught from the other side.
|
||||
let mut t = Session::default();
|
||||
t.execute("mesh.block", &json!({"body": "A", "kind": "hex8", "corners": cube(0.0), "divisions": [2, 2, 2]})).unwrap();
|
||||
assert!(t.execute("mesh.block", &json!({"body": "B", "kind": "hex8", "corners": cube(1.0), "divisions": [1, 1, 1]})).is_err());
|
||||
assert!(t.execute("mesh.block", &json!({"body": "B", "kind": "hex8", "corners": cube(1.0), "divisions": [1, 1, 1], "merge": 0})).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curved_edges_and_hostile_parameters() {
|
||||
let mut s = Session::default();
|
||||
let corners = json!([[1, 0, 0], [2, 0, 0], [0, 2, 0], [0, 1, 0], [1, 0, 1], [2, 0, 1], [0, 2, 1], [0, 1, 1]]);
|
||||
let edges = json!([
|
||||
{"corners": [2, 3], "arc": {"center": [0, 0, 0]}},
|
||||
{"corners": [6, 7], "arc": {"center": [0, 0, 1]}},
|
||||
{"corners": [1, 4], "ellipse": {"center": [0, 0, 0], "axes": [[1, 0, 0], [0, 1, 0]]}},
|
||||
{"corners": [5, 8], "arc": {"center": [0, 0, 1]}, "name": "inner_top"}
|
||||
]);
|
||||
let r =
|
||||
s.execute("mesh.block", &json!({"body": "Ring", "kind": "tet10", "corners": corners, "divisions": [2, 4, 1], "edges": edges})).unwrap();
|
||||
assert_eq!(r["elements"], 48);
|
||||
let q = s.execute("mesh.quality", &json!({})).unwrap();
|
||||
let exact = std::f64::consts::PI * 3.0 / 4.0;
|
||||
assert!((q["volume"].as_f64().unwrap() - exact).abs() < 1e-3 * exact, "{q}");
|
||||
|
||||
let journal = s.journal.len();
|
||||
for bad in [
|
||||
json!({}),
|
||||
json!({"body": "B", "kind": "hex8", "corners": cube(5.0), "divisions": [1, 1]}),
|
||||
json!({"body": "B", "kind": "quad4", "corners": cube(5.0), "divisions": [1, 1, 1]}),
|
||||
json!({"body": "B", "kind": "hex8", "corners": cube(5.0), "divisions": [1, 1, 1], "faces": {"7": "x"}}),
|
||||
json!({"body": "B", "kind": "hex8", "corners": cube(5.0), "divisions": [1, 1, 1], "faces": {"one": "x"}}),
|
||||
json!({"body": "B", "kind": "hex8", "corners": cube(5.0), "divisions": [1, 1, 1], "vertices": {"0": "x"}}),
|
||||
json!({"body": "B", "kind": "hex8", "corners": cube(5.0), "divisions": [1, 1, 1], "edges": [{"corners": [1, 7], "name": "x"}]}),
|
||||
json!({"body": "B", "kind": "hex8", "corners": cube(5.0), "divisions": [1, 1, 1], "edges": [{"corners": [0, 1], "name": "x"}]}),
|
||||
json!({"body": "B", "kind": "hex8", "corners": cube(5.0), "divisions": [1, 1, 1], "edges": [{"corners": [1, 2]}]}),
|
||||
json!({"body": "B", "kind": "hex8", "corners": cube(5.0), "divisions": [1, 1, 1], "merge": -1}),
|
||||
json!({"body": "B", "kind": "hex8", "corners": cube(5.0), "divisions": [1, 1, 1], "merge": 1}),
|
||||
json!({"body": "B", "kind": "hex8", "corners": cube(5.0), "divisions": [1e9, 1e9, 1e9]}),
|
||||
json!({"body": "B", "kind": "hex8", "corners": cube(1e300), "divisions": [1, 1, 1]}),
|
||||
json!({"body": "", "kind": "hex8", "corners": cube(5.0), "divisions": [1, 1, 1]}),
|
||||
json!({"body": "B", "kind": "hex8", "corners": cube(5.0), "divisions": [1, 1, 1], "colour": "red"}),
|
||||
] {
|
||||
assert!(s.execute("mesh.block", &bad).is_err(), "{bad}");
|
||||
}
|
||||
assert_eq!(s.journal.len(), journal);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
//! The command registry. Each area has its own module; [`ALL`] lists every command.
|
||||
|
||||
mod block;
|
||||
pub(crate) mod deck;
|
||||
pub(crate) mod define;
|
||||
mod edit;
|
||||
@@ -32,6 +33,7 @@ pub static ALL: &[CommandSpec] = &[
|
||||
mesh::SURFACE_CHECK,
|
||||
mesh::PLANAR,
|
||||
mesh::TET_GENERATE,
|
||||
block::BLOCK,
|
||||
tags::TAG,
|
||||
tags::TAG_CLEAR,
|
||||
model::UNITS,
|
||||
|
||||
@@ -54,7 +54,7 @@ pub const FLUID: CommandSpec = CommandSpec {
|
||||
pub const PROBE: CommandSpec = CommandSpec {
|
||||
id: "solve.probe",
|
||||
label: "Probe Results",
|
||||
params: r#"{"step": "Load", "nodes": [1, 2], "mode": 1, "frame": 0} — the stored result at nodes: displacement, reaction, stress and von Mises (static), the shape of mode `mode` (frequency, from 1), or displacement and velocity in field frame `frame` (explicit, from 0; the last by default) at `nodes` and, with `elements` (explicit), each element's stress, von Mises stress, strain, equivalent plastic strain, damage, temperature and whether it has eroded; a coupled reference point's label gives its six displacements (translations, rotations) and reactions (forces, moments). Fluid steps: {"step": "Flow", "points": [[x, y, z]], "frame": 10 (default the last)} gives velocity and pressure in the cells at the points; {"step": "Flow", "forces": ["x_min", "F2:top", "Body1/F2:top"]} gives those walls' force histories"#,
|
||||
params: r#"{"step": "Load", "nodes": [1, 2], "mode": 1, "frame": 0} — the stored result at nodes: displacement, reaction, stress and von Mises (static), the shape of mode `mode` (frequency, from 1), or displacement and velocity in field frame `frame` (explicit, from 0; the last by default) at `nodes` (or at the nodes nearest to points: "at": [[x, y, z], ...], lowest label on a tie, each entry then giving its `xyz` and `distance`) and, with `elements` (explicit), each element's stress, von Mises stress, strain, equivalent plastic strain, damage, temperature and whether it has eroded; a coupled reference point's label gives its six displacements (translations, rotations) and reactions (forces, moments). Fluid steps: {"step": "Flow", "points": [[x, y, z]], "frame": 10 (default the last)} gives velocity and pressure in the cells at the points; {"step": "Flow", "forces": ["x_min", "F2:top", "Body1/F2:top"]} gives those walls' force histories"#,
|
||||
kind: CommandKind::Model,
|
||||
run: probe,
|
||||
};
|
||||
@@ -345,15 +345,55 @@ fn probe(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
format!("step `{step}` has no result; solve it first")
|
||||
})
|
||||
})?;
|
||||
let result = match result.as_ref() {
|
||||
StepResult::Explicit(r) => {
|
||||
let list = |key| if p.get(key).is_some() { labels(p, key, MAX_PROBE) } else { Ok(Vec::new()) };
|
||||
return probe_frame(r, &step, &list("nodes")?, &list("elements")?, p);
|
||||
if let StepResult::Fluid(r) = result.as_ref() {
|
||||
return probe_fluid(r, p);
|
||||
}
|
||||
let (nodes, located) = match (p.get("nodes"), p.get("at")) {
|
||||
(Some(_), None) => (labels(p, "nodes", MAX_PROBE)?, None),
|
||||
(None, Some(_)) => {
|
||||
let at = points(p, "at", MAX_PROBE)?;
|
||||
let found = nearest_nodes(&s.model, result.nodes(), &at)?;
|
||||
(found.iter().map(|(l, _)| *l).collect(), Some(found))
|
||||
}
|
||||
StepResult::Fluid(r) => return probe_fluid(r, p),
|
||||
StepResult::Implicit(r) => r,
|
||||
(None, None) if matches!(result.as_ref(), StepResult::Explicit(_)) => (Vec::new(), None),
|
||||
_ => return Err(EngineError::Params("give either `nodes` or `at`".into())),
|
||||
};
|
||||
let nodes = labels(p, "nodes", MAX_PROBE)?;
|
||||
let mut out = match result.as_ref() {
|
||||
StepResult::Explicit(r) => {
|
||||
let elements = if p.get("elements").is_some() { labels(p, "elements", MAX_PROBE)? } else { Vec::new() };
|
||||
probe_frame(r, &step, &nodes, &elements, p)?
|
||||
}
|
||||
StepResult::Implicit(r) => probe_implicit(r, &step, &nodes, p)?,
|
||||
StepResult::Fluid(r) => probe_fluid(r, p)?,
|
||||
};
|
||||
if let (Some(found), Some(entries)) = (located, out.get_mut("nodes").and_then(Value::as_array_mut)) {
|
||||
for (entry, (label, distance)) in entries.iter_mut().zip(found) {
|
||||
if let Some(obj) = entry.as_object_mut() {
|
||||
obj.insert("xyz".into(), json!(s.model.node(label).map(|n| n.xyz)));
|
||||
obj.insert("distance".into(), json!(distance));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The node with a result nearest to each point (lowest label on a tie), and its distance.
|
||||
fn nearest_nodes(model: &modelcraft_model::Model, with_result: &[u32], at: &[[f64; 3]]) -> Result<Vec<(u32, f64)>> {
|
||||
at.iter()
|
||||
.map(|p| {
|
||||
with_result
|
||||
.iter()
|
||||
.filter_map(|&l| {
|
||||
model.node(l).map(|n| (l, ((n.xyz[0] - p[0]).powi(2) + (n.xyz[1] - p[1]).powi(2) + (n.xyz[2] - p[2]).powi(2)).sqrt()))
|
||||
})
|
||||
.fold(None, |best: Option<(u32, f64)>, (l, d)| if best.is_none_or(|(_, bd)| d < bd) { Some((l, d)) } else { best })
|
||||
.ok_or_else(|| EngineError::Params("the result has no nodes to probe".into()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The result of an implicit step at nodes.
|
||||
fn probe_implicit(result: &ImplicitResult, step: &str, nodes: &[u32], p: &Value) -> Result<Value> {
|
||||
let locate = |label: u32| {
|
||||
result
|
||||
.locate(label)
|
||||
@@ -507,6 +547,15 @@ pub(crate) mod tests {
|
||||
let probe = s.execute("solve.probe", &json!({"step": "Bend", "nodes": [11, 1]})).unwrap();
|
||||
assert!(probe["nodes"][0]["displacement"][2].as_f64().unwrap() < -3.8, "{probe}");
|
||||
assert!(probe["nodes"][1]["reaction"][2].as_f64().unwrap() != 0.0, "{probe}");
|
||||
// By location: the nearest node, with where it is.
|
||||
let at = s.execute("solve.probe", &json!({"step": "Bend", "at": [[10.1, -0.2, 0.1], [0, 0, 0]]})).unwrap();
|
||||
assert_eq!((at["nodes"][0]["node"].clone(), at["nodes"][1]["node"].clone()), (json!(11), json!(1)), "{at}");
|
||||
assert_eq!(at["nodes"][0]["displacement"], probe["nodes"][0]["displacement"]);
|
||||
assert_eq!(at["nodes"][0]["xyz"], json!([10.0, 0.0, 0.0]));
|
||||
assert!((at["nodes"][0]["distance"].as_f64().unwrap() - 0.06f64.sqrt()).abs() < 1e-12);
|
||||
for bad in [json!({"step": "Bend"}), json!({"step": "Bend", "nodes": [1], "at": [[0, 0, 0]]}), json!({"step": "Bend", "at": [[0, 0]]})] {
|
||||
assert!(s.execute("solve.probe", &bad).is_err(), "{bad}");
|
||||
}
|
||||
|
||||
let f = s.execute("solve.frequency", &json!({"mass": "lumped"})).unwrap();
|
||||
assert_eq!(f["modes"].as_array().unwrap().len(), 4, "{f}");
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
//! Mapped blocks: a structured mesh of one six-sided region given by its eight corners and,
|
||||
//! optionally, curved edges (circular and elliptical arcs).
|
||||
//!
|
||||
//! The interior follows the edge-based transfinite interpolation of Gordon and Hall (1973,
|
||||
//! "Construction of curvilinear co-ordinate systems and applications to mesh generation",
|
||||
//! Int. J. Numer. Meth. Engng 7, 461–477): the twelve edge curves blended linearly across the
|
||||
//! block, minus twice the trilinear interpolation of the corners. It reproduces every edge
|
||||
//! exactly, gives Coons patches on the faces and the trilinear map for straight edges.
|
||||
//!
|
||||
//! Elements: hex8 and hex20 per cell; tet4 and tet10 by Kuhn's split of each cell into six
|
||||
//! tetrahedra along its main diagonal (conforming between the cells of a block); wedge6 by
|
||||
//! splitting each cell's first-second-direction face along its diagonal and extruding along the
|
||||
//! third. Quadratic elements take their mid-side nodes from the same map, so they lie on the
|
||||
//! curved edges.
|
||||
//!
|
||||
//! Every element is checked with the certified Jacobian test ([`crate::quality`]); a block
|
||||
//! whose corners are not in hex8 order or whose curves fold it is an error.
|
||||
|
||||
use modelcraft_model::{ElementKind, MAX_COORD};
|
||||
|
||||
use crate::quality::{Validity, element_validity};
|
||||
use crate::{Error, Result};
|
||||
|
||||
type P = [f64; 3];
|
||||
|
||||
/// Most elements one block makes.
|
||||
pub const MAX_BLOCK_ELEMENTS: usize = 4_000_000;
|
||||
|
||||
/// Most divisions along one direction.
|
||||
pub const MAX_DIVISIONS: usize = 100_000;
|
||||
|
||||
/// The corners of the block's twelve edges, grouped by direction: four along 1→2, four along
|
||||
/// 1→4, four along 1→5, each from the corner where its direction's parameter is 0 to the one
|
||||
/// where it is 1. Block edge numbers in [`BlockMesh::edges`] index this table.
|
||||
pub const EDGES: [[usize; 2]; 12] = [[0, 1], [3, 2], [4, 5], [7, 6], [0, 3], [1, 2], [4, 7], [5, 6], [0, 4], [1, 5], [2, 6], [3, 7]];
|
||||
|
||||
/// Corner `c` of the block in parameter space (hex8 order).
|
||||
const CORNERS: [[usize; 3]; 8] = [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]];
|
||||
|
||||
/// The shape of one block edge between its two corners.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Curve {
|
||||
Line,
|
||||
/// The shorter circular arc about `center` (both corners at the same distance from it).
|
||||
Arc {
|
||||
center: P,
|
||||
},
|
||||
/// The shorter arc of the ellipse `center + a cos t + b sin t` (`a` and `b` perpendicular)
|
||||
/// between the two corners, which must lie on it.
|
||||
Ellipse {
|
||||
center: P,
|
||||
axes: [P; 2],
|
||||
},
|
||||
}
|
||||
|
||||
/// One block to mesh.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Block {
|
||||
pub kind: ElementKind,
|
||||
/// The corners in hex8 order: 1-2-3-4 counter-clockwise seen from 5-6-7-8's side.
|
||||
pub corners: [P; 8],
|
||||
/// Elements along the directions 1→2, 1→4 and 1→5.
|
||||
pub divisions: [usize; 3],
|
||||
/// Last element size over first along each direction (geometric grading); 1 is uniform.
|
||||
pub bias: [f64; 3],
|
||||
/// The curve of each edge, by corner pair (0-based corners).
|
||||
pub curves: Vec<([usize; 2], Curve)>,
|
||||
}
|
||||
|
||||
/// The mesh of a block: points, elements as point indices in the model's node order, and where
|
||||
/// element faces and edges lie on the block's faces and edges.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct BlockMesh {
|
||||
pub points: Vec<P>,
|
||||
pub elements: Vec<Vec<u32>>,
|
||||
/// `(element, face 1-based, block face 0..6)`: block faces in hex8 face order.
|
||||
pub faces: Vec<(u32, u8, u8)>,
|
||||
/// `(element, edge 1-based, block edge 0..12)`: block edges as in [`EDGES`].
|
||||
pub edges: Vec<(u32, u8, u8)>,
|
||||
/// The point at each block corner.
|
||||
pub corners: [u32; 8],
|
||||
}
|
||||
|
||||
impl Block {
|
||||
/// The block edge ([`EDGES`] index) joining two 0-based corners, either way round.
|
||||
pub fn edge_index(corners: [usize; 2]) -> Option<usize> {
|
||||
EDGES.iter().position(|e| *e == corners || *e == [corners[1], corners[0]])
|
||||
}
|
||||
}
|
||||
|
||||
fn sub(a: P, b: P) -> P {
|
||||
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
|
||||
}
|
||||
fn add_scaled(a: P, s: f64, b: P) -> P {
|
||||
[a[0] + s * b[0], a[1] + s * b[1], a[2] + s * b[2]]
|
||||
}
|
||||
fn dot(a: P, b: P) -> f64 {
|
||||
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
}
|
||||
fn cross(a: P, b: P) -> P {
|
||||
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
|
||||
}
|
||||
fn norm(a: P) -> f64 {
|
||||
dot(a, a).sqrt()
|
||||
}
|
||||
|
||||
/// An edge curve resolved against its end points: `point(s)` for `s` in `[0, 1]`.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Path {
|
||||
Line { from: P, to: P },
|
||||
Ellipse { center: P, a: P, b: P, start: f64, sweep: f64 },
|
||||
}
|
||||
|
||||
impl Path {
|
||||
fn new(curve: Curve, from: P, to: P) -> Result<Path> {
|
||||
let (center, a, b) = match curve {
|
||||
Curve::Line => return Ok(Path::Line { from, to }),
|
||||
Curve::Arc { center } => {
|
||||
let (p, q) = (sub(from, center), sub(to, center));
|
||||
let r = norm(p);
|
||||
if !(r > 0.0) || (norm(q) - r).abs() > 1e-9 * r.max(norm(q)) {
|
||||
return Err(Error::Invalid(format!(
|
||||
"an arc's corners {from:?} and {to:?} must be at the same distance from its centre {center:?}"
|
||||
)));
|
||||
}
|
||||
let n = cross(p, q);
|
||||
if !(norm(n) > 1e-12 * r * r) {
|
||||
return Err(Error::Invalid(format!(
|
||||
"the arc from {from:?} to {to:?} about {center:?} has no plane (it spans 0 or 180 degrees); split it"
|
||||
)));
|
||||
}
|
||||
let b = cross(n, p);
|
||||
(center, p, [b[0] * r / norm(b), b[1] * r / norm(b), b[2] * r / norm(b)])
|
||||
}
|
||||
Curve::Ellipse { center, axes: [a, b] } => (center, a, b),
|
||||
};
|
||||
let (la, lb) = (norm(a), norm(b));
|
||||
if !(la > 0.0 && lb > 0.0) || dot(a, b).abs() > 1e-9 * la * lb {
|
||||
return Err(Error::Invalid("an ellipse's two axes must be non-zero and perpendicular".into()));
|
||||
}
|
||||
let angle = |p: P| -> Result<f64> {
|
||||
let d = sub(p, center);
|
||||
let (c, s) = (dot(d, a) / (la * la), dot(d, b) / (lb * lb));
|
||||
let back = add_scaled(add_scaled(center, c, a), s, b);
|
||||
if norm(sub(back, p)) > 1e-9 * la.max(lb) || ((c * c + s * s).sqrt() - 1.0).abs() > 1e-9 {
|
||||
return Err(Error::Invalid(format!("corner {p:?} is not on the edge's ellipse or arc about {center:?}")));
|
||||
}
|
||||
Ok(s.atan2(c))
|
||||
};
|
||||
let start = angle(from)?;
|
||||
let mut sweep = angle(to)? - start;
|
||||
if sweep > std::f64::consts::PI {
|
||||
sweep -= 2.0 * std::f64::consts::PI;
|
||||
} else if sweep <= -std::f64::consts::PI {
|
||||
sweep += 2.0 * std::f64::consts::PI;
|
||||
}
|
||||
if !(sweep.abs() > 1e-12) || std::f64::consts::PI - sweep.abs() < 1e-9 {
|
||||
return Err(Error::Invalid(format!("the arc from {from:?} to {to:?} spans 0 or 180 degrees; split it")));
|
||||
}
|
||||
Ok(Path::Ellipse { center, a, b, start, sweep })
|
||||
}
|
||||
|
||||
fn point(&self, s: f64) -> P {
|
||||
match *self {
|
||||
Path::Line { from, to } => add_scaled(from, s, sub(to, from)),
|
||||
Path::Ellipse { center, a, b, start, sweep } => {
|
||||
let t = start + s * sweep;
|
||||
add_scaled(add_scaled(center, t.cos(), a), t.sin(), b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The parameters of `n` elements along one direction with last/first size ratio `bias`, at
|
||||
/// `order` points per element (`order · n + 1` values from 0 to 1; mid-side values halfway).
|
||||
fn parameters(n: usize, bias: f64, order: usize) -> Vec<f64> {
|
||||
let q = if n > 1 { bias.powf(1.0 / (n - 1) as f64) } else { 1.0 };
|
||||
let mut ends = Vec::with_capacity(n + 1);
|
||||
let (mut sum, mut size) = (0.0, 1.0);
|
||||
ends.push(0.0);
|
||||
for _ in 0..n {
|
||||
sum += size;
|
||||
ends.push(sum);
|
||||
size *= q;
|
||||
}
|
||||
let ends: Vec<f64> = ends.iter().map(|e| e / sum).collect();
|
||||
let mut out = Vec::with_capacity(order * n + 1);
|
||||
for w in ends.windows(2) {
|
||||
let (a, b) = (w[0], w[1]);
|
||||
out.push(a);
|
||||
if order == 2 {
|
||||
out.push(0.5 * (a + b));
|
||||
}
|
||||
}
|
||||
out.push(1.0);
|
||||
out
|
||||
}
|
||||
|
||||
/// The map from the unit cube onto the block.
|
||||
struct Map {
|
||||
corners: [P; 8],
|
||||
edges: [Path; 12],
|
||||
}
|
||||
|
||||
impl Map {
|
||||
fn point(&self, u: f64, v: f64, w: f64) -> P {
|
||||
let e = |k: usize, s: f64| self.edges[k].point(s);
|
||||
let terms: [(f64, P); 12] = [
|
||||
((1.0 - v) * (1.0 - w), e(0, u)),
|
||||
(v * (1.0 - w), e(1, u)),
|
||||
((1.0 - v) * w, e(2, u)),
|
||||
(v * w, e(3, u)),
|
||||
((1.0 - u) * (1.0 - w), e(4, v)),
|
||||
(u * (1.0 - w), e(5, v)),
|
||||
((1.0 - u) * w, e(6, v)),
|
||||
(u * w, e(7, v)),
|
||||
((1.0 - u) * (1.0 - v), e(8, w)),
|
||||
(u * (1.0 - v), e(9, w)),
|
||||
(u * v, e(10, w)),
|
||||
((1.0 - u) * v, e(11, w)),
|
||||
];
|
||||
let mut x = [0.0; 3];
|
||||
for (f, p) in terms {
|
||||
x = add_scaled(x, f, p);
|
||||
}
|
||||
for (c, at) in self.corners.iter().zip(CORNERS) {
|
||||
let f = |bit: usize, t: f64| if bit == 1 { t } else { 1.0 - t };
|
||||
x = add_scaled(x, -2.0 * f(at[0], u) * f(at[1], v) * f(at[2], w), *c);
|
||||
}
|
||||
x
|
||||
}
|
||||
}
|
||||
|
||||
/// Lattice offsets of each element of one cell, in units of the lattice (two per cell for
|
||||
/// quadratic kinds), in the model's node order.
|
||||
fn cell_elements(kind: ElementKind) -> Vec<Vec<[usize; 3]>> {
|
||||
let scale = |c: [usize; 3], m: usize| [c[0] * m, c[1] * m, c[2] * m];
|
||||
match kind {
|
||||
ElementKind::Hex8 => vec![CORNERS.to_vec()],
|
||||
ElementKind::Hex20 => {
|
||||
let mut nodes: Vec<[usize; 3]> = CORNERS.iter().map(|c| scale(*c, 2)).collect();
|
||||
for edge in kind.edges() {
|
||||
if let (Some(&a), Some(&b)) = (edge.first(), edge.get(1))
|
||||
&& let (Some(pa), Some(pb)) = (CORNERS.get(a), CORNERS.get(b))
|
||||
{
|
||||
nodes.push([pa[0] + pb[0], pa[1] + pb[1], pa[2] + pb[2]]);
|
||||
}
|
||||
}
|
||||
vec![nodes]
|
||||
}
|
||||
ElementKind::Tet4 | ElementKind::Tet10 => {
|
||||
let m = if kind == ElementKind::Tet10 { 2 } else { 1 };
|
||||
[[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]]
|
||||
.iter()
|
||||
.map(|perm| {
|
||||
let mut v = [[0usize; 3]; 4];
|
||||
for s in 0..3 {
|
||||
v[s + 1] = v[s];
|
||||
v[s + 1][perm[s]] = 1;
|
||||
}
|
||||
// Even permutations are positively oriented; swap two nodes of the others.
|
||||
let odd = matches!(perm, [0, 2, 1] | [1, 0, 2] | [2, 1, 0]);
|
||||
if odd {
|
||||
v.swap(1, 2);
|
||||
}
|
||||
let mut nodes: Vec<[usize; 3]> = v.iter().map(|c| scale(*c, m)).collect();
|
||||
if m == 2 {
|
||||
for [a, b] in [[0, 1], [1, 2], [2, 0], [0, 3], [1, 3], [2, 3]] {
|
||||
nodes.push([v[a][0] + v[b][0], v[a][1] + v[b][1], v[a][2] + v[b][2]]);
|
||||
}
|
||||
}
|
||||
nodes
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
ElementKind::Wedge6 => vec![
|
||||
vec![[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1]],
|
||||
vec![[0, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1], [1, 1, 1], [0, 1, 1]],
|
||||
],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Mesh one block.
|
||||
pub fn block(b: &Block) -> Result<BlockMesh> {
|
||||
let kind = b.kind;
|
||||
if !matches!(kind, ElementKind::Hex8 | ElementKind::Hex20 | ElementKind::Tet4 | ElementKind::Tet10 | ElementKind::Wedge6) {
|
||||
return Err(Error::Invalid(format!("a block is meshed with hex8, hex20, tet4, tet10 or wedge6 elements, not {}", kind.name())));
|
||||
}
|
||||
if b.corners.iter().flatten().any(|c| !c.is_finite() || c.abs() > MAX_COORD) {
|
||||
return Err(Error::Invalid(format!("block corners must be finite and within ±{MAX_COORD}")));
|
||||
}
|
||||
if b.divisions.iter().any(|&n| n == 0 || n > MAX_DIVISIONS) {
|
||||
return Err(Error::Invalid(format!("divisions must be 1 to {MAX_DIVISIONS} in each direction")));
|
||||
}
|
||||
if b.bias.iter().any(|&r| !(r.is_finite() && (1e-6..=1e6).contains(&r))) {
|
||||
return Err(Error::Invalid("bias must be between 1e-6 and 1e6 (1 is uniform)".into()));
|
||||
}
|
||||
let per_cell = cell_elements(kind).len();
|
||||
let cells = b.divisions.iter().try_fold(1usize, |acc, &n| acc.checked_mul(n));
|
||||
let count = cells.and_then(|c| c.checked_mul(per_cell)).filter(|&n| n <= MAX_BLOCK_ELEMENTS).ok_or_else(|| {
|
||||
Error::Limit(format!("a block makes at most {MAX_BLOCK_ELEMENTS} elements; {:?} divisions of {} are more", b.divisions, kind.name()))
|
||||
})?;
|
||||
|
||||
let mut curves = [Curve::Line; 12];
|
||||
let mut seen = [false; 12];
|
||||
for (pair, curve) in &b.curves {
|
||||
let k = Block::edge_index(*pair)
|
||||
.ok_or_else(|| Error::Invalid(format!("corners {} and {} are not joined by a block edge", pair[0] + 1, pair[1] + 1)))?;
|
||||
if std::mem::replace(&mut seen[k], true) {
|
||||
return Err(Error::Invalid(format!("the edge from corner {} to {} is given twice", pair[0] + 1, pair[1] + 1)));
|
||||
}
|
||||
if let Some(slot) = curves.get_mut(k) {
|
||||
*slot = *curve;
|
||||
}
|
||||
}
|
||||
let mut paths = [Path::Line { from: [0.0; 3], to: [0.0; 3] }; 12];
|
||||
for (k, [from, to]) in EDGES.iter().enumerate() {
|
||||
paths[k] = Path::new(curves[k], b.corners[*from], b.corners[*to])?;
|
||||
}
|
||||
let map = Map { corners: b.corners, edges: paths };
|
||||
|
||||
let order = if kind.is_quadratic() { 2 } else { 1 };
|
||||
let params: [Vec<f64>; 3] = [0, 1, 2].map(|d| parameters(b.divisions[d], b.bias[d], order));
|
||||
let size = [0, 1, 2].map(|d| params[d].len());
|
||||
let index = |c: [usize; 3]| c[0] + size[0] * (c[1] + size[1] * c[2]);
|
||||
|
||||
// Elements as lattice coordinates; then number the lattice points they use, in lattice order.
|
||||
let pattern = cell_elements(kind);
|
||||
let mut lattice: Vec<Vec<[usize; 3]>> = Vec::with_capacity(count);
|
||||
for k in 0..b.divisions[2] {
|
||||
for j in 0..b.divisions[1] {
|
||||
for i in 0..b.divisions[0] {
|
||||
let base = [i * order, j * order, k * order];
|
||||
for e in &pattern {
|
||||
lattice.push(e.iter().map(|o| [base[0] + o[0], base[1] + o[1], base[2] + o[2]]).collect());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let total = size.iter().product::<usize>();
|
||||
let mut number: Vec<u32> = vec![u32::MAX; total];
|
||||
for e in &lattice {
|
||||
for c in e {
|
||||
if let Some(slot) = number.get_mut(index(*c)) {
|
||||
*slot = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut points = Vec::new();
|
||||
for k in 0..size[2] {
|
||||
for j in 0..size[1] {
|
||||
for i in 0..size[0] {
|
||||
if let Some(slot) = number.get_mut(index([i, j, k]))
|
||||
&& *slot == 0
|
||||
{
|
||||
*slot = u32::try_from(points.len()).map_err(|_| Error::Limit("too many points in one block".into()))?;
|
||||
points.push(map.point(params[0][i], params[1][j], params[2][k]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let point_of = |c: [usize; 3]| number.get(index(c)).copied().filter(|&n| n != u32::MAX);
|
||||
let last = [0, 1, 2].map(|d| size[d] - 1);
|
||||
let mut corners = [0u32; 8];
|
||||
for (slot, c) in corners.iter_mut().zip(CORNERS) {
|
||||
*slot = point_of([c[0] * last[0], c[1] * last[1], c[2] * last[2]]).ok_or_else(|| Error::Failed("a block corner has no point".into()))?;
|
||||
}
|
||||
|
||||
// Block faces in hex8 face order: (direction, at the end?).
|
||||
const FACES: [(usize, bool); 6] = [(2, false), (2, true), (1, false), (0, true), (1, true), (0, false)];
|
||||
let on_face = |c: &[usize; 3], f: (usize, bool)| c[f.0] == if f.1 { last[f.0] } else { 0 };
|
||||
let corner_count = kind.corner_count();
|
||||
let mut elements = Vec::with_capacity(lattice.len());
|
||||
let mut faces = Vec::new();
|
||||
let mut edges = Vec::new();
|
||||
for (e, nodes) in lattice.iter().enumerate() {
|
||||
let label = u32::try_from(e).map_err(|_| Error::Limit("too many elements in one block".into()))?;
|
||||
let ids: Vec<u32> =
|
||||
nodes.iter().map(|c| point_of(*c)).collect::<Option<_>>().ok_or_else(|| Error::Failed("an element node has no point".into()))?;
|
||||
let xyz: Vec<P> = ids.iter().map(|&i| points.get(i as usize).copied().unwrap_or([f64::NAN; 3])).collect();
|
||||
let check = element_validity(kind, &xyz)?;
|
||||
if check.validity != Validity::Valid {
|
||||
let cell = nodes.first().map(|c| [c[0] / order, c[1] / order, c[2] / order]).unwrap_or_default();
|
||||
return Err(Error::Invalid(format!(
|
||||
"the block's {} element in cell {cell:?} is inverted or degenerate: give the corners in hex8 order (1-2-3-4 counter-clockwise seen from 5-6-7-8) and curves that do not fold the block",
|
||||
kind.name()
|
||||
)));
|
||||
}
|
||||
for (f, local) in kind.faces().iter().enumerate() {
|
||||
let cs: Vec<&[usize; 3]> = local.iter().filter(|&&i| i < corner_count).filter_map(|&i| nodes.get(i)).collect();
|
||||
for (bf, side) in FACES.iter().enumerate() {
|
||||
if cs.iter().all(|c| on_face(c, *side)) {
|
||||
faces.push((label, f as u8 + 1, bf as u8));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (k, local) in kind.edges().iter().enumerate() {
|
||||
let (Some(a), Some(c)) = (local.first().and_then(|&i| nodes.get(i)), local.get(1).and_then(|&i| nodes.get(i))) else { continue };
|
||||
for (be, [from, to]) in EDGES.iter().enumerate() {
|
||||
// The block edge is where two of the three lattice coordinates are fixed.
|
||||
let along = (0..3).find(|&d| CORNERS[*from][d] != CORNERS[*to][d]).unwrap_or(0);
|
||||
let fixed = |p: &[usize; 3]| (0..3).filter(|&d| d != along).all(|d| p[d] == CORNERS[*from][d] * last[d]);
|
||||
if fixed(a) && fixed(c) {
|
||||
edges.push((label, k as u8 + 1, be as u8));
|
||||
}
|
||||
}
|
||||
}
|
||||
elements.push(ids);
|
||||
}
|
||||
Ok(BlockMesh { points, elements, faces, edges, corners })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn unit(kind: ElementKind, divisions: [usize; 3]) -> Block {
|
||||
let corners = CORNERS.map(|c| [c[0] as f64, c[1] as f64, c[2] as f64]);
|
||||
Block { kind, corners, divisions, bias: [1.0; 3], curves: Vec::new() }
|
||||
}
|
||||
|
||||
fn volume(kind: ElementKind, m: &BlockMesh) -> f64 {
|
||||
m.elements
|
||||
.iter()
|
||||
.map(|e| {
|
||||
let xyz: Vec<P> = e.iter().map(|&i| m.points[i as usize]).collect();
|
||||
element_validity(kind, &xyz).unwrap().volume
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_kind_fills_a_cube_with_valid_elements_and_shared_nodes() {
|
||||
for (kind, per_cell, points) in [
|
||||
(ElementKind::Hex8, 1, 27),
|
||||
(ElementKind::Hex20, 1, 81),
|
||||
(ElementKind::Tet4, 6, 27),
|
||||
(ElementKind::Tet10, 6, 125),
|
||||
(ElementKind::Wedge6, 2, 27),
|
||||
] {
|
||||
let m = block(&unit(kind, [2, 2, 2])).unwrap();
|
||||
assert_eq!(m.elements.len(), 8 * per_cell, "{kind:?}");
|
||||
assert_eq!(m.points.len(), points, "{kind:?}");
|
||||
assert!((volume(kind, &m) - 1.0).abs() < 1e-12, "{kind:?}");
|
||||
// Each block face is covered by element faces of total area 1: count them instead.
|
||||
let per_face = |bf: u8| m.faces.iter().filter(|f| f.2 == bf).count();
|
||||
let expect = if matches!(kind, ElementKind::Hex8 | ElementKind::Hex20) { 4 } else { 8 };
|
||||
for bf in 0..6 {
|
||||
let n = per_face(bf);
|
||||
// Wedges have quads on the sides and triangles top and bottom.
|
||||
let want = if kind == ElementKind::Wedge6 && bf >= 2 { 4 } else { expect };
|
||||
assert_eq!(n, want, "{kind:?} face {bf}");
|
||||
}
|
||||
for be in 0..12 {
|
||||
// Two segments per block edge (an edge segment may belong to several elements).
|
||||
let mut segments: Vec<[u32; 2]> = m
|
||||
.edges
|
||||
.iter()
|
||||
.filter(|e| e.2 == be)
|
||||
.map(|&(e, k, _)| {
|
||||
let local = kind.edges()[k as usize - 1];
|
||||
let el = &m.elements[e as usize];
|
||||
let mut s = [el[local[0]], el[local[1]]];
|
||||
s.sort();
|
||||
s
|
||||
})
|
||||
.collect();
|
||||
segments.sort();
|
||||
segments.dedup();
|
||||
assert_eq!(segments.len(), 2, "{kind:?} edge {be}");
|
||||
}
|
||||
assert_eq!(m.points[m.corners[6] as usize], [1.0, 1.0, 1.0]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arcs_and_ellipses_put_every_edge_node_on_the_curve() {
|
||||
// A quarter annulus a = 1, b = 2, extruded 0.5, hex20 so mid-side nodes are on the arcs.
|
||||
let (a, b) = (1.0, 2.0);
|
||||
let corners = [[a, 0.0, 0.0], [b, 0.0, 0.0], [0.0, b, 0.0], [0.0, a, 0.0], [a, 0.0, 0.5], [b, 0.0, 0.5], [0.0, b, 0.5], [0.0, a, 0.5]];
|
||||
let o = |z: f64| [0.0, 0.0, z];
|
||||
let curves = vec![
|
||||
([1, 2], Curve::Arc { center: o(0.0) }),
|
||||
([0, 3], Curve::Arc { center: o(0.0) }),
|
||||
([5, 6], Curve::Arc { center: o(0.5) }),
|
||||
([4, 7], Curve::Ellipse { center: o(0.5), axes: [[a, 0.0, 0.0], [0.0, a, 0.0]] }),
|
||||
];
|
||||
let m = block(&Block { kind: ElementKind::Hex20, corners, divisions: [3, 6, 2], bias: [1.0; 3], curves }).unwrap();
|
||||
for p in &m.points {
|
||||
let r = p[0].hypot(p[1]);
|
||||
assert!(r > a - 1e-12 && r < b + 1e-12, "{p:?}");
|
||||
}
|
||||
let exact = std::f64::consts::PI / 4.0 * (b * b - a * a) * 0.5;
|
||||
let v = volume(ElementKind::Hex20, &m);
|
||||
assert!((v - exact).abs() < 1e-4 * exact, "{v} vs {exact}");
|
||||
// The inner face's nodes are on r = a.
|
||||
let elements = &m.elements;
|
||||
let inner: Vec<u32> = m
|
||||
.faces
|
||||
.iter()
|
||||
.filter(|f| f.2 == 5)
|
||||
.flat_map(|&(e, f, _)| ElementKind::Hex20.faces()[f as usize - 1].iter().map(move |&i| elements[e as usize][i]))
|
||||
.collect();
|
||||
assert!(!inner.is_empty());
|
||||
assert!(inner.iter().all(|&i| (m.points[i as usize][0].hypot(m.points[i as usize][1]) - a).abs() < 1e-12));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grading_and_bad_blocks() {
|
||||
let mut g = unit(ElementKind::Hex8, [4, 1, 1]);
|
||||
g.bias = [8.0, 1.0, 1.0];
|
||||
let m = block(&g).unwrap();
|
||||
let xs: Vec<f64> = (0..5).map(|i| m.points[i][0]).collect();
|
||||
let (first, last) = (xs[1] - xs[0], xs[4] - xs[3]);
|
||||
assert!((last / first - 8.0).abs() < 1e-9, "{xs:?}");
|
||||
|
||||
let mut inverted = unit(ElementKind::Hex8, [1, 1, 1]);
|
||||
inverted.corners.swap(1, 3);
|
||||
assert!(block(&inverted).is_err());
|
||||
let mut off = unit(ElementKind::Hex8, [1, 1, 1]);
|
||||
off.curves = vec![([0, 1], Curve::Arc { center: [0.3, 0.0, 0.0] })];
|
||||
assert!(block(&off).is_err(), "corners at different distances from the centre");
|
||||
let mut half = unit(ElementKind::Hex8, [1, 1, 1]);
|
||||
half.curves = vec![([0, 1], Curve::Arc { center: [0.5, 0.0, 0.0] })];
|
||||
assert!(block(&half).is_err(), "180 degrees has no plane");
|
||||
let mut diagonal = unit(ElementKind::Hex8, [1, 1, 1]);
|
||||
diagonal.curves = vec![([0, 6], Curve::Line)];
|
||||
assert!(block(&diagonal).is_err(), "not an edge");
|
||||
for divisions in [[0, 1, 1], [MAX_DIVISIONS + 1, 1, 1], [MAX_DIVISIONS, MAX_DIVISIONS, MAX_DIVISIONS]] {
|
||||
assert!(block(&unit(ElementKind::Hex8, divisions)).is_err(), "{divisions:?}");
|
||||
}
|
||||
assert!(block(&unit(ElementKind::Quad4, [1, 1, 1])).is_err());
|
||||
let mut nan = unit(ElementKind::Hex8, [1, 1, 1]);
|
||||
nan.corners[0][0] = f64::NAN;
|
||||
assert!(block(&nan).is_err());
|
||||
let mut bias = unit(ElementKind::Hex8, [2, 1, 1]);
|
||||
bias.bias[0] = 0.0;
|
||||
assert!(block(&bias).is_err());
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod block;
|
||||
pub mod cdt;
|
||||
pub mod delaunay;
|
||||
pub mod planar;
|
||||
|
||||
Reference in new issue
Block a user