-
Notifications
You must be signed in to change notification settings - Fork 675
Geometry debug function #4012
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
viktormai
wants to merge
42
commits into
openmc-dev:develop
Choose a base branch
from
viktormai:geometry-debug-function
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Geometry debug function #4012
Changes from all commits
Commits
Show all changes
42 commits
Select commit
Hold shift + click to select a range
c547b57
Add filter_get_bins interface (not efficient for plotter)
paulromano 7f668bc
Combine filter index search with id_map
paulromano 2287926
Implement new openmc_raster_plot function
paulromano b12b92e
Avoid duplicated code in get_raster_map
paulromano e86c060
Remove get_plot_bins for Filter
paulromano 2e81cb0
Add tests for raster_plot
paulromano d80198b
Small doc fix
paulromano 3db1e54
Initial shot at arbitrary orientation slice plots
paulromano adf7ad3
Remove 'axes' and reorder arguments in lib.raster_plot
paulromano fceb98c
Support u_span and v_span in Model.raster_plot
paulromano ac0e900
Rename raster -> slice for consistency
paulromano 4d6c610
Merge branch 'develop' into slice-plot-api
paulromano 6533034
Restore original (arbitrary) direction for slice plotting
paulromano 4824efa
Revert changes to comments
paulromano 1485504
Merge branch 'develop' into slice-plot-api
paulromano 447efcc
Merge branch 'develop' into slice-plot-api
paulromano 4e225e2
Initial overlap checking support using new slice plot API
viktormai 064e68f
Added unit test for 2 cell and 3 cell overlap returns
viktormai e5a8b14
Fixed overlap logic to be based off slice_data name
viktormai 5d0bd24
Merged main branch with overlap changes
viktormai 54017c1
Fixed show_overlaps_ syntax
viktormai 943478d
Fixed _dll bindings bug
viktormai a0f5a6f
Added test file
viktormai 99f74be
Clang format
viktormai d4757f4
Remove unwanted test files
viktormai 25a68b6
Fixed clang format 2
viktormai ba4fad4
geometry_debug button for 3D
viktormai 532189c
Fixed geometry_debug with temporary session
viktormai 4856b59
geometry_debug() cleanup and added test file
viktormai 18b38c5
Merged with current upstream
viktormai 7e3c718
Updated with upstream
viktormai f885b65
Modified for new overlap changes
viktormai 3381c65
Improved internal/external grouping
viktormai a7791fc
Region separation updates
viktormai 1adf056
Improved geometry_debug workflow
viktormai bc96c3d
Removed erosion for overlaps
viktormai b648c61
Used BoundingBox class, cleaned up
viktormai 57457cd
Added return section
viktormai 96e09d1
Small edits
paulromano 2ad13e7
Docstring fixes
paulromano 5ac0e93
Cleaned up rounding
viktormai e821265
Merge branch 'geometry-debug-function' of github.com:viktormai/openmc…
viktormai File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ | |
| import lxml.etree as ET | ||
| import numpy as np | ||
| from scipy.optimize import curve_fit | ||
| from scipy import ndimage | ||
|
|
||
| import openmc | ||
| import openmc._xml as xml | ||
|
|
@@ -28,6 +29,37 @@ | |
| from openmc.utility_funcs import change_directory | ||
|
|
||
|
|
||
| def classify_undefined_regions(cell_ids: np.ndarray) -> np.ndarray: | ||
| """Find internal undefined pixels in a 2D cell-ID slice. | ||
|
|
||
| Internal undefined pixels are those enclosed by defined pixels (i.e., holes | ||
| in the defined-pixel mask), as opposed to undefined pixels connected to the | ||
| slice boundary, which may represent void outside the model. The | ||
| classification is based only on connectivity within the sampled pixel grid, | ||
| so it does not guarantee true geometric interior classification. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| cell_ids : numpy.ndarray | ||
| Two-dimensional array of cell IDs for a slice, which can be obtained | ||
| from the :meth:`openmc.Model.slice_data` method. | ||
|
|
||
| Returns | ||
| ------- | ||
| numpy.ndarray of bool | ||
| Boolean mask of undefined pixels not connected to the boundary of the | ||
| sampled slice, i.e., undefined interior holes in the sampled grid. | ||
| """ | ||
|
|
||
| _NOT_FOUND = -2 | ||
| if cell_ids is None: | ||
| raise TypeError("cell_ids must be a 2D numpy array, got None") | ||
|
|
||
| undefined = (cell_ids == _NOT_FOUND) | ||
|
|
||
| # Internal undefined pixels are holes in the defined-pixel mask. | ||
| return ndimage.binary_fill_holes(~undefined) & undefined | ||
|
|
||
| # Protocol for a function that is passed to search_keff | ||
| class ModelModifier(Protocol): | ||
| def __call__(self, val: float, **kwargs: Any) -> None: | ||
|
|
@@ -1145,7 +1177,6 @@ def id_map( | |
| pixels=pixels, | ||
| basis=basis, | ||
| show_overlaps=color_overlaps, | ||
| level=-1, | ||
| include_properties=False, | ||
| **init_kwargs, | ||
| ) | ||
|
|
@@ -2894,6 +2925,260 @@ def _replace_infinity(value): | |
| # Take a wild guess as to how many rays are needed | ||
| self.settings.particles = 2 * int(max_length) | ||
|
|
||
| def geometry_debug( | ||
| self, | ||
| lower_left: Sequence[float], | ||
| upper_right: Sequence[float], | ||
| n_samples: int | Sequence[int], | ||
| print_summary: bool = False, | ||
| **init_kwargs, | ||
| ) -> dict[str, Any]: | ||
| """Sample a 3D region to identify overlap and undefined locations. | ||
|
|
||
| The region between `lower_left` and `upper_right` is sampled on a | ||
| regular 3D grid by taking a sequence of 2D slices in z. Overlap and | ||
| undefined locations are identified from cells marked with the overlap | ||
| and undefined sentinels, respectively. A 3D bounding box is returned for | ||
| each unique overlap pair and for each distinct internal undefined region | ||
| (found via 3D connected-component labeling), in a summary dictionary. | ||
| This function is meant to be called from an input file on a 3D box | ||
| encapsulating the entire model. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| lower_left : Sequence[float] | ||
| Lower-left corner of the sampled 3D region. | ||
| upper_right : Sequence[float] | ||
| Upper-right corner of the sampled 3D region. | ||
| n_samples : int or Sequence[int] | ||
| Number of sample points in the x, y, and z directions. If a single | ||
| integer is given, the value is split into all three directions. | ||
| print_summary : bool, optional | ||
| Whether to print a summary of overlap and undefined sample results. | ||
| **init_kwargs | ||
| Keyword arguments passed to :meth:`Model.init_lib`. | ||
|
|
||
| Returns | ||
| ------- | ||
| result : dict | ||
| Dictionary with the following key-value pairs: | ||
|
|
||
| ``"overlap_boxes"`` : list of dict | ||
| Detected overlap regions. Each dictionary contains ``"key"``, | ||
| identifying the overlapping cells, and ``"bbox"``, containing | ||
| the region's world-coordinate bounding box. | ||
|
|
||
| ``"undefined_boxes"`` : list of dict | ||
| Detected internal undefined regions. Each dictionary contains | ||
| ``"bbox"``, containing the region's world-coordinate bounding | ||
| box, and ``"under_resolved"``, indicating whether the region | ||
| may be too thin for the sampling resolution. | ||
|
|
||
| ``"n_overlaps"`` : int | ||
| Number of detected overlap regions. | ||
|
|
||
| ``"n_undefined_regions"`` : int | ||
| Number of detected internal undefined regions. | ||
|
|
||
| ``"under_resolved"`` : bool | ||
| Whether any undefined region may be under-resolved. | ||
| """ | ||
| import openmc.lib | ||
|
|
||
| _OVERLAP = -3 | ||
|
|
||
| init_kwargs.setdefault('output', False) | ||
| init_kwargs.setdefault('args', ['-c']) | ||
|
|
||
| # Accepts 3 separate samples (for x y and z) or just one number | ||
| if isinstance(n_samples, int): | ||
| if n_samples < 1: | ||
| raise ValueError("n_samples must be >= 1") | ||
|
|
||
| lower_left_arr = np.asarray(lower_left, dtype=float) | ||
| upper_right_arr = np.asarray(upper_right, dtype=float) | ||
|
|
||
| width = upper_right_arr - lower_left_arr | ||
| if np.any(width <= 0.0): | ||
| raise ValueError("upper_right must be greater than lower_left in all dimensions") | ||
|
|
||
| # Choose nx, ny, nz proportional to the physical widths so that: | ||
| # nx * ny * nz ≈ n_samples and voxel sizes are similar in x/y/z. | ||
| scale = np.cbrt(n_samples / np.prod(width)) | ||
| nx, ny, nz = np.maximum(1, np.rint(scale * width).astype(int)) | ||
| else: | ||
| if len(n_samples) != 3: | ||
| raise ValueError("n_samples must be an int or a length-3 iterable") | ||
| nx, ny, nz = n_samples | ||
|
|
||
| nx, ny, nz = int(nx), int(ny), int(nz) | ||
|
|
||
| if nx <= 0 or ny <= 0 or nz <= 0: | ||
| raise ValueError("All n_samples values must be positive") | ||
|
|
||
| if len(lower_left) != 3: | ||
| raise ValueError("lower_left must be a length-3 iterable") | ||
| if len(upper_right) != 3: | ||
| raise ValueError("upper_right must be a length-3 iterable") | ||
|
|
||
| x0, y0, z0 = lower_left | ||
| x1, y1, z1 = upper_right | ||
|
|
||
| dz = (z1 - z0) / nz | ||
|
|
||
| u_span = (x1 - x0, 0.0, 0.0) | ||
| v_span = (0.0, y1 - y0, 0.0) | ||
|
|
||
| # Each unique overlap key (universe, cell1, cell2) gets its own bounding | ||
| # box, accumulated in world coordinates across all z-slices. Internal | ||
| # undefined pixels are stacked into a 3D volume and labeled afterwards. | ||
| overlap_boxes = {} | ||
| internal_volume = np.zeros((nz, ny, nx), dtype=bool) | ||
|
|
||
| with openmc.lib.TemporarySession(self, **init_kwargs): | ||
| for k in range(nz): | ||
| z = z0 + (k + 0.5) * dz | ||
| origin = ((x0 + x1) / 2.0, (y0 + y1) / 2.0, z) | ||
|
|
||
| geom_data, _ = openmc.lib.slice_data( | ||
|
paulromano marked this conversation as resolved.
|
||
| origin=origin, | ||
| u_span=u_span, | ||
| v_span=v_span, | ||
| pixels=(nx, ny), | ||
| show_overlaps=True, | ||
| include_properties=False, | ||
| ) | ||
|
|
||
| cell_ids = geom_data[:, :, 0] | ||
|
|
||
| overlap_data = openmc.lib.slice_data_overlap_info() | ||
|
|
||
| # Union each overlap key's bounding box across z-slices. | ||
| for overlap_idx, key in enumerate(overlap_data): | ||
| encoded_id = _OVERLAP - overlap_idx - 1 | ||
| pix = np.argwhere(cell_ids == encoded_id) | ||
| if pix.size == 0: | ||
| continue | ||
|
|
||
| key_t = tuple(int(v) for v in key) | ||
| xc = x0 + (pix[:, 1] + 0.5) * (x1 - x0) / nx | ||
| yc = y1 - (pix[:, 0] + 0.5) * (y1 - y0) / ny | ||
|
Comment on lines
+3064
to
+3065
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks like it just uses the pixel centers. I think it would be more conservative to use the bounds of the pixel itself rather than just its center (if I'm interpreting it correctly, this is already how it works for the undefined pixels?). |
||
|
|
||
| slice_box = openmc.BoundingBox( | ||
| (float(xc.min()), float(yc.min()), z), | ||
| (float(xc.max()), float(yc.max()), z), | ||
| ) | ||
| if key_t in overlap_boxes: | ||
| overlap_boxes[key_t] |= slice_box | ||
| else: | ||
| overlap_boxes[key_t] = slice_box | ||
|
|
||
| internal_volume[k] = classify_undefined_regions(cell_ids) | ||
|
|
||
| overlap_boxes = [ | ||
| {"key": key_t, "bbox": bbox} for key_t, bbox in overlap_boxes.items() | ||
| ] | ||
|
|
||
| # Overlaps are not flagged for resolution: whether an overlap exists and | ||
| # which cells collide is determined by the (universe, cell1, cell2) key | ||
|
|
||
| # Label spatially-connected undefined regions in 3D and build a | ||
| # world-coordinate bounding box for each connected component. A feature | ||
| # thinner than the sample spacing can rasterize with breaks and split | ||
| # into several regions, so give a suggestion to the user to increase n_samples. | ||
|
|
||
| undefined_boxes = [] | ||
| if internal_volume.any(): | ||
| structure = ndimage.generate_binary_structure(3, 1) # face connectivity | ||
| labeled, _ = ndimage.label(internal_volume, structure=structure) | ||
| for region_id, sl in enumerate(ndimage.find_objects(labeled), start=1): | ||
| if sl is None: | ||
| continue | ||
| kz, ky, kx = sl # slice objects over the (z, y, x) index axes | ||
|
|
||
| # Voxel-edge world extents | ||
| x_lo = x0 + kx.start * (x1 - x0) / nx | ||
| x_hi = x0 + kx.stop * (x1 - x0) / nx | ||
| # The y (row) axis is flipped in world coordinates (row 0 == y1) | ||
| y_hi = y1 - ky.start * (y1 - y0) / ny | ||
| y_lo = y1 - ky.stop * (y1 - y0) / ny | ||
| z_lo = z0 + kz.start * dz | ||
| z_hi = z0 + kz.stop * dz | ||
|
|
||
| # Local-thickness test: a bounding box is misleading for thin | ||
| # curved shells (e.g. an annular gap whose bbox is large but | ||
| # which is only ~1 voxel thick radially). Erode the region's | ||
| # voxel mask; if erosion empties it, the region is nowhere | ||
| # thicker than ~2 voxels and is under-resolved. | ||
| mask = (labeled[sl] == region_id) | ||
| under_resolved = not ndimage.binary_erosion(mask).any() | ||
|
|
||
| bbox = openmc.BoundingBox( | ||
| (x_lo, y_lo, z_lo), | ||
| (x_hi, y_hi, z_hi), | ||
| ) | ||
| undefined_boxes.append({ | ||
| "bbox": bbox, | ||
| "under_resolved": bool(under_resolved), | ||
| }) | ||
|
|
||
| under_resolved = any(b["under_resolved"] for b in undefined_boxes) | ||
|
|
||
| result = { | ||
| "overlap_boxes": overlap_boxes, | ||
| "undefined_boxes": undefined_boxes, | ||
| "under_resolved": under_resolved, | ||
| } | ||
|
|
||
| if under_resolved: | ||
| n_un = sum(b["under_resolved"] for b in undefined_boxes) | ||
| warnings.warn( | ||
| f"Sampling resolution may be insufficient: {n_un} undefined " | ||
| "region(s) are resolved by <= 2 voxels across their thinnest " | ||
| "dimension, so they may be fragmented. " | ||
| "Consider increasing n_samples." | ||
| ) | ||
|
|
||
| if print_summary: | ||
| print("Geometry debug summary:") | ||
|
|
||
| if result["overlap_boxes"]: | ||
| print(f" Overlaps found: {result['n_overlaps']}") | ||
| for box in result["overlap_boxes"]: | ||
| ll, ur = box["bbox"].lower_left, box["bbox"].upper_right | ||
| print( | ||
| f" cells {box['key']}: " | ||
| f"x[{ll[0]:.4g}, {ur[0]:.4g}] " | ||
| f"y[{ll[1]:.4g}, {ur[1]:.4g}] " | ||
| f"z[{ll[2]:.4g}, {ur[2]:.4g}]" | ||
| ) | ||
| else: | ||
| print(" Overlap bounding boxes: None") | ||
|
|
||
| if result["undefined_boxes"]: | ||
| print(f" Undefined regions found: {result['n_undefined_regions']}") | ||
| for i, box in enumerate(result["undefined_boxes"], start=1): | ||
| flag = " [under-resolved]" if box["under_resolved"] else "" | ||
| ll, ur = box["bbox"].lower_left, box["bbox"].upper_right | ||
| print( | ||
| f" region {i}: " | ||
| f"x[{ll[0]:.4g}, {ur[0]:.4g}] " | ||
| f"y[{ll[1]:.4g}, {ur[1]:.4g}] " | ||
| f"z[{ll[2]:.4g}, {ur[2]:.4g}]{flag}" | ||
| ) | ||
| else: | ||
| print(" Undefined bounding boxes: None") | ||
|
|
||
| if result["under_resolved"]: | ||
| print( | ||
| "WARNING: some undefined regions are resolved by <= 2 " | ||
| "voxels across their thinnest dimension and may be " | ||
| "fragmented or missed; increase n_samples so thin features " | ||
| "span at least 3 voxels." | ||
| ) | ||
|
|
||
| return result | ||
|
|
||
| def keff_search( | ||
| self, | ||
| func: ModelModifier, | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.