From fb56d9c6b2c0f5be066c7c43b7a88341d34aae60 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 17 Jan 2024 23:33:04 -0600 Subject: [PATCH 1/5] Add a category property to DAGSet --- dagmc/dagnav.py | 50 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/dagmc/dagnav.py b/dagmc/dagnav.py index 385451e..0a00848 100644 --- a/dagmc/dagnav.py +++ b/dagmc/dagnav.py @@ -5,7 +5,7 @@ from typing import Optional, Dict import numpy as np -from pymoab import core, types, rng +from pymoab import core, types, rng, tag class DAGModel: @@ -86,23 +86,40 @@ def __eq__(self, other): def __repr__(self): return f'{type(self).__name__} {self.id}, {self.num_triangles()} triangles' + def _tag_get_data(self, tag: tag.Tag): + return self.model.mb.tag_get_data(tag, self.handle, flat=True)[0] + + def _tag_set_data(self, tag: tag.Tag, value): + self.model.mb.tag_set_data(tag, self.handle, value) + @property - def id(self): - """Return the DAGMC set's ID. - """ - return self.model.mb.tag_get_data(self.model.id_tag, self.handle, flat=True)[0] + def id(self) -> int: + """Return the DAGMC set's ID.""" + return self._tag_get_data(self.model.id_tag) @id.setter - def id(self, i): - """Set the DAGMC set's ID. - """ - self.model.mb.tag_set_data(self.model.id_tag, self.handle, i) + def id(self, i: int): + """Set the DAGMC set's ID.""" + self._tag_set_data(self.model.id_tag, i) @property - def geom_dimension(self): - """Return the DAGMC set's geometry dimension. - """ - return self.model.mb.tag_get_data(self.model.geom_dimension_tag, self.handle, flat=True)[0] + def geom_dimension(self) -> int: + """Return the DAGMC set's geometry dimension.""" + return self._tag_get_data(self.model.geom_dimension_tag) + + @geom_dimension.setter + def geom_dimension(self, dimension: int): + self._tag_set_data(self.model.geom_dimension_tag, dimension) + + @property + def category(self) -> str: + """Return the DAGMC set's category.""" + return self._tag_get_data(self.model.category_tag) + + @category.setter + def category(self, category: str): + """Set the DAGMC set's category.""" + self._tag_set_data(self.model.category_tag, category) @abstractmethod def _get_triangle_sets(self): @@ -377,10 +394,9 @@ def create(cls, model, name=None, group_id=None) -> Group: """Create a new group instance with the given name""" mb = model.mb # add necessary tags for this meshset to be identified as a group - group_handle = mb.create_meshset() - mb.tag_set_data(model.category_tag, group_handle, 'Group') - mb.tag_set_data(model.geom_dimension_tag, group_handle, 4) - group = cls(model, group_handle) + group = cls(model, mb.create_meshset()) + group.category = 'Group' + group.geom_dimension = 4 if name is not None: group.name = name if group_id is not None: From ca48503b06015a54041f3707c538d96517b5e4e6 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sun, 21 Jan 2024 16:05:43 -0600 Subject: [PATCH 2/5] Add checks on geom_dimension and category for volumes, surfaces, and groups --- dagmc/dagnav.py | 48 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/dagmc/dagnav.py b/dagmc/dagnav.py index 0a00848..5b88d3d 100644 --- a/dagmc/dagnav.py +++ b/dagmc/dagnav.py @@ -3,6 +3,7 @@ from functools import cached_property from itertools import chain from typing import Optional, Dict +from warnings import warn import numpy as np from pymoab import core, types, rng, tag @@ -76,7 +77,7 @@ class DAGSet: """ Generic functionality for a DAGMC EntitySet. """ - def __init__(self, model: DAGModel, handle): + def __init__(self, model: DAGModel, handle: np.uint64): self.model = model self.handle = handle @@ -227,6 +228,16 @@ def get_triangle_coordinate_mapping(self, compress=False): class Surface(DAGSet): + _category = 'Surface' + _geom_dimension = 2 + + def __init__(self, model: DAGModel, handle: np.uint64): + super().__init__(model, handle) + if self.geom_dimension != self._geom_dimension: + warn(f"DAGMC surface with global ID {self.id} does not have geom_dimension=2.") + if self.category != self._category: + warn(f"DAGMC surface with global ID {self.id} does not have category='Surface'.") + def get_volumes(self): """Get the parent volumes of this surface. """ @@ -242,6 +253,16 @@ def _get_triangle_sets(self): class Volume(DAGSet): + _category: str = 'Volume' + _geom_dimension: int = 3 + + def __init__(self, model: DAGModel, handle: np.uint64): + super().__init__(model, handle) + if self.geom_dimension != self._geom_dimension: + warn(f"DAGMC volume with global ID {self.id} does not have geom_dimension=3.") + if self.category != self._category: + warn(f"DAGMC volume with global ID {self.id} does not have category='Volume'.") + @property def groups(self) -> list[Group]: """Get list of groups containing this volume.""" @@ -289,8 +310,17 @@ def num_triangles(self): def _get_triangle_sets(self): return [s.handle for s in self.get_surfaces().values()] + class Group(DAGSet): + _category: str = 'Group' + _geom_dimension: int = 4 + + def __init__(self, model: DAGModel, handle: np.uint64): + super().__init__(model, handle) + if self.geom_dimension != self._geom_dimension: + warn(f'DAGMC group with global ID {self.id} does not have geom_dimension=4.') + def __contains__(self, ent_set: DAGSet): return any(vol.handle == ent_set.handle for vol in chain( self.get_volumes().values(), self.get_surfaces().values())) @@ -390,15 +420,17 @@ def merge(self, other_group): other_group.handle = self.handle @classmethod - def create(cls, model, name=None, group_id=None) -> Group: + def create(cls, model: DAGModel, name: Optional[str] = None, group_id: Optional[int] = None) -> Group: """Create a new group instance with the given name""" - mb = model.mb # add necessary tags for this meshset to be identified as a group - group = cls(model, mb.create_meshset()) - group.category = 'Group' - group.geom_dimension = 4 + ent_set = DAGSet(model, model.mb.create_meshset()) + ent_set.category = cls._category + ent_set.geom_dimension = cls._geom_dimension + if group_id is not None: + ent_set.id = group_id + + # Now that entity set has proper tags, create Group, assign name, and return + group = cls(model, ent_set.handle) if name is not None: group.name = name - if group_id is not None: - group.id = group_id return group From 75e8ea23f194f497b1b24a2e778589ae3e3b9a50 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 22 Jan 2024 23:10:43 -0600 Subject: [PATCH 3/5] Update check for tags on groups --- dagmc/dagnav.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dagmc/dagnav.py b/dagmc/dagnav.py index 5b88d3d..bf7de70 100644 --- a/dagmc/dagnav.py +++ b/dagmc/dagnav.py @@ -318,8 +318,8 @@ class Group(DAGSet): def __init__(self, model: DAGModel, handle: np.uint64): super().__init__(model, handle) - if self.geom_dimension != self._geom_dimension: - warn(f'DAGMC group with global ID {self.id} does not have geom_dimension=4.') + if self.category != self._category: + warn(f"DAGMC group with global ID {self.id} does not have category='Group'.") def __contains__(self, ent_set: DAGSet): return any(vol.handle == ent_set.handle for vol in chain( From b9148735333acf7ea4aab99708fcdd342b36fe78 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 1 Feb 2024 00:10:39 -0600 Subject: [PATCH 4/5] Implement consistency check for category and geom_dimension tags --- dagmc/dagnav.py | 83 ++++++++++++++++++++++++++++++++++++---------- test/test_basic.py | 30 +++++++++++++++++ 2 files changed, 96 insertions(+), 17 deletions(-) diff --git a/dagmc/dagnav.py b/dagmc/dagnav.py index bf7de70..6a348f0 100644 --- a/dagmc/dagnav.py +++ b/dagmc/dagnav.py @@ -62,15 +62,33 @@ def category_tag(self): """Returns the category tag used to intidate the use of meshset. Values include "Group", "Volume", "Surface". "Curve" and "Vertex" are also present in the model options but those classes are not supported in this package. """ - return self.mb.tag_get_handle(types.CATEGORY_TAG_NAME) + return self.mb.tag_get_handle( + types.CATEGORY_TAG_NAME, + types.CATEGORY_TAG_SIZE, + types.MB_TYPE_OPAQUE, + types.MB_TAG_SPARSE, + create_if_missing=True + ) @cached_property def name_tag(self): - return self.mb.tag_get_handle(types.NAME_TAG_NAME) + return self.mb.tag_get_handle( + types.NAME_TAG_NAME, + types.NAME_TAG_SIZE, + types.MB_TYPE_OPAQUE, + types.MB_TAG_SPARSE, + create_if_missing=True, + ) @cached_property def geom_dimension_tag(self): - return self.mb.tag_get_handle(types.GEOM_DIMENSION_TAG_NAME) + return self.mb.tag_get_handle( + types.GEOM_DIMENSION_TAG_NAME, + 1, + types.MB_TYPE_INTEGER, + types.MB_TAG_SPARSE, + create_if_missing=True, + ) class DAGSet: @@ -81,6 +99,38 @@ def __init__(self, model: DAGModel, handle: np.uint64): self.model = model self.handle = handle + def _check_category_and_dimension(self): + """Check for consistency of category and geom_dimension tags""" + stype = self._category.lower() + geom_dimension = self.geom_dimension + category = self.category + + if geom_dimension != -1: + # If geom_dimension is assigned but not consistent, raise exception + if geom_dimension != self._geom_dimension: + raise ValueError(f"DAGMC {stype} ID={self.id} has geom_dimension={geom_dimension}.") + + # If category is unassigned, assign it based on geom_dimension + if category is None: + if stype == 'group': + warn(f"Assigned category {self._category} to {stype} '{self.name}'.") + else: + warn(f"Assigned category {self._category} to {stype} ID={self.id}.") + self.category = self._category + + if category is not None: + # If category is assigned but not consistent, raise exception + if category != self._category: + raise ValueError(f"DAGMC {stype} ID={self.id} has category={category}.") + + # If geom_dimension is unassigned, assign it based on category + if geom_dimension == -1: + if stype == 'group': + warn(f"Assigned geom_dimension={self._geom_dimension} to {stype} '{self.name}'.") + else: + warn(f"Assigned geom_dimension={self._geom_dimension} to {stype} ID={self.id}.") + self.geom_dimension = self._geom_dimension + def __eq__(self, other): return self.handle == other.handle @@ -113,9 +163,12 @@ def geom_dimension(self, dimension: int): self._tag_set_data(self.model.geom_dimension_tag, dimension) @property - def category(self) -> str: + def category(self) -> Optional[str]: """Return the DAGMC set's category.""" - return self._tag_get_data(self.model.category_tag) + try: + return self._tag_get_data(self.model.category_tag) + except RuntimeError: + return None @category.setter def category(self, category: str): @@ -233,10 +286,7 @@ class Surface(DAGSet): def __init__(self, model: DAGModel, handle: np.uint64): super().__init__(model, handle) - if self.geom_dimension != self._geom_dimension: - warn(f"DAGMC surface with global ID {self.id} does not have geom_dimension=2.") - if self.category != self._category: - warn(f"DAGMC surface with global ID {self.id} does not have category='Surface'.") + self._check_category_and_dimension() def get_volumes(self): """Get the parent volumes of this surface. @@ -258,10 +308,7 @@ class Volume(DAGSet): def __init__(self, model: DAGModel, handle: np.uint64): super().__init__(model, handle) - if self.geom_dimension != self._geom_dimension: - warn(f"DAGMC volume with global ID {self.id} does not have geom_dimension=3.") - if self.category != self._category: - warn(f"DAGMC volume with global ID {self.id} does not have category='Volume'.") + self._check_category_and_dimension() @property def groups(self) -> list[Group]: @@ -318,17 +365,19 @@ class Group(DAGSet): def __init__(self, model: DAGModel, handle: np.uint64): super().__init__(model, handle) - if self.category != self._category: - warn(f"DAGMC group with global ID {self.id} does not have category='Group'.") + self._check_category_and_dimension() def __contains__(self, ent_set: DAGSet): return any(vol.handle == ent_set.handle for vol in chain( self.get_volumes().values(), self.get_surfaces().values())) @property - def name(self) -> str: + def name(self) -> Optional[str]: """Returns the name of this group.""" - return self.model.mb.tag_get_data(self.model.name_tag, self.handle, flat=True)[0] + try: + return self.model.mb.tag_get_data(self.model.name_tag, self.handle, flat=True)[0] + except RuntimeError: + return None @name.setter def name(self, val: str): diff --git a/test/test_basic.py b/test/test_basic.py index 5ed63af..7e06e05 100644 --- a/test/test_basic.py +++ b/test/test_basic.py @@ -188,3 +188,33 @@ def test_to_vtk(tmpdir_factory, request): vtk_iter = filter(line_filter, vtk_file) gold_iter = filter(line_filter, gold_file) assert all(l1 == l2 for l1, l2 in zip(vtk_iter, gold_iter)) + + +@pytest.mark.parametrize("category,dim", [('Surface', 2), ('Volume', 3), ('Group', 4)]) +def test_empty_category(category, dim): + # Create a volume that has no category assigned + mb = core.Core() + model = dagmc.DAGModel(mb) + ent_set = dagmc.DAGSet(model, mb.create_meshset()) + ent_set.geom_dimension = dim + + # Instantiating using the proper class (Surface, Volume, Group) should + # result in the category tag getting assigned + with pytest.warns(UserWarning): + obj = getattr(dagmc, category)(model, ent_set.handle) + assert obj.category == category + + +@pytest.mark.parametrize("category,dim", [('Surface', 2), ('Volume', 3), ('Group', 4)]) +def test_empty_geom_dimension(category, dim): + # Create a volume that has no geom_dimension assigned + mb = core.Core() + model = dagmc.DAGModel(mb) + ent_set = dagmc.DAGSet(model, mb.create_meshset()) + ent_set.category = category + + # Instantiating using the proper class (Surface, Volume, Group) should + # result in the geom_dimension tag getting assigned + with pytest.warns(UserWarning): + obj = getattr(dagmc, category)(model, ent_set.handle) + assert obj.geom_dimension == dim From e6f8b7cc17c56d2245441df6841dec20c4d1c829 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 1 Feb 2024 21:42:50 -0600 Subject: [PATCH 5/5] Add check that at least one of geom_dimension and category is assigned --- dagmc/dagnav.py | 18 ++++++++---------- test/test_basic.py | 8 ++++++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/dagmc/dagnav.py b/dagmc/dagnav.py index 6a348f0..b81569d 100644 --- a/dagmc/dagnav.py +++ b/dagmc/dagnav.py @@ -102,35 +102,33 @@ def __init__(self, model: DAGModel, handle: np.uint64): def _check_category_and_dimension(self): """Check for consistency of category and geom_dimension tags""" stype = self._category.lower() + identifier = f"{stype} '{self.name}'" if stype == 'group' else f"{stype} ID={self.id}" geom_dimension = self.geom_dimension category = self.category if geom_dimension != -1: # If geom_dimension is assigned but not consistent, raise exception if geom_dimension != self._geom_dimension: - raise ValueError(f"DAGMC {stype} ID={self.id} has geom_dimension={geom_dimension}.") + raise ValueError(f"DAGMC {identifier} has geom_dimension={geom_dimension}.") # If category is unassigned, assign it based on geom_dimension if category is None: - if stype == 'group': - warn(f"Assigned category {self._category} to {stype} '{self.name}'.") - else: - warn(f"Assigned category {self._category} to {stype} ID={self.id}.") + warn(f"Assigned category {self._category} to {identifier}.") self.category = self._category if category is not None: # If category is assigned but not consistent, raise exception if category != self._category: - raise ValueError(f"DAGMC {stype} ID={self.id} has category={category}.") + raise ValueError(f"DAGMC {identifier} has category={category}.") # If geom_dimension is unassigned, assign it based on category if geom_dimension == -1: - if stype == 'group': - warn(f"Assigned geom_dimension={self._geom_dimension} to {stype} '{self.name}'.") - else: - warn(f"Assigned geom_dimension={self._geom_dimension} to {stype} ID={self.id}.") + warn(f"Assigned geom_dimension={self._geom_dimension} to {identifier}.") self.geom_dimension = self._geom_dimension + if geom_dimension == -1 and category is None: + raise ValueError(f"{identifier} has no category or geom_dimension tags assigned.") + def __eq__(self, other): return self.handle == other.handle diff --git a/test/test_basic.py b/test/test_basic.py index 7e06e05..2f649de 100644 --- a/test/test_basic.py +++ b/test/test_basic.py @@ -218,3 +218,11 @@ def test_empty_geom_dimension(category, dim): with pytest.warns(UserWarning): obj = getattr(dagmc, category)(model, ent_set.handle) assert obj.geom_dimension == dim + + +@pytest.mark.parametrize("cls", [dagmc.Surface, dagmc.Volume, dagmc.Group]) +def test_missing_tags(cls): + model = dagmc.DAGModel(core.Core()) + handle = model.mb.create_meshset() + with pytest.raises(ValueError): + cls(model, handle)