diff --git a/docs/fasthenry-compat.md b/docs/fasthenry-compat.md index c14d6de..ebf83a2 100644 --- a/docs/fasthenry-compat.md +++ b/docs/fasthenry-compat.md @@ -105,14 +105,14 @@ records how each documented field maps. |---|---|---| | `G x1= y1= z1= x2= y2= z2= x3= y3= z3=` (three corner points) | Supported, differs | The plane must be axis-aligned and parallel to the xy plane (`z1 = z2 = z3`, each edge along x or y): this engine's plane model is an axis-aligned rectangle. A tilted or rotated plane is rejected by name rather than squared off. The corner points give the plane's mid-thickness surface, as a segment's nodes give its axis. The three points also fix the **plane's own coordinate system** — `p1` the origin, `p1 → p2` its x-direction, `p2 → p3` its y-direction — which is the frame every `x…`/`y…` pair of *lengths* in the clauses below is stated in; see "Decision: a `contact` clause's `x…`/`y…` pair is in plane coordinates" below (issue #118). | | `thick=` | Supported | Plane thickness. | -| `seg1=`, `seg2=` | Supported, differs | Become the background cell counts of this engine's own cell-centre PEEC mesh, not FastHenry's panel mesh: equal counts mean equal resolution, not an identical node set. | +| `seg1=`, `seg2=` | Supported, differs | Become the background cell counts of this engine's own cell-centre PEEC mesh, not FastHenry's panel mesh: equal counts mean equal resolution, not an identical node set. A uniform plane (no `file=`) missing `seg1`/`seg2` (or either one) is an error in both modes, as in FastHenry; the only exemption is the `--fasthenry-compat` `file=NONE` single-cell case (issue #143, see the `file=NONE` row). | | `sigma=` | Supported, differs (opt-in) | Per deck unit, falling back to the `.default` conductivity (`.default sigma=` or `.default rho=`). A plane with neither is a line-numbered error by default; under `--fasthenry-compat` it is copper, 5.8e7 S/m physical whatever the `.units`, with a line-numbered warning on the statement (`ground plane 'G1' has no conductivity; using copper (5.8e7 S/m, FastHenry default)`), exactly as for a segment and for the same reason — operator decision: defaults under compat, strict otherwise (issue #142; see `E … sigma` above). The extension `G` form follows the same rule. | | `nhinc=` | Supported | Filaments through the plane's thickness. | | `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. | -| `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=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. Issue #143: under `--fasthenry-compat`, `file=NONE` with neither `seg1`/`seg2` nor a `contact initial_grid` is the single root cell — it meshes as `contact initial_grid (1, 1)` (the `contact` refinements still apply) with a warning on the `G` line; native mode keeps the `has no 'seg1'` error. | | `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. | | `hole rect (x1, y1, z1, x2, y2, z2)` | Supported | Two opposite corners, as documented, onto the plane model's rectangular hole. The redundant `z` coordinates are checked against the plane's slab, so a rectangle meant for another plane cannot land here silently. A rectangle (like a `hole point`/`circle`, `contact rect` or `contact decay_rect`) wholly outside the plane's footprint is accepted with a line-numbered warning — see the decision below (issue #105). | diff --git a/fasterhenry-cli/src/inp.rs b/fasterhenry-cli/src/inp.rs index c8613b4..9c48bc8 100644 --- a/fasterhenry-cli/src/inp.rs +++ b/fasterhenry-cli/src/inp.rs @@ -1643,6 +1643,8 @@ 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, + /// Whether the statement carried `file=NONE`. + file_none: bool, /// `sigma=` / `rho=` (S/m), or the `.default` conductivity. sigma: Option, /// `nhinc=` (1 unless set). @@ -1808,6 +1810,7 @@ impl<'a> PlaneStatement<'a> { ), )); } + self.file_none = true; } "nx" | "ny" => { return Err(err( @@ -2209,6 +2212,16 @@ impl<'a> PlaneStatement<'a> { } } + /// Whether, under `--fasthenry-compat`, this `file=NONE` plane gives no + /// initial grid at all (no `seg1`/`seg2`, no `contact initial_grid`) and + /// so meshes as a single root cell (issue #143). + fn single_root_cell(&self) -> bool { + self.compat + && self.file_none + && self.segments == [None, None] + && self.initial_grid.is_none() + } + /// Checks the statement's geometry — three corners of an axis-aligned /// rectangle parallel to xy, both cell counts, a positive thickness, /// and a conductivity — and returns the frame the plane's features are @@ -2264,18 +2277,23 @@ impl<'a> PlaneStatement<'a> { } }; let mut cells = [0usize; 2]; - cells[axis1] = self.segments[0].ok_or_else(|| { - err( - line, - format!("ground plane '{head}' has no 'seg1' (cells along p1→p2)"), - ) - })?; - cells[axis2] = self.segments[1].ok_or_else(|| { - err( - line, - format!("ground plane '{head}' has no 'seg2' (cells along p2→p3)"), - ) - })?; + if self.single_root_cell() { + // `file=NONE` with no grid at all: one root cell (issue #143). + cells = [1, 1]; + } else { + cells[axis1] = self.segments[0].ok_or_else(|| { + err( + line, + format!("ground plane '{head}' has no 'seg1' (cells along p1→p2)"), + ) + })?; + cells[axis2] = self.segments[1].ok_or_else(|| { + err( + line, + format!("ground plane '{head}' has no 'seg2' (cells along p2→p3)"), + ) + })?; + } let thickness = self.thickness.ok_or_else(|| { err( line, @@ -2337,6 +2355,13 @@ impl<'a> PlaneStatement<'a> { /// off the plane), but neither is an error. fn finish(self) -> Result<(PlaneSpec, Vec, Vec), ParseError> { let frame = self.frame()?; + let single_root_cell = self.single_root_cell().then(|| ParseWarning { + line: self.line, + message: format!( + "ground plane '{}' has 'file=NONE' and no 'seg1'/'seg2' or 'contact initial_grid': its initial grid is a single cell (as 'contact initial_grid (1, 1)'), refined only by its 'contact' clauses", + self.head + ), + }); let PlaneStatement { head, nhinc, @@ -2353,6 +2378,9 @@ impl<'a> PlaneStatement<'a> { mut warnings, .. } = self; + if let Some(warning) = single_root_cell { + warnings.push((0, warning)); + } // The meshed initial grid's holes are cut into the *initial* grid, // before anything else refines it, so they come first. @@ -6569,6 +6597,51 @@ Gp x1=0 y1=0 z1=0 x2=10 y2=0 z2=0 x3=10 y3=6 z3=0 ); } + /// Issue #143: under compat, `file=NONE` with no initial grid is a single + /// root cell, warned about on the `G` line; native mode and a uniform + /// plane missing `seg1`/`seg2` stay errors. + #[test] + fn compat_file_none_without_a_grid_is_a_single_cell() { + let opts = ParseOptions { + fasthenry_compat: true, + }; + let g = "\ +Gp x1=0 y1=0 z1=0 x2=10 y2=0 z2=0 x3=10 y3=6 z3=0 ++ thick=0.04"; + let compat = |statement: &str| { + parse_with_options_reporting(&format!("title\n{}", plane_deck(statement, "")), opts) + }; + let (bare, warnings) = compat(&format!("{g} file=NONE")).unwrap(); + let explicit = parse_ok(&plane_deck(&format!("{g} contact initial_grid (1, 1)"), "")); + assert_eq!(bare.geometry, explicit.geometry); + assert_eq!(warnings.len(), 1, "{warnings:?}"); + assert_eq!(warnings[0].line, 4); + assert!(warnings[0].message.contains("single cell")); + + // Contact refinements still apply. + let refine = "contact rect (5, 3, 0, 4, 4, 1, 1)"; + let (refined, _) = compat(&format!("{g} file=NONE {refine}")).unwrap(); + let by_hand = parse_ok(&plane_deck( + &format!("{g} contact initial_grid (1, 1) {refine}"), + "", + )); + assert_eq!(refined.geometry, by_hand.geometry); + assert_ne!(refined.geometry, bare.geometry); + + // An explicit grid means no warning. + let (_, warnings) = compat(&format!("{g} file=NONE seg1=2 seg2=2")).unwrap(); + assert!(warnings.is_empty(), "{warnings:?}"); + + // Native mode: unchanged error. + let error = parse(&plane_deck(&format!("{g} file=NONE"), "")).unwrap_err(); + assert!(error.message.contains("has no 'seg1'"), "{error}"); + // Uniform plane missing seg1/seg2, or only one: error in both modes. + for body in [g.to_string(), format!("{g} seg1=3"), format!("{g} seg2=3")] { + assert!(compat(&body).is_err(), "{body}"); + assert!(parse(&plane_deck(&body, "")).is_err(), "{body}"); + } + } + /// `seg1` counts cells along `p1 → p2` and `seg2` along `p2 → p3`, /// whichever axis each of those edges runs along. #[test]