Skip to content

Commit b3c0c61

Browse files
Docs update
1 parent 4594b3e commit b3c0c61

5 files changed

Lines changed: 112 additions & 51 deletions

File tree

docs/API.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ Group cells by universe, compute bounding boxes, resolve cell complements. **Req
9898
int alea_build_spatial_index(alea_system_t* sys);
9999
```
100100
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.
102102
103103
**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.
104104
@@ -235,7 +235,7 @@ Remove cells whose estimated volume is at or below `threshold`. The `volumes` ar
235235
### alea_tighten_cell_bbox
236236
237237
```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,
239239
double tol, alea_bbox_t* bbox);
240240
```
241241

@@ -512,7 +512,7 @@ int alea_slice_curves_get(const alea_slice_curves_t* curves,
512512
513513
Get curve data at index. The `alea_curve_t` struct contains:
514514
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`
516516
- `surface_id`: which surface generated this curve
517517
- `data`: union with geometry-specific fields (point + direction for lines, center + radius for circles, etc.)
518518
- `t_min`, `t_max`: parameter range

docs/ARCHITECTURE.md

Lines changed: 90 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ alea_system_t
2626
├── mixtures[] Material mixtures
2727
├── transforms[] Transform matrices (TR cards, TRCL, FILL transforms)
2828
├── universes[] Universe groupings (built on demand)
29-
├── instance_cache Cache for materialized universe instances
30-
├── spatial_index KD-tree for fast cell instance queries
29+
├── spatial_index BVH over cell instances for fast spatial queries
3130
├── surface_bvh Bounding volume hierarchy for ray tracing
3231
├── config Tolerance, export options, void parameters
3332
├── stats Dedup hits, memory usage, conversion counts
@@ -112,6 +111,8 @@ Cells are looked up by ID through a hash table (`cell_index`), giving O(1) acces
112111

113112
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).
114113

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+
115116
Alea deduplicates automatically:
116117

117118
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
179180

180181
### Spatial index
181182

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.
183184

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.
185186

186187
## How Export Works
187188

@@ -201,21 +202,15 @@ Export (`alea_export_mcnp` or `alea_export_openmc`) does:
201202

202203
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.
203204

204-
## Lazy Universe Instantiation
205+
## Lazy Universe Evaluation
205206

206207
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.
207208

208-
Instead, Alea uses lazy instantiation:
209+
Instead:
209210

210211
- **At load time**: fill parameters are stored on the cell. Nothing is expanded.
211212
- **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.
219214

220215
## Ray Tracing
221216

@@ -230,7 +225,7 @@ Ray tracing (`alea_raycast`) finds all cells intersected by a ray, in order:
230225
5. Walk along the ray, at each intersection testing which cell the point is in
231226
6. Build segments: consecutive regions of same-cell occupancy
232227

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.
234229

235230
### Cell-aware approach
236231

@@ -263,32 +258,94 @@ Ray-surface intersection routines respect all geometric constraints stored on th
263258

264259
## Spatial Index
265260

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.
261+
**Files:** `src/core/alea_spatial.h`, `src/core/alea_spatial.c`
262+
263+
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.
267275

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.
269277

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
274279

275-
Querying:
280+
Four query types are available:
276281

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.
280286

281287
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.
282288

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.
296+
297+
## Surface BVH
298+
299+
**Files:** `src/raycast/bvh.h`, `src/raycast/bvh.c`
300+
301+
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+
283331
## Cell Adjacency
284332

285333
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.
286334

287-
Building adjacency:
335+
### Building adjacency
288336

289337
1. For each cell, find which surfaces bound it (from the CSG tree)
290338
2. For each surface, find which other cells use the same surface
291339
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
292349

293350
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.
294351

@@ -339,3 +396,11 @@ The `libalea_full.a` archive combines all modules. Use it unless binary size mat
339396
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.
340397

341398
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.
399+
400+
## See Also
401+
402+
- [Concepts](CONCEPTS.md) — domain concepts (surfaces, sense, cells, universes, lattices, materials)
403+
- [Surface Deduplication](SURFACE_DEDUP.md) — full dedup pipeline: canonicalization, hashing, tolerance equality, canonical surface maps, XOR sense correction
404+
- [API Reference](API.md) — complete public function listing
405+
- [Tutorial](TUTORIAL.md) — C API walkthrough with working examples
406+
- [Lua Tutorial](LUA_TUTORIAL.md) — Lua binding reference and examples

docs/CONCEPTS.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ Every cell has:
6666

6767
- **Cell ID**: the MCNP cell number (unique identifier)
6868
- **Material ID**: which material fills the cell (0 = void)
69-
- **Density**: material density (negative = g/cm^3, positive = atoms/barn-cm, matching MCNP convention)
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)
7070
- **Universe ID**: which universe the cell belongs to (default 0)
7171
- **Region**: a CSG tree node defining the shape
7272
- **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
265265
```c
266266
#include "alea_raycast.h"
267267

268-
alea_raycast_result_t* result = alea_raycast(sys,
268+
alea_raycast_result_t* result = alea_raycast_result_create();
269+
alea_raycast(sys,
269270
ox, oy, oz, // ray origin
270271
dx, dy, dz, // ray direction
271272
max_distance, // maximum distance to trace
272-
-1); // universe (-1 = root)
273+
result);
273274
```
274275
275276
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:
458459
| 10 | `ALEA_ERR_UNSUPPORTED` | Feature not supported |
459460
| 11 | `ALEA_ERR_UNSUPPORTED_SURFACE` | Surface type not supported |
460461
| 12 | `ALEA_ERR_EXPORT_FAILED` | Export operation failed |
461-
| 13 | `ALEA_ERR_INTERRUPTED` | Operation interrupted (Ctrl+C) |
462-
| 14 | `ALEA_ERR_NOT_FOUND` | Item not found |
463-
| 15 | `ALEA_ERR_EMPTY` | Collection is empty |
464-
| 16 | `ALEA_ERR_OVERFLOW` | Buffer too small, result truncated |
462+
| 13 | `ALEA_ERR_NOT_IMPLEMENTED` | Feature not yet implemented |
463+
| 14 | `ALEA_ERR_INTERRUPTED` | Operation interrupted (Ctrl+C) |
464+
| 15 | `ALEA_ERR_NOT_FOUND` | Item not found |
465+
| 16 | `ALEA_ERR_EMPTY` | Collection is empty |
466+
| 17 | `ALEA_ERR_OVERFLOW` | Buffer too small, result truncated |

docs/SURFACE_DEDUP.md

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -479,17 +479,10 @@ This is the simplest formula that correctly handles all four combinations.
479479

480480
### MCNP vs OpenMC difference
481481

482-
The XOR formula is the same in both exporters. The difference is in how surface
483-
*coefficients* are emitted:
484-
485-
- **MCNP exporter**: un-canonicalizes plane coefficients by flipping signs
486-
according to `pos_node->inverted`. The exported surface may have opposite
487-
coefficients from the canonical primitive. The XOR formula accounts for this.
488-
489-
- **OpenMC exporter**: emits canonical coefficients as-is (no
490-
un-canonicalization). The XOR formula still works because
491-
`prim_to_surface_inverted` records the same `pos_node->inverted` flag used
492-
by the canonical surface map.
482+
The XOR formula is the same in both exporters. Both exporters un-canonicalize
483+
plane coefficients by flipping signs according to `pos_node->inverted`, so the
484+
exported surface restores the original orientation. The XOR formula accounts for
485+
this in both cases.
493486

494487
---
495488

0 commit comments

Comments
 (0)