diff --git a/docs/conf.py b/docs/conf.py index d90b9116..b56d8c43 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -119,7 +119,7 @@ import sphinx_rtd_theme html_theme = "sphinx_rtd_theme" - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + # html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] pass except: html_theme = "default" @@ -313,10 +313,10 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { - "https://docs.python.org/": None, + "https://docs.python.org/": ("https://docs.python.org/", None), # 'http://docs.scipy.org/doc/numpy/': None, # 'http://docs.scipy.org/doc/scipy/reference/': None, - "http://propertiespy.readthedocs.io/en/latest/": None, + "http://propertiespy.readthedocs.io/en/latest/": ("http://propertiespy.readthedocs.io/en/latest/", None), } linkcheck_ignore = ["https://gmggroup.org"] linkcheck_retries = 10 diff --git a/docs/content/surface.rst b/docs/content/surface.rst index a6e576f5..5b7762a9 100644 --- a/docs/content/surface.rst +++ b/docs/content/surface.rst @@ -13,11 +13,17 @@ Elements .. autoclass:: omf.surface.Surface -.. image:: /images/SurfaceGridGeometry.png +.. image:: /images/grid2_tensor.svg :align: center .. autoclass:: omf.surface.TensorGridSurface +.. image:: /images/grid2_regular.svg + :align: center + +.. autoclass:: omf.surface.RegularGridSurface + + Attributes ---------- diff --git a/docs/images/SurfaceGeometry.png b/docs/images/SurfaceGeometry.png deleted file mode 100644 index 5ea00e75..00000000 Binary files a/docs/images/SurfaceGeometry.png and /dev/null differ diff --git a/docs/images/grid2_regular.svg b/docs/images/grid2_regular.svg new file mode 100644 index 00000000..105fd967 --- /dev/null +++ b/docs/images/grid2_regular.svg @@ -0,0 +1,431 @@ + + + + + + + + + + + + + + + u + + v + + A 2D regular grid with size [a, b] and count [5, 4] + + + + + + + + + + + + + + a + b + b + b + b + a + a + a + a + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/grid2_tensor.svg b/docs/images/grid2_tensor.svg new file mode 100644 index 00000000..71c21f84 --- /dev/null +++ b/docs/images/grid2_tensor.svg @@ -0,0 +1,426 @@ + + + + + + + + + + + + + + + u + + v + + A 2D tensor grid + + + + + + + + + + + + + + a₀ + b₃ + b₂ + b₁ + b₀ + a₁ + a₂ + a₃ + a₄ + v = [ b₀ b₁ b₃ b₃ ] + u = [ a₀ a₁ a₂ a₃ a₄ ] + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/omf/__init__.py b/omf/__init__.py index 5ac8d2a5..40728d34 100644 --- a/omf/__init__.py +++ b/omf/__init__.py @@ -20,7 +20,7 @@ ) from .lineset import LineSet from .pointset import PointSet -from .surface import Surface, TensorGridSurface +from .surface import Surface, TensorGridSurface, RegularGridSurface from .texture import ProjectedTexture, UVMappedTexture from .fileio import load, save, __version__ diff --git a/omf/attribute.py b/omf/attribute.py index f0fccb53..458ff932 100644 --- a/omf/attribute.py +++ b/omf/attribute.py @@ -118,7 +118,7 @@ def deserialize(cls, value, trusted=False, strict=False, assert_valid=False, **k array_dtype = DATA_TYPE_LOOKUP_TO_NUMPY[value["data_type"]] if value["data_type"] == "BooleanArray": int_arr = np.frombuffer(array_binary, dtype="uint8") - bit_arr = np.unpackbits(int_arr)[: np.product(value["shape"])] + bit_arr = np.unpackbits(int_arr)[: np.prod(value["shape"])] arr = bit_arr.astype(array_dtype) else: arr = np.frombuffer(array_binary, dtype=array_dtype) diff --git a/omf/surface.py b/omf/surface.py index aac10016..72499953 100644 --- a/omf/surface.py +++ b/omf/surface.py @@ -1,7 +1,10 @@ """surface.py: Surface element definitions""" + import numpy as np import properties +EPSILON = np.finfo(float).eps + from .base import ProjectElement from .attribute import ArrayInstanceProperty from .texture import HasTexturesMixin @@ -78,12 +81,12 @@ class TensorGridSurface(BaseSurfaceElement): # pylint: disable=R0901 tensor_u = properties.List( "Grid cell widths, u-direction", - properties.Float("", min=0.0), + properties.Float("", min=EPSILON), coerce=True, ) tensor_v = properties.List( "Grid cell widths, v-direction", - properties.Float("", min=0.0), + properties.Float("", min=EPSILON), coerce=True, ) axis_u = properties.Vector3( @@ -116,7 +119,7 @@ def num_cells(self): @properties.validator def _validate_mesh(self): """Check if mesh content is built correctly""" - if not np.abs(self.axis_u.dot(self.axis_v)) < 1e-6: + if not np.abs(self.axis_u.dot(self.axis_v)) < EPSILON: raise properties.ValidationError("axis_u and axis_v must be orthogonal") if self.offset_w is properties.undefined or self.offset_w is None: return True @@ -126,3 +129,64 @@ def _validate_mesh(self): "{nnode}".format(zlen=len(self.offset_w.array), nnode=self.num_nodes) ) return True + + +class RegularGridSurface(BaseSurfaceElement): # pylint: disable=R0901 + """Surface element with regular spacing in each dimension""" + + schema = "org.omf.v2.element.surfaceregulargrid" + + cell_count = properties.List( + "Number of cells along u, and v axes", + properties.Integer("", min=1), + min_length=2, + max_length=2, + ) + cell_size = properties.List( + "Size of cells in the u and v dimensions", + properties.Float("", min=EPSILON), + min_length=2, + max_length=2, + ) + axis_u = properties.Vector3( + "Vector orientation of u-direction", + default="X", + length=1, + ) + axis_v = properties.Vector3( + "Vector orientation of v-direction", + default="Y", + length=1, + ) + offset_w = ArrayInstanceProperty( + "Node offset", + shape=("*",), + dtype=float, + required=False, + ) + + @property + def num_nodes(self): + """Number of nodes (vertices)""" + cell_count = self.cell_count + return (cell_count[0] + 1) * (cell_count[1] + 1) + + @property + def num_cells(self): + """Number of cells (faces)""" + cell_count = self.cell_count + return cell_count[0] * cell_count[1] + + @properties.validator + def _validate_mesh(self): + """Check if mesh content is built correctly""" + if not np.abs(self.axis_u.dot(self.axis_v)) < 1e-6: + raise properties.ValidationError("axis_u and axis_v must be orthogonal") + if self.offset_w is properties.undefined or self.offset_w is None: + return True + elif len(self.offset_w.array) != self.num_nodes: + raise properties.ValidationError( + "Length of offset_w, {zlen}, must equal number of nodes, " + "{nnode}".format(zlen=len(self.offset_w.array), nnode=self.num_nodes) + ) + return True diff --git a/tests/test_surface.py b/tests/test_surface.py index 88b064d6..053b15dd 100644 --- a/tests/test_surface.py +++ b/tests/test_surface.py @@ -1,4 +1,5 @@ """Tests for Surface validation""" + import numpy as np import pytest @@ -21,8 +22,8 @@ def test_surface(): elem.validate() -def test_surfacegrid(): - """Test surface grid geometry validation""" +def test_tensor_surface(): + """Test tensor surface grid geometry validation""" elem = omf.surface.TensorGridSurface() elem.tensor_u = [1.0, 1.0] elem.tensor_v = [2.0, 2.0, 2.0] @@ -38,3 +39,23 @@ def test_surfacegrid(): elem.offset_w = np.random.rand(6) with pytest.raises(ValueError): elem.validate() + + +def test_regular_surface(): + """Test regular surface geometry validation""" + elem = omf.surface.RegularGridSurface() + elem.origin = [0.0, 0.0, 0.0] + elem.cell_size = [1.0, 1.0] + elem.cell_count = [2, 2] + assert elem.validate() + assert elem.location_length("vertices") == 9 + assert elem.location_length("faces") == 4 + with pytest.raises(ValueError): + elem.cell_size = [0.0, 1.0] + with pytest.raises(ValueError): + elem.origin = [0.0, 0.0] + elem.offset_w = np.random.rand(9) + elem.validate() + elem.offset_w = np.random.rand(4) + with pytest.raises(ValueError): + elem.validate()