From 7095151f396e3d5670a7929ec13b70b3c0504673 Mon Sep 17 00:00:00 2001 From: Robb Walters Date: Mon, 5 Oct 2026 17:20:29 -0700 Subject: [PATCH 1/4] feat: support segwid1/segwid2 meshed planes on uniform planes (#155) Co-Authored-By: Claude Sonnet 5.5 Loom-Story: 2AMLogic/fasterhenry#155 Loom-Trace-Id: 57f5786320a6fd5a1db99c699d5b3f64 Loom-Build: 0.19.732 c1ef52e4d1926fd6f224abe962829326121161da clean --- CHANGELOG.md | 6 +- docs/fasthenry-compat.md | 3 +- fasterhenry-cli/README.md | 2 +- fasterhenry-cli/src/inp.rs | 223 ++++++++++++++++++++++++++++++++++--- fasterhenry/src/plane.rs | 25 ++++- 5 files changed, 241 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4c1bca0..d37ae4c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ breaking changes to the API or the command-line interface; patch releases with cells `(w/2)·scale_factor^min(|tan θ|, |cot θ|)`, and a line-numbered warning that this approximates FastHenry's staircase refinement. Native mode still rejects it. +- `G` planes accept `segwid1=`/`segwid2=` (meshed planes): a per-bar width + cap on uniform planes, with a warning when above the spacing; on + nonuniform planes an error natively and ignored with a warning under + `--fasthenry-compat` (issue #155). - Deck reader: `relx=` / `rely=` / `relz=` on a corner-point `G` statement (issue #146), the documented offset (User's Guide §1.3.9), in every mode. It is added to every node-reference, hole and contact coordinate on the @@ -199,7 +203,7 @@ breaking changes to the API or the command-line interface; patch releases planes is rejected rather than silently attaching to one. `nhinc=` is also accepted on the shorthand form. Every remaining documented plane parameter is rejected by name, with the - statement's line number and the alternative: `rh`, `segwid1`/`segwid2`, + statement's line number and the alternative: `rh`, `relx`/`rely`/`relz`, `file`, and every hole or contact shape other than `rect` (`point`, `circle`, `decay_rect`, `line`, `trace`, `connection`, the `initial_*`/`equiv_*` forms and the user-defined `user1…user7`) — nothing on a `G` statement is diff --git a/docs/fasthenry-compat.md b/docs/fasthenry-compat.md index 1b478cac..5c338aec 100644 --- a/docs/fasthenry-compat.md +++ b/docs/fasthenry-compat.md @@ -111,7 +111,8 @@ records how each documented field maps. | `rho=` | Supported | Issue #88. Per deck unit, the exact reciprocal of `sigma=` and accepted on the corner-point statement itself (continuation lines included); naming both `sigma=` and `rho=` on one statement is a line-numbered error, as it is elsewhere. | | `relx=`/`rely=`/`relz=` | Supported | Issue #146. The documented offset (User's Guide §1.3.9), default 0, in every mode: it is added to every in-plane coordinate on the statement — each `N (x, y, z)` node reference, each `hole` shape's points or centre, and each `contact` clause's points, ends or centre (`rect`, `decay_rect`, `point`, `line`, `trace`, `equiv_rect`, `connection`) — but **not** to the corner points `x1…z3`, so the mesh is unchanged while the connections move. A `relx=0.3` deck reads exactly as the same deck with those coordinates written 0.3 larger. Only coordinates (points, centres) move; widths, cell sizes and radii do not (they are lengths — see the plane-coordinate decision below, issue #118). Position-independent: a key written after the clauses still applies to them, and a repeated key's last value wins for every point. A `relz` that lifts a clause or node out of the plane's slab is the same line-numbered error the shifted coordinate would be. | | Empty coordinate field in a node reference, `N (, y, z)` | Supported (compat only) | Issue #146. Under `--fasthenry-compat`, one empty field reads as 0 before the `relx`/`rely`/`relz` offset, with a line-numbered warning. Natively it is a line-numbered error, and two or more empty fields are an error in both modes. Clause value lists (`hole …`, `contact …`) are unaffected. | -| `rh=`, `segwid1=`/`segwid2=` | Not supported | Each rejected by name with the reason and the alternative (plane filaments are uniform; bar widths follow the cells). Nothing on a `G` statement is silently ignored. | +| `rh=` | Not supported | Rejected by name with the reason and the alternative (plane filaments are uniform; use `nhinc`). Nothing on a `G` statement is silently ignored. | +| `segwid1=`/`segwid2=` | Supported | Issue #155. The documented meshed plane (User's Guide §1.3.9), a per-bar width override on a uniform (`seg1`/`seg2`) plane: a bar parallel to p1→p2 gets width `min(segwid1, its cell width across)`, and likewise `segwid2` for bars parallel to p2→p3. The mesh, nodes and bar count are unchanged. A value above the spacing is a line-numbered warning and the full width is used. On a nonuniform plane (`file=NONE`, `contact initial_grid`, any `contact` clause) it is a line-numbered error natively and ignored with a line-numbered warning under `--fasthenry-compat`. A non-positive width is an error. | | `file=NONE` | Supported | Issue #122. The nonuniform-plane description defines `file=` as an *input* — the file holding the plane's discretization hierarchy — and `NONE` as "no such file": the hierarchy is a single root cell, discretized at run time from the statement's own clauses. That is the only case this reader ever has, so `file=NONE` is accepted as the no-op it is and the documented `file=NONE contact initial_grid (10,12)` spelling reads as written. Matched without regard to case, like every other token this reader interprets. | | `file=` (a named hierarchy file) | Not supported | Issue #122. Rejected by name on the statement's own line: a stored nonuniform-discretization hierarchy is an *input* this reader does not read (not, as the message used to say, an output it does not write), and silently ignoring it would mesh the plane at a resolution the deck never asked for. The error names the file, says only `file=NONE` is accepted, and points at the in-statement alternatives — `seg1`/`seg2` or `contact initial_grid (n1, n2)` plus the `contact` refinement clauses, which is what this reader discretizes from. | | In-plane node `N (x, y, z)` | Supported | An ordinary deck node belonging to the plane; a reference to it lands on the nearest live cell-centre node of that plane. | diff --git a/fasterhenry-cli/README.md b/fasterhenry-cli/README.md index 6cbb2193..66bb07e1 100644 --- a/fasterhenry-cli/README.md +++ b/fasterhenry-cli/README.md @@ -199,7 +199,7 @@ the statement itself, alongside `nhinc=` — naming both on one statement, continuation lines included, is a line-numbered error, and naming neither falls back to the `.default` conductivity. Every other documented plane parameter is either mapped or **rejected by name** with the statement's line -number — `rh`, `segwid1`/`segwid2`, `relx`/`rely`/`relz`, `file`, and every +number — `rh`, `relx`/`rely`/`relz`, `file`, and every hole or contact shape other than `rect`. Nothing on a `G` statement is silently ignored. `tests/data/plane_fasthenry.inp` and `tests/data/plane_extension.inp` are diff --git a/fasterhenry-cli/src/inp.rs b/fasterhenry-cli/src/inp.rs index 342a07ce..23798207 100644 --- a/fasterhenry-cli/src/inp.rs +++ b/fasterhenry-cli/src/inp.rs @@ -446,8 +446,14 @@ //! are an error in both modes. //! * The remaining documented plane parameters are rejected by name too, //! each with the reason and the alternative: `rh` (plane filaments are -//! uniform) and `segwid1`/`segwid2` (bar widths follow the cells). -//! Nothing on a `G` statement is silently ignored. +//! uniform). Nothing on a `G` statement is silently ignored. +//! * **`segwid1`/`segwid2`** (the documented meshed plane) cap the width of +//! the bars parallel to p1→p2 and to p2→p3 at `min(segwid, cell width +//! across the bar)`; the mesh and bar count are unchanged. A value above +//! the spacing warns and the full width is used. On a nonuniform plane +//! (`file=NONE`, `contact initial_grid`, any `contact` clause) they are +//! an error natively and ignored with a warning under +//! [`ParseOptions::fasthenry_compat`]. //! //! # Semantics //! @@ -589,6 +595,9 @@ struct PlaneSpec { plane: GroundPlane, /// Filaments across each plane bar's thickness (`nhinc=`; 1 by default). nhinc: usize, + /// `segwid1=`/`segwid2=` as caps on the bar width, in global axes: + /// `[x-directed bars, y-directed bars]`, metres (`None`: full width). + bar_width: [Option; 2], } /// An in-plane node awaiting assembly: the declared node slot it occupies, @@ -1667,6 +1676,11 @@ struct PlaneStatement<'a> { /// Whether that clause was the meshed form, which additionally punches /// the documented checkerboard of holes into the initial grid. meshed_grid: bool, + /// `segwid1=` / `segwid2=`: bar width (metres) along p1→p2 and along + /// p2→p3, each with the clause's physical line and source order. + segwid: [Option<(f64, ClauseAt)>; 2], + /// Whether `file=NONE` was written (the plane is then nonuniform). + file_none: bool, /// `sigma=` / `rho=` (S/m), or the `.default` conductivity. sigma: Option, /// `nhinc=` (1 unless set). @@ -1803,12 +1817,17 @@ impl<'a> PlaneStatement<'a> { )); } "segwid1" | "segwid2" => { - return Err(err( - line, - format!( - "ground plane '{head}': '{key}' (an explicit plane-segment width) is not supported; this engine's plane bars take their width from the cell they span (set seg1/seg2, or refine locally with '.contact')" - ), - )); + let width = parse_number(raw, line)? * unit; + if width <= 0.0 { + return Err(err( + line, + format!( + "ground plane '{head}': '{key}' needs a width > 0 (got '{key}={raw}')" + ), + )); + } + self.segwid[usize::from(key == "segwid2")] = + Some((width, (self.clause_line, self.clause_seq))); } // Read by `apply_offset` before any other item (issue #146). "relx" | "rely" | "relz" => {} @@ -1824,7 +1843,8 @@ impl<'a> PlaneStatement<'a> { // case, as every other token this reader interprets is. A // *named* file is rejected by name: an input this reader // does not read, not an output it does not write. - if !raw.eq_ignore_ascii_case("NONE") { + self.file_none = raw.eq_ignore_ascii_case("NONE"); + if !self.file_none { return Err(err( line, format!( @@ -1845,7 +1865,7 @@ impl<'a> PlaneStatement<'a> { return Err(err( line, format!( - "unknown ground-plane parameter '{other}' (supported: x1…z3, thick, seg1, seg2, sigma, rho, nhinc, relx, rely, relz)" + "unknown ground-plane parameter '{other}' (supported: x1…z3, thick, seg1, seg2, segwid1, segwid2, sigma, rho, nhinc, relx, rely, relz)" ), )); } @@ -2373,10 +2393,21 @@ impl<'a> PlaneStatement<'a> { /// off the plane), but neither is an error. fn finish(self) -> Result<(PlaneSpec, Vec, Vec), ParseError> { let frame = self.frame()?; + // A plane is nonuniform once anything but `seg1`/`seg2` shapes its + // mesh; a meshed-plane bar width has no meaning there. + let nonuniform = self.file_none + || self.initial_grid.is_some() + || !(self.contact_rects.is_empty() + && self.contact_decays.is_empty() + && self.contact_lines.is_empty() + && self.contact_traces.is_empty() + && self.contact_equivs.is_empty()); let PlaneStatement { head, nhinc, meshed_grid, + segwid, + compat, mut nodes, hole_rects, hole_points, @@ -2390,6 +2421,58 @@ impl<'a> PlaneStatement<'a> { .. } = self; + // `segwid1` / `segwid2`: caps on the bars parallel to p1→p2 and to + // p2→p3. Each bar's cross-spacing is the cell width across it. + let mut bar_width = [None, None]; + for (index, entry) in segwid.iter().enumerate() { + let Some((width, (clause_line, order))) = *entry else { + continue; + }; + let key = format!("segwid{}", index + 1); + if nonuniform { + if !compat { + return Err(err( + clause_line, + format!( + "ground plane '{head}': '{key}' needs a uniform plane, but this plane's mesh is nonuniform ('file=NONE', 'contact initial_grid' or a 'contact' clause); drop '{key}', or read the deck with --fasthenry-compat, which ignores it" + ), + )); + } + warnings.push(( + order, + ParseWarning { + line: clause_line, + message: format!( + "ground plane '{head}': '{key}' is ignored on a nonuniform plane (full-width bars are used)" + ), + }, + )); + continue; + } + // Bars parallel to p1→p2 run along `axis1` and are as wide as + // the cell across the other axis. + let along = if index == 0 { + frame.axis1 + } else { + 1 - frame.axis1 + }; + let across = 1 - along; + #[allow(clippy::cast_precision_loss)] + let spacing = (frame.hi[across] - frame.lo[across]) / frame.cells[across] as f64; + if width > spacing * (1.0 + 1e-9) { + warnings.push(( + order, + ParseWarning { + line: clause_line, + message: format!( + "ground plane '{head}': '{key}' ({width} metres) is greater than the segment separation ({spacing} metres); the full width is used" + ), + }, + )); + } + bar_width[along] = Some(width); + } + // The meshed initial grid's holes are cut into the *initial* grid, // before anything else refines it, so they come first. let mut holes = if meshed_grid { @@ -2563,6 +2646,7 @@ impl<'a> PlaneStatement<'a> { equipotentials, }, nhinc, + bar_width, }, nodes, { @@ -3937,6 +4021,7 @@ impl DeckBuilder { equipotentials: Vec::new(), }, nhinc, + bar_width: [None, None], }); Ok(()) } @@ -4185,7 +4270,7 @@ fn build_plane_meshes<'a>( for spec in planes { let centres = spec .plane - .build_into(geometry) + .build_into_narrowed(geometry, spec.bar_width) .map_err(|error| ParseError { line: 0, message: error.to_string(), @@ -7000,6 +7085,118 @@ Gp x1=0 y1=0 z1=0 x2=10 y2=0 z2=0 x3=10 y3=6 z3=0 assert_eq!(spaced.geometry, tight.geometry); } + /// Widths (metres) of the plane's bars, split by direction: the bars + /// along global x, then those along global y. The via's own segment is + /// excluded (it is not horizontal). + fn plane_bar_widths(deck: &Deck) -> (Vec, Vec) { + let (mut along_x, mut along_y) = (Vec::new(), Vec::new()); + for segment in deck.geometry.segments() { + let d = [ + segment.b.x - segment.a.x, + segment.b.y - segment.a.y, + segment.b.z - segment.a.z, + ]; + if d[2].abs() > 1e-12 { + continue; + } + if d[0].abs() > d[1].abs() { + along_x.push(segment.width); + } else { + along_y.push(segment.width); + } + } + (along_x, along_y) + } + + fn segwid_deck(extra: &str) -> String { + plane_deck( + &format!( + "\ +Gp x1=0 y1=0 z1=0 x2=10 y2=0 z2=0 x3=10 y3=10 z3=0 ++ thick=0.04 seg1=4 seg2=4 {extra}" + ), + "", + ) + } + + /// `segwid1`/`segwid2` only cap the bar width: the mesh and bar count + /// are unchanged, `segwid1` narrows the bars parallel to p1→p2 and + /// `segwid2` those parallel to p2→p3. + #[test] + fn segwid_narrows_the_bars_of_its_own_direction_only() { + let plain = parse_ok(&segwid_deck("")); + let (px, py) = plane_bar_widths(&plain); + assert!(px.iter().chain(&py).all(|w| (w - 2.5e-3).abs() < 1e-12)); + for (extra, x_width, y_width) in [ + ("segwid1=0.5 segwid2=0.5", 0.5e-3, 0.5e-3), + ("segwid1=0.5", 0.5e-3, 2.5e-3), + ("segwid2=0.5", 2.5e-3, 0.5e-3), + ] { + let (deck, warnings) = parse_reporting(&segwid_deck(extra)).unwrap(); + assert!(warnings.is_empty(), "{warnings:?}"); + let (x, y) = plane_bar_widths(&deck); + assert_eq!((x.len(), y.len()), (px.len(), py.len()), "{extra}"); + assert!(x.iter().all(|w| (w - x_width).abs() < 1e-12), "{extra}"); + assert!(y.iter().all(|w| (w - y_width).abs() < 1e-12), "{extra}"); + assert_eq!( + deck.geometry.segment_count(), + plain.geometry.segment_count() + ); + } + } + + /// `segwid1` follows the p1→p2 edge, not global x. + #[test] + fn segwid1_follows_the_first_edge_on_a_rotated_plane() { + let deck = parse_ok(&plane_deck( + "\ +Gp x1=0 y1=0 z1=0 x2=0 y2=10 z2=0 x3=10 y3=10 z3=0 ++ thick=0.04 seg1=4 seg2=4 segwid1=0.5", + "", + )); + let (x, y) = plane_bar_widths(&deck); + assert!(y.iter().all(|w| (w - 0.5e-3).abs() < 1e-12)); + assert!(x.iter().all(|w| (w - 2.5e-3).abs() < 1e-12)); + } + + /// A width above the node spacing warns on its own line and uses the + /// full spacing. + #[test] + fn segwid_above_the_spacing_warns_and_uses_full_width() { + let (deck, warnings) = parse_reporting(&segwid_deck("segwid2=3")).unwrap(); + assert_eq!(warnings.len(), 1, "{warnings:?}"); + assert_eq!(warnings[0].line, 4); + assert!(warnings[0].message.contains("'segwid2'")); + assert!(warnings[0].message.contains("segment separation")); + let (x, y) = plane_bar_widths(&deck); + assert!(x.iter().chain(&y).all(|w| (w - 2.5e-3).abs() < 1e-12)); + } + + /// On a nonuniform plane, native mode errors and compat ignores the + /// width with a line-numbered warning. + #[test] + fn segwid_on_a_nonuniform_plane_is_compat_only_ignored() { + for extra in [ + "segwid1=0.5 file=NONE", + "segwid1=0.5 contact point (5, 5, 0, 1, 1)", + ] { + let text = segwid_deck(extra); + let error = parse(&text).unwrap_err(); + assert_eq!(error.line, 4); + assert!(error.message.contains("'segwid1'"), "{}", error.message); + let (deck, warnings) = + parse_with_options_reporting(&format!("title\n{text}"), COMPAT).unwrap(); + assert!( + warnings + .iter() + .any(|w| w.line == 5 && w.message.contains("'segwid1' is ignored")), + "{warnings:?}" + ); + let (x, y) = plane_bar_widths(&deck); + assert!(x.iter().chain(&y).all(|w| *w > 0.5e-3 + 1e-9)); + } + } + /// Every documented plane parameter this engine cannot represent is /// rejected by name, on the statement's own line — never ignored. #[test] @@ -7017,8 +7214,8 @@ Gp x1=0 y1=0 z1=0 x2=10 y2=0 z2=0 x3=10 y3=6 z3=0 }; for (extra, expected) in [ ("nhinc=3 rh=2", "'rh'"), - ("segwid1=0.5", "'segwid1'"), - ("segwid2=0.5", "'segwid2'"), + ("segwid1=0", "'segwid1' needs a width > 0"), + ("segwid2=-1", "'segwid2' needs a width > 0"), // Only a *named* discretization hierarchy is rejected; the // documented `file=NONE` is accepted as the no-op it is — see // `file_none_is_the_documented_no_op_and_a_named_hierarchy_file_is_rejected`. diff --git a/fasterhenry/src/plane.rs b/fasterhenry/src/plane.rs index 436de683..b2837adf 100644 --- a/fasterhenry/src/plane.rs +++ b/fasterhenry/src/plane.rs @@ -1056,6 +1056,27 @@ impl GroundPlane { &self, geometry: &mut Geometry, ) -> Result>>, PlaneError> { + self.build_into_narrowed(geometry, [None, None]) + } + + /// As [`GroundPlane::build_into`], with an optional cap on the width of + /// the bars of each direction: `max_width[0]` caps the x-directed bars + /// and `max_width[1]` the y-directed ones. A bar's width is + /// `min(cap, its cell's extent across)`, so a cap above the cell + /// spacing changes nothing; the mesh, nodes and bar count are + /// unchanged. This is the meshed-plane (narrowed-bar) form of a uniform + /// plane. + /// + /// # Errors + /// + /// See [`PlaneError`]; a non-positive cap is rejected by the geometry + /// builder as a bad segment width. + pub fn build_into_narrowed( + &self, + geometry: &mut Geometry, + max_width: [Option; 2], + ) -> Result>>, PlaneError> { + let cap = |axis: usize, width: f64| max_width[axis].map_or(width, |cap| cap.min(width)); let mesh = self.mesh()?; let (nx, ny) = (mesh.nx(), mesh.ny()); let ties = self.ties(&mesh); @@ -1099,7 +1120,7 @@ impl GroundPlane { geometry.add_segment(SegmentDef::new( here, right, - mesh.dy(j), + cap(0, mesh.dy(j)), self.thickness, self.sigma, ))?; @@ -1111,7 +1132,7 @@ impl GroundPlane { geometry.add_segment(SegmentDef::new( here, up, - mesh.dx(i), + cap(1, mesh.dx(i)), self.thickness, self.sigma, ))?; From 1ed4004af8a4f12331a3e64280c593f7af946657 Mon Sep 17 00:00:00 2001 From: Loom CI Date: Thu, 15 Oct 2026 11:31:52 +0000 Subject: [PATCH 2/4] test: cover segwid on file=NONE planes and through the CLI (#155) Address Judge feedback on PR #162: a bare file=NONE plane (main's #143 single root cell) is nonuniform, so segwid is ignored with a warning under compat; add a CLI-level test for the native error, compat warning and above-spacing warning. Co-Authored-By: Claude Sonnet 5.5 Loom-Story: 2AMLogic/fasterhenry#155 Loom-Trace-Id: 57f5786320a6fd5a1db99c699d5b3f64 Loom-Build: 0.19.1162 f1ee755eeca1954f35bf83d19f4b84ad51feb9f5 clean --- fasterhenry-cli/src/inp.rs | 29 ++++++++++++++++++ fasterhenry-cli/tests/cli.rs | 57 ++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/fasterhenry-cli/src/inp.rs b/fasterhenry-cli/src/inp.rs index 8e51b18f..9a50534d 100644 --- a/fasterhenry-cli/src/inp.rs +++ b/fasterhenry-cli/src/inp.rs @@ -7547,6 +7547,35 @@ Gp x1=0 y1=0 z1=0 x2=0 y2=10 z2=0 x3=10 y3=10 z3=0 } } + /// A bare `file=NONE` plane (issue #143: a single root cell under + /// compat) is nonuniform, so `segwid` follows the same rule as on any + /// nonuniform plane: ignored with a line-numbered warning under compat, + /// an error natively (the plane has no `seg1`/`seg2` there). + #[test] + fn segwid_on_a_bare_file_none_plane_is_ignored_under_compat() { + let text = plane_deck( + "\ +Gp x1=0 y1=0 z1=0 x2=10 y2=0 z2=0 x3=10 y3=10 z3=0 ++ thick=0.04 file=NONE segwid1=0.5", + "", + ); + assert!(parse(&text).is_err()); + let (deck, warnings) = + parse_with_options_reporting(&format!("title\n{text}"), COMPAT).unwrap(); + assert!( + warnings + .iter() + .any(|w| w.line == 5 && w.message.contains("'segwid1' is ignored")), + "{warnings:?}" + ); + assert!( + warnings.iter().any(|w| w.message.contains("single cell")), + "{warnings:?}" + ); + let (x, y) = plane_bar_widths(&deck); + assert!(x.iter().chain(&y).all(|w| *w > 0.5e-3 + 1e-9)); + } + /// Every documented plane parameter this engine cannot represent is /// rejected by name, on the statement's own line — never ignored. #[test] diff --git a/fasterhenry-cli/tests/cli.rs b/fasterhenry-cli/tests/cli.rs index 6745db4d..86c4c474 100644 --- a/fasterhenry-cli/tests/cli.rs +++ b/fasterhenry-cli/tests/cli.rs @@ -372,6 +372,63 @@ fn a_hole_off_its_plane_solves_with_a_line_numbered_warning() { assert!(warning.contains("ground plane 'Gplane'"), "{warning}"); } +/// `segwid1=` on a nonuniform plane (issue #155) is a line-numbered error +/// natively and is ignored with a line-numbered warning under +/// `--fasthenry-compat`; `segwid1=` above the cell spacing on a uniform plane +/// solves with a line-numbered warning. +#[test] +fn segwid_reaches_the_executable_with_line_numbered_diagnostics() { + let directory = scratch("segwid-diagnostics"); + let deck = |plane_tail: &str| { + format!( + "\ +.units mm +.default sigma=5.8e4 +Gp x1=0 y1=0 z1=0 x2=10 y2=0 z2=0 x3=10 y3=10 z3=0 ++ thick=0.04 {plane_tail} +Nt x=5 y=5 z=0.5 +Nb x=5 y=5 z=0 +Ev Nt Nb w=0.2 h=0.2 +.external Nt Nb +.freq fmin=1 fmax=1 ndec=1 +.end +" + ) + }; + std::fs::write( + directory.join("nonuniform.inp"), + deck("seg1=4 seg2=4 segwid1=0.5 file=NONE"), + ) + .unwrap(); + std::fs::write(directory.join("wide.inp"), deck("seg1=4 seg2=4 segwid2=9")).unwrap(); + + let native = fasterhenry(&directory, &["run", "nonuniform.inp"]); + assert!(!native.status.success()); + let text = stderr(&native); + assert!( + text.contains("line 4") && text.contains("'segwid1'"), + "{text}" + ); + + let compat = fasterhenry(&directory, &["run", "--fasthenry-compat", "nonuniform.inp"]); + assert!(compat.status.success(), "{}", stderr(&compat)); + let text = stderr(&compat); + assert!( + text.lines() + .any(|l| l.starts_with("warning: line 4: ") && l.contains("'segwid1' is ignored")), + "{text}" + ); + + let wide = fasterhenry(&directory, &["run", "wide.inp"]); + assert!(wide.status.success(), "{}", stderr(&wide)); + let text = stderr(&wide); + assert!( + text.lines() + .any(|l| l.starts_with("warning: line 4: ") && l.contains("segment separation")), + "{text}" + ); +} + /// A MAT result small enough to sit entirely in the `BufWriter` fails only /// at the final drain. `/dev/full` opens fine (so `File::create` succeeds) /// and rejects every write with ENOSPC; before the explicit flush the error From e7137f90adaff757eadc2df62d4ef2976448229e Mon Sep 17 00:00:00 2001 From: "loom-fleet-dispatch[bot]" <313063438+loom-fleet-dispatch[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:09:21 +0000 Subject: [PATCH 3/4] chore: re-date required checks for PR #162 (#8248 guard, automated by #8508) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit intentionally changes NOTHING in the tree — it exists only to give every required check a fresh started_at, because the #8248 required-check-freshness guard blocked this merge on evidence that predates the base branch's current tip, and the merge token lacks actions:write to re-run the stale check directly. Stale-Check: Clean-room check (no FastHenry-derived text) Stale-Clause: this required check has no entry in the input-scope table and no declaration in `.loom/stale-check-inputs.json`, so which files it reads is unknown Coupled-Base-Path: docs/fasthenry-compat.md Coupled-PR-Path: none From 0533573e8e44aa7c99e31a460d77ad97ae07bb69 Mon Sep 17 00:00:00 2001 From: "loom-fleet-dispatch[bot]" <313063438+loom-fleet-dispatch[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:41:05 +0000 Subject: [PATCH 4/4] chore: re-date required checks for PR #162 (#8248 guard, automated by #8508) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit intentionally changes NOTHING in the tree — it exists only to give every required check a fresh started_at, because the #8248 required-check-freshness guard blocked this merge on evidence that predates the base branch's current tip, and the merge token lacks actions:write to re-run the stale check directly. Stale-Check: Clean-room check (no FastHenry-derived text) Stale-Clause: this required check has no entry in the input-scope table and no declaration in `.loom/stale-check-inputs.json`, so which files it reads is unknown Coupled-Base-Path: .agents/skills/loom-builder/SKILL.md Coupled-PR-Path: none