Skip to content

fix: obstruction masks and cell-centred slice planes - #145

Merged
m-fehr merged 8 commits into
masterfrom
fix/obstruction-masks
Oct 15, 2026
Merged

m-fehr merged 8 commits into
masterfrom
fix/obstruction-masks

Conversation

@m-fehr

@m-fehr m-fehr commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #132.

What masked=True means

masked=True keeps its original meaning: it hides where the obstructions are, now without the errors described in #132. A new option, mask="placeholders", hides only the values FDS did not compute but filled with the ambient value, and keeps the gas values FDS writes on the faces of an obstruction:

slc.to_global(masked=True)                       # the obstructions (default, as before)
slc.to_global(masked=True, mask="placeholders")  # only values FDS did not compute

The option exists for slices, Plot3D and Smoke3D (to_global, Smoke3D.to_physical) and as Mesh.get_obstruction_mask(..., mode=...) / get_obstruction_mask_slice(..., mode=...).

Fixed

before now
Cell-centred mask cells x1 … x2 on every axis (one too many) cells x1 … x2-1 inside the obstruction
Node-based mask nodes x1 … x2-1: lower face masked, upper face not nodes x1 … x2, both faces
Plane of a cell-centred 2D slice one of the two cells next to node k, chosen by float rounding the cell below node k, as FDS writes SLCC … k k; the integer indices from the .smv file are kept on SubSlice (bound_indices, index_ranges())
to_global(return_coordinates=True) for cell-centred slices node positions cell centres (e.g. PBZ=0.4 → z = 0.45, as Smokeview labels it)
Several obstruction parts with the same ID in a mesh only the first masked all (Obstruction.get_subobstructions)
get_2d_slice_from_3d cut slices defined with XB at the mesh's index; cell-centred cuts on a node chosen by rounding cuts at the slice's own index; on a node takes the cell above it, like FDS for PBZ

The placeholder mode follows FDS's own rule (CORNER_VALUE in dump.f90): a node gets the ambient value if all cells of its mesh around it are solid.

Checked against Smokeview and FDS

New test case slcf_masks_fds611 (FDS 6.11.1, input in tests/cases/fds_inputs/slcf_masks.fds, so the release and nightly checks run it too): three meshes, one adjoining another only partly, and obstructions inside a mesh, across a mesh boundary, at the outside of the domain, two sharing a face, one with a hole, and ones ending at a mesh boundary.

  • Smokeview: three slices were read off by hand in Smokeview 6.11.2 (node-based PBZ=0.7 and PBY=0.8, cell-centred PBZ=0.4 through to_global(masked=True)); all 600 areas agree with fdsreader (master: 181/200 on the cell-centred one). The readings are tests.
  • Obstruction mask agrees with the obstruction coordinates in the .smv file on every slice and mesh.
  • Placeholder mask blocks exactly the values FDS wrote as ambient (20 °C) after 10 s of fire, on every slice and mesh; every placeholder lies inside the obstruction mask.
  • The tests with fake obstructions in tests/test_slice_to_global.py keep their expectations from master.

Behaviour changes (for the release notes)

  • masked=True gives different arrays: cell-centred masks are one cell smaller per side, node-based masks are no longer shifted towards the lower corner, cell-centred planes may move to the other cell layer.
  • Obstructions without thickness block no cells any more (no cell lies inside them; Smokeview does not blank them), but still the nodes on them.
  • Mesh.get_obstruction_mask_slice() returns the mask in the shape of the subslice's data.
  • Results without masked=True are unchanged.

Not included

  • extent of a cell-centred plane still lies on node k, while its coordinates now are the cell centre.
  • Solids defined with &GEOM are not part of either mask; velocity slices (face values) follow a different rule in FDS.

The slices, Plot3D and Smoke3D example notebooks were run again; the slices example no longer needs the known-issue note.

🤖 Generated with Claude Code

m-fehr and others added 8 commits October 15, 2026 13:17
Verified on an FDS 6.11.1 case: FDS writes a placeholder (the ambient
temperature) into the cells inside an obstruction and into the nodes
inside it, while the nodes on its faces get values of the gas next to
it, unless there is no gas beyond the face (the obstruction continues
into the neighbouring mesh, or the face lies on the outside of the
domain).

- Masks now block exactly those cells and nodes. Cell-centred masks
  blocked one cell too many on every axis; node masks blocked the lower
  face nodes, which hold gas values, and missed the inner node at the
  upper face. Meshes know which of their faces lie on the outside of
  the domain (Mesh.exterior_faces).
- SubSlice keeps the node indices from the .smv file. A cell-centred
  plane SLCC ... k k holds the cell below node k; its coordinates, its
  mask and to_global use that cell instead of picking one of the two
  cells next to node k by float rounding.
- to_global(return_coordinates=True) returns the cell centres for
  cell-centred slices instead of node positions.
- The slices example gets a cell-centred slice to show the masked wall
  (the wall is one cell thick, so the node-based slice has no node
  inside it); the known-issue note is removed and the slices, Plot3D
  and Smoke3D notebooks are run again.

This changes the results of masked=True for slices, Plot3D and Smoke3D.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that FDS writes the placeholder into a node whenever
all cells of its mesh around it are solid (CORNER_VALUE in dump.f90,
and Smokeview blanks nodes the same way), not per obstruction:

- Masks are now built from the union of the solid cells of all
  obstructions in a mesh; a node is blocked if all of the mesh's cells
  around it are solid. This covers obstructions sharing a face, ending
  at the edge of the mesh (also with gas beyond in the neighbouring
  mesh), at the outside of the domain, and holes. Mesh.exterior_faces,
  the bounding box test and the pass over all mesh pairs are gone.
- All parts of an obstruction in a mesh are masked
  (Obstruction.get_subobstructions), not only the first.
- get_2d_slice_from_3d updates the node indices of the cut slice and
  cuts slices defined with XB at their own index instead of the mesh's.

The test case gets a third mesh that adjoins only part of a face and
obstructions for each of these cases; the mask matches the placeholder
values of FDS 6.11.1 on every slice and mesh. The notebooks are run
again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- get_obstruction_mask maps the visible times of an obstruction to
  indices once instead of searching all times for each of them (was
  quadratic in the number of time steps), and pads the cells outside
  the mesh with np.pad.
- get_2d_slice_from_3d: cutting a cell-centred slice at a node takes
  the cell above it, as FDS does for e.g. PBZ (was decided by float
  rounding), and the cut subslice gets a 2D dimension.
- get_obstruction_mask_slice's docstring points to the rules.
- Tests: the stored/fresh case handling and the cache-free loading are
  shared in tests/fds_cases.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
masked=True keeps its original meaning, where the obstructions are, with
the bugs of #132 fixed: cells inside an obstruction (not one more), and
nodes inside it and on both of its faces (not shifted towards the lower
corner). Obstructions without thickness still mask one row, so thin walls
stay visible.

mask="placeholders" (to_global of slices, Plot3D and Smoke3D;
Mesh.get_obstruction_mask(mode=...)) instead hides only the values FDS
did not compute but filled with the ambient value, so the gas values on
the faces of an obstruction are kept.

The tests check the obstruction mask against the obstruction coordinates
in the .smv file and the placeholder mask against FDS's output; the tests
with fake obstructions keep their expectations from master. The slices
example shows the masked wall again with a node-based slice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compared by hand with Smokeview on the slice PBZ=0.7 of the test case:
199 of 200 areas agreed. The one difference was the part of an
obstruction that ends at a mesh boundary, which FDS writes into the
neighbouring mesh without thickness: the cell-centred mask blocked the
cell after it, although no cell lies inside it and Smokeview does not
blank it. Obstructions without thickness now block no cells, but still
the nodes on them, so thin walls stay visible in node-based data.

The Smokeview reading is a test now; all 200 areas agree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The slice PBY=0.8 read off by hand in Smokeview, where the plane lies on
faces of obstructions, crosses one across the mesh boundary and one with
a hole: all 200 areas agree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cell-centred slice PBZ=0.4 read off by hand in Smokeview, checked
through to_global(masked=True): all 200 cells agree (master: 181).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The local mask arrays in to_global of slices, Plot3D and Smoke3D are
  renamed, so the new parameter mask is no longer rebound.
- Both mask modes walk the obstruction parts through one helper,
  Mesh._obstruction_parts.
- The docstring of get_obstruction_mask says where the default mask and
  Smokeview's blanking differ (node-based data), and Smoke3D's docstring
  lists mask with the other parameters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.18182% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
fdsreader/slcf/slice.py 98.14% 0 Missing and 1 partial ⚠️
fdsreader/smoke3d/smoke3d.py 75.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@m-fehr
m-fehr merged commit 90be54a into master Oct 15, 2026
10 checks passed
@m-fehr
m-fehr deleted the fix/obstruction-masks branch October 15, 2026 17:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Obstruction masks are off by one cell/node (obstructions one cell too thick, cell-centred slice plane can be wrong)

1 participant