lindhard/geometry.rs
1//! Target geometry: the engine-facing [`Geometry`] trait, the 1D layered
2//! [`Stack`], the 3D [`VoxelGrid`], the triangle-mesh [`MeshGeometry`] and the
3//! constructive-solid-geometry [`CsgGeometry`].
4//!
5//! # Coordinates
6//!
7//! `x` is depth, measured from the front surface (`x = 0`) into the target;
8//! `y` and `z` are lateral. Lengths are metres (SI, see [`crate::units`]).
9//! A [`Stack`] is an ordered list of layers, each homogeneous and laterally
10//! infinite (the planar slab model of amorphous-target binary-collision codes,
11//! e.g. J. P. Biersack and L. G. Haggmark, Nucl. Instrum. Methods 174 (1980)
12//! 257). The last layer may be semi-infinite (a substrate); otherwise the
13//! target has a back face at its total thickness.
14//!
15//! # The `Geometry` contract
16//!
17//! A geometry is a set of numbered *regions* (layers of a stack, voxels of a
18//! grid), each filled by one of a numbered list of *materials*. A transport
19//! engine keeps a particle's current region, asks [`Geometry::exit`] how far
20//! a straight flight can go before something changes, truncates the free path
21//! there, and carries the unused optical depth into the next region (the
22//! same contract as the layer interface of the M0 engine). "Something
23//! changes" means the material changes, the particle leaves the target, or
24//! it wraps through a periodic boundary; crossing into a region of the same
25//! material is *not* an event, so a voxel boundary inside a homogeneous block
26//! costs nothing. Per-material data (scattering, stopping) is cached by
27//! material index, never per region.
28//!
29//! # Voxel grids
30//!
31//! [`VoxelGrid`] is a regular box `[0, Lx] x [0, Ly] x [0, Lz]`, with
32//! `L = n h` for `n` voxels of spacing `h` on each axis, and the origin at the
33//! corner of the front face (`x = 0` is the front surface, so a grid can
34//! encode a [`Stack`] with the layers along `x`). Voxel `(i, j, k)` has the
35//! flat region index `i + nx (j + ny k)` (x fastest). It owns the half-open
36//! box `[i h, (i + 1) h)` on each axis, so a point exactly on an interior
37//! face belongs to the voxel with the higher index, as a depth exactly on a
38//! layer interface belongs to the deeper layer of a stack.
39//!
40//! Each axis is either [`Boundary::Vacuum`] (a particle reaching either end
41//! of the axis leaves the target) or [`Boundary::Periodic`] (it re-enters at
42//! the opposite face with the same direction; the travelled length and so the
43//! energy loss are those of the unwrapped path). Faces on the `x` axis are
44//! reported as [`Face::Front`] (`x = 0`) and [`Face::Back`] (`x = Lx`); faces
45//! on `y` and `z` are [`Face::Side`].
46//!
47//! The exact ray traversal is the 3D digital differential analyser of
48//! J. Amanatides and A. Woo, "A fast voxel traversal algorithm for ray
49//! tracing", Eurographics '87 (1987) 3-10: the distance to the next face of
50//! each axis is `(face - position) / direction`, the axis with the smallest
51//! distance is crossed, and the face distances are recomputed from the face
52//! coordinates (not accumulated), so rounding does not drift along a long
53//! ray. Axes with a zero direction component are never crossed. When several
54//! axes reach a face at exactly the same distance (an edge or corner), they
55//! are crossed together, so the ray passes directly to the diagonal voxel
56//! without visiting the voxels it only touches. If a tied crossing leaves the
57//! target through several vacuum faces, the face of the lowest axis (`x`,
58//! then `y`, then `z`) is reported.
59
60//! # Triangle meshes
61//!
62//! [`MeshGeometry`] (module [`mesh`]) is a set of closed STL/OBJ triangle
63//! solids, one region each, with a BVH for ray queries. Its overlap, surface
64//! and tolerance rules, the `Face` mapping of its escapes, the loaders and the
65//! watertightness rules are in the [`mesh`] module docs.
66
67//! # CSG
68//!
69//! [`CsgGeometry`] (module [`csg`]) is a set of [`Csg`] solids built from box,
70//! cylinder and half-space [`Primitive`]s by union, intersection and
71//! difference, one region per top-level solid, classified along each flight by
72//! ray casting. It follows the mesh ownership and tolerance rules, so a box
73//! gives the same events as a CSG solid, a mesh or a voxel grid; the rules,
74//! the handling of unbounded half-spaces and the algorithms are in the
75//! [`csg`] module docs.
76
77mod bvh;
78pub mod csg;
79pub mod mesh;
80mod stack;
81#[cfg(test)]
82mod test_util;
83mod voxel;
84
85pub use csg::{Csg, CsgGeometry, Primitive};
86pub use mesh::{MeshGeometry, TriMesh};
87pub use stack::{Layer, Stack};
88pub use voxel::{Boundary, VoxelGrid};
89
90use crate::material::Material;
91
92/// Errors from building a [`Stack`], a [`VoxelGrid`], a [`MeshGeometry`] or a
93/// [`CsgGeometry`].
94#[derive(Debug, Clone, PartialEq, thiserror::Error)]
95pub enum GeometryError {
96 /// No layers and no substrate.
97 #[error("a stack needs at least one layer or a substrate")]
98 Empty,
99 /// A layer thickness was zero, negative or not finite.
100 #[error("layer {index} has invalid thickness {thickness_m} m: must be finite and positive")]
101 InvalidThickness {
102 /// Layer index (0 = front).
103 index: usize,
104 /// Offending value.
105 thickness_m: f64,
106 },
107 /// A voxel grid needs at least one material.
108 #[error("a voxel grid needs at least one material")]
109 NoMaterials,
110 /// A voxel grid dimension was zero.
111 #[error("voxel grid dimension on axis {axis} is zero")]
112 ZeroDimension {
113 /// Axis (0 = x, 1 = y, 2 = z).
114 axis: usize,
115 },
116 /// A voxel spacing was zero, negative, not finite, or so large that the
117 /// grid extent overflows.
118 #[error("voxel spacing {spacing_m} m on axis {axis} is invalid: must be finite and positive, with a finite extent")]
119 InvalidSpacing {
120 /// Axis (0 = x, 1 = y, 2 = z).
121 axis: usize,
122 /// Offending value.
123 spacing_m: f64,
124 },
125 /// The number of voxels does not fit in `usize`.
126 #[error("voxel grid dimensions {dims:?} overflow the voxel count")]
127 TooManyVoxels {
128 /// Requested dimensions.
129 dims: [usize; 3],
130 },
131 /// The material-index array does not have one entry per voxel.
132 #[error("voxel grid needs {expected} material indices, got {actual}")]
133 CellCountMismatch {
134 /// `nx ny nz`.
135 expected: usize,
136 /// Length supplied.
137 actual: usize,
138 },
139 /// A voxel names a material that does not exist.
140 #[error("voxel {cell} has material index {index}, but there are only {n_materials} materials")]
141 MaterialIndexOutOfRange {
142 /// Flat voxel index.
143 cell: usize,
144 /// Offending material index.
145 index: u32,
146 /// Number of materials supplied.
147 n_materials: usize,
148 },
149 /// A mesh or CSG geometry needs at least one solid.
150 #[error("a mesh or CSG geometry needs at least one solid")]
151 NoSolids,
152 /// A solid names a material that does not exist.
153 #[error(
154 "solid {solid} has material index {index}, but there are only {n_materials} materials"
155 )]
156 SolidMaterialOutOfRange {
157 /// Solid index (listing order).
158 solid: usize,
159 /// Offending material index.
160 index: u32,
161 /// Number of materials supplied.
162 n_materials: usize,
163 },
164 /// A mesh file could not be parsed. `line` is 1-based (0 for a binary file).
165 #[error("{format} parse error (line {line}): {message}")]
166 MeshParse {
167 /// `"STL"` or `"OBJ"`.
168 format: &'static str,
169 /// Line number, or 0 when not applicable.
170 line: usize,
171 /// What was wrong.
172 message: String,
173 },
174 /// The file-unit scale of a mesh loader was not finite and positive.
175 #[error("mesh unit scale {unit_m} m is invalid: must be finite and positive")]
176 MeshInvalidScale {
177 /// Offending value.
178 unit_m: f64,
179 },
180 /// A mesh has no triangles.
181 #[error("the mesh has no triangles")]
182 MeshEmpty,
183 /// A mesh vertex coordinate is NaN or infinite.
184 #[error("triangle {triangle} has a non-finite coordinate")]
185 MeshNonFinite {
186 /// Triangle index (file order).
187 triangle: usize,
188 },
189 /// A triangle has zero (or negligible) area.
190 #[error("triangle {triangle} is degenerate (zero area): vertices {vertices:?}")]
191 MeshDegenerateTriangle {
192 /// Triangle index (file order).
193 triangle: usize,
194 /// Its three vertex positions, m.
195 vertices: [[f64; 3]; 3],
196 },
197 /// An edge belongs to only one triangle: the mesh is not watertight.
198 #[error("mesh is not watertight: edge {edge:?} has no neighbouring triangle traversing it in the opposite direction")]
199 MeshEdgeNotShared {
200 /// The two vertex positions, m.
201 edge: [[f64; 3]; 2],
202 },
203 /// Two triangles traverse an edge in the same direction (inconsistent
204 /// orientation, or more than two triangles on the edge).
205 #[error(
206 "mesh orientation is inconsistent: edge {edge:?} is traversed twice in the same direction"
207 )]
208 MeshInconsistentOrientation {
209 /// The two vertex positions, m.
210 edge: [[f64; 3]; 2],
211 },
212 /// The signed volume is not positive: the normals point inward.
213 #[error("mesh has signed volume {volume_m3} m^3: triangles must be counter-clockwise seen from outside")]
214 MeshNotOutward {
215 /// The signed volume, m^3.
216 volume_m3: f64,
217 },
218 /// A CSG solid has an invalid primitive (non-finite or non-positive
219 /// size, zero axis or normal), an empty union or intersection, or an
220 /// empty bounding box.
221 #[error("CSG solid {solid} is invalid: {reason}")]
222 CsgInvalid {
223 /// Solid index (listing order).
224 solid: usize,
225 /// What was wrong.
226 reason: &'static str,
227 },
228 /// A top-level CSG solid is unbounded (a half-space not bounded by the
229 /// combination it is in; see the [`csg`] module docs).
230 #[error("CSG solid {solid} is unbounded: half-spaces must be bounded by an intersection or difference")]
231 CsgUnbounded {
232 /// Solid index (listing order).
233 solid: usize,
234 },
235}
236
237/// Which face of the target a particle left through.
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239pub enum Face {
240 /// The front surface, `x = 0` (backscattering, sputtering).
241 Front,
242 /// The back face, `x = Lx` of a finite stack or voxel grid (transmission).
243 Back,
244 /// A lateral face of a voxel grid, `y = 0`, `y = Ly`, `z = 0` or
245 /// `z = Lz` (only reachable with a [`Boundary::Vacuum`] lateral axis).
246 /// For electrons it releases the particle whatever the `EscapeRule`,
247 /// which only governs the back face.
248 Side,
249}
250
251/// What happens at the end of a flight segment returned by
252/// [`Geometry::exit`].
253#[derive(Debug, Clone, Copy, PartialEq)]
254pub enum ExitOutcome {
255 /// The particle continues in `region` at `pos` (the crossing point,
256 /// wrapped into the domain if a periodic boundary was crossed).
257 Enter {
258 /// The region entered.
259 region: usize,
260 /// Position after the crossing, m.
261 pos: [f64; 3],
262 },
263 /// The particle reaches the target surface.
264 Escape {
265 /// Which face.
266 face: Face,
267 /// Outward unit normal of the face.
268 normal: [f64; 3],
269 },
270}
271
272/// The first event along a straight flight.
273#[derive(Debug, Clone, Copy, PartialEq)]
274pub struct Exit {
275 /// Path length from the start of the flight to the event, m (the flight is
276 /// truncated here).
277 pub distance: f64,
278 /// The point where the event happens, on the crossed face (before any
279 /// periodic wrap), m.
280 pub at: [f64; 3],
281 /// What the event is.
282 pub outcome: ExitOutcome,
283}
284
285/// The outcome of a flight query, [`Geometry::flight`].
286#[derive(Debug, Clone, Copy, PartialEq)]
287pub enum Flight {
288 /// An event happens within the flight limit (see [`Geometry::exit`]).
289 Event(Exit),
290 /// No event within the limit: the whole flight stays in one material,
291 /// and ends in `region`. This may differ from the starting region when
292 /// the flight crossed faces between regions of the same material (voxels
293 /// of one block). A face reached exactly at the limit counts as crossed,
294 /// so `region` is the region the particle is entering there, the same
295 /// directional ownership as an [`ExitOutcome::Enter`].
296 Clear {
297 /// The region containing the end of the flight.
298 region: usize,
299 },
300}
301
302/// The engine-facing description of a target. See the module docs for the
303/// contract. Implemented by [`Stack`], [`VoxelGrid`], [`MeshGeometry`] and
304/// [`CsgGeometry`].
305pub trait Geometry: Sync {
306 /// Number of distinct materials.
307 fn n_materials(&self) -> usize;
308
309 /// Material `index` (`0..n_materials()`).
310 fn material(&self, index: usize) -> &Material;
311
312 /// Number of regions.
313 fn n_regions(&self) -> usize;
314
315 /// Index of the material filling `region`.
316 fn material_index(&self, region: usize) -> usize;
317
318 /// The region containing `pos`, or `None` outside the target (or for a
319 /// non-finite position). Ownership of shared faces: see the module docs.
320 fn locate(&self, pos: [f64; 3]) -> Option<usize>;
321
322 /// Where a beam enters by default: the origin for a [`Stack`], the centre
323 /// of the front face for a [`VoxelGrid`].
324 fn entry_point(&self) -> [f64; 3];
325
326 /// The first event along the straight flight from `pos` (inside `region`,
327 /// or on one of its faces) in the unit direction `dir`, if it happens at
328 /// a path length of at most `limit`; otherwise `None`. The event is a
329 /// change of material, a periodic wrap, or leaving the target.
330 ///
331 /// `dir` must be a finite unit vector; a zero or non-finite direction
332 /// returns `None`.
333 fn exit(&self, region: usize, pos: [f64; 3], dir: [f64; 3], limit: f64) -> Option<Exit>;
334
335 /// [`Geometry::exit`], also reporting the region the flight ends in when
336 /// there is no event, so a transport engine can keep its particle's
337 /// region consistent with its position after crossing same-material
338 /// faces. The default suits a geometry whose regions are each a whole
339 /// material run along every flight (a [`Stack`], where leaving a layer is
340 /// always an event): without an event the region is unchanged.
341 fn flight(&self, region: usize, pos: [f64; 3], dir: [f64; 3], limit: f64) -> Flight {
342 match self.exit(region, pos, dir, limit) {
343 Some(e) => Flight::Event(e),
344 None => Flight::Clear { region },
345 }
346 }
347
348 /// Whether `pos` lies outside the target in vacuum, used to skip weak-
349 /// collision partners that would lie beyond a surface. For a [`Stack`]
350 /// this is in front of the front face only (as in the M0 engine).
351 fn in_vacuum(&self, pos: [f64; 3]) -> bool;
352
353 /// This geometry as a layered [`Stack`], if it is one. Tallies defined
354 /// per layer (dose, depth-dose, interface transmission) need it.
355 fn as_stack(&self) -> Option<&Stack> {
356 None
357 }
358}