Allow editing cell geometry definition#945
Conversation
…dd Test Cases Signed-off-by: DIGVIJAY <144053736+digvijay-y@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds user-facing utilities to traverse and edit a cell’s CSG geometry tree, addressing #737 by enabling divider replacement and leaf iteration.
Changes:
- Added
HalfSpace.replace()/_replace_recursive()to swapSurface/Celldividers throughout a geometry tree. - Added
__iter__toHalfSpaceandUnitHalfSpacefor depth-first leaf traversal. - Added unit tests plus minor documentation/demo formatting updates.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
montepy/surfaces/half_space.py |
Implements replace() and iteration over geometry leaves; adjusts duplicate-surface remapping logic. |
tests/test_geometry.py |
Adds test coverage for replace() and __iter__. |
doc/source/changelog.rst |
Documents the new feature (but currently has RST formatting issues). |
doc/source/conf.py |
Removes trailing whitespace in nitpick_ignore. |
demo/_config.py |
Minor formatting (blank line). |
demo/1_fusion_radial_build.ipynb |
Minor formatting fix (newline). |
demo/answers/1_fusion_radial_build.ipynb |
Formatting cleanup in example code cells. |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
| def replace( | ||
| self, | ||
| old_divider: montepy.surfaces.surface.Surface | montepy.Cell, | ||
| new_divider: montepy.surfaces.surface.Surface | montepy.Cell, | ||
| ) -> None: |
There was a problem hiding this comment.
replace() is documented and typed to support swapping either Surface dividers or Cell complements, but the added tests cover only surface replacement. Add at least one test that replaces a complemented Cell divider and asserts both the geometry leaves and cell.complements are updated correctly (including removal of the old cell).
| replaced = self._replace_recursive(old_divider, new_divider) | ||
| if not replaced: | ||
| raise ValueError( | ||
| f"{old_divider} (number: {old_divider.number}) not found in geometry tree." | ||
| ) |
There was a problem hiding this comment.
replace() can be called with the same object for both arguments (old_divider is new_divider). In that case _replace_recursive() will report success, but the cleanup loop later will still remove old_divider from the parent cell’s surfaces/complements, leaving the geometry referencing a divider that is no longer in the cell collection. Add an early return/no-op when old_divider is new_divider (or skip container removal in that case).
There was a problem hiding this comment.
Let's just add a guard like:
if new_divider is old_divider:
raise ValueError(...)| if not isinstance( | ||
| old_divider, (montepy.surfaces.surface.Surface, montepy.Cell) | ||
| ): | ||
| raise TypeError( | ||
| f"old_divider must be a Surface or Cell. {old_divider} given." | ||
| ) | ||
| if not isinstance( | ||
| new_divider, (montepy.surfaces.surface.Surface, montepy.Cell) | ||
| ): | ||
| raise TypeError( | ||
| f"new_divider must be a Surface or Cell. {new_divider} given." | ||
| ) |
There was a problem hiding this comment.
replace() currently assumes the geometry tree has been linked via update_pointers() (i.e., leaf dividers are Surface/Cell objects). If the tree still contains integer dividers from parsing, _replace_recursive() will fail to match and this raises a misleading ValueError (“not found”). Consider explicitly validating the tree is initialized (e.g., by calling _get_leaf_objects() up front, or checking for Integral dividers) and raising IllegalState with the existing “Run Cell.update_pointers” guidance instead.
There was a problem hiding this comment.
I would suggest using the Integral check and raising IllegalState error. This is quite an edge case that I don't think a ton of effort needs to be put into resolving it (e.g., _get_leaf_objects)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
@digvijay-y thanks for doing this. It will be a few days before I will be able to review this. |
MicahGale
left a comment
There was a problem hiding this comment.
Thanks again @digvijay-y.
A few thoughts:
- This is a good first implementation, just some slight improvements can be made.
- I messed up the GHA so coveralls doesn't run. There is some uncovered code. Could you check if these were your changes?
montepy/surfaces/half_space.py 366 4 98.91% 208, 429, 473, 484
- Could you review the self-check of your code?
| " 184: 0.306,\n", | ||
| " 186: 0.284\n", | ||
| "}\n", | ||
| "w_natural = {182: 0.265, 183: 0.143, 184: 0.306, 186: 0.284}\n", |
There was a problem hiding this comment.
Could you revert this? I don't like how black formatted this, and demo is exempt from formatting.
| def replace( | ||
| self, | ||
| old_divider: montepy.surfaces.surface.Surface | montepy.Cell, | ||
| new_divider: montepy.surfaces.surface.Surface | montepy.Cell, | ||
| ) -> None: |
| old_divider: montepy.surfaces.surface.Surface | montepy.Cell, | ||
| new_divider: montepy.surfaces.surface.Surface | montepy.Cell, |
There was a problem hiding this comment.
the type montepy.Surface should work as well and would be clearer.
| old_divider : Surface, Cell | ||
| the divider to be replaced. | ||
| new_divider : Surface, Cell |
There was a problem hiding this comment.
| old_divider : Surface, Cell | |
| the divider to be replaced. | |
| new_divider : Surface, Cell | |
| old_divider : Surface or Cell | |
| the divider to be replaced. | |
| new_divider : Surface or Cell |
The numpy standard is to use or.
| if not isinstance( | ||
| old_divider, (montepy.surfaces.surface.Surface, montepy.Cell) | ||
| ): | ||
| raise TypeError( | ||
| f"old_divider must be a Surface or Cell. {old_divider} given." | ||
| ) | ||
| if not isinstance( | ||
| new_divider, (montepy.surfaces.surface.Surface, montepy.Cell) | ||
| ): | ||
| raise TypeError( | ||
| f"new_divider must be a Surface or Cell. {new_divider} given." | ||
| ) |
There was a problem hiding this comment.
I would suggest using the Integral check and raising IllegalState error. This is quite an edge case that I don't think a ton of effort needs to be put into resolving it (e.g., _get_leaf_objects)
| def _make_linked_geometry(*surfs): | ||
| """Helper: build a parent cell whose geometry is already linked. | ||
|
|
||
| Returns (parent_cell, half_space) where every UnitHalfSpace leaf has | ||
| self._cell set so the old-divider cleanup path in replace() is exercised. | ||
| """ | ||
| parent = montepy.Cell() | ||
| parent.number = 99 | ||
| # Populate parent.surfaces up front so all surfaces are present before replace() | ||
| for surf in surfs: | ||
| parent.surfaces.append(surf) | ||
| half_space = None | ||
| for surf in surfs: | ||
| leaf = +surf | ||
| # Wire _cell directly since dividers are already resolved objects, not ints | ||
| leaf._cell = parent | ||
| half_space = leaf if half_space is None else half_space & leaf | ||
| return parent, half_space |
There was a problem hiding this comment.
These tests probably live better in tests/test_geometry_integration.py. I think you will have fixtures there you can use for this. At the very least the cell should be coming from a @pytest.fixture I think.
| def __len__(self): | ||
| return 1 | ||
|
|
||
| def __iter__(self): |
There was a problem hiding this comment.
I like this addition; surprised it hadn't existed before.
|
|
||
| def test_replace_surface_basic(): | ||
| """replace() swaps old surface for new one throughout the tree.""" | ||
| surf1 = montepy.CylinderOnAxis() |
There was a problem hiding this comment.
just use montepy.CylinderOnAxis(number=1) to be more concise.
| surf2.number = 2 | ||
| surf3.number = 3 | ||
| half_space = +surf1 & -surf2 & +surf3 | ||
| assert len(list(half_space)) == len(half_space) |
There was a problem hiding this comment.
This is calling __iter__ twice. Should just do len(half_space) == 3
| dividers = [leaf.divider for leaf in leaves] | ||
| assert surf3 in dividers | ||
| assert surf1 not in dividers | ||
| assert surf2 in dividers |
There was a problem hiding this comment.
We should also verify that the geometry is written to file correctly. I think test_geometry_integration.py has a function for doing this verify_export or something along those lines.
Pull Request Checklist for MontePy
Description
Fixes #737
General Checklist
blackversion 25 or 26.LLM Disclosure
Are you?
Were any large language models (LLM or "AI") used in to generate any of this code?
Documentation Checklist
First-Time Contributor Checklist
pyproject.tomlif you wish to do so.Additional Notes for Reviewers
Ensure that:
📚 Documentation preview 📚: https://montepy--945.org.readthedocs.build/en/945/