You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/API.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -98,7 +98,7 @@ Group cells by universe, compute bounding boxes, resolve cell complements. **Req
98
98
intalea_build_spatial_index(alea_system_t* sys);
99
99
```
100
100
101
-
Build a KD-tree over cell instances for fast point queries. Called automatically on first query if not called explicitly. Useful to control when the (potentially slow) build happens.
101
+
Build a BVH over cell instances for fast point and region queries. Called automatically on first query if not called explicitly. Useful to control when the (potentially slow) build happens.
102
102
103
103
**Thread safety**: Call this before any concurrent ray tracing or point queries. The lazy build on first use is not thread-safe — concurrent calls may corrupt shared caches. A warning is logged if lazy building occurs.
104
104
@@ -235,7 +235,7 @@ Remove cells whose estimated volume is at or below `threshold`. The `volumes` ar
235
235
### alea_tighten_cell_bbox
236
236
237
237
```c
238
-
int alea_tighten_cell_bbox(const alea_system_t* sys, int cell_index,
238
+
int alea_tighten_cell_bbox(const alea_system_t* sys, size_t cell_index,
239
239
double tol, alea_bbox_t* bbox);
240
240
```
241
241
@@ -512,7 +512,7 @@ int alea_slice_curves_get(const alea_slice_curves_t* curves,
512
512
513
513
Get curve data at index. The `alea_curve_t` struct contains:
514
514
515
-
- `type`: one of `ALEA_CURVE_LINE`, `ALEA_CURVE_CIRCLE`, `ALEA_CURVE_ELLIPSE`, `ALEA_CURVE_LINE_SEGMENT`, `ALEA_CURVE_ARC`, `ALEA_CURVE_ELLIPSE_ARC`, `ALEA_CURVE_POLYGON`, `ALEA_CURVE_PARALLEL_LINES`
515
+
- `type`: one of `ALEA_CURVE_NONE`, `ALEA_CURVE_POINT`, `ALEA_CURVE_LINE`, `ALEA_CURVE_LINE_SEGMENT`, `ALEA_CURVE_RAY`, `ALEA_CURVE_CIRCLE`, `ALEA_CURVE_ARC`, `ALEA_CURVE_ELLIPSE`, `ALEA_CURVE_ELLIPSE_ARC`, `ALEA_CURVE_PARABOLA`, `ALEA_CURVE_HYPERBOLA`, `ALEA_CURVE_POLYGON`, `ALEA_CURVE_QUARTIC`, `ALEA_CURVE_PARALLEL_LINES`
516
516
- `surface_id`: which surface generated this curve
517
517
- `data`: union with geometry-specific fields (point + direction for lines, center + radius for circles, etc.)
@@ -112,6 +111,8 @@ Cells are looked up by ID through a hash table (`cell_index`), giving O(1) acces
112
111
113
112
Large models contain many duplicate surfaces. A tokamak model might define the same cylindrical surface dozens of times with slightly different floating-point coefficients (because different CAD tools or human authors wrote them independently).
114
113
114
+
For the full pipeline — canonicalization, hashing, tolerance-based equality, canonical surface maps, and the XOR sense correction formula — see [Surface Deduplication](SURFACE_DEDUP.md).
115
+
115
116
Alea deduplicates automatically:
116
117
117
118
1.**Canonicalize**: Normalize the primitive so the first non-zero coefficient is positive. This means `0x + 0y - 1z + 5 = 0` and `0x + 0y + 1z - 5 = 0` become the same canonical form. The `inverted` flag on the node records whether the sign was flipped.
@@ -179,9 +180,9 @@ Element computation is O(1) — no iteration over elements. A lattice with 100,0
179
180
180
181
### Spatial index
181
182
182
-
For the first point query, a spatial index (KD-tree over cell instances) is built lazily. Subsequent queries use it to narrow down candidate cells, avoiding a linear scan of all cells.
183
+
For the first point query, a spatial index (BVH over cell instances) is built lazily. Subsequent queries use it to narrow down candidate cells, avoiding a linear scan of all cells.
183
184
184
-
The spatial index is particularly important for models with deep universe hierarchies, where the naive approach would test cells at every level.
185
+
The spatial index is particularly important for models with deep universe hierarchies, where the naive approach would test cells at every level. See [Spatial Index](#spatial-index) for the full design.
185
186
186
187
## How Export Works
187
188
@@ -201,21 +202,15 @@ Export (`alea_export_mcnp` or `alea_export_openmc`) does:
201
202
202
203
The key invariant: the `inverted` flag is never applied to surface coefficients during output. Surfaces are always emitted with their canonical coefficients. The sense in the cell expression absorbs the inversion.
203
204
204
-
## Lazy Universe Instantiation
205
+
## Lazy Universe Evaluation
205
206
206
207
When you load a model with universe fills, Alea does NOT immediately materialize every universe instance. A model with 1000 uses of universe 5 would need 1000 copies of every primitive and node in universe 5 — that's expensive and usually unnecessary.
207
208
208
-
Instead, Alea uses lazy instantiation:
209
+
Instead:
209
210
210
211
-**At load time**: fill parameters are stored on the cell. Nothing is expanded.
211
212
-**At query time**: the point is inverse-transformed and the query descends into the original universe. No copies needed.
212
-
-**At flatten time** (explicit `alea_flatten()`): instances are materialized. Each primitive is cloned and transformed to global coordinates. The instance cache prevents re-materializing the same universe+transform pair.
213
-
214
-
The instance cache (`alea_instance_cache_t`) maps `(universe_id, transform_id)` to a materialized instance. Each instance stores:
215
-
216
-
- A primitive remap table (old primitive IDs to new ones)
217
-
- A node remap table
218
-
- The set of cloned cell roots
213
+
-**At flatten time** (explicit `alea_flatten()`): instances are materialized. Each primitive is cloned and transformed to global coordinates.
219
214
220
215
## Ray Tracing
221
216
@@ -230,7 +225,7 @@ Ray tracing (`alea_raycast`) finds all cells intersected by a ray, in order:
230
225
5. Walk along the ray, at each intersection testing which cell the point is in
231
226
6. Build segments: consecutive regions of same-cell occupancy
232
227
233
-
The BVH traversal uses a bounded stack (128 entries, supporting trees up to ~64 levels deep). A warning is logged if the stack overflows.
228
+
The BVH traversal uses a bounded stack (128 entries, supporting trees up to ~64 levels deep). A warning is logged if the stack overflows. See [Surface BVH](#surface-bvh) for details on the construction and traversal algorithm.
234
229
235
230
### Cell-aware approach
236
231
@@ -263,32 +258,94 @@ Ray-surface intersection routines respect all geometric constraints stored on th
263
258
264
259
## Spatial Index
265
260
266
-
The spatial index (`alea_spatial_index_t`) is a KD-tree that stores **cell instances** — combinations of (cell, transform, depth) that represent a specific materialized position of a cell in global coordinates.
The spatial index (`alea_spatial_index_t`) is a BVH (bounding volume hierarchy) that stores **cell instances** — combinations of (cell, transform, depth) that represent a specific materialized position of a cell in global coordinates.
264
+
265
+
**Why a BVH over instances, not over raw geometry:** a model with deep universe hierarchies and thousands of fill placements would require flattening every surface into global coordinates to build a traditional spatial index. That is expensive in both time and memory, and destroys the compact representation of shared universes. Instead, the spatial index flattens only the **bounding boxes**: for each cell instance, it records `(cell_index, transform, global_bbox)` — lightweight metadata. The actual CSG geometry stays in its original universe-local form and is evaluated on demand through the stored transform. This keeps the index small while still enabling O(log N) spatial queries.
266
+
267
+
### Building the index
268
+
269
+
1. Walk the universe hierarchy recursively (`collect_instances_recursive`)
270
+
2. For each terminal cell (one with a material, not a fill), compute its bounding box in global coordinates by transforming the local bbox through the accumulated transform chain, then clip against the parent cell's bbox
271
+
3. For each container cell, record it too (needed for fill descent during point queries)
272
+
4. Build a BVH over all instances using **median splits** on centroids (leaf size = 4, max depth = 30)
273
+
274
+
**Why median splits instead of SAH:** the spatial index is built over cell instances whose bounding boxes often overlap heavily (nested universes, lattice elements). SAH cost estimation assumes non-overlapping primitives and provides little benefit here, while median splits are simpler and faster to build — important because the index is rebuilt whenever the model changes.
267
275
268
-
Building the index:
276
+
The build is **thread-safe**: an atomic CAS on `spatial_build_state` (0=pending → 1=building → 2=done) ensures only one thread builds while others spin-wait. The index is lazy-built on first query if not built explicitly.
269
277
270
-
1. Walk the universe hierarchy recursively
271
-
2. For each terminal cell (one with a material, not a fill), record its bounding box in global coordinates
272
-
3. For each container cell, record it too (needed for fill descent)
273
-
4. Build a KD-tree over all instances
278
+
### Querying
274
279
275
-
Querying:
280
+
Four query types are available:
276
281
277
-
1. Find all instances whose bounding box contains the query point
278
-
2. For each candidate, evaluate the cell's CSG tree (with inverse transform if needed)
279
-
3. Return the best match (innermost, deepest in hierarchy)
282
+
-**Region query** (`alea_spatial_query_region`): find all terminal instances whose bbox overlaps a query bbox. Used by the slice module to find cells intersecting a cutting plane.
283
+
-**Z-slice query** (`alea_spatial_query_slice_z`): convenience wrapper that converts a Z-plane slice into a thin bbox region query.
284
+
-**Point query** (`alea_spatial_query_point`): find all instances (including non-terminal) at a point, sorted by depth. Used for full hierarchy traversal.
285
+
-**Point-in-cell with coherence** (`alea_spatial_find_cells_at_point`): high-level query with a thread-local coherence cache. Caches the last found cell path so consecutive nearby queries (adjacent pixels in a grid) reuse the result without re-traversing the BVH.
280
286
281
287
The spatial index avoids linear scans over cells. On a model with 10,000 cells, a point query touches maybe 5-10 cells instead of all 10,000.
282
288
289
+
### Role in slice plotting
290
+
291
+
The spatial index is central to both slice operations:
292
+
293
+
1.**Curve computation** (`alea_compute_slice_curves_spatial`): converts the slice plane into a thin bounding box, queries the BVH to find which cell instances intersect, deduplicates hits by `(cell_index, transform)`, then computes analytical curve intersections only for those cells. Without the spatial index, every cell in the model would need to be tested.
294
+
295
+
2.**Grid-based cell identification** (`alea_find_cells_grid`): uses `alea_spatial_find_cells_at_point` per pixel with the coherence cache. Combined with per-universe adjacency walking, this gives interactive-speed slicing even on models with hundreds of thousands of cells.
The surface BVH (`alea_bvh_t`) is a separate acceleration structure from the spatial index. It indexes **primitive surface bounding boxes** and is used exclusively for **ray-surface intersection** queries.
302
+
303
+
**Why a separate BVH for surfaces:** ray tracing and spatial queries have fundamentally different access patterns. Ray tracing needs to find which surfaces a ray crosses, ordered by distance. Spatial queries need to find which cells contain a point or overlap a region. Trying to use one structure for both would either degrade ray traversal performance (because cell instances can deeply overlap) or fail at spatial queries (because surface BVHs have no concept of cell containment). Two purpose-built structures, each optimal for its query type, are simpler and faster than one compromise.
304
+
305
+
### Construction
306
+
307
+
The surface BVH uses **SAH (Surface Area Heuristic)** with 16 bins per axis:
308
+
309
+
1. For each surface in `sys->surfaces`, compute its bounding box
310
+
2. Recursively partition using SAH cost estimation: for each axis, bin the surfaces by centroid position, evaluate `cost = traversal_cost + (SA_left * N_left + SA_right * N_right) / SA_parent * intersect_cost`, and pick the split with minimum cost
311
+
3. Stop splitting when leaf size ≤ 4 or depth exceeds 30
312
+
313
+
**Why SAH here but not for the spatial index:** surface primitives in a ray tracing context have relatively tight, non-overlapping bounding boxes. SAH excels in this regime — it produces trees where rays traverse fewer nodes by putting large surfaces in large nodes and grouping spatially coherent surfaces together. The build cost (O(N log N) with binning) is acceptable because the BVH is built once and reused for many ray queries.
314
+
315
+
The BVH nodes are **64 bytes** (cache-line-friendly): bbox + child indices + leaf metadata. Surface indices are reordered for cache locality during traversal.
316
+
317
+
### Traversal
318
+
319
+
`alea_bvh_traverse` does **stack-based** (not recursive) traversal:
320
+
321
+
- Precomputes inverse ray direction and sign bits
322
+
- Uses a 128-entry stack (supports ~64 tree levels)
323
+
- Pushes the far child first so the near child is processed next
324
+
- Calls a user callback for each candidate surface whose bbox the ray hits
325
+
- Falls back to linear scan (`raycast_surfaces_linear`) if BVH is disabled or the build failed
326
+
327
+
### Lazy build and invalidation
328
+
329
+
The surface BVH is cached on `sys->surface_bvh` and lazy-built on first raycast via `alea_raycast_ensure_caches()`. The `bvh_dirty` flag triggers a rebuild when surfaces are added or modified. If the surface count changes since the last build, the BVH is also rebuilt.
330
+
283
331
## Cell Adjacency
284
332
285
333
Cell adjacency is an optimization for grid-based queries (slicing). The idea: if you know pixel (i, j) is in cell A, then pixel (i+1, j) is probably in cell A or one of its neighbors.
286
334
287
-
Building adjacency:
335
+
### Building adjacency
288
336
289
337
1. For each cell, find which surfaces bound it (from the CSG tree)
290
338
2. For each surface, find which other cells use the same surface
291
339
3. Two cells sharing a surface are neighbors
340
+
4. Deduplicate using a bitset (one 64-bit word array, ~14.5 KB for 116K cells) with dirty-word tracking for O(neighbors) reset per cell
341
+
342
+
**Why a 128-cell threshold:** surfaces shared by more than 128 cells (`ADJACENCY_MAX_CELLS_PER_SURFACE`) are skipped when building adjacency. Global planes (e.g., a bounding plane used by thousands of cells) create O(n²) cliques — every cell sharing that plane becomes a neighbor of every other. These cliques waste memory without helping local adjacency walking, because a global plane doesn't imply geometric proximity. Skipping high-fanout surfaces keeps the neighbor lists small and focused on actual geometric neighbors.
343
+
344
+
### Memory layout
345
+
346
+
All neighbor data uses a **pool allocator** (`sys->neighbor_pool`): a single `malloc` for the entire adjacency structure. This avoids heap fragmentation from thousands of small per-cell neighbor arrays. `alea_destroy` and `alea_reset` check `neighbor_pool` before attempting per-cell `free(neighbors)`.
347
+
348
+
### Usage in grid queries
292
349
293
350
During grid queries, after finding the cell for one pixel, the next pixel first checks the same cell and its neighbors before falling back to a full search. This gives ~10x speedup on grid queries because consecutive pixels almost always share a cell or neighbor.
294
351
@@ -339,3 +396,11 @@ The `libalea_full.a` archive combines all modules. Use it unless binary size mat
339
396
The **Lua binding** layer (`src/lua_bind/`) wraps the public C API for the interactive CLI (`bin/alea`). It is compiled into the CLI binary, not shipped as a separate library.
340
397
341
398
Module boundaries follow a dependency rule: MCNP and OpenMC depend on Core, but not on each other. Raycast and Slice depend on Core but not on MCNP or OpenMC. Render depends on Raycast and Core. Mesh depends on Core. Core depends on nothing except the util/ layer.
Copy file name to clipboardExpand all lines: docs/CONCEPTS.md
+9-7Lines changed: 9 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -66,7 +66,7 @@ Every cell has:
66
66
67
67
-**Cell ID**: the MCNP cell number (unique identifier)
68
68
-**Material ID**: which material fills the cell (0 = void)
69
-
-**Density**: material density (negative = g/cm^3, positive = atoms/barn-cm, matching MCNPconvention)
69
+
-**Density**: material density (always stored as a positive value internally, with a separate `is_mass_density` flag to distinguish g/cm^3 from atoms/barn-cm; MCNP's sign convention is applied only at export time)
70
70
-**Universe ID**: which universe the cell belongs to (default 0)
71
71
-**Region**: a CSG tree node defining the shape
72
72
-**Fill**: optionally, a universe that fills this cell instead of a material
@@ -265,11 +265,12 @@ Alea includes a ray tracing module that casts rays through the geometry and repo
265
265
```c
266
266
#include"alea_raycast.h"
267
267
268
-
alea_raycast_result_t* result = alea_raycast(sys,
268
+
alea_raycast_result_t* result = alea_raycast_result_create();
269
+
alea_raycast(sys,
269
270
ox, oy, oz, // ray origin
270
271
dx, dy, dz, // ray direction
271
272
max_distance, // maximum distance to trace
272
-
-1); // universe (-1 = root)
273
+
result);
273
274
```
274
275
275
276
Each intersection records the cell entered, the material, the distance along the ray, and the surface crossed.
@@ -458,7 +459,8 @@ Error codes are defined in `alea_types.h` as the `alea_error_t` enum:
458
459
| 10 |`ALEA_ERR_UNSUPPORTED`| Feature not supported |
459
460
| 11 |`ALEA_ERR_UNSUPPORTED_SURFACE`| Surface type not supported |
0 commit comments