From 6ed83ef55504e237a41c64230127ceb329d0b997 Mon Sep 17 00:00:00 2001 From: DIGVIJAY <144053736+digvijay-y@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:16:40 +0000 Subject: [PATCH 01/29] __iter__ for halfspace and unit --- montepy/surfaces/half_space.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/montepy/surfaces/half_space.py b/montepy/surfaces/half_space.py index f6a66ca12..05a20c1d1 100644 --- a/montepy/surfaces/half_space.py +++ b/montepy/surfaces/half_space.py @@ -439,6 +439,23 @@ def __len__(self): length += len(self.right) return length + def __iter__(self): + """Iterate over all :class:`UnitHalfSpace` leaves in depth-first order. + + This allows you to walk every leaf of the geometry tree, for example:: + + for unit in cell.geometry: + print(unit.divider, unit.side) + + Yields + ------ + UnitHalfSpace + each leaf node in the tree, left subtree before right. + """ + yield from iter(self.left) + if self.right is not None: + yield from iter(self.right) + def __eq__(self, other): # don't allow subclassing on right side if type(self) != type(other): @@ -745,6 +762,16 @@ def num(obj): def __len__(self): return 1 + def __iter__(self): + """Iterate over this leaf node. + + Yields + ------ + UnitHalfSpace + this leaf itself. + """ + yield self + def __eq__(self, other): if not isinstance(other, UnitHalfSpace): raise TypeError("UnitHalfSpace can't be equal to other type") @@ -753,3 +780,4 @@ def __eq__(self, other): and self.divider is other.divider and self.side == other.side ) + \ No newline at end of file From c2df19952c152e3a7c14bc07b8d955170b54df0e Mon Sep 17 00:00:00 2001 From: DIGVIJAY <144053736+digvijay-y@users.noreply.github.com> Date: Tue, 14 Apr 2026 17:23:52 +0000 Subject: [PATCH 02/29] Add replace and Replace_recursive func --- doc/source/changelog.rst | 3 +- doc/source/conf.py | 1 - montepy/surfaces/half_space.py | 50 +++++++++++++++++++++++++++++++++- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 2ff7950cc..a26648e14 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -14,6 +14,7 @@ MontePy Changelog * ``Cell.universe`` can now be set to ``None`` (or deleted via ``del cell.universe``) to reset the universe assignment back to the default (:issue:`902`). * Added ``extend_renumber`` to ``NumberedObjectCollection`` with related test cases (:issue:`881`). * Made :class:`montepy.data_inputs.importance.Importance` more ``dict``-like with ``keys``, ``values``, and ``items`` functions (:pull:`921`). +* API to Allow editing cell geometry definition (:issue:`945`). **Bugs Fixed** @@ -31,8 +32,6 @@ MontePy Changelog * Enable Sphinx nitpicky mode and fix ~30 broken cross-references in the developer guide, user guide, and migration docs (:issue:`889`). * Remove redundant "montepy.*" prefix from navigation in the API docs (:issue:`901`). -1.3 releases -============ 1.3.0 -------------- diff --git a/doc/source/conf.py b/doc/source/conf.py index 604a4dbd6..7fd1f9644 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -181,7 +181,6 @@ ("py:class", "sly.yacc.ParserMeta"), ("py:class", "sly.yacc.YaccProduction"), ("py:class", "InitInput"), - # Subpackages referenced with :mod: in docs; autodoc indexes individual classes # but not the package-level modules themselves # typing.Union is not in the Python intersphinx inventory as a py:data target diff --git a/montepy/surfaces/half_space.py b/montepy/surfaces/half_space.py index 05a20c1d1..73ee8a4c9 100644 --- a/montepy/surfaces/half_space.py +++ b/montepy/surfaces/half_space.py @@ -247,6 +247,49 @@ def remove_duplicate_surfaces( if self.right is not None: self.right.remove_duplicate_surfaces(new_deleting_dict) + def replace( + self, + old_divider: montepy.surfaces.surface.Surface | montepy.Cell, + new_divider: montepy.surfaces.surface.Surface | montepy.Cell, + ) -> None: + """Replace all occurrences of a divider in this geometry tree. + + Parameters + ---------- + old_divider : Surface, Cell + the divider to be replaced. + new_divider : Surface, Cell + the divider to replace it with. + + Raises + ------ + TypeError + if either argument is not a Surface or Cell. + ValueError + if old_divider is not found in the geometry tree. + """ + 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." + ) + replaced = self._replace_recursive(old_divider, new_divider) + if not replaced: + raise ValueError(f"{old_divider} not found in geometry tree.") + + def _replace_recursive(self, old_divider, new_divider) -> bool: + replaced = self.left._replace_recursive(old_divider, new_divider) + if self.right is not None: + replaced |= self.right._replace_recursive(old_divider, new_divider) + return replaced + def _get_leaf_objects(self): """Get all of the leaf objects for this tree. @@ -699,6 +742,12 @@ def _update_node(self): self._node.value = self.divider.number self._node.is_negative = not self.side + def _replace_recursive(self, old_divider, new_divider) -> bool: + replaced = self.left._replace_recursive(old_divider, new_divider) + if self.right is not None: + replaced |= self.right._replace_recursive(old_divider, new_divider) + return replaced + def _get_leaf_objects(self): if isinstance( self._divider, (montepy.cell.Cell, montepy.surfaces.surface.Surface) @@ -780,4 +829,3 @@ def __eq__(self, other): and self.divider is other.divider and self.side == other.side ) - \ No newline at end of file From b56277e63520fa185ab48b2e79025478244f57ca Mon Sep 17 00:00:00 2001 From: DIGVIJAY <144053736+digvijay-y@users.noreply.github.com> Date: Tue, 14 Apr 2026 17:24:57 +0000 Subject: [PATCH 03/29] updated Changelog --- doc/source/changelog.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index a26648e14..3df309108 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -32,7 +32,6 @@ MontePy Changelog * Enable Sphinx nitpicky mode and fix ~30 broken cross-references in the developer guide, user guide, and migration docs (:issue:`889`). * Remove redundant "montepy.*" prefix from navigation in the API docs (:issue:`901`). - 1.3.0 -------------- From 612ac5db36a2412e2cf0cb72a4f4b103daef243e Mon Sep 17 00:00:00 2001 From: DIGVIJAY <144053736+digvijay-y@users.noreply.github.com> Date: Sat, 25 Apr 2026 10:51:20 +0000 Subject: [PATCH 04/29] Enhance replace() method in HalfSpace and UnitHalfSpace classes and Add Test Cases Signed-off-by: DIGVIJAY <144053736+digvijay-y@users.noreply.github.com> --- demo/_config.py | 1 + demo/answers/1_fusion_radial_build.ipynb | 16 +-- doc/source/changelog.rst | 7 + montepy/surfaces/half_space.py | 30 ++++- tests/test_geometry.py | 155 +++++++++++++++++++++++ 5 files changed, 193 insertions(+), 16 deletions(-) diff --git a/demo/_config.py b/demo/_config.py index fde62824e..b25e70e52 100644 --- a/demo/_config.py +++ b/demo/_config.py @@ -12,4 +12,5 @@ def IFrame(src, width=X_RES, height=Y_RES, extras=None, **kwargs): async def install_montepy(): if "pyodide" in sys.modules: import piplite + await piplite.install("montepy") diff --git a/demo/answers/1_fusion_radial_build.ipynb b/demo/answers/1_fusion_radial_build.ipynb index f8b7dba0c..38d4dbd5b 100644 --- a/demo/answers/1_fusion_radial_build.ipynb +++ b/demo/answers/1_fusion_radial_build.ipynb @@ -343,7 +343,7 @@ "#\n", "# Iterate over the cells and their numbers, thanks to items()\n", "for num, cell in problem.cells.items():\n", - " #print the cell comments\n", + " # print the cell comments\n", " print(num, cell.comments)" ] }, @@ -486,12 +486,7 @@ }, "outputs": [], "source": [ - "w_natural = {\n", - " 182: 0.265,\n", - " 183: 0.143,\n", - " 184: 0.306,\n", - " 186: 0.284\n", - "}\n", + "w_natural = {182: 0.265, 183: 0.143, 184: 0.306, 186: 0.284}\n", "tungsten.clear()\n", "for mass, abundance in w_natural.items():\n", " tungsten.add_nuclide(f\"W-{mass}.82c\", abundance)\n", @@ -765,9 +760,10 @@ "outputs": [], "source": [ "import numpy as np\n", - "MIN_RADIUS = 115.01 # [cm]\n", - "MAX_RADIUS = 198.99 # [cm]\n", - "CAN_THICKNESS = 1 #[cm]\n", + "\n", + "MIN_RADIUS = 115.01 # [cm]\n", + "MAX_RADIUS = 198.99 # [cm]\n", + "CAN_THICKNESS = 1 # [cm]\n", "\n", "radii = np.arange(MIN_RADIUS, MAX_RADIUS, 5)\n", "for radius in radii:\n", diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 3df309108..421ee3c57 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -2,6 +2,13 @@ MontePy Changelog ***************** +Next Release +============ + +**Features Added** +* Added :func:`~montepy.surfaces.half_space.HalfSpace.replace` to swap dividers in a cell geometry tree, and ``__iter__`` to traverse geometry leaves (:issue:`737`). + + 1.4 releases ============ diff --git a/montepy/surfaces/half_space.py b/montepy/surfaces/half_space.py index 73ee8a4c9..0161847a7 100644 --- a/montepy/surfaces/half_space.py +++ b/montepy/surfaces/half_space.py @@ -264,7 +264,8 @@ def replace( Raises ------ TypeError - if either argument is not a Surface or Cell. + if either argument is not a Surface or Cell, or if they are not the + same kind (e.g. one is a Surface and the other is a Cell). ValueError if old_divider is not found in the geometry tree. """ @@ -280,9 +281,26 @@ def replace( raise TypeError( f"new_divider must be a Surface or Cell. {new_divider} given." ) + if isinstance(old_divider, montepy.Cell) != isinstance( + new_divider, montepy.Cell + ): + raise TypeError( + f"old_divider and new_divider must both be Surfaces or both be Cells. " + f"Got {type(old_divider).__name__} and {type(new_divider).__name__}." + ) replaced = self._replace_recursive(old_divider, new_divider) if not replaced: - raise ValueError(f"{old_divider} not found in geometry tree.") + raise ValueError( + f"{old_divider} (number: {old_divider.number}) not found in geometry tree." + ) + if self._cell is not None: + container = ( + self._cell.complements + if isinstance(old_divider, montepy.Cell) + else self._cell.surfaces + ) + if old_divider in container: + container.remove(old_divider) def _replace_recursive(self, old_divider, new_divider) -> bool: replaced = self.left._replace_recursive(old_divider, new_divider) @@ -743,10 +761,10 @@ def _update_node(self): self._node.is_negative = not self.side def _replace_recursive(self, old_divider, new_divider) -> bool: - replaced = self.left._replace_recursive(old_divider, new_divider) - if self.right is not None: - replaced |= self.right._replace_recursive(old_divider, new_divider) - return replaced + if self._divider is old_divider: + self.divider = new_divider + return True + return False def _get_leaf_objects(self): if isinstance( diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 40a000e67..c1238fdbe 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -546,3 +546,158 @@ def test_update_operators_in_node(): half_space.operator = Operator.UNION half_space._update_values() assert half_space.node.format() == "-1 : 1" + + +# ── replace() tests ─────────────────────────────────────────────────────────── + + +def _make_linked_geometry(*surfs): + """Helper: build a parent cell whose geometry is already linked via update_pointers. + + 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 + half_space = None + for surf in surfs: + leaf = +surf + # wire the leaf to the parent cell so _cell is set + surf_col = montepy.surface_collection.Surfaces(list(parent.surfaces) + [surf]) + leaf.update_pointers(montepy.cells.Cells(), surf_col, parent) + half_space = leaf if half_space is None else half_space & leaf + return parent, half_space + + +def test_replace_surface_basic(): + """replace() swaps old surface for new one throughout the tree.""" + surf1 = montepy.CylinderOnAxis() + surf2 = montepy.CylinderOnAxis() + surf3 = montepy.CylinderOnAxis() + surf1.number = 1 + surf2.number = 2 + surf3.number = 3 + half_space = +surf1 & -surf2 + half_space.replace(surf1, surf3) + # surf3 should now appear in place of surf1 + leaves = list(half_space) + dividers = [leaf.divider for leaf in leaves] + assert surf3 in dividers + assert surf1 not in dividers + assert surf2 in dividers + + +def test_replace_surface_updates_cell_surfaces(): + """After replace(), old surface is removed from cell.surfaces and new one is present.""" + surf1 = montepy.CylinderOnAxis() + surf2 = montepy.CylinderOnAxis() + surf3 = montepy.CylinderOnAxis() + surf1.number = 1 + surf2.number = 2 + surf3.number = 3 + parent, half_space = _make_linked_geometry(surf1, surf2) + half_space.replace(surf1, surf3) + assert surf3 in parent.surfaces + assert surf1 not in parent.surfaces + assert surf2 in parent.surfaces # untouched + + +def test_replace_on_leaf(): + """replace() called directly on a UnitHalfSpace (leaf) works correctly.""" + surf1 = montepy.CylinderOnAxis() + surf2 = montepy.CylinderOnAxis() + surf1.number = 1 + surf2.number = 2 + leaf = +surf1 + leaf.replace(surf1, surf2) + assert leaf.divider is surf2 + + +def test_replace_not_found_raises(): + """replace() raises ValueError when old_divider is absent from the tree.""" + surf1 = montepy.CylinderOnAxis() + surf2 = montepy.CylinderOnAxis() + surf3 = montepy.CylinderOnAxis() + surf1.number = 1 + surf2.number = 2 + surf3.number = 3 + half_space = +surf1 & -surf2 + with pytest.raises(ValueError): + half_space.replace(surf3, surf1) + + +def test_replace_wrong_kind_raises(): + """replace() raises TypeError when mixing Surface and Cell arguments.""" + surf = montepy.CylinderOnAxis() + surf.number = 1 + cell = montepy.Cell() + cell.number = 2 + half_space = +surf + with pytest.raises(TypeError): + half_space.replace(surf, cell) + + +def test_replace_invalid_type_raises(): + """replace() raises TypeError when either argument is not a Surface or Cell.""" + surf = montepy.CylinderOnAxis() + surf.number = 1 + half_space = +surf + with pytest.raises(TypeError): + half_space.replace("not a surface", surf) + with pytest.raises(TypeError): + half_space.replace(surf, 42) + + +def test_replace_subclass_surface(): + """replace() accepts any Surface subclass for either argument (no false type errors).""" + surf1 = montepy.CylinderOnAxis() # subclass of Surface + surf2 = montepy.AxisPlane() # different subclass of Surface + surf1.number = 1 + surf2.number = 2 + half_space = +surf1 & -surf1 + # Should not raise — both are Surfaces regardless of subclass + half_space.replace(surf1, surf2) + for leaf in half_space: + assert leaf.divider is surf2 + + +# ── __iter__ tests ──────────────────────────────────────────────────────────── + + +def test_halfspace_iter_leaves_in_order(): + """HalfSpace.__iter__ yields all UnitHalfSpace leaves depth-first left-to-right.""" + surf1 = montepy.CylinderOnAxis() + surf2 = montepy.CylinderOnAxis() + surf3 = montepy.CylinderOnAxis() + surf1.number = 1 + surf2.number = 2 + surf3.number = 3 + half_space = (+surf1 & -surf2) | +surf3 + leaves = list(half_space) + assert len(leaves) == 3 + assert all(isinstance(leaf, UnitHalfSpace) for leaf in leaves) + assert leaves[0].divider is surf1 + assert leaves[1].divider is surf2 + assert leaves[2].divider is surf3 + + +def test_halfspace_iter_count_matches_len(): + """len(half_space) equals the number of leaves yielded by iteration.""" + surf1 = montepy.CylinderOnAxis() + surf2 = montepy.CylinderOnAxis() + surf3 = montepy.CylinderOnAxis() + surf1.number = 1 + surf2.number = 2 + surf3.number = 3 + half_space = +surf1 & -surf2 & +surf3 + assert len(list(half_space)) == len(half_space) + + +def test_unit_halfspace_iter(): + """UnitHalfSpace.__iter__ yields exactly itself.""" + surf = montepy.CylinderOnAxis() + surf.number = 1 + leaf = +surf + leaves = list(leaf) + assert len(leaves) == 1 + assert leaves[0] is leaf From 7275600e9fe9f9689aa0784501d01d2676014153 Mon Sep 17 00:00:00 2001 From: DIGVIJAY <144053736+digvijay-y@users.noreply.github.com> Date: Sun, 26 Apr 2026 03:52:47 +0000 Subject: [PATCH 05/29] Fix half_space.py for testcase failures --- demo/1_fusion_radial_build.ipynb | 3 ++- montepy/surfaces/half_space.py | 25 ++++++++++++++++--------- tests/test_geometry.py | 10 ++++++---- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/demo/1_fusion_radial_build.ipynb b/demo/1_fusion_radial_build.ipynb index 5cfcee649..68e5f8792 100644 --- a/demo/1_fusion_radial_build.ipynb +++ b/demo/1_fusion_radial_build.ipynb @@ -18,7 +18,8 @@ "## Scenario\n", "* Trying to model a Tokamak fusion power plant\n", "* You have simple, but \"buggy\", radial build model\n", - "* You are trying to perform a parametric sweep" + "* You are trying to perform a parametric sweep\n", + "\n" ] }, { diff --git a/montepy/surfaces/half_space.py b/montepy/surfaces/half_space.py index 0161847a7..9f2963c78 100644 --- a/montepy/surfaces/half_space.py +++ b/montepy/surfaces/half_space.py @@ -293,14 +293,18 @@ def replace( raise ValueError( f"{old_divider} (number: {old_divider.number}) not found in geometry tree." ) - if self._cell is not None: - container = ( - self._cell.complements - if isinstance(old_divider, montepy.Cell) - else self._cell.surfaces - ) - if old_divider in container: - container.remove(old_divider) + # _cell is only set on UnitHalfSpace leaves, not on internal HalfSpace nodes. + # Find the parent cell via any leaf and remove the old divider from its collection. + for leaf in self: + if leaf._cell is not None: + container = ( + leaf._cell.complements + if isinstance(old_divider, montepy.Cell) + else leaf._cell.surfaces + ) + if old_divider in container: + container.remove(old_divider) + break def _replace_recursive(self, old_divider, new_divider) -> bool: replaced = self.left._replace_recursive(old_divider, new_divider) @@ -824,7 +828,10 @@ def num(obj): if isinstance(self.divider, ValueNode) or type(new_obj) == type( self.divider ): - self.divider = new_obj + # Bypass the divider setter to avoid triggering container.append; + # remove_duplicate_surfaces is only remapping references, not + # adding new surfaces — the clone process handles that separately. + self._divider = new_obj def __len__(self): return 1 diff --git a/tests/test_geometry.py b/tests/test_geometry.py index c1238fdbe..8680615a1 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -552,19 +552,21 @@ def test_update_operators_in_node(): def _make_linked_geometry(*surfs): - """Helper: build a parent cell whose geometry is already linked via update_pointers. + """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 the leaf to the parent cell so _cell is set - surf_col = montepy.surface_collection.Surfaces(list(parent.surfaces) + [surf]) - leaf.update_pointers(montepy.cells.Cells(), surf_col, parent) + # 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 From 90e06a075728d2974ce1786b8f1c4c919f877efb Mon Sep 17 00:00:00 2001 From: DIGVIJAY <144053736+digvijay-y@users.noreply.github.com> Date: Sun, 26 Apr 2026 03:59:05 +0000 Subject: [PATCH 06/29] Fix formatting of changelog entry for HalfSpace.replace method --- doc/source/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 421ee3c57..ddffceef4 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -6,7 +6,7 @@ Next Release ============ **Features Added** -* Added :func:`~montepy.surfaces.half_space.HalfSpace.replace` to swap dividers in a cell geometry tree, and ``__iter__`` to traverse geometry leaves (:issue:`737`). +* Added ``replace`` :class:`~montepy.surfaces.half_space.HalfSpace` to swap dividers in a cell geometry tree, and ``__iter__`` to traverse geometry leaves (:issue:`737`). 1.4 releases From 1f78af1b614cfd2bc81d3ed0d56d9ebb9d72c859 Mon Sep 17 00:00:00 2001 From: DIGVIJAY <144053736+digvijay-y@users.noreply.github.com> Date: Sun, 26 Apr 2026 09:30:57 +0530 Subject: [PATCH 07/29] Update montepy/surfaces/half_space.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- montepy/surfaces/half_space.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/montepy/surfaces/half_space.py b/montepy/surfaces/half_space.py index 9f2963c78..bd7eb250a 100644 --- a/montepy/surfaces/half_space.py +++ b/montepy/surfaces/half_space.py @@ -828,10 +828,9 @@ def num(obj): if isinstance(self.divider, ValueNode) or type(new_obj) == type( self.divider ): - # Bypass the divider setter to avoid triggering container.append; - # remove_duplicate_surfaces is only remapping references, not - # adding new surfaces — the clone process handles that separately. - self._divider = new_obj + # Use the divider setter so any parent cell bookkeeping stays + # synchronized with the remapped geometry. + self.divider = new_obj def __len__(self): return 1 From fcd41b451fea1941c674f57d6e09ddd19d7d1e04 Mon Sep 17 00:00:00 2001 From: DIGVIJAY <144053736+digvijay-y@users.noreply.github.com> Date: Sun, 26 Apr 2026 09:35:00 +0530 Subject: [PATCH 08/29] Update doc/source/changelog.rst Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- doc/source/changelog.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index ddffceef4..3452b2631 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -6,9 +6,8 @@ Next Release ============ **Features Added** -* Added ``replace`` :class:`~montepy.surfaces.half_space.HalfSpace` to swap dividers in a cell geometry tree, and ``__iter__`` to traverse geometry leaves (:issue:`737`). - +* Added ``HalfSpace.replace`` to swap dividers in a cell geometry tree, and ``HalfSpace.__iter__`` to traverse geometry leaves (:issue:`737`). 1.4 releases ============ From acfd6dbc246a78f30f16d6a690bcf4bd57d292cd Mon Sep 17 00:00:00 2001 From: DIGVIJAY <144053736+digvijay-y@users.noreply.github.com> Date: Wed, 20 May 2026 17:05:26 +0000 Subject: [PATCH 09/29] changes --- doc/source/changelog.rst | 1 + montepy/surfaces/half_space.py | 24 +++++++++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 3452b2631..160d34fbe 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -8,6 +8,7 @@ Next Release **Features Added** * Added ``HalfSpace.replace`` to swap dividers in a cell geometry tree, and ``HalfSpace.__iter__`` to traverse geometry leaves (:issue:`737`). + 1.4 releases ============ diff --git a/montepy/surfaces/half_space.py b/montepy/surfaces/half_space.py index bd7eb250a..ecc585a42 100644 --- a/montepy/surfaces/half_space.py +++ b/montepy/surfaces/half_space.py @@ -249,16 +249,16 @@ def remove_duplicate_surfaces( def replace( self, - old_divider: montepy.surfaces.surface.Surface | montepy.Cell, - new_divider: montepy.surfaces.surface.Surface | montepy.Cell, + old_divider: montepy.Surface | montepy.Cell, + new_divider: montepy.Surface | montepy.Cell, ) -> None: """Replace all occurrences of a divider in this geometry tree. Parameters ---------- - old_divider : Surface, Cell + old_divider : Surface or Cell the divider to be replaced. - new_divider : Surface, Cell + new_divider : Surface or Cell the divider to replace it with. Raises @@ -267,7 +267,11 @@ def replace( if either argument is not a Surface or Cell, or if they are not the same kind (e.g. one is a Surface and the other is a Cell). ValueError - if old_divider is not found in the geometry tree. + if old_divider is not found in the geometry tree, or if + old_divider and new_divider are the same object. + IllegalState + if the geometry tree has not been linked via update_pointers() + (i.e. leaf dividers are still integers from parsing). """ if not isinstance( old_divider, (montepy.surfaces.surface.Surface, montepy.Cell) @@ -288,6 +292,16 @@ def replace( f"old_divider and new_divider must both be Surfaces or both be Cells. " f"Got {type(old_divider).__name__} and {type(new_divider).__name__}." ) + if new_divider is old_divider: + raise ValueError( + "new_divider and old_divider are the same object; nothing to replace." + ) + for leaf in self: + if isinstance(leaf._divider, Integral): + raise IllegalState( + "Geometry tree has not been linked to objects yet. " + "Run Cell.update_pointers() before calling replace()." + ) replaced = self._replace_recursive(old_divider, new_divider) if not replaced: raise ValueError( From 2736384889223615c868acb9a22b1914f9a38e75 Mon Sep 17 00:00:00 2001 From: DIGVIJAY <144053736+digvijay-y@users.noreply.github.com> Date: Thu, 21 May 2026 05:52:40 +0000 Subject: [PATCH 10/29] update tests --- tests/test_geometry.py | 126 +++++++++++++++++++++++++++++++++-------- 1 file changed, 101 insertions(+), 25 deletions(-) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 8680615a1..5ff5073c6 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -551,34 +551,37 @@ def test_update_operators_in_node(): # ── replace() tests ─────────────────────────────────────────────────────────── -def _make_linked_geometry(*surfs): - """Helper: build a parent cell whose geometry is already linked. +@pytest.fixture +def make_linked_geometry(): + """Fixture factory: 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. + Returns a callable that accepts surfaces/cells and produces + (parent_cell, half_space) where every UnitHalfSpace leaf has + _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 + + def _factory(*surfs): + 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 + + return _factory def test_replace_surface_basic(): """replace() swaps old surface for new one throughout the tree.""" - surf1 = montepy.CylinderOnAxis() - surf2 = montepy.CylinderOnAxis() - surf3 = montepy.CylinderOnAxis() - surf1.number = 1 - surf2.number = 2 - surf3.number = 3 + surf1 = montepy.CylinderOnAxis(number=1) + surf2 = montepy.CylinderOnAxis(number=2) + surf3 = montepy.CylinderOnAxis(number=3) half_space = +surf1 & -surf2 half_space.replace(surf1, surf3) # surf3 should now appear in place of surf1 @@ -589,7 +592,7 @@ def test_replace_surface_basic(): assert surf2 in dividers -def test_replace_surface_updates_cell_surfaces(): +def test_replace_surface_updates_cell_surfaces(make_linked_geometry): """After replace(), old surface is removed from cell.surfaces and new one is present.""" surf1 = montepy.CylinderOnAxis() surf2 = montepy.CylinderOnAxis() @@ -597,7 +600,7 @@ def test_replace_surface_updates_cell_surfaces(): surf1.number = 1 surf2.number = 2 surf3.number = 3 - parent, half_space = _make_linked_geometry(surf1, surf2) + parent, half_space = make_linked_geometry(surf1, surf2) half_space.replace(surf1, surf3) assert surf3 in parent.surfaces assert surf1 not in parent.surfaces @@ -663,6 +666,79 @@ def test_replace_subclass_surface(): assert leaf.divider is surf2 +def test_replace_cell_complement(make_linked_geometry): + """replace() works on Cell complement dividers and updates cell.complements.""" + cell1 = montepy.Cell() + cell2 = montepy.Cell() + cell3 = montepy.Cell() + cell1.number = 1 + cell2.number = 2 + cell3.number = 3 + + parent = montepy.Cell() + parent.number = 99 + parent.complements.append(cell1) + parent.complements.append(cell2) + + # ~cell produces HalfSpace(UnitHalfSpace(cell, True, True), COMPLEMENT, None) + hs1 = ~cell1 + hs2 = ~cell2 + for leaf in hs1: + leaf._cell = parent + for leaf in hs2: + leaf._cell = parent + half_space = hs1 & hs2 + + half_space.replace(cell1, cell3) + + dividers = [leaf.divider for leaf in half_space] + assert cell3 in dividers + assert cell1 not in dividers + assert cell2 in dividers # untouched + assert cell3 in parent.complements + assert cell1 not in parent.complements + + +def test_replace_same_object_raises(): + """replace() raises ValueError when old_divider and new_divider are the same object.""" + surf = montepy.CylinderOnAxis() + surf.number = 1 + half_space = +surf + with pytest.raises(ValueError): + half_space.replace(surf, surf) + + +def test_replace_same_number_different_object(make_linked_geometry): + """replace() with a new surface sharing the same number as old must not corrupt cell.surfaces.""" + surf1 = montepy.CylinderOnAxis() + surf2 = montepy.CylinderOnAxis() + surf3 = montepy.CylinderOnAxis() + surf1.number = 1 + surf2.number = 2 + # surf3 intentionally gets the same number as surf1 + surf3.number = 1 + parent, half_space = make_linked_geometry(surf1, surf2) + # Should not raise NumberConflictError or leave the collection in a broken state + half_space.replace(surf1, surf3) + assert surf3 in parent.surfaces + assert surf1 not in parent.surfaces + assert surf2 in parent.surfaces # untouched + + +def test_replace_unlinked_raises(): + """replace() raises IllegalState when the geometry tree has not been linked.""" + from montepy.exceptions import IllegalState + + surf1 = montepy.CylinderOnAxis() + surf2 = montepy.CylinderOnAxis() + surf1.number = 1 + surf2.number = 2 + # Build a tree with integer dividers (as if freshly parsed, not update_pointers'd) + leaf = UnitHalfSpace(1, True, False) # integer divider + with pytest.raises(IllegalState): + leaf.replace(surf1, surf2) + + # ── __iter__ tests ──────────────────────────────────────────────────────────── @@ -692,7 +768,7 @@ def test_halfspace_iter_count_matches_len(): surf2.number = 2 surf3.number = 3 half_space = +surf1 & -surf2 & +surf3 - assert len(list(half_space)) == len(half_space) + assert len(half_space) == 3 def test_unit_halfspace_iter(): From 77c9c1327df2f7930d0aab450638b9f41f55ba86 Mon Sep 17 00:00:00 2001 From: DIGVIJAY <144053736+digvijay-y@users.noreply.github.com> Date: Sat, 23 May 2026 15:08:54 +0000 Subject: [PATCH 11/29] Update testcases --- montepy/surfaces/half_space.py | 34 ++++++++++++++++++++++------------ tests/test_geometry.py | 10 +++++++--- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/montepy/surfaces/half_space.py b/montepy/surfaces/half_space.py index ecc585a42..a6812a7c0 100644 --- a/montepy/surfaces/half_space.py +++ b/montepy/surfaces/half_space.py @@ -296,29 +296,39 @@ def replace( raise ValueError( "new_divider and old_divider are the same object; nothing to replace." ) + # Validate the tree is fully linked before touching anything. for leaf in self: if isinstance(leaf._divider, Integral): raise IllegalState( "Geometry tree has not been linked to objects yet. " "Run Cell.update_pointers() before calling replace()." ) + # Remove old_divider from the parent cell's container BEFORE calling + # _replace_recursive. The divider setter appends new_divider when it + # runs; if old_divider is still present at that point and shares a number + # with new_divider, the collection can raise NumberConflictError or skip + # the append, leaving bookkeeping in an inconsistent state. + cell = None + for leaf in self: + if leaf._cell is not None: + cell = leaf._cell + break + if cell is not None: + container = ( + cell.complements + if isinstance(old_divider, montepy.Cell) + else cell.surfaces + ) + if old_divider in container: + container.remove(old_divider) replaced = self._replace_recursive(old_divider, new_divider) if not replaced: + # Replacement failed — restore old_divider so the cell stays consistent. + if cell is not None and old_divider not in container: + container.append(old_divider) raise ValueError( f"{old_divider} (number: {old_divider.number}) not found in geometry tree." ) - # _cell is only set on UnitHalfSpace leaves, not on internal HalfSpace nodes. - # Find the parent cell via any leaf and remove the old divider from its collection. - for leaf in self: - if leaf._cell is not None: - container = ( - leaf._cell.complements - if isinstance(old_divider, montepy.Cell) - else leaf._cell.surfaces - ) - if old_divider in container: - container.remove(old_divider) - break def _replace_recursive(self, old_divider, new_divider) -> bool: replaced = self.left._replace_recursive(old_divider, new_divider) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 5ff5073c6..37ec92a21 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -720,9 +720,12 @@ def test_replace_same_number_different_object(make_linked_geometry): parent, half_space = make_linked_geometry(surf1, surf2) # Should not raise NumberConflictError or leave the collection in a broken state half_space.replace(surf1, surf3) - assert surf3 in parent.surfaces - assert surf1 not in parent.surfaces - assert surf2 in parent.surfaces # untouched + # Numbered_object_collection.__contains__ is number-based, so use identity checks + # to distinguish surf1 and surf3 which share the same number. + surfaces = list(parent.surfaces) + assert any(s is surf3 for s in surfaces) + assert not any(s is surf1 for s in surfaces) + assert any(s is surf2 for s in surfaces) # untouched def test_replace_unlinked_raises(): @@ -779,3 +782,4 @@ def test_unit_halfspace_iter(): leaves = list(leaf) assert len(leaves) == 1 assert leaves[0] is leaf + \ No newline at end of file From 51d93db42f64332921093d45ccd0c28c8fa75180 Mon Sep 17 00:00:00 2001 From: DIGVIJAY <144053736+digvijay-y@users.noreply.github.com> Date: Sat, 23 May 2026 15:16:56 +0000 Subject: [PATCH 12/29] Black format --- tests/test_geometry.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 37ec92a21..57875312c 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -782,4 +782,3 @@ def test_unit_halfspace_iter(): leaves = list(leaf) assert len(leaves) == 1 assert leaves[0] is leaf - \ No newline at end of file From 1e2c2ca6530458b0e19796a7d95ee98c192ab27b Mon Sep 17 00:00:00 2001 From: DIGVIJAY <144053736+digvijay-y@users.noreply.github.com> Date: Sat, 23 May 2026 15:22:32 +0000 Subject: [PATCH 13/29] revert: restore demo/answers notebook to pre-black formatting --- demo/answers/1_fusion_radial_build.ipynb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/demo/answers/1_fusion_radial_build.ipynb b/demo/answers/1_fusion_radial_build.ipynb index 38d4dbd5b..f8b7dba0c 100644 --- a/demo/answers/1_fusion_radial_build.ipynb +++ b/demo/answers/1_fusion_radial_build.ipynb @@ -343,7 +343,7 @@ "#\n", "# Iterate over the cells and their numbers, thanks to items()\n", "for num, cell in problem.cells.items():\n", - " # print the cell comments\n", + " #print the cell comments\n", " print(num, cell.comments)" ] }, @@ -486,7 +486,12 @@ }, "outputs": [], "source": [ - "w_natural = {182: 0.265, 183: 0.143, 184: 0.306, 186: 0.284}\n", + "w_natural = {\n", + " 182: 0.265,\n", + " 183: 0.143,\n", + " 184: 0.306,\n", + " 186: 0.284\n", + "}\n", "tungsten.clear()\n", "for mass, abundance in w_natural.items():\n", " tungsten.add_nuclide(f\"W-{mass}.82c\", abundance)\n", @@ -760,10 +765,9 @@ "outputs": [], "source": [ "import numpy as np\n", - "\n", - "MIN_RADIUS = 115.01 # [cm]\n", - "MAX_RADIUS = 198.99 # [cm]\n", - "CAN_THICKNESS = 1 # [cm]\n", + "MIN_RADIUS = 115.01 # [cm]\n", + "MAX_RADIUS = 198.99 # [cm]\n", + "CAN_THICKNESS = 1 #[cm]\n", "\n", "radii = np.arange(MIN_RADIUS, MAX_RADIUS, 5)\n", "for radius in radii:\n", From ffb03ced118e8e88d0714a0d16489b38e423eb01 Mon Sep 17 00:00:00 2001 From: DIGVIJAY <144053736+digvijay-y@users.noreply.github.com> Date: Sat, 23 May 2026 15:29:41 +0000 Subject: [PATCH 14/29] old_divider --- montepy/surfaces/half_space.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/montepy/surfaces/half_space.py b/montepy/surfaces/half_space.py index a6812a7c0..efb02265c 100644 --- a/montepy/surfaces/half_space.py +++ b/montepy/surfaces/half_space.py @@ -324,11 +324,17 @@ def replace( replaced = self._replace_recursive(old_divider, new_divider) if not replaced: # Replacement failed — restore old_divider so the cell stays consistent. - if cell is not None and old_divider not in container: + if cell is not None and not any(s is old_divider for s in container): container.append(old_divider) raise ValueError( f"{old_divider} (number: {old_divider.number}) not found in geometry tree." ) + # The UnitHalfSpace.divider setter may silently skip appending new_divider + # if an object with the same number already appears in the container (e.g. + # when new_divider.number == old_divider.number and old_divider was just + # removed). Explicitly ensure new_divider is present by identity. + if cell is not None and not any(s is new_divider for s in container): + container.append(new_divider) def _replace_recursive(self, old_divider, new_divider) -> bool: replaced = self.left._replace_recursive(old_divider, new_divider) From d681116fe62ee74d95dcec1ca55bd7834a9563d9 Mon Sep 17 00:00:00 2001 From: digvijay-y <144053736+digvijay-y@users.noreply.github.com> Date: Sat, 30 May 2026 16:35:58 +0530 Subject: [PATCH 15/29] pass tests through verify_export and simplify logic --- montepy/surfaces/half_space.py | 19 ++++++------------- tests/test_geometry.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/montepy/surfaces/half_space.py b/montepy/surfaces/half_space.py index efb02265c..47f658f70 100644 --- a/montepy/surfaces/half_space.py +++ b/montepy/surfaces/half_space.py @@ -303,11 +303,9 @@ def replace( "Geometry tree has not been linked to objects yet. " "Run Cell.update_pointers() before calling replace()." ) - # Remove old_divider from the parent cell's container BEFORE calling - # _replace_recursive. The divider setter appends new_divider when it - # runs; if old_divider is still present at that point and shares a number - # with new_divider, the collection can raise NumberConflictError or skip - # the append, leaving bookkeeping in an inconsistent state. + # Remove old_divider from the parent cell's container before calling + # _replace_recursive so replacing with a different object that reuses the + # same number does not trip the collection's conflict checks. cell = None for leaf in self: if leaf._cell is not None: @@ -323,18 +321,13 @@ def replace( container.remove(old_divider) replaced = self._replace_recursive(old_divider, new_divider) if not replaced: - # Replacement failed — restore old_divider so the cell stays consistent. - if cell is not None and not any(s is old_divider for s in container): + # Replacement failed, so restore the original divider to keep the + # parent cell's collection consistent. + if cell is not None: container.append(old_divider) raise ValueError( f"{old_divider} (number: {old_divider.number}) not found in geometry tree." ) - # The UnitHalfSpace.divider setter may silently skip appending new_divider - # if an object with the same number already appears in the container (e.g. - # when new_divider.number == old_divider.number and old_divider was just - # removed). Explicitly ensure new_divider is present by identity. - if cell is not None and not any(s is new_divider for s in container): - container.append(new_divider) def _replace_recursive(self, old_divider, new_divider) -> bool: replaced = self.left._replace_recursive(old_divider, new_divider) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 57875312c..ef22a5f12 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -4,6 +4,7 @@ from montepy.geometry_operators import Operator from montepy.input_parser import syntax_node from montepy.surfaces.half_space import HalfSpace, UnitHalfSpace +from tests.test_cell_problem import verify_export as verify_cell_export def test_halfspace_init(): @@ -728,6 +729,25 @@ def test_replace_same_number_different_object(make_linked_geometry): assert any(s is surf2 for s in surfaces) # untouched +def test_replace_same_number_different_object_verify_export(): + """Replacing with a same-number surface still exports and re-parses cleanly.""" + surf1 = montepy.CylinderOnAxis(number=1) + surf2 = montepy.CylinderOnAxis(number=2) + surf3 = montepy.CylinderOnAxis(number=1) + parent = montepy.Cell(number=99) + parent.geometry = +surf1 & -surf2 + parent.importance.neutron = 1.0 + for leaf in parent.geometry: + leaf._cell = parent + + parent.geometry.replace(surf1, surf3) + + surfaces = list(parent.surfaces) + assert any(s is surf3 for s in surfaces) + assert not any(s is surf1 for s in surfaces) + verify_cell_export(parent) + + def test_replace_unlinked_raises(): """replace() raises IllegalState when the geometry tree has not been linked.""" from montepy.exceptions import IllegalState From 60c1d1c6fe4ebee2aa89337d05a5e029641ef316 Mon Sep 17 00:00:00 2001 From: "Micah D. Gale" Date: Thu, 28 May 2026 09:01:42 -0500 Subject: [PATCH 16/29] Claude: Auto inject description meta from docstrings into page. --- doc/source/conf.py | 38 ++++++++++++++++++++++++++++++++++++++ tests/test_edge_cases.py | 4 +++- tests/test_material.py | 6 ++++-- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 7fd1f9644..bab6212e2 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -199,3 +199,41 @@ # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] + + +def _extract_docstring_summary(obj): + """Extract first sentence of docstring for SEO meta description.""" + if not obj.__doc__: + return None + doc = obj.__doc__.strip() + lines = doc.split("\n") + first_line = lines[0].strip() + if not first_line: + first_line = next((l.strip() for l in lines if l.strip()), None) + if first_line and len(first_line) > 5: + return first_line[:150] + return None + + +def _add_meta_descriptions(app, docname, source): + """Inject meta directives for API docs from Python docstrings.""" + if not docname.startswith("api/generated/"): + return + + obj_name = docname.replace("api/generated/", "") + try: + parts = obj_name.split(".") + obj = montepy + for part in parts: + obj = getattr(obj, part) + + summary = _extract_docstring_summary(obj) + if summary: + meta_directive = f".. meta::\n :description lang=en: {summary}\n\n" + source[0] = meta_directive + source[0] + except (AttributeError, ImportError): + pass + + +def setup(app): + app.connect("source-read", _add_meta_descriptions) diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index 909b296ab..708f35440 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -120,7 +120,9 @@ def test_geometry_comments(): :(-11536 97 -401 ) $ C 3 Lower water :(-11546 97 -401 ) $ C 4 Lower water :(-11556 97 -401 ) $ C 5 Lower water - :(-11576 97 -401 ) imp:n=1 $ C 7 Lower water""".split("\n") + :(-11576 97 -401 ) imp:n=1 $ C 7 Lower water""".split( + "\n" + ) input_obj = montepy.input_parser.mcnp_input.Input( in_strs, montepy.input_parser.block_type.BlockType.CELL ) diff --git a/tests/test_material.py b/tests/test_material.py index b2b04cb26..0283ea587 100644 --- a/tests/test_material.py +++ b/tests/test_material.py @@ -47,7 +47,8 @@ def big_material(_): @pytest.fixture def parsed_material(_): - return Material("""M1 1001.00c 0.05 + return Material( + """M1 1001.00c 0.05 1001.04c 0.05 1001.80c 0.05 1001.04p 0.05 @@ -63,7 +64,8 @@ def parsed_material(_): 95242 0.05 27560.50c 0.05 94239 0.05 - 28000.60c 0.05""") + 28000.60c 0.05""" + ) @pytest.fixture def materials(_, big_material, parsed_material): From 28f5d6b46e5ba7f2685fc2012757043466eec863 Mon Sep 17 00:00:00 2001 From: "Micah D. Gale" Date: Thu, 28 May 2026 09:45:01 -0500 Subject: [PATCH 17/29] Explicitly set logo alt text with Claude suggesting how. --- doc/source/conf.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/source/conf.py b/doc/source/conf.py index bab6212e2..a6569baef 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -130,6 +130,9 @@ html_theme = "pydata_sphinx_theme" html_theme_options = { "navbar_start": ["navbar-logo", "project", "version"], + "logo": { + "alt_text": "MontePy - Documentation Home. The image is the MontePy logo: a red over white ball with a python inside of it.", + }, "icon_links": [ { "name": "GitHub", From 1313abbf43663f280addac1352dca0a7c8b325a8 Mon Sep 17 00:00:00 2001 From: "Micah D. Gale" Date: Thu, 28 May 2026 10:14:32 -0500 Subject: [PATCH 18/29] Simplified alt text inline with W3 guidance. --- doc/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index a6569baef..d940aa72f 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -131,7 +131,7 @@ html_theme_options = { "navbar_start": ["navbar-logo", "project", "version"], "logo": { - "alt_text": "MontePy - Documentation Home. The image is the MontePy logo: a red over white ball with a python inside of it.", + "alt_text": "MontePy documentation home.", }, "icon_links": [ { From 631d879a33e4d25a37b13d81f104364e15a54675 Mon Sep 17 00:00:00 2001 From: Micah Gale Date: Mon, 1 Jun 2026 10:53:59 -0500 Subject: [PATCH 19/29] Claude: create an "extension" to include schema.org data --- doc/source/_extension/schema_org.py | 42 +++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 doc/source/_extension/schema_org.py diff --git a/doc/source/_extension/schema_org.py b/doc/source/_extension/schema_org.py new file mode 100644 index 000000000..8141e0600 --- /dev/null +++ b/doc/source/_extension/schema_org.py @@ -0,0 +1,42 @@ +"""Sphinx extension to inject schema.org JSON-LD structured data. + +Add to conf.py: + extensions = [..., "schema_org"] + + schema_org_configs = { + "index": { + "@type": "SoftwareApplication", + "name": "MontePy", + ... + } + } +""" + +import json +from typing import Any, Dict + +from docutils import nodes +from sphinx.application import Sphinx + + +def add_schema_org_data(app: Sphinx, pagename: str, templatename: str, context: Dict, doctree: Any) -> None: + """Add schema.org JSON-LD to page if configured.""" + configs = app.config.schema_org_configs + if not configs or pagename not in configs: + return + + schema_data = configs[pagename] + schema_json = json.dumps(schema_data, indent=2) + context['schema_org_data'] = schema_json + + +def setup(app: Sphinx) -> Dict[str, Any]: + """Register the extension.""" + app.add_config_value("schema_org_configs", {}, "html") + app.connect("html-page-context", add_schema_org_data) + + return { + "version": "0.1.0", + "parallel_read_safe": True, + "parallel_write_safe": True, + } From 3e54c2bfeba0301752011a60834df692172da32d Mon Sep 17 00:00:00 2001 From: Micah Gale Date: Mon, 1 Jun 2026 10:54:53 -0500 Subject: [PATCH 20/29] Claude: made template for schema.org --- doc/source/_templates/page.html | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 doc/source/_templates/page.html diff --git a/doc/source/_templates/page.html b/doc/source/_templates/page.html new file mode 100644 index 000000000..f5c199882 --- /dev/null +++ b/doc/source/_templates/page.html @@ -0,0 +1,10 @@ +{% extends "!page.html" %} + +{% block body %} + {{ super() }} + {% if pagename == 'index' and schema_org_data %} + + {% endif %} +{% endblock %} From 0f55b632cb9bc5e906332527542a18259923930b Mon Sep 17 00:00:00 2001 From: Micah Gale Date: Mon, 1 Jun 2026 10:55:44 -0500 Subject: [PATCH 21/29] Claude: hackily include the schema.org extension. --- doc/source/conf.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/doc/source/conf.py b/doc/source/conf.py index d940aa72f..075863929 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,3 +1,61 @@ +"""Sphinx extension to inject schema.org JSON-LD structured data. + +Add to conf.py: + extensions = [..., "schema_org"] + + schema_org_configs = { + "index": { + "@type": "SoftwareApplication", + "name": "MontePy", + ... + } + } +""" + +import json +from typing import Any, Dict + +from docutils import nodes +from sphinx.application import Sphinx + + +def add_schema_org_data(app: Sphinx, pagename: str, templatename: str, context: Dict, doctree: Any) -> None: + """Add schema.org JSON-LD to page if configured.""" + configs = app.config.schema_org_configs + if not configs or pagename not in configs: + return + + schema_data = configs[pagename] + schema_json = json.dumps(schema_data, indent=2) + context['schema_org_data'] = schema_json + + +def setup(app: Sphinx) -> Dict[str, Any]: + """Register the extension.""" + app.add_config_value("schema_org_configs", {}, "html") + app.connect("html-page-context", add_schema_org_data) + + return { + "version": "0.1.0", + "parallel_read_safe": True, + "parallel_write_safe": True, + } +(base) +(mgale@INL436227)-(~/dev/montepy) (seo_opt) + ^_^->cat doc/source/_templates/page.html +{% extends "!page.html" %} + +{% block body %} + {{ super() }} + {% if pagename == 'index' and schema_org_data %} + + {% endif %} +{% endblock %} +(base) +(mgale@INL436227)-(~/dev/montepy) (seo_opt) + ^_^->cat doc/source/conf.py # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full From 167878f553d78cb37a3ed85cecfada1e8a09b789 Mon Sep 17 00:00:00 2001 From: Micah Gale Date: Mon, 1 Jun 2026 21:55:44 -0500 Subject: [PATCH 22/29] Claude: fixed schema.org extension. --- doc/source/_extension/schema_org.py | 1 - doc/source/_templates/page.html | 4 +- doc/source/conf.py | 114 ++++++++++++++-------------- 3 files changed, 58 insertions(+), 61 deletions(-) diff --git a/doc/source/_extension/schema_org.py b/doc/source/_extension/schema_org.py index 8141e0600..66711aa1a 100644 --- a/doc/source/_extension/schema_org.py +++ b/doc/source/_extension/schema_org.py @@ -15,7 +15,6 @@ import json from typing import Any, Dict -from docutils import nodes from sphinx.application import Sphinx diff --git a/doc/source/_templates/page.html b/doc/source/_templates/page.html index f5c199882..4d4ae32e6 100644 --- a/doc/source/_templates/page.html +++ b/doc/source/_templates/page.html @@ -1,8 +1,8 @@ {% extends "!page.html" %} -{% block body %} +{% block extrahead %} {{ super() }} - {% if pagename == 'index' and schema_org_data %} + {% if schema_org_data %} diff --git a/doc/source/conf.py b/doc/source/conf.py index 075863929..4ab855c8f 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,61 +1,3 @@ -"""Sphinx extension to inject schema.org JSON-LD structured data. - -Add to conf.py: - extensions = [..., "schema_org"] - - schema_org_configs = { - "index": { - "@type": "SoftwareApplication", - "name": "MontePy", - ... - } - } -""" - -import json -from typing import Any, Dict - -from docutils import nodes -from sphinx.application import Sphinx - - -def add_schema_org_data(app: Sphinx, pagename: str, templatename: str, context: Dict, doctree: Any) -> None: - """Add schema.org JSON-LD to page if configured.""" - configs = app.config.schema_org_configs - if not configs or pagename not in configs: - return - - schema_data = configs[pagename] - schema_json = json.dumps(schema_data, indent=2) - context['schema_org_data'] = schema_json - - -def setup(app: Sphinx) -> Dict[str, Any]: - """Register the extension.""" - app.add_config_value("schema_org_configs", {}, "html") - app.connect("html-page-context", add_schema_org_data) - - return { - "version": "0.1.0", - "parallel_read_safe": True, - "parallel_write_safe": True, - } -(base) -(mgale@INL436227)-(~/dev/montepy) (seo_opt) - ^_^->cat doc/source/_templates/page.html -{% extends "!page.html" %} - -{% block body %} - {{ super() }} - {% if pagename == 'index' and schema_org_data %} - - {% endif %} -{% endblock %} -(base) -(mgale@INL436227)-(~/dev/montepy) (seo_opt) - ^_^->cat doc/source/conf.py # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full @@ -73,6 +15,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: import sys sys.path.insert(0, os.path.abspath("../..")) +sys.path.insert(0, os.path.abspath("_extension")) import montepy # -- Project information ----------------------------------------------------- @@ -100,8 +43,63 @@ def setup(app: Sphinx) -> Dict[str, Any]: "sphinx_copybutton", "autodocsumm", "jupyterlite_sphinx", + "schema_org", ] +# -- Schema.org structured data ---------------------------------------------- +# Drives JSON-LD injection via the schema_org extension. +# Only pages listed here get a