From 4483527918ff254ecc316b9c6d7616cc98bad864 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 20 Sep 2024 16:39:51 -0700 Subject: [PATCH 01/13] Provide ParameterCollection.where for better iteration of parameters Allows selective iteration of parameters that meet, by their definition not their value, meet a certain condition. This would be useful to iterate over all parameters on a ``Block`` that are relevant for neutronics calculations with ```python for p in block.p.where(lambda pd: "neutronics" in pd.categories): ... ``` The argument is a function that should return true for a given parameter and can be complicated ```python block.p.where( lambda pd: ( pd.atLocation(ParamLocation.EDGES) or pd.atLocation(ParamLocation.CORNERS) ) ) ``` Closes #1898 --- .../parameters/parameterCollections.py | 28 ++++++- armi/reactor/tests/test_parameters.py | 73 +++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/armi/reactor/parameters/parameterCollections.py b/armi/reactor/parameters/parameterCollections.py index 02d6dbe95..e47e68a77 100644 --- a/armi/reactor/parameters/parameterCollections.py +++ b/armi/reactor/parameters/parameterCollections.py @@ -14,7 +14,7 @@ import copy import pickle -from typing import Any, Optional, List, Set +from typing import Any, Optional, List, Set, Iterator, Callable import sys import numpy as np @@ -493,6 +493,32 @@ def restoreBackup(self, paramsToApply): pd.assigned = SINCE_ANYTHING self.assigned = SINCE_ANYTHING + def where( + self, f: Callable[[parameterDefinitions.Parameter], bool] + ) -> Iterator[parameterDefinitions.Parameter]: + """Produce an iterator over parameters that meet some criteria + + Parameters + ---------- + f : callable function f(parameter) -> bool + Function to check if a parameter should be fetched during the iteration. + + Returns + ------- + iterator of :class:`armi.reactor.parameters.Parameter` + Iterator, **not** list or tuple, that produces each parameter that + meets ``f(parameter) == True``. + + Examples + -------- + >>> block = r.core[0][0] + >>> pdef = block.p.paramDefs + >>> for param in pdef.where(lambda pd: pd.atLocation(ParamLocation.EDGES)): + ... print(param.name, block.p[param.name]) + + """ + return filter(f, self.paramDefs) + def collectPluginParameters(pm): """Apply parameters from plugins to their respective object classes.""" diff --git a/armi/reactor/tests/test_parameters.py b/armi/reactor/tests/test_parameters.py index e2068e8ae..59422a43a 100644 --- a/armi/reactor/tests/test_parameters.py +++ b/armi/reactor/tests/test_parameters.py @@ -13,6 +13,7 @@ # limitations under the License. """Tests of the Parameters class.""" import copy +import typing import unittest from armi.reactor import parameters @@ -502,3 +503,75 @@ class MockPCChild(MockPC): pcc = MockPCChild() with self.assertRaises(AssertionError): pcc.whatever = 33 + + +class ParamCollectionWhere(unittest.TestCase): + class ScopeParamCollection(parameters.ParameterCollection): + pDefs = parameters.ParameterDefinitionCollection() + with pDefs.createBuilder() as pb: + pb.defParam( + name="empty", + description="Bare", + location=None, + categories=None, + units="", + ) + pb.defParam( + name="keff", + description="keff", + location=parameters.ParamLocation.VOLUME_INTEGRATED, + categories=[parameters.Category.neutronics], + units="", + ) + pb.defParam( + name="cornerFlux", + description="corner flux", + location=parameters.ParamLocation.CORNERS, + categories=[ + parameters.Category.neutronics, + ], + units="", + ) + pb.defParam( + name="edgeTemperature", + description="edge temperature", + location=parameters.ParamLocation.EDGES, + categories=[parameters.Category.thermalHydraulics], + units="", + ) + + pc: typing.ClassVar[parameters.ParameterCollection] + + @classmethod + def setUpClass(cls) -> None: + """Define a couple useful parameters with categories, locations, etc.""" + cls.pc = cls.ScopeParamCollection() + + def test_onCategory(self): + names = {"keff", "cornerFlux"} + for p in self.pc.where( + lambda pd: parameters.Category.neutronics in pd.categories + ): + names.remove(p.name) + self.assertFalse(names, msg=f"{names=} should be empty!") + + def test_onLocation(self): + names = { + "edgeTemperature", + } + for p in self.pc.where( + lambda pd: pd.atLocation(parameters.ParamLocation.EDGES) + ): + names.remove(p.name) + self.assertFalse(names, msg=f"{names=} should be empty!") + + def test_complicated(self): + names = { + "cornerFlux", + } + for p in self.pc.where( + lambda pd: pd.atLocation(parameters.ParamLocation.CORNERS) + and parameters.Category.neutronics in pd.categories + ): + names.remove(p.name) + self.assertFalse(names, msg=f"{names=} should be empty") From 0c5f542272dc7201867a3b1c760266de5d51bb28 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 20 Sep 2024 16:47:38 -0700 Subject: [PATCH 02/13] Type hint ParameterCollection.__eq__ --- armi/reactor/parameters/parameterCollections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armi/reactor/parameters/parameterCollections.py b/armi/reactor/parameters/parameterCollections.py index e47e68a77..42b84c0e2 100644 --- a/armi/reactor/parameters/parameterCollections.py +++ b/armi/reactor/parameters/parameterCollections.py @@ -359,7 +359,7 @@ def __contains__(self, name): else: return name in self._hist - def __eq__(self, other): + def __eq__(self, other: "ParameterCollection"): if not isinstance(other, self.__class__): return False From 6fc7204bf239dd26fa2204c527b566eba2bd4d43 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 20 Sep 2024 16:47:49 -0700 Subject: [PATCH 03/13] Type hint and doc ParameterCollection.__iter__ --- armi/reactor/parameters/parameterCollections.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/armi/reactor/parameters/parameterCollections.py b/armi/reactor/parameters/parameterCollections.py index 42b84c0e2..f0db55369 100644 --- a/armi/reactor/parameters/parameterCollections.py +++ b/armi/reactor/parameters/parameterCollections.py @@ -374,7 +374,8 @@ def __eq__(self, other: "ParameterCollection"): return True - def __iter__(self): + def __iter__(self) -> Iterator[str]: + """Iterate over names of assigned parameters define on this collection.""" return ( pd.name for pd in self.paramDefs From 3bb13f7e97e115d7c6403ef57e6fbc5840e711fe Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 13 Sep 2024 09:24:24 -0700 Subject: [PATCH 04/13] Provide Parameter.hasCategory (cherry picked from commit 4162cd7331c3f346f89a521340377615664c6235) --- armi/reactor/parameters/parameterDefinitions.py | 4 ++++ armi/reactor/tests/test_parameters.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/armi/reactor/parameters/parameterDefinitions.py b/armi/reactor/parameters/parameterDefinitions.py index 472c7367a..7ad5ccec0 100644 --- a/armi/reactor/parameters/parameterDefinitions.py +++ b/armi/reactor/parameters/parameterDefinitions.py @@ -421,6 +421,10 @@ def atLocation(self, loc): """True if parameter is defined at location.""" return self.location and self.location & loc + def hasCategory(self, category: str) -> bool: + """True if a parameter has a specific category.""" + return category in self.categories + class ParameterDefinitionCollection: """ diff --git a/armi/reactor/tests/test_parameters.py b/armi/reactor/tests/test_parameters.py index 59422a43a..aa7c256d0 100644 --- a/armi/reactor/tests/test_parameters.py +++ b/armi/reactor/tests/test_parameters.py @@ -457,10 +457,18 @@ class MockPC(parameters.ParameterCollection): self.assertEqual(p2.categories, set(["awesome", "stuff", "bacon"])) self.assertEqual(p3.categories, set(["bacon"])) + for p in [p1, p2, p3]: + self._testCategoryConsistency(p) + self.assertEqual(set(pc.paramDefs.inCategory("awesome")), set([p1, p2])) self.assertEqual(set(pc.paramDefs.inCategory("stuff")), set([p1, p2])) self.assertEqual(set(pc.paramDefs.inCategory("bacon")), set([p2, p3])) + def _testCategoryConsistency(self, p: parameters.Parameter): + for category in p.categories: + self.assertTrue(p.hasCategory(category)) + self.assertFalse(p.hasCategory("this_shouldnot_exist")) + def test_parameterCollectionsHave__slots__(self): """Tests we prevent accidental creation of attributes.""" self.assertEqual( From 83e126f8da3b8fab6510309d0a779a58a4ca84ec Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 20 Sep 2024 16:50:10 -0700 Subject: [PATCH 05/13] Use Parameter.hasCategory in ParameterCollection.where tests --- armi/reactor/tests/test_parameters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/armi/reactor/tests/test_parameters.py b/armi/reactor/tests/test_parameters.py index aa7c256d0..dbefc9e44 100644 --- a/armi/reactor/tests/test_parameters.py +++ b/armi/reactor/tests/test_parameters.py @@ -558,7 +558,7 @@ def setUpClass(cls) -> None: def test_onCategory(self): names = {"keff", "cornerFlux"} for p in self.pc.where( - lambda pd: parameters.Category.neutronics in pd.categories + lambda pd: pd.hasCategory(parameters.Category.neutronics) ): names.remove(p.name) self.assertFalse(names, msg=f"{names=} should be empty!") @@ -579,7 +579,7 @@ def test_complicated(self): } for p in self.pc.where( lambda pd: pd.atLocation(parameters.ParamLocation.CORNERS) - and parameters.Category.neutronics in pd.categories + and pd.hasCategory(parameters.Category.neutronics) ): names.remove(p.name) self.assertFalse(names, msg=f"{names=} should be empty") From 18e52193072c9be00ff8f488cf36172655d8ce59 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 20 Sep 2024 16:53:44 -0700 Subject: [PATCH 06/13] Test Parameters from ParameterCollection.where pass the filter function --- armi/reactor/tests/test_parameters.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/armi/reactor/tests/test_parameters.py b/armi/reactor/tests/test_parameters.py index dbefc9e44..1e1953fe6 100644 --- a/armi/reactor/tests/test_parameters.py +++ b/armi/reactor/tests/test_parameters.py @@ -560,6 +560,7 @@ def test_onCategory(self): for p in self.pc.where( lambda pd: pd.hasCategory(parameters.Category.neutronics) ): + self.assertTrue(p.hasCategory(parameters.Category.neutronics), msg=p) names.remove(p.name) self.assertFalse(names, msg=f"{names=} should be empty!") @@ -570,6 +571,7 @@ def test_onLocation(self): for p in self.pc.where( lambda pd: pd.atLocation(parameters.ParamLocation.EDGES) ): + self.assertTrue(p.atLocation(parameters.ParamLocation.EDGES), msg=p) names.remove(p.name) self.assertFalse(names, msg=f"{names=} should be empty!") @@ -577,9 +579,13 @@ def test_complicated(self): names = { "cornerFlux", } - for p in self.pc.where( - lambda pd: pd.atLocation(parameters.ParamLocation.CORNERS) - and pd.hasCategory(parameters.Category.neutronics) - ): + + def check(p: parameters.Parameter) -> bool: + return p.atLocation(parameters.ParamLocation.CORNERS) and p.hasCategory( + parameters.Category.neutronics + ) + + for p in self.pc.where(check): + self.assertTrue(check(p), msg=p) names.remove(p.name) self.assertFalse(names, msg=f"{names=} should be empty") From e335f07375eecf33f49a7de1d41cfc8a7b4a4826 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 20 Sep 2024 16:54:58 -0700 Subject: [PATCH 07/13] Add docs for ParameterCollectionWhere tests --- armi/reactor/tests/test_parameters.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/armi/reactor/tests/test_parameters.py b/armi/reactor/tests/test_parameters.py index 1e1953fe6..5116cde41 100644 --- a/armi/reactor/tests/test_parameters.py +++ b/armi/reactor/tests/test_parameters.py @@ -514,6 +514,8 @@ class MockPCChild(MockPC): class ParamCollectionWhere(unittest.TestCase): + """Tests for ParameterCollection.where.""" + class ScopeParamCollection(parameters.ParameterCollection): pDefs = parameters.ParameterDefinitionCollection() with pDefs.createBuilder() as pb: @@ -556,6 +558,7 @@ def setUpClass(cls) -> None: cls.pc = cls.ScopeParamCollection() def test_onCategory(self): + """Test the use of Parameter.hasCategory on filtering.""" names = {"keff", "cornerFlux"} for p in self.pc.where( lambda pd: pd.hasCategory(parameters.Category.neutronics) @@ -565,6 +568,7 @@ def test_onCategory(self): self.assertFalse(names, msg=f"{names=} should be empty!") def test_onLocation(self): + """Test the use of Parameter.atLocation in filtering.""" names = { "edgeTemperature", } @@ -576,6 +580,7 @@ def test_onLocation(self): self.assertFalse(names, msg=f"{names=} should be empty!") def test_complicated(self): + """Test a multi-condition filter.""" names = { "cornerFlux", } From 6efe6e1e484a40ea7a1f3f79d6b35a2c68013896 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 20 Sep 2024 17:00:18 -0700 Subject: [PATCH 08/13] Add release notes for PR 1899 --- doc/release/0.4.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/release/0.4.rst b/doc/release/0.4.rst index 162032549..5d68258ba 100644 --- a/doc/release/0.4.rst +++ b/doc/release/0.4.rst @@ -13,6 +13,10 @@ New Features #. Adding ``--skip-inspection`` flag to ``CompareCases`` CLI. (`PR#1842 `_) #. Provide utilities for determining location of a rotated object in a hexagonal lattice (``getIndexOfRotatedCell``). (`PR#1846 `_) +#. Provide ``Parameter.hasCategory`` for quickly checking if a parameter is defined with a given category. + (`PR#1899 `_) +#. Provide ``ParameterCollection.where`` for efficient iteration over parameters who's definition + matches a given condition. (`PR#1899 `_) #. TBD API Changes From 9493ef61c866dd5ffb14119b28bffd59f010da8a Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 20 Sep 2024 17:01:02 -0700 Subject: [PATCH 09/13] Fix ParameterCollection.where docstring --- armi/reactor/parameters/parameterCollections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armi/reactor/parameters/parameterCollections.py b/armi/reactor/parameters/parameterCollections.py index f0db55369..10eee345b 100644 --- a/armi/reactor/parameters/parameterCollections.py +++ b/armi/reactor/parameters/parameterCollections.py @@ -497,7 +497,7 @@ def restoreBackup(self, paramsToApply): def where( self, f: Callable[[parameterDefinitions.Parameter], bool] ) -> Iterator[parameterDefinitions.Parameter]: - """Produce an iterator over parameters that meet some criteria + """Produce an iterator over parameters that meet some criteria. Parameters ---------- From 81b55eed386d3f54aaf0ffe2f0a479716e420fcf Mon Sep 17 00:00:00 2001 From: Drew Johnson Date: Mon, 23 Sep 2024 10:50:04 -0700 Subject: [PATCH 10/13] Update armi/reactor/tests/test_parameters.py Co-authored-by: John Stilley <1831479+john-science@users.noreply.github.com> --- armi/reactor/tests/test_parameters.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/armi/reactor/tests/test_parameters.py b/armi/reactor/tests/test_parameters.py index 5116cde41..b603208b7 100644 --- a/armi/reactor/tests/test_parameters.py +++ b/armi/reactor/tests/test_parameters.py @@ -550,8 +550,6 @@ class ScopeParamCollection(parameters.ParameterCollection): units="", ) - pc: typing.ClassVar[parameters.ParameterCollection] - @classmethod def setUpClass(cls) -> None: """Define a couple useful parameters with categories, locations, etc.""" From f822d376b6d4693c1903ba7253a680bd12cb1db1 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Mon, 23 Sep 2024 11:01:15 -0700 Subject: [PATCH 11/13] Remove unused typing import in test_parameters.py --- armi/reactor/tests/test_parameters.py | 1 - 1 file changed, 1 deletion(-) diff --git a/armi/reactor/tests/test_parameters.py b/armi/reactor/tests/test_parameters.py index b603208b7..2cdb26fa7 100644 --- a/armi/reactor/tests/test_parameters.py +++ b/armi/reactor/tests/test_parameters.py @@ -13,7 +13,6 @@ # limitations under the License. """Tests of the Parameters class.""" import copy -import typing import unittest from armi.reactor import parameters From 1f090d170700e5d3e06542ae02eef173110b9446 Mon Sep 17 00:00:00 2001 From: Drew Johnson Date: Wed, 25 Sep 2024 15:47:51 -0700 Subject: [PATCH 12/13] Update doc/release/0.4.rst Co-authored-by: John Stilley <1831479+john-science@users.noreply.github.com> --- doc/release/0.4.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release/0.4.rst b/doc/release/0.4.rst index 20d464284..45f7dba34 100644 --- a/doc/release/0.4.rst +++ b/doc/release/0.4.rst @@ -16,7 +16,7 @@ New Features #. Allow merging a component with zero area into another component. (`PR#1858 `_) #. Provide ``Parameter.hasCategory`` for quickly checking if a parameter is defined with a given category. (`PR#1899 `_) -#. Provide ``ParameterCollection.where`` for efficient iteration over parameters who's definition +#. Provide ``ParameterCollection.where`` for efficient iteration over parameters who's definition. matches a given condition. (`PR#1899 `_) #. Plugins can provide the ``getAxialExpansionChanger`` hook to customize axial expansion. (`PR#1870 Date: Wed, 25 Sep 2024 15:53:21 -0700 Subject: [PATCH 13/13] Apply suggestions from code review --- armi/reactor/tests/test_parameters.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/armi/reactor/tests/test_parameters.py b/armi/reactor/tests/test_parameters.py index 2cdb26fa7..38f47141b 100644 --- a/armi/reactor/tests/test_parameters.py +++ b/armi/reactor/tests/test_parameters.py @@ -566,9 +566,7 @@ def test_onCategory(self): def test_onLocation(self): """Test the use of Parameter.atLocation in filtering.""" - names = { - "edgeTemperature", - } + names = {"edgeTemperature"} for p in self.pc.where( lambda pd: pd.atLocation(parameters.ParamLocation.EDGES) ): @@ -578,9 +576,7 @@ def test_onLocation(self): def test_complicated(self): """Test a multi-condition filter.""" - names = { - "cornerFlux", - } + names = {"cornerFlux"} def check(p: parameters.Parameter) -> bool: return p.atLocation(parameters.ParamLocation.CORNERS) and p.hasCategory(