From a0a702946dd86239892e4b4d15b5bbe082f26171 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Mon, 27 Feb 2023 11:40:19 +1300 Subject: [PATCH 01/42] Ignore vscode settings. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index bbfef545..377d2727 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,6 @@ ENV/ deps/ cover/ + +# VSCode project settings +.vscode/ From 2a864bf439d92bc04e198746e8eca86aeb35c22e Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Mon, 27 Feb 2023 11:41:59 +1300 Subject: [PATCH 02/42] Fix test failures with latest NumPy. --- omf/blockmodel.py | 18 +++++++++--------- tests/test_blockmodel.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/omf/blockmodel.py b/omf/blockmodel.py index b9668319..27ec58f1 100644 --- a/omf/blockmodel.py +++ b/omf/blockmodel.py @@ -2,8 +2,8 @@ import numpy as np import properties -from .base import ProjectElement from .attribute import ArrayInstanceProperty +from .base import ProjectElement class BaseBlockModel(ProjectElement): @@ -269,7 +269,7 @@ def reset_cbc(self): if not self.block_count: raise ValueError("cannot reset cbc until block_count is set") cbc_len = np.prod(self.block_count) - self.cbc = np.ones(cbc_len, dtype=np.bool) + self.cbc = np.ones(cbc_len, dtype=bool) class RegularSubBlockModel(BaseBlockModel): @@ -388,7 +388,7 @@ def num_cells(self): def location_length(self, location): """Return correct attribute length based on location""" if location == "parent_blocks": - return np.sum(self.cbc.array.astype(np.bool)) + return np.sum(self.cbc.array.astype(bool)) return self.num_cells def reset_cbc(self): @@ -484,7 +484,7 @@ def validate_cbc(self, change): instance=self, reason="invalid", ) - if np.max(value.array) > 8 ** 8 or np.min(value.array) < 0: + if np.max(value.array) > 8**8 or np.min(value.array) < 0: raise properties.ValidationError( "cbc must have values between 0 and 8^8", prop="cbc", @@ -526,7 +526,7 @@ def num_cells(self): def location_length(self, location): """Return correct attribute length based on location""" if location == "parent_blocks": - return np.sum(self.cbc.array.astype(np.bool)) + return np.sum(self.cbc.array.astype(bool)) return self.num_cells def reset_cbc(self): @@ -604,7 +604,7 @@ def get_level(cls, curve_value): Level comes from the last 4 bits, with values between 0 and 8 """ - return curve_value & (2 ** cls.level_bits - 1) + return curve_value & (2**cls.level_bits - 1) @classmethod def level_width(cls, level): @@ -648,8 +648,8 @@ def refine(self, index, ijk=None, refinements=1): ) new_width = self.level_width(level + refinements) - new_pointers = np.indices([2 ** refinements] * 3) - new_pointers = new_pointers.reshape(3, (2 ** refinements) ** 3).T + new_pointers = np.indices([2**refinements] * 3) + new_pointers = new_pointers.reshape(3, (2**refinements) ** 3).T new_pointers = new_pointers * new_width pointer = self.get_pointer(curve_value) @@ -875,7 +875,7 @@ def num_cells(self): def location_length(self, location): """Return correct attribute length based on location""" if location == "parent_blocks": - return np.sum(self.cbc.array.astype(np.bool)) + return np.sum(self.cbc.array.astype(bool)) return self.num_cells def reset_cbc(self): diff --git a/tests/test_blockmodel.py b/tests/test_blockmodel.py index 836b3ef9..f2b7cfa9 100644 --- a/tests/test_blockmodel.py +++ b/tests/test_blockmodel.py @@ -661,7 +661,7 @@ def test_sub_block_attributes(self): assert block_model.validate() assert block_model.location_length("parent_blocks") == 8 assert block_model.location_length("") == 8 - block_model.cbc = np.array([1] + [0] * 7, dtype=np.int) + block_model.cbc = np.array([1] + [0] * 7, dtype=int) with pytest.raises(properties.ValidationError): block_model.validate() block_model.sub_block_corners = np.array([[-0.5, 2, 0]]) From fa89c46b917cf8f849603b2e85eb346b8e5f9213 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Tue, 28 Feb 2023 12:22:54 +1300 Subject: [PATCH 03/42] Sketched out the basics of new block models. --- omf/__init__.py | 9 +- omf/blockmodel.py | 1006 +++++++++----------------------- omf/composite.py | 14 +- tests/test_blockmodel.py | 1185 +++++++++++++++++++------------------- 4 files changed, 859 insertions(+), 1355 deletions(-) diff --git a/omf/__init__.py b/omf/__init__.py index 5ac8d2a5..fdc629f0 100644 --- a/omf/__init__.py +++ b/omf/__init__.py @@ -1,11 +1,14 @@ """omf: API library for Open Mining Format file interchange format""" from .base import Project from .blockmodel import ( - ArbitrarySubBlockModel, - OctreeSubBlockModel, + FreeformSubblockDefinition, + FreeformSubblockedModel, + OctreeSubblockDefinition, RegularBlockModel, - RegularSubBlockModel, + RegularSubblockDefinition, + SubblockedModel, TensorGridBlockModel, + VariableZSubblockDefinition, ) from .composite import Composite from .attribute import ( diff --git a/omf/blockmodel.py b/omf/blockmodel.py index 27ec58f1..e15d6d1d 100644 --- a/omf/blockmodel.py +++ b/omf/blockmodel.py @@ -1,13 +1,70 @@ -"""blockmodel.py: Block Model element definitions""" +"""blockmodel2.py: New Block Model element definitions""" import numpy as np import properties -from .attribute import ArrayInstanceProperty from .base import ProjectElement -class BaseBlockModel(ProjectElement): - """Basic orientation properties for all block models""" +def _shrink_uint(obj, attr): + arr = getattr(obj, attr) + assert arr.min() >= 0 + t = np.min_scalar_type(arr.max()) + setattr(obj, attr, arr.astype(t)) + + +class _BlockCount(properties.Array): + def __init__(self, doc, **kw): + super().__init__(doc, **kw, dtype=int, shape=(3,)) + + def validate(self, instance, value): + """Check shape and dtype of the count and that items are >= min.""" + value = super().validate(instance, value) + for item in value: + if item < 1: + if instance is None: + msg = f"block counts must be >= 1" + else: + cls = instance.__class__.__name__ + msg = f"{cls}.{self.name} counts must be >= 1" + raise properties.ValidationError(msg, prop=self.name, instance=instance) + return value + + +class _OctreeSubblockCount(_BlockCount): + def validate(self, instance, value): + """Check shape and dtype of the count and that items are >= min.""" + value = super().validate(instance, value) + for item in value: + l = np.log2(item) + if np.trunc(l) != l: + if instance is None: + msg = f"octree block counts must be powers of two" + else: + cls = instance.__class__.__name__ + msg = f"{cls}.{self.name} octree counts must be powers of two" + raise properties.ValidationError(msg, prop=self.name, instance=instance) + return value + + +class _BlockSize(properties.Array): + def __init__(self, doc, **kw): + super().__init__(doc, **kw, dtype=float, shape=(3,)) + + def validate(self, instance, value): + """Check shape and dtype of the count and that items are >= min.""" + value = super().validate(instance, value) + for item in value: + if item <= 0.0: + if instance is None: + msg = f"block size elements must be > 0.0" + else: + msg = f"{instance.__class__.__name__}.{self.name} elements must be > 0.0" + raise properties.ValidationError(msg, prop=self.name, instance=instance) + return value + + +class _BaseBlockModel(ProjectElement): + """Basic orientation properties and indexing for all block models""" axis_u = properties.Vector3( "Vector orientation of u-direction", @@ -26,8 +83,9 @@ class BaseBlockModel(ProjectElement): ) corner = properties.Vector3( "Corner of the block model relative to Project coordinate reference system", - default=[0.0, 0.0, 0.0], + default="zero", ) + _valid_locations = ("cells", "parent_blocks") @properties.validator def _validate_axes(self): @@ -50,66 +108,50 @@ def parent_block_count(self): raise NotImplementedError() def ijk_to_index(self, ijk): - """Return index for single ijk triple""" - return self.ijk_array_to_indices([ijk])[0] - - def ijk_array_to_indices(self, ijk_array): - """Return an array of indices for a list of ijk triples""" - blocks = self.parent_block_count - if not blocks: - raise AttributeError("parent_block_count is required to calculate index") - if not isinstance(ijk_array, (list, tuple, np.ndarray)): - raise ValueError("ijk_array must be a list of length-3 ijk values") - ijk_array = np.array(ijk_array) - if len(ijk_array.shape) != 2 or ijk_array.shape[1] != 3: - raise ValueError("ijk_array must be n x 3 array") - if not np.array_equal(ijk_array, ijk_array.astype(np.uint32)): - raise ValueError("ijk values must be non-negative integers") - if np.any(np.max(ijk_array, axis=0) >= blocks): - raise ValueError( - "ijk must be less than parent_block_count in each dimension" - ) - index = np.ravel_multi_index( - multi_index=ijk_array.T, - dims=blocks, - order="F", - ) - return index + """Map IJK triples to flat indices for a singoe triple or an array, preseving shape.""" + arr = np.asarray(ijk) + if arr.dtype.kind not in "ui": + raise TypeError(f"'ijk' must be integer typed, found {arr.dtype}") + match arr.shape: + case (*output_shape, 3): + shaped = arr.reshape(-1, 3) + case _: + raise ValueError( + "'ijk' must have 3 elements or be an array with shape (*_, 3)" + ) + count = self.parent_block_count + if (shaped < 0).any() or (shaped >= count).any(): + raise IndexError(f"0 <= ijk < ({count[0]}, {count[1]}, {count[2]}) failed") + indices = np.ravel_multi_index(multi_index=shaped.T, dims=count, order="F") + if output_shape == (): + return indices[0] + else: + return indices.reshape(output_shape) def index_to_ijk(self, index): - """Return ijk triple for single index""" - return self.indices_to_ijk_array([index])[0] - - def indices_to_ijk_array(self, indices): - """Return an array of ijk triples for an array of indices""" - blocks = self.parent_block_count - if not blocks: - raise AttributeError( - "parent_block_count is required to calculate ijk values" - ) - if not isinstance(indices, (list, tuple, np.ndarray)): - raise ValueError("indices must be a list of index values") - indices = np.array(indices) - if len(indices.shape) != 1: - raise ValueError("indices must be 1D array") - if not np.array_equal(indices, indices.astype(np.uint64)): - raise ValueError("indices values must be non-negative integers") - if np.max(indices) >= np.prod(blocks): - raise ValueError("indices must be less than total number of parent blocks") - ijk = np.unravel_index( - indices=indices, - shape=blocks, - order="F", - ) - ijk_array = np.c_[ijk[0], ijk[1], ijk[2]] - return ijk_array + """Map flat indices to IJK triples for a singoe index or an array, preserving shape.""" + arr = np.asarray(index) + if arr.dtype.kind not in "ui": + raise TypeError(f"'index' must be integer typed, found {arr.dtype}") + output_shape = arr.shape + (3,) + shaped = arr.reshape(-1) + count = self.parent_block_count + if (shaped < 0).any() or (shaped >= np.prod(count)).any(): + raise IndexError(f"0 <= index < {np.prod(count)} failed") + ijk = np.unravel_index(indices=shaped, shape=count, order="F") + return np.c_[ijk[0], ijk[1], ijk[2]].reshape(output_shape) -class TensorGridBlockModel(BaseBlockModel): - """Block model with variable spacing in each dimension""" +class TensorGridBlockModel(_BaseBlockModel): + """Block model with variable spacing in each dimension. + + Unlike the rest of the block models attributes here can also be on the block vertices. + """ schema = "org.omf.v2.element.blockmodel.tensorgrid" + _valid_locations = ("vertices",) + _BaseBlockModel._valid_locations + tensor_u = properties.Array( "Tensor cell widths, u-direction", shape=("*",), @@ -126,761 +168,243 @@ class TensorGridBlockModel(BaseBlockModel): dtype=float, ) - _valid_locations = ("vertices", "cells", "parent_blocks") + @properties.validator("tensor_u") + @properties.validator("tensor_v") + @properties.validator("tensor_w") + def _validate_tensor(self, change): + tensor = change["value"] + if (tensor <= 0.0).any(): + raise properties.ValidationError( + "Tensor spacings must all be greater than zero", + prop=change["name"], + instance=self, + reason="invalid", + ) - def location_length(self, location): - """Return correct attribute length based on location""" - if location == "vertices": - return self.num_nodes - return self.num_cells + def _require_tensors(self): + if self.tensor_u is None or self.tensor_v is None or self.tensor_w is None: + raise ValueError("tensors haven't been set yet") - def _tensors_defined(self): - """Check if all tensors are defined""" - tensors = [self.tensor_u, self.tensor_v, self.tensor_w] - return all((tensor is not None for tensor in tensors)) + @property + def parent_block_count(self): + self._require_tensors() + return np.array( + (len(self.tensor_u), len(self.tensor_v), len(self.tensor_w)), dtype=int + ) @property def num_nodes(self): """Number of nodes (vertices)""" - if not self._tensors_defined(): - return None - nodes = ( - (len(self.tensor_u) + 1) - * (len(self.tensor_v) + 1) - * (len(self.tensor_w) + 1) - ) - return nodes + return np.prod(self.parent_block_count + 1) @property def num_cells(self): """Number of cells""" - if not self._tensors_defined(): - return None - cells = len(self.tensor_u) * len(self.tensor_v) * len(self.tensor_w) - return cells + return np.prod(self.parent_block_count) - @property - def parent_block_count(self): - """Number of parent blocks equals number of blocks""" - if not self._tensors_defined(): - return None - blocks = [len(self.tensor_u), len(self.tensor_v), len(self.tensor_w)] - return blocks + def location_length(self, location): + """Return correct attribute length for 'location'.""" + match location: + case "vertices": + return self.num_nodes + case "cells" | "parent_blocks" | "": + return self.num_cells + case _: + raise ValueError(f"unknown location type: {location!r}") -class RegularBlockModel(BaseBlockModel): - """Block model with constant spacing in each dimension""" +class RegularBlockModel(_BaseBlockModel): + """Block model with constant spacing in each dimension.""" schema = "org.omf.v2.elements.blockmodel.regular" - block_count = properties.List( - "Number of blocks along u, v, and w axes", - properties.Integer("", min=1), - min_length=3, - max_length=3, - ) - block_size = properties.List( - "Size of blocks in the u, v, and w dimensions", - properties.Float("", min=0), - min_length=3, - max_length=3, - ) - cbc = ArrayInstanceProperty( - "Compressed block count - for regular block models this must " - "have length equal to the product of block_count and all values " - "must be 1 (if attributes exist on the block) or 0; the default " - "is an array of 1s", - shape=("*",), - dtype=(int, bool), - ) - - _valid_locations = ("cells", "parent_blocks") - - @properties.Array( - "Compressed block index - used for indexing attributes " - "into the block model; must have length equal to the " - "product of block_count plus 1 and monotonically increasing", - shape=("*",), - dtype=int, - coerce=False, - ) - def cbi(self): - """Compressed block index""" - if self.cbc is None: - return None - # Recalculating the sum on the fly is faster than checking md5 - cbi = np.concatenate( - [ - np.array([0], dtype=np.uint32), - np.cumsum(self.cbc, dtype=np.uint32), - ] - ) - return cbi - - @properties.validator("block_size") - def _validate_size_is_not_zero(self, change): - """Ensure block sizes are non-zero""" - if 0 in change["value"]: - raise properties.ValidationError( - "Block size cannot be 0", - prop="block_size", - instance=self, - reason="invalid", - ) - - @properties.validator("cbc") - def validate_cbc(self, change): - """Ensure cbc is correct size and values""" - value = change["value"] - if self.block_count and len(value.array) != np.prod(self.block_count): - raise properties.ValidationError( - "cbc must have length equal to the product of block_count", - prop="cbc", - instance=self, - reason="invalid", - ) - if np.max(value.array) > 1 or np.min(value.array) < 0: - raise properties.ValidationError( - "cbc must have only values 0 or 1", - prop="cbc", - instance=self, - reason="invalid", - ) + block_count = _BlockCount("Number of blocks along u, v, and w axes") + block_size = _BlockSize("Size of blocks in the u, v, and w directions") @property def num_cells(self): - """Number of cells from last value in the compressed block index""" - cbi = self.cbi - if cbi is None: - return None - return cbi[-1] # pylint: disable=E1136 - - def location_length(self, location): - """Return correct attribute length based on location""" - return self.num_cells + """The number of cells, which in this case are always parent blocks.""" + return np.prod(self.parent_block_count) @property def parent_block_count(self): """Number of parent blocks equals number of blocks""" return self.block_count - def reset_cbc(self): - """Reset cbc to no sub-blocks""" - if not self.block_count: - raise ValueError("cannot reset cbc until block_count is set") - cbc_len = np.prod(self.block_count) - self.cbc = np.ones(cbc_len, dtype=bool) - + def location_length(self, location): + """Return correct attribute length for 'location'.""" + match location: + case "cells" | "parent_blocks" | "": + return self.num_cells + case _: + raise ValueError(f"unknown location type: {location!r}") -class RegularSubBlockModel(BaseBlockModel): - """Block model with one level of sub-blocking possible in each parent block""" - schema = "org.omf.v2.elements.blockmodel.sub" +class RegularSubblockDefinition: + """The simplest gridded sub-block definition.""" - parent_block_count = properties.List( - "Number of parent blocks along u, v, and w axes", - properties.Integer("", min=1), - min_length=3, - max_length=3, - ) - sub_block_count = properties.List( - "Number of sub blocks in each parent block, along u, v, and w axes", - properties.Integer("", min=1), - min_length=3, - max_length=3, - ) - parent_block_size = properties.List( - "Size of parent blocks in the u, v, and w dimensions", - properties.Float("", min=0), - min_length=3, - max_length=3, - ) - cbc = ArrayInstanceProperty( - "Compressed block count - for regular sub block models this must " - "have length equal to the product of parent_block_count and all " - "values must be the product of sub_block_count (if attributes " - "exist on the sub blocks), 1 (if attributes exist on the parent " - "block) or 0; the default is an array of 1s", - shape=("*",), - dtype=(int, bool), + count = _BlockCount( + "The maximum number of sub-blocks inside a parent in each direction." ) - _valid_locations = ("parent_blocks", "sub_blocks") + def validate_subblocks(self, _corners): + """Checks the sub-blocks within one parent block.""" + # XXX can we check for overlaps efficiently? - @properties.Array( - "Compressed block index - used for indexing attributes " - "into the sub block model; must have length equal to the " - "product of parent_block_count plus 1 and monotonically increasing", - shape=("*",), - dtype=int, - coerce=False, - ) - def cbi(self): - """Compressed block index""" - if self.cbc is None: - return None - cbi = np.concatenate( - [ - np.array([0], dtype=np.uint64), - np.cumsum(self.cbc, dtype=np.uint64), - ] - ) - return cbi - @properties.List( - "Size of sub blocks in the u, v, and w dimensions", - properties.Float("", min=0), - min_length=3, - max_length=3, - ) - def sub_block_size(self): - """Computed sub block size""" - if not self.sub_block_count or not self.parent_block_size: - return None - return self.parent_block_size / np.array(self.sub_block_count) - - @properties.validator("parent_block_size") - def _validate_size_is_not_zero(self, change): - """Ensure block sizes are non-zero""" - if 0 in change["value"]: - raise properties.ValidationError( - "Block size cannot be 0", - prop="parent_block_size", - instance=self, - reason="invalid", - ) +class OctreeSubblockDefinition(RegularSubblockDefinition): + """Sub-blocks form an octree inside the parent block. - @properties.validator("cbc") - def validate_cbc(self, change): - """Ensure cbc is correct size and values""" - value = change["value"] - if not self.parent_block_count: - pass - elif len(value.array) != np.prod(self.parent_block_count): - raise properties.ValidationError( - "cbc must have length equal to the product of parent_block_count", - prop="cbc", - instance=self, - reason="invalid", - ) - if not self.sub_block_count: - pass - elif np.any( - (value.array != 1) - & (value.array != 0) - & (value.array != np.prod(self.sub_block_count)) - ): - raise properties.ValidationError( - "cbc must have only values of prod(sub_block_count), 1, or 0", - prop="cbc", - instance=self, - reason="invalid", - ) + Cut the parent block in half in all directions to create eight sub-blocks. Repeat that + division for some or all of those new sub-blocks. Continue doing that until the limit + on sub-block count is reached or until the sub-blocks accurately model the inputs. - @property - def num_cells(self): - """Number of cells from last value in the compressed block index""" - cbi = self.cbi - if cbi is None: - return None - return cbi[-1] # pylint: disable=E1136 + This definition also allows the lower level cuts to be omitted in one or two axes, + giving a maximum sub-block count of (16, 16, 4) for example rather than requiring + all axes to be equal. + """ - def location_length(self, location): - """Return correct attribute length based on location""" - if location == "parent_blocks": - return np.sum(self.cbc.array.astype(bool)) - return self.num_cells - - def reset_cbc(self): - """Reset cbc to no sub-blocks""" - if not self.parent_block_count: - raise ValueError("cannot reset cbc until parent_block_count is set") - cbc_len = np.prod(self.parent_block_count) - self.cbc = np.ones(cbc_len, dtype=np.uint32) - - def refine(self, ijk): - """Refine parent blocks at a single ijk or a list of multiple ijks""" - if self.cbc is None or not self.sub_block_count: - raise ValueError( - "Cannot refine sub block model without specifying number " - "of parent and sub blocks" - ) - try: - inds = self.ijk_array_to_indices(ijk) - except ValueError: - inds = self.ijk_to_index(ijk) - self.cbc.array[inds] = np.prod(self.sub_block_count) # pylint: disable=E1137 + count = _OctreeSubblockCount( + "The maximum number of sub-blocks inside a parent in each direction." + ) + def validate_subblocks(self, corners): + """Checks the sub-blocks within one parent block.""" + super().validate_subblocks(corners) + # TODO check that blocks lie on the octree -class OctreeSubBlockModel(BaseBlockModel): - """Block model where sub-blocks follow an octree pattern in each parent""" - schema = "org.omf.v2.elements.blockmodel.octree" +class SubblockedModel(_BaseBlockModel): + """A model where regular parent blocks are divided into sub-blocks that align with a grid. - max_level = 8 # Maximum times blocks can be subdivided - level_bits = 4 # Enough for 0 to 8 refinements + The sub-blocks here must align with a regular grid within the parent block. What that grid + is and how blocks are generated within it is defined by the sub-block definition. + """ - parent_block_count = properties.List( - "Number of parent blocks along u, v, and w axes", - properties.Integer("", min=1), - min_length=3, - max_length=3, - ) - parent_block_size = properties.List( - "Size of parent blocks in the u, v, and w dimensions", - properties.Float("", min=0), - min_length=3, - max_length=3, - ) - cbc = ArrayInstanceProperty( - "Compressed block count - for octree sub block models this must " - "have length equal to the product of parent_block_count and each " - "value must be equal to the number of octree sub blocks within " - "the corresponding parent block (since max level is 8 in each " - "dimension, the max number of sub blocks in a parent is (2^8)^3), " - "1 (if parent block is not subdivided) or 0 (if parent block is " - "unused); the default is an array of 1s", - shape=("*",), - dtype=(int, bool), + schema = "org.omf.v2.elements.blockmodel.subblocked" + + parent_block_count = _BlockCount("Number of parent blocks along u, v, and w axes") + parent_block_size = _BlockSize( + "Size of parent blocks in the u, v, and w directions" ) - zoc = ArrayInstanceProperty( - "Z-order curves - sub block location pointer and level, encoded as bits", - shape=("*",), + subblock_parent_indices = properties.Array( + "The parent block IJK index of each sub-block", + shape=("*", 3), dtype=int, ) + subblock_corners = properties.Array( + """The positions of the sub-block corners on the grid within their parent block. - _valid_locations = ("parent_blocks", "sub_blocks") + The columns are (min_i, min_j, min_k, max_i, max_j, max_k). Values must be + greater than or equal to zero and less than or equal to the maximum number of + sub-blocks in that axis. - @properties.Array( - "Compressed block index - used for indexing attributes " - "into the sub block model; must have length equal to the " - "product of parent_block_count plus 1 and monotonically increasing", - shape=("*",), + Sub-blocks must stay within the parent block and should not overlap. Gaps are + allowed but it will be impossible for 'cell' attributes to assign values to + those areas. + """, + shape=("*", 6), dtype=int, - coerce=False, ) - def cbi(self): - """Compressed block index""" - if self.cbc is None: - return None - cbi = np.concatenate( - [ - np.array([0], dtype=np.uint64), - np.cumsum(self.cbc, dtype=np.uint64), - ] - ) - return cbi - - @properties.validator("cbc") - def validate_cbc(self, change): - """Ensure cbc is correct size and values""" - value = change["value"] - if not self.parent_block_count: - pass - elif len(value.array) != np.prod(self.parent_block_count): - raise properties.ValidationError( - "cbc must have length equal to the product of parent_block_count", - prop="cbc", - instance=self, - reason="invalid", - ) - if np.max(value.array) > 8**8 or np.min(value.array) < 0: - raise properties.ValidationError( - "cbc must have values between 0 and 8^8", - prop="cbc", - instance=self, - reason="invalid", - ) - - @properties.validator("zoc") - def validate_zoc(self, change): - """Ensure Z-order curve array is correct length and valid values""" - value = change["value"] - cbi = self.cbi - if cbi is None: - pass - elif len(value.array) != cbi[-1]: - raise properties.ValidationError( - "zoc must have length equal to maximum compressed block index value", - prop="zoc", - instance=self, - reason="invalid", - ) - max_curve_value = 268435448 # -> 0b1111111111111111111111111000 - if np.max(value.array) > max_curve_value or np.min(value.array) < 0: - raise properties.ValidationError( - "zoc must have values between 0 and 8^8", - prop="cbc", - instance=self, - reason="invalid", - ) + subblock_definition = properties.Instance( + "Defines the structure of sub-blocks within each parent block for this model.", + RegularSubblockDefinition, + ) @property - def num_cells(self): - """Number of cells from last value in the compressed block index""" - cbi = self.cbi - if cbi is None: - return None - return cbi[-1] # pylint: disable=E1136 - - def location_length(self, location): - """Return correct attribute length based on location""" - if location == "parent_blocks": - return np.sum(self.cbc.array.astype(bool)) - return self.num_cells - - def reset_cbc(self): - """Reset cbc to no sub-blocks""" - if not self.parent_block_count: - raise ValueError("cannot reset cbc until parent_block_count is set") - cbc_len = np.prod(self.parent_block_count) - self.cbc = np.ones(cbc_len, dtype=np.uint32) - - def reset_zoc(self): - """Reset zoc to no sub-blocks""" - if not self.parent_block_count: - raise ValueError("cannot reset zoc until parent_block_count is set") - zoc_len = np.prod(self.parent_block_count) - self.zoc = np.zeros(zoc_len, dtype=np.int32) - - @staticmethod - def bitrange(index, width, start, end): - """Extract a bit range as an integer - - [start, end) is inclusive lower bound, exclusive upper bound. - """ - return index >> (width - end) & ((2 ** (end - start)) - 1) + def subblock_count(self): + return self.subblock_definition.count - @classmethod - def get_curve_value(cls, pointer, level): - """Get Z-order curve value from pointer and level + # XXX add num_cells property and implement location_length - Values range from 0 (pointer=[0, 0, 0], level=0) to - 268435448 (pointer=[255, 255, 255], level=8). - """ - idx = 0 - iwidth = cls.max_level * 3 - for i in range(iwidth): - bitoff = cls.max_level - (i // 3) - 1 - poff = 3 - (i % 3) - 1 - bitrange = ( - cls.bitrange( - index=pointer[3 - 1 - poff], - width=cls.max_level, - start=bitoff, - end=bitoff + 1, - ) - << i + def _check_lengths(self): + if len(self.subblock_parent_indices) != len(self.subblock_corners): + raise ValueError( + "'subblock_parent_indices' and 'subblock_corners' arrays must be the same length" ) - idx |= bitrange - return (idx << cls.level_bits) + level - - @classmethod - def get_pointer(cls, curve_value): - """Get pointer value from Z-order curve value - Pointer values are length-3 with values between 0 and 255 - """ - index = curve_value >> cls.level_bits - pointer = [0] * 3 - iwidth = cls.max_level * 3 - for i in range(iwidth): - bitrange = ( - cls.bitrange( - index=index, - width=iwidth, - start=i, - end=i + 1, - ) - << (iwidth - i - 1) // 3 + def _check_parent_indices(self): + indices = self.subblock_parent_indices + count = self.parent_block_count + if (indices < 0).any() or (indices >= count).any(): + raise IndexError( + f"0 <= subblock_parent_indices < ({count[0]}, {count[1]}, {count[2]}) failed" ) - pointer[i % 3] |= bitrange - pointer.reverse() - return pointer - - @classmethod - def get_level(cls, curve_value): - """Get level value from Z-order curve value - - Level comes from the last 4 bits, with values between 0 and 8 - """ - return curve_value & (2**cls.level_bits - 1) - - @classmethod - def level_width(cls, level): - """Width of a level, in bits - Max level of 8 has level width of 1; min level of 0 has level - width of 256. - """ - if not 0 <= level <= cls.max_level: - raise ValueError("level must be between 0 and {}".format(cls.max_level)) - return 2 ** (cls.max_level - level) + def _check_inside_parent(self): + min_corner = self.subblock_corners[:, :3] + max_corner = self.subblock_corners[:, 3:] + count = self.subblock_count + if min_corner.dtype.kind != "u" and not (0 <= min_corner).all(): + raise IndexError("0 <= min_corner failed") + if not (min_corner < max_corner).all(): + raise IndexError("min_corner < max_corner failed") + if not (max_corner <= count).all(): + raise IndexError( + f"max_corner <= ({count[0]}, {count[1]}, {count[2]}) failed" + ) - def refine(self, index, ijk=None, refinements=1): - """Subdivide at the given index + @properties.validator + def _validate_subblocks(self): + self._check_lengths() + self._check_parent_indices() + self._check_inside_parent() + # Check corners against the definition. + # TODO check that sub-blocks in each parent are adjacent or remove that requirement + indices = self.ijk_to_index(self.subblock_parent_indices) + for index in np.unique(indices): + corners = self.subblock_corners[indices == index, :] # XXX slow + self.subblock_definition.validate_subblocks(corners) + # Cast to the smallest unsigned integer type. + _shrink_uint(self, "subblock_parent_indices") + _shrink_uint(self, "subblock_corners") - .. note:: - This method is for demonstration only. - It is impractical and not intended to build an octree blockmodel - using this method alone. - If ijk is provided, index is relative to ijk parent block. - Otherwise, index is relative to the entire block model. +class FreeformSubblockDefinition: + """Unconstrained free-form sub-block definition. - By default, blocks are refined a single level, from 1 sub-block - to 8 sub-blocks. However, a greater number of refinements may be - specified, where the final number of sub-blocks equals - (2**refinements)**3. - """ - cbi = self.cbi - if ijk is not None: - index += int(cbi[self.ijk_to_index(ijk)]) - parent_index = np.sum(index >= cbi) - 1 # pylint: disable=W0143 - if not 0 <= index < len(self.zoc): - raise ValueError("index must be between 0 and {}".format(len(self.zoc))) - - curve_value = self.zoc[index] - level = self.get_level(curve_value) - if not 0 <= refinements <= self.max_level - level: - raise ValueError( - "refinements must be between 0 and {}".format(self.max_level - level) - ) - new_width = self.level_width(level + refinements) + Doesn't provide any limitations on or explanation of sub-block positions. + """ - new_pointers = np.indices([2**refinements] * 3) - new_pointers = new_pointers.reshape(3, (2**refinements) ** 3).T - new_pointers = new_pointers * new_width + def validate_subblocks(self, _corners): + """Checks the sub-blocks within one parent block.""" + # XXX can we check for overlaps efficiently? - pointer = self.get_pointer(curve_value) - new_pointers = new_pointers + pointer - new_curve_values = sorted( - [ - self.get_curve_value(pointer, level + refinements) - for pointer in new_pointers - ] - ) +class VariableZSubblockDefinition(FreeformSubblockDefinition): + def validate_subblocks(self, corners): + """Checks the sub-blocks within one parent block.""" + super().validate_subblocks(corners) + # TODO check that blocks lie on the octree - self.cbc.array[parent_index] += len(new_curve_values) - 1 - self.zoc = np.concatenate( - [ - self.zoc[:index], - new_curve_values, - self.zoc[index + 1 :], - ] - ) +class FreeformSubblockedModel(_BaseBlockModel): + """A model where regular parent blocks are divided into free-form sub-blocks.""" -class ArbitrarySubBlockModel(BaseBlockModel): - """Block model with arbitrary, variable sub-blocks""" + schema = "org.omf.v2.elements.blockmodel.freeform_subblocked" - parent_block_count = properties.List( - "Number of parent blocks along u, v, and w axes", - properties.Integer("", min=1), - min_length=3, - max_length=3, - ) - parent_block_size = properties.List( - "Size of parent blocks in the u, v, and w dimensions", - properties.Float("", min=0), - min_length=3, - max_length=3, - ) - cbc = ArrayInstanceProperty( - "Compressed block count - for arbitrary sub block models this must " - "have length equal to the product of parent_block_count and each " - "value must be equal to the number of sub blocks within the " - "corresponding parent block, 1 (if attributes exist on the parent " - "block) or 0; the default is an array of 1s", - shape=("*",), - dtype=(int, bool), - ) - sub_block_corners = ArrayInstanceProperty( - "Block corners normalized 0-1 relative to parent block", - shape=("*", 3), - dtype=float, + parent_block_count = _BlockCount("Number of parent blocks along u, v, and w axes") + parent_block_size = _BlockSize( + "Size of parent blocks in the u, v, and w directions" ) - sub_block_sizes = ArrayInstanceProperty( - "Block widths normalized 0-1 relative to parent block", + subblock_parent_indices = properties.Array( + "The parent block IJK index of each sub-block", shape=("*", 3), - dtype=float, - ) - - _valid_locations = ("parent_blocks", "sub_blocks") - - @properties.Array( - "Compressed block index - used for indexing attributes " - "into the sub block model; must have length equal to the " - "product of parent_block_count plus 1 and monotonically increasing", - shape=("*",), dtype=int, - coerce=False, ) - def cbi(self): - """Compressed block index""" - if self.cbc is None: - return None - cbi = np.r_[ - np.array([0], dtype=np.uint64), - np.cumsum(self.cbc, dtype=np.uint64), - ] - return cbi - - @properties.Array( - "Block centroids normalized 0-1 relative to parent block", - shape=("*", 3), - dtype=float, - coerce=False, - ) - def sub_block_centroids(self): - """Block centroids normalized 0-1 relative to parent block + subblock_corners = properties.Array( + """The positions of the sub-block corners within their parent block. - Computed from sub_block_corners and sub_block_sizes - """ - if self.sub_block_corners is None or self.sub_block_sizes is None: - return None - return self.sub_block_corners.array + self.sub_block_sizes.array / 2 - - @properties.Array( - "Block corners relative to parent block", - shape=("*", 3), - dtype=float, - coerce=False, - ) - def sub_block_corners_absolute(self): - """Block corners relative to parent block + Positions are relative to the parent block, with 0.0 being the minimum side and 1.0 the + maximum side. - Computed from sub_block_corners and sub_block_sizes - """ - if self.sub_block_corners is None or self.parent_block_size is None: - return None - cbc = self.cbc - all_indices = np.array(range(len(cbc)), dtype=np.uint64) - unique_parent_ijks = self.indices_to_ijk_array(all_indices) - parent_ijks = np.repeat(unique_parent_ijks, cbc, axis=0) - corners = parent_ijks + self.sub_block_corners - return corners * self.parent_block_size - - @properties.Array( - "Block centroids relative to parent block", - shape=("*", 3), + Sub-blocks must stay within the parent block and should not overlap. Gaps are allowed + but it will be impossible for 'cell' attributes to assign values to those areas. + """, + shape=("*", 6), dtype=float, - coerce=False, ) - def sub_block_centroids_absolute(self): - """Block centroids relative to parent block - - Computed from sub_block_corners and sub_block_sizes - """ - if self.sub_block_centroids is None or self.parent_block_size is None: - return None - cbc = self.cbc - all_indices = np.array(range(len(cbc)), dtype=np.uint64) - unique_parent_ijks = self.indices_to_ijk_array(all_indices) - parent_ijks = np.repeat(unique_parent_ijks, cbc, axis=0) - centroids = parent_ijks + self.sub_block_centroids - return centroids * self.parent_block_size - - @properties.Array( - "Block widths relative to parent block", - shape=("*", 3), - dtype=float, - coerce=False, + subblock_definition = properties.Instance( + "Defines the structure of sub-blocks within each parent block for this model.", + FreeformSubblockDefinition, ) - def sub_block_sizes_absolute(self): - """Block widths relative to parent block - - Computed from sub_block_corners and sub_block_sizes - """ - if self.sub_block_sizes is None or self.parent_block_size is None: - return None - return self.sub_block_sizes.array * self.parent_block_size - - @properties.validator("parent_block_size") - def _validate_size_is_not_zero(self, change): - """Ensure parent blocks are non-zero""" - if 0 in change["value"]: - raise properties.ValidationError( - "Block size cannot be 0", - prop="parent_block_size", - instance=self, - reason="invalid", - ) - - @properties.validator("cbc") - def validate_cbc(self, change): - """Ensure cbc is correct size and values""" - value = change["value"] - if not self.parent_block_count: - pass - elif len(value.array) != np.prod(self.parent_block_count): - raise properties.ValidationError( - "cbc must have length equal to the product of parent_block_count", - prop="cbc", - instance=self, - reason="invalid", - ) - if np.min(value.array) < 0: - raise properties.ValidationError( - "cbc values must be non-negative", - prop="cbc", - instance=self, - reason="invalid", - ) - return value - - def validate_sub_block_attributes(self, value, prop_name): - """Ensure value is correct length""" - cbi = self.cbi - if cbi is None: - return value - if len(value) != cbi[-1]: - raise properties.ValidationError( - "{} attributes must have length equal to " - "total number of sub blocks".format(prop_name), - prop=prop_name, - instance=self, - reason="invalid", - ) - return value - - @properties.validator("sub_block_corners") - def _validate_sub_block_corners(self, change): - """Validate sub block corners array is correct length""" - change["value"] = self.validate_sub_block_attributes( - change["value"], "sub_block_corners" - ) - - @properties.validator("sub_block_sizes") - def _validate_sub_block_sizes(self, change): - """Validate sub block size array is correct length and positive""" - value = self.validate_sub_block_attributes(change["value"], "sub_block_sizes") - if np.min(value.array) <= 0: - raise properties.ValidationError( - "sub block sizes must be positive", - prop="sub_block_sizes", - instance=self, - reason="invalid", - ) - - @property - def num_cells(self): - """Number of cells from last value in the compressed block index""" - cbi = self.cbi - if cbi is None: - return None - return cbi[-1] # pylint: disable=E1136 - - def location_length(self, location): - """Return correct attribute length based on location""" - if location == "parent_blocks": - return np.sum(self.cbc.array.astype(bool)) - return self.num_cells - - def reset_cbc(self): - """Reset cbc to no sub-blocks""" - if not self.parent_block_count: - raise ValueError("cannot reset cbc until parent_block_count is set") - cbc_len = np.prod(self.parent_block_count) - self.cbc = np.ones(cbc_len, dtype=np.uint32) diff --git a/omf/composite.py b/omf/composite.py index cf0e860e..d50bc067 100644 --- a/omf/composite.py +++ b/omf/composite.py @@ -3,10 +3,9 @@ from .base import ProjectElement from .blockmodel import ( - ArbitrarySubBlockModel, - OctreeSubBlockModel, + FreeformSubblockedModel, RegularBlockModel, - RegularSubBlockModel, + SubblockedModel, TensorGridBlockModel, ) from .lineset import LineSet @@ -33,14 +32,13 @@ class is created, then create an identical subclass so the docs prop=properties.Union( "", ( - RegularBlockModel, - RegularSubBlockModel, - OctreeSubBlockModel, - TensorGridBlockModel, - ArbitrarySubBlockModel, + FreeformSubblockedModel, LineSet, PointSet, + RegularBlockModel, + SubblockedModel, Surface, + TensorGridBlockModel, TensorGridSurface, ), ), diff --git a/tests/test_blockmodel.py b/tests/test_blockmodel.py index f2b7cfa9..cb50ce5f 100644 --- a/tests/test_blockmodel.py +++ b/tests/test_blockmodel.py @@ -6,13 +6,10 @@ import omf -class BlockModelTester(omf.blockmodel.BaseBlockModel): - """Dummy Block Model class for overriding parent_block_count""" - - parent_block_count = None - - def location_length(self, location): - return 0 +def _make_regular(count): + return omf.blockmodel.RegularBlockModel( + block_count=count, block_size=[1.0, 1.0, 1.0] + ) class MockArray(omf.base.BaseModel): @@ -24,47 +21,38 @@ class MockArray(omf.base.BaseModel): def test_ijk_index_errors(): """Test ijk indexing into parent blocks errors as expected""" - block_model = BlockModelTester() - with pytest.raises(AttributeError): - block_model.ijk_to_index([0, 0, 0]) - with pytest.raises(AttributeError): - block_model.index_to_ijk(0) - block_model.parent_block_count = [3, 4, 5] - with pytest.raises(ValueError): + block_model = _make_regular([3, 4, 5]) + with pytest.raises(TypeError): block_model.ijk_to_index("a") - with pytest.raises(ValueError): + with pytest.raises(TypeError): block_model.index_to_ijk("a") with pytest.raises(ValueError): block_model.ijk_to_index([0, 0]) - with pytest.raises(ValueError): - block_model.index_to_ijk([[1], [2]]) - with pytest.raises(ValueError): + with pytest.raises(TypeError): block_model.ijk_to_index([0, 0, 0.5]) - with pytest.raises(ValueError): + with pytest.raises(TypeError): block_model.index_to_ijk(0.5) - with pytest.raises(ValueError): + with pytest.raises(IndexError): block_model.ijk_to_index([0, 0, 5]) - with pytest.raises(ValueError): + with pytest.raises(IndexError): block_model.index_to_ijk(60) - - with pytest.raises(ValueError): - block_model.ijk_array_to_indices("a") - with pytest.raises(ValueError): - block_model.indices_to_ijk_array("a") - with pytest.raises(ValueError): - block_model.ijk_array_to_indices([[0, 0, 5], [0, 0, 3]]) - with pytest.raises(ValueError): - block_model.indices_to_ijk_array([0, 1, 60]) + with pytest.raises(IndexError): + block_model.ijk_to_index([[0, 0, 5], [0, 0, 3]]) + with pytest.raises(IndexError): + block_model.index_to_ijk([0, 1, 60]) def test_ijk_index_arrays(): """Test ijk array indexing into parent blocks works as expected""" - block_model = BlockModelTester() - block_model.parent_block_count = [3, 4, 5] - ijks = [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1), (2, 3, 4)] - indices = [0, 1, 3, 12, 59] - assert np.array_equal(block_model.ijk_array_to_indices(ijks), indices) - assert np.array_equal(block_model.indices_to_ijk_array(indices), ijks) + block_model = _make_regular([3, 4, 5]) + ijk = [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1), (2, 3, 4)] + index = [0, 1, 3, 12, 59] + assert np.array_equal(block_model.ijk_to_index(ijk), index) + assert np.array_equal(block_model.index_to_ijk(index), ijk) + ijk = [[(0, 0, 0), (1, 0, 0)], [(0, 1, 0), (0, 0, 1)]] + index = [(0, 1), (3, 12)] + assert np.array_equal(block_model.ijk_to_index(ijk), index) + assert np.array_equal(block_model.index_to_ijk(index), ijk) @pytest.mark.parametrize( @@ -73,43 +61,40 @@ def test_ijk_index_arrays(): ) def test_ijk_index(ijk, index): """Test ijk indexing into parent blocks works as expected""" - block_model = BlockModelTester() - block_model.parent_block_count = [3, 4, 5] + block_model = _make_regular([3, 4, 5]) assert block_model.ijk_to_index(ijk) == index assert np.array_equal(block_model.index_to_ijk(index), ijk) -def test_tensorblockmodel(): - """Test volume grid geometry validation""" - elem = omf.TensorGridBlockModel() - assert elem.num_nodes is None - assert elem.num_cells is None - assert elem.parent_block_count is None - elem.tensor_u = [1.0, 1.0] - elem.tensor_v = [2.0, 2.0, 2.0] - elem.tensor_w = [3.0] - assert elem.parent_block_count == [2, 3, 1] - assert elem.validate() - assert elem.location_length("vertices") == 24 - assert elem.location_length("cells") == 6 - elem.axis_v = [1.0, 1.0, 0] - with pytest.raises(ValueError): - elem.validate() - elem.axis_v = "Y" +# def test_tensorblockmodel(): +# """Test volume grid geometry validation""" +# elem = omf.TensorGridBlockModel() +# assert elem.num_nodes is None +# assert elem.num_cells is None +# assert elem.parent_block_count is None +# elem.tensor_u = [1.0, 1.0] +# elem.tensor_v = [2.0, 2.0, 2.0] +# elem.tensor_w = [3.0] +# assert elem.parent_block_count == [2, 3, 1] +# assert elem.validate() +# assert elem.location_length("vertices") == 24 +# assert elem.location_length("cells") == 6 +# elem.axis_v = [1.0, 1.0, 0] +# with pytest.raises(ValueError): +# elem.validate() +# elem.axis_v = "Y" # pylint: disable=W0143 class TestRegularBlockModel: """Test class for regular block model functionality""" - bm_class = omf.RegularBlockModel - @pytest.mark.parametrize( "block_count", ([2, 2], [2, 2, 2, 2], [0, 2, 2], [2, 2, 0.5]) ) def test_bad_block_count(self, block_count): """Test mismatched block_count""" - block_model = self.bm_class(block_size=[1.0, 2.0, 3.0]) + block_model = omf.RegularBlockModel(block_size=[1.0, 2.0, 3.0]) with pytest.raises(properties.ValidationError): block_model.block_count = block_count block_model.validate() @@ -119,569 +104,563 @@ def test_bad_block_count(self, block_count): ) def test_bad_block_size(self, block_size): """Test mismatched block_size""" - block_model = self.bm_class(block_count=[2, 2, 2]) + block_model = omf.RegularBlockModel(block_count=[2, 2, 2]) with pytest.raises(properties.ValidationError): block_model.block_size = block_size block_model.validate() def test_uninstantiated(self): """Test all attributes are None on instantiation""" - block_model = self.bm_class() + block_model = omf.RegularBlockModel() assert block_model.block_count is None assert block_model.block_size is None - assert block_model.cbc is None - assert block_model.cbi is None assert block_model.num_cells is None - with pytest.raises(ValueError): - block_model.reset_cbc() def test_num_cells(self): """Test num_cells calculation is correct""" - block_model = self.bm_class( + block_model = omf.RegularBlockModel( block_count=[2, 2, 2], block_size=[1.0, 2.0, 3.0], ) - assert block_model.parent_block_count == [2, 2, 2] - block_model.reset_cbc() - assert block_model.num_cells == 8 - assert block_model.location_length("") == 8 - block_model.cbc = np.array([0, 0, 0, 0, 1, 1, 1, 1]) - assert block_model.num_cells == 4 - - def test_cbc(self): - """Test cbc access and validation is correct""" - block_model = self.bm_class( - block_count=[2, 2, 2], - block_size=[1.0, 2.0, 3.0], - ) - block_model.reset_cbc() - assert block_model.validate() - assert np.all(block_model.cbc == np.ones(8)) - block_model.cbc.array[0] = 0 - assert block_model.validate() - with pytest.raises(properties.ValidationError): - block_model.cbc = np.ones(7, dtype="int8") - block_model.cbc = np.ones(8, dtype="uint8") - with pytest.raises(properties.ValidationError): - block_model.cbc.array[0] = 2 - block_model.validate() - with pytest.raises(properties.ValidationError): - block_model.cbc.array[0] = -1 - block_model.validate() - - def test_cbi(self): - """Test cbi access and validation is correct""" - block_model = self.bm_class() - assert block_model.cbi is None - block_model.block_count = [2, 2, 2] - block_model.block_size = [1.0, 2.0, 3.0] - block_model.reset_cbc() - assert np.all(block_model.cbi == np.array(range(9), dtype="int8")) - block_model.cbc.array[0] = 0 - assert np.all( - block_model.cbi - == np.r_[np.array([0], dtype="int8"), np.array(range(8), dtype="int8")] - ) - - -class TestRegularSubBlockModel: - """Test class for regular sub block model functionality""" - - bm_class = omf.RegularSubBlockModel - - @pytest.mark.parametrize( - "block_count", ([2, 2], [2, 2, 2, 2], [0, 2, 2], [2, 2, 0.5]) - ) - @pytest.mark.parametrize("attr", ("parent_block_count", "sub_block_count")) - def test_bad_block_count(self, block_count, attr): - """Test mismatched block_count""" - block_model = self.bm_class(parent_block_size=[1.0, 2.0, 3.0]) - with pytest.raises(properties.ValidationError): - setattr(block_model, attr, block_count) - block_model.validate() - - @pytest.mark.parametrize( - "block_size", ([2.0, 2.0], [2.0, 2.0, 2.0, 2.0], [-1.0, 2, 2], [0.0, 2, 2]) - ) - def test_bad_block_size(self, block_size): - """Test mismatched block_size""" - block_model = self.bm_class(parent_block_count=[2, 2, 2]) - with pytest.raises(properties.ValidationError): - block_model.parent_block_size = block_size - block_model.validate() - - def test_uninstantiated(self): - """Test all attributes are None on instantiation""" - block_model = self.bm_class() - assert block_model.parent_block_count is None - assert block_model.sub_block_count is None - assert block_model.parent_block_size is None - assert block_model.sub_block_size is None - assert block_model.cbc is None - assert block_model.cbi is None - assert block_model.num_cells is None - with pytest.raises(ValueError): - block_model.reset_cbc() - with pytest.raises(ValueError): - block_model.refine([0, 0, 0]) - block_model.validate_cbc({"value": MockArray()}) - - def test_num_cells(self): - """Test num_cells calculation is correct""" - block_model = self.bm_class( - parent_block_count=[2, 2, 2], - sub_block_count=[2, 2, 2], - parent_block_size=[1.0, 2.0, 3.0], - ) - block_model.reset_cbc() + np.testing.assert_array_equal(block_model.parent_block_count, [2, 2, 2]) assert block_model.num_cells == 8 - block_model.cbc = np.array([0, 0, 0, 0, 1, 1, 1, 1]) - assert block_model.num_cells == 4 - block_model.refine([1, 1, 1]) - assert block_model.num_cells == 11 - - def test_cbc(self): - """Test cbc access and validation is correct""" - block_model = self.bm_class( - parent_block_count=[2, 2, 2], - sub_block_count=[3, 4, 5], - parent_block_size=[1.0, 2.0, 3.0], - ) - block_model.reset_cbc() - assert block_model.validate() - assert np.all(block_model.cbc == np.ones(8)) - block_model.cbc.array[0] = 0 - assert block_model.validate() - block_model.cbc.array[0] = 60 - assert block_model.validate() - with pytest.raises(properties.ValidationError): - block_model.cbc = np.ones(7, dtype="int8") - block_model.cbc = np.ones(8, dtype="uint8") - with pytest.raises(properties.ValidationError): - block_model.cbc.array[0] = 2 - block_model.validate() - with pytest.raises(properties.ValidationError): - block_model.cbc.array[0] = -1 - block_model.validate() - - def test_cbi(self): - """Test cbi access and validation is correct""" - block_model = self.bm_class() - assert block_model.cbi is None - block_model.parent_block_count = [2, 2, 2] - block_model.sub_block_count = [3, 4, 5] - block_model.parent_block_size = [1.0, 2.0, 3.0] - block_model.reset_cbc() - assert np.all(block_model.cbi == np.array(range(9), dtype="int8")) - block_model.cbc.array[0] = 0 - assert np.all( - block_model.cbi - == np.r_[np.array([0], dtype="int8"), np.array(range(8), dtype="int8")] - ) - block_model.refine([1, 0, 0]) - assert np.all( - block_model.cbi - == np.r_[ - np.array([0, 0], dtype="int8"), np.array(range(60, 67), dtype="int8") - ] - ) - - def test_location_length(self): - """Ensure location length updates as expected with block refinement""" - block_model = self.bm_class( - parent_block_count=[2, 2, 2], - sub_block_count=[3, 4, 5], - parent_block_size=[1.0, 2.0, 3.0], - ) - block_model.reset_cbc() + assert block_model.location_length("cells") == 8 assert block_model.location_length("parent_blocks") == 8 - assert block_model.location_length("sub_blocks") == 8 - block_model.refine([0, 0, 0]) - assert block_model.location_length("parent_blocks") == 8 - assert block_model.location_length("sub_blocks") == 67 - - -class TestOctreeSubBlockModel: - """Test class for octree sub block model""" - bm_class = omf.OctreeSubBlockModel - - @pytest.mark.parametrize( - "block_count", ([2, 2], [2, 2, 2, 2], [0, 2, 2], [2, 2, 0.5]) - ) - def test_bad_block_count(self, block_count): - """Test mismatched block_count""" - block_model = self.bm_class(parent_block_size=[1.0, 2.0, 3.0]) - with pytest.raises(properties.ValidationError): - block_model.parent_block_size = block_count - block_model.validate() - - @pytest.mark.parametrize( - "block_size", ([2.0, 2.0], [2.0, 2.0, 2.0, 2.0], [-1.0, 2, 2], [0.0, 2, 2]) - ) - def test_bad_block_size(self, block_size): - """Test mismatched block_size""" - block_model = self.bm_class(parent_block_count=[2, 2, 2]) - with pytest.raises(properties.ValidationError): - block_model.parent_block_count = block_size - block_model.validate() - - def test_uninstantiated(self): - """Test all attributes are None on instantiation""" - block_model = self.bm_class() - assert block_model.parent_block_count is None - assert block_model.parent_block_size is None - assert block_model.cbc is None - assert block_model.cbi is None - assert block_model.zoc is None - assert block_model.num_cells is None - block_model.validate_cbc({"value": MockArray()}) - block_model.validate_zoc({"value": MockArray()}) - with pytest.raises(ValueError): - block_model.reset_cbc() - with pytest.raises(ValueError): - block_model.reset_zoc() - - def test_num_cells(self): - """Test num_cells calculation is correct""" - block_model = self.bm_class( - parent_block_count=[2, 2, 2], - parent_block_size=[1.0, 2.0, 3.0], - ) - block_model.reset_cbc() - assert block_model.num_cells == 8 - block_model.cbc = np.array([0, 0, 0, 0, 1, 1, 1, 1]) - assert block_model.num_cells == 4 - - def test_cbc(self): - """Test cbc access and validation is correct""" - block_model = self.bm_class( - parent_block_count=[2, 2, 2], - parent_block_size=[1.0, 2.0, 3.0], - ) - block_model.reset_cbc() - block_model.reset_zoc() - assert block_model.validate() - assert np.all(block_model.cbc == np.ones(8)) - block_model.cbc.array[0] = 0 - block_model.zoc = block_model.zoc[1:] - assert block_model.validate() - with pytest.raises(properties.ValidationError): - block_model.cbc = np.ones(7, dtype="int8") - block_model.cbc = np.ones(8, dtype="uint8") - block_model.zoc = np.zeros(8, dtype="uint8") - assert block_model.validate() - with pytest.raises(properties.ValidationError): - block_model.cbc.array[0] = 2 - block_model.validate() - with pytest.raises(properties.ValidationError): - block_model.cbc.array[0] = -1 - block_model.validate() - - def test_cbi(self): - """Test cbi access and validation is correct""" - block_model = self.bm_class() - assert block_model.cbi is None - block_model.parent_block_count = [2, 2, 2] - block_model.parent_block_size = [1.0, 2.0, 3.0] - block_model.reset_cbc() - assert np.all(block_model.cbi == np.array(range(9), dtype=np.uint64)) - block_model.cbc.array[0] = 0 - assert np.all( - block_model.cbi - == np.r_[ - np.array([0], dtype=np.uint64), np.array(range(8), dtype=np.uint64) - ] - ) - - def test_zoc(self): - """Test z-order curves""" - block_model = self.bm_class( - parent_block_count=[2, 2, 2], - parent_block_size=[1.0, 2.0, 3.0], - ) - block_model.reset_cbc() - block_model.reset_zoc() - assert np.all(block_model.zoc == np.zeros(8)) - with pytest.raises(properties.ValidationError): - block_model.zoc = np.zeros(7, dtype=np.uint64) - with pytest.raises(properties.ValidationError): - block_model.zoc = np.r_[np.zeros(7), -1.0].astype(np.uint64) - with pytest.raises(properties.ValidationError): - block_model.zoc = np.r_[np.zeros(7), 268435448 + 1].astype(np.uint64) - block_model.zoc = np.r_[np.zeros(7), 268435448].astype(np.uint64) - assert block_model.validate() - - @pytest.mark.parametrize( - ("pointer", "level", "curve_value"), - [ - ([1, 16, 0], 7, 131095), - ([0, 0, 0], 0, 0), - ([255, 255, 255], 8, 268435448), - ], - ) - def test_curve_values(self, pointer, level, curve_value): - """Test curve value functions""" - assert self.bm_class.get_curve_value(pointer, level) == curve_value - assert self.bm_class.get_level(curve_value) == level - assert self.bm_class.get_pointer(curve_value) == pointer - - def test_level_width(self): - """Test level width function""" - with pytest.raises(ValueError): - self.bm_class.level_width(9) - - def test_refinement(self): - """Test refinement method""" - block_model = self.bm_class( - parent_block_count=[2, 2, 2], - parent_block_size=[5.0, 5.0, 5.0], - ) - block_model.reset_cbc() - block_model.reset_zoc() - assert len(block_model.zoc) == 8 - assert all(zoc == 0 for zoc in block_model.zoc) - block_model.refine(0) - assert len(block_model.zoc) == 15 - assert block_model.location_length("parent_blocks") == 8 - assert block_model.location_length("") == 15 - assert np.array_equal(block_model.cbc, [8] + [1] * 7) - assert np.array_equal(block_model.cbi, [0] + list(range(8, 16))) - assert np.array_equal( - block_model.zoc, - [ - block_model.get_curve_value([0, 0, 0], 1), - block_model.get_curve_value([128, 0, 0], 1), - block_model.get_curve_value([0, 128, 0], 1), - block_model.get_curve_value([128, 128, 0], 1), - block_model.get_curve_value([0, 0, 128], 1), - block_model.get_curve_value([128, 0, 128], 1), - block_model.get_curve_value([0, 128, 128], 1), - block_model.get_curve_value([128, 128, 128], 1), - ] - + [0] * 7, - ) - block_model.refine(2, refinements=2) - assert len(block_model.zoc) == 78 - assert np.array_equal(block_model.cbc, [71] + [1] * 7) - assert np.array_equal(block_model.cbi, [0] + list(range(71, 79))) - assert block_model.zoc[2] == block_model.get_curve_value([0, 128, 0], 3) - assert block_model.zoc[3] == block_model.get_curve_value([32, 128, 0], 3) - assert block_model.zoc[4] == block_model.get_curve_value([0, 160, 0], 3) - assert block_model.zoc[5] == block_model.get_curve_value([32, 160, 0], 3) - assert block_model.zoc[6] == block_model.get_curve_value([0, 128, 32], 3) - assert block_model.zoc[64] == block_model.get_curve_value([64, 224, 96], 3) - assert block_model.zoc[65] == block_model.get_curve_value([96, 224, 96], 3) - assert block_model.zoc[66] == block_model.get_curve_value([128, 128, 0], 1) - block_model.refine(0, [1, 0, 0]) - assert len(block_model.zoc) == 85 - assert np.array_equal(block_model.cbc, [71, 8] + [1] * 6) - with pytest.raises(ValueError): - block_model.refine(85) - with pytest.raises(ValueError): - block_model.refine(-1) - with pytest.raises(ValueError): - block_model.refine(1, [1, 1, 1]) - with pytest.raises(ValueError): - block_model.refine(2, refinements=-1) - with pytest.raises(ValueError): - block_model.refine(2, refinements=6) - - -class TestArbitrarySubBlockModel: - """Test class for ArbitrarySubBlockModel""" - - bm_class = omf.ArbitrarySubBlockModel - - @pytest.mark.parametrize( - "block_count", ([2, 2], [2, 2, 2, 2], [0, 2, 2], [2, 2, 0.5]) - ) - def test_bad_block_count(self, block_count): - """Test mismatched block_count""" - block_model = self.bm_class(parent_block_size=[1.0, 2.0, 3.0]) - with pytest.raises(properties.ValidationError): - block_model.parent_block_size = block_count - block_model.validate() - - @pytest.mark.parametrize( - "block_size", ([2.0, 2.0], [2.0, 2.0, 2.0, 2.0], [-1.0, 2, 2], [0.0, 2, 2]) - ) - def test_bad_block_size(self, block_size): - """Test mismatched block_size""" - block_model = self.bm_class(parent_block_count=[2, 2, 2]) - with pytest.raises(properties.ValidationError): - block_model.parent_block_count = block_size - block_model.validate() - - def test_uninstantiated(self): - """Test all attributes are None on instantiation""" - block_model = self.bm_class() - assert block_model.parent_block_count is None - assert block_model.parent_block_size is None - assert block_model.cbc is None - assert block_model.cbi is None - assert block_model.sub_block_corners is None - assert block_model.sub_block_sizes is None - assert block_model.sub_block_centroids is None - assert block_model.sub_block_corners_absolute is None - assert block_model.sub_block_sizes_absolute is None - assert block_model.sub_block_centroids_absolute is None - assert block_model.num_cells is None - block_model.validate_cbc({"value": MockArray()}) - with pytest.raises(ValueError): - block_model.reset_cbc() - - def test_num_cells(self): - """Test num_cells calculation is correct""" - block_model = self.bm_class( - parent_block_count=[2, 2, 2], - parent_block_size=[1.0, 2.0, 3.0], - ) - block_model.reset_cbc() - assert block_model.num_cells == 8 - block_model.cbc = np.array([0, 0, 0, 0, 1, 1, 1, 1]) - assert block_model.num_cells == 4 - - def test_cbc(self): - """Test cbc access and validation is correct""" - block_model = self.bm_class( - parent_block_count=[2, 2, 2], - parent_block_size=[1.0, 2.0, 3.0], - ) - with pytest.raises(properties.ValidationError): - block_model.validate() - block_model.sub_block_corners = np.zeros((8, 3)) - block_model.sub_block_sizes = np.ones((8, 3)) - block_model.reset_cbc() - assert block_model.validate() - assert np.all(block_model.cbc == np.ones(8)) - block_model.cbc.array[0] = 0 - with pytest.raises(properties.ValidationError): - block_model.validate() - block_model.sub_block_corners = np.zeros((7, 3)) - block_model.sub_block_sizes = np.ones((7, 3)) - assert block_model.validate() - with pytest.raises(properties.ValidationError): - block_model.cbc = np.ones(7, dtype="int8") - block_model.cbc = np.ones(8, dtype="uint8") - block_model.sub_block_corners = np.zeros((8, 3)) - block_model.sub_block_sizes = np.ones((8, 3)) - with pytest.raises(properties.ValidationError): - block_model.cbc.array[0] = 2 - block_model.validate() - with pytest.raises(properties.ValidationError): - block_model.cbc.array[0] = -1 - block_model.validate() - - def test_cbi(self): - """Test cbi access and validation is correct""" - block_model = self.bm_class() - assert block_model.cbi is None - block_model.parent_block_count = [2, 2, 2] - block_model.parent_block_size = [1.0, 2.0, 3.0] - block_model.reset_cbc() - assert np.all(block_model.cbi == np.array(range(9), dtype=np.uint64)) - block_model.cbc.array[0] = 0 - assert np.all( - block_model.cbi - == np.r_[ - np.array([0], dtype=np.uint64), np.array(range(8), dtype=np.uint64) - ] - ) - - def test_validate_sub_block_attrs(self): - """Test sub block attribute validation""" - block_model = self.bm_class() - value = [1, 2, 3] - assert block_model.validate_sub_block_attributes(value, "") is value - block_model.parent_block_count = [2, 2, 2] - block_model.parent_block_size = [1.0, 2.0, 3.0] - block_model.reset_cbc() - with pytest.raises(properties.ValidationError): - block_model.validate_sub_block_attributes(value, "") - - def test_validate_sub_block_sizes(self): - """Test sub block size validation""" - block_model = self.bm_class() - block_model.sub_block_sizes = [[1.0, 2, 3]] - with pytest.raises(properties.ValidationError): - block_model.sub_block_sizes = [[0.0, 1, 2]] - - def test_sub_block_attributes(self): - """Test sub block attributes""" - block_model = self.bm_class( - parent_block_count=[2, 2, 2], - parent_block_size=[1.0, 2.0, 3.0], - ) - block_model.reset_cbc() - with pytest.raises(properties.ValidationError): - block_model.sub_block_sizes = np.ones((3, 3)) - with pytest.raises(properties.ValidationError): - block_model.sub_block_sizes = np.r_[np.ones((7, 3)), [[1.0, 1.0, 0]]] - block_model.sub_block_sizes = np.ones((8, 3)) - assert np.array_equal( - block_model.sub_block_sizes_absolute, np.array([[1.0, 2.0, 3.0]] * 8) - ) - assert block_model.sub_block_centroids is None - assert block_model.sub_block_centroids_absolute is None - with pytest.raises(properties.ValidationError): - block_model.sub_block_corners = np.zeros((3, 3)) - block_model.sub_block_corners = np.zeros((8, 3)) - assert np.array_equal( - block_model.sub_block_corners_absolute, - np.array( - [ - [0.0, 0, 0], - [1.0, 0, 0], - [0.0, 2, 0], - [1.0, 2, 0], - [0.0, 0, 3], - [1.0, 0, 3], - [0.0, 2, 3], - [1.0, 2, 3], - ] - ), - ) - assert np.array_equal(block_model.sub_block_centroids, np.ones((8, 3)) * 0.5) - assert np.array_equal( - block_model.sub_block_centroids_absolute, - np.array( - [ - [0.5, 1, 1.5], - [1.5, 1, 1.5], - [0.5, 3, 1.5], - [1.5, 3, 1.5], - [0.5, 1, 4.5], - [1.5, 1, 4.5], - [0.5, 3, 4.5], - [1.5, 3, 4.5], - ] - ), - ) - assert block_model.validate() - assert block_model.location_length("parent_blocks") == 8 - assert block_model.location_length("") == 8 - block_model.cbc = np.array([1] + [0] * 7, dtype=int) - with pytest.raises(properties.ValidationError): - block_model.validate() - block_model.sub_block_corners = np.array([[-0.5, 2, 0]]) - block_model.sub_block_sizes = np.array([[0.5, 0.5, 2]]) - assert block_model.validate() - assert block_model.location_length("parent_blocks") == 1 - assert block_model.location_length("") == 1 - assert np.array_equal( - block_model.sub_block_centroids, np.array([[-0.25, 2.25, 1]]) - ) - assert np.array_equal( - block_model.sub_block_corners_absolute, np.array([[-0.5, 4, 0]]) - ) - assert np.array_equal( - block_model.sub_block_sizes_absolute, np.array([[0.5, 1, 6]]) - ) - assert np.array_equal( - block_model.sub_block_centroids_absolute, np.array([[-0.25, 4.5, 3]]) - ) - assert block_model.validate() + # def test_cbc(self): + # """Test cbc access and validation is correct""" + # block_model = omf.RegularBlockModel( + # block_count=[2, 2, 2], + # block_size=[1.0, 2.0, 3.0], + # ) + # block_model.reset_cbc() + # assert block_model.validate() + # assert np.all(block_model.cbc == np.ones(8)) + # block_model.cbc.array[0] = 0 + # assert block_model.validate() + # with pytest.raises(properties.ValidationError): + # block_model.cbc = np.ones(7, dtype="int8") + # block_model.cbc = np.ones(8, dtype="uint8") + # with pytest.raises(properties.ValidationError): + # block_model.cbc.array[0] = 2 + # block_model.validate() + # with pytest.raises(properties.ValidationError): + # block_model.cbc.array[0] = -1 + # block_model.validate() + + # def test_cbi(self): + # """Test cbi access and validation is correct""" + # block_model = omf.RegularBlockModel() + # assert block_model.cbi is None + # block_model.block_count = [2, 2, 2] + # block_model.block_size = [1.0, 2.0, 3.0] + # block_model.reset_cbc() + # assert np.all(block_model.cbi == np.array(range(9), dtype="int8")) + # block_model.cbc.array[0] = 0 + # assert np.all( + # block_model.cbi + # == np.r_[np.array([0], dtype="int8"), np.array(range(8), dtype="int8")] + # ) + + +# class TestRegularSubBlockModel: +# """Test class for regular sub block model functionality""" + +# bm_class = omf.RegularSubBlockModel + +# @pytest.mark.parametrize( +# "block_count", ([2, 2], [2, 2, 2, 2], [0, 2, 2], [2, 2, 0.5]) +# ) +# @pytest.mark.parametrize("attr", ("parent_block_count", "sub_block_count")) +# def test_bad_block_count(self, block_count, attr): +# """Test mismatched block_count""" +# block_model = self.bm_class(parent_block_size=[1.0, 2.0, 3.0]) +# with pytest.raises(properties.ValidationError): +# setattr(block_model, attr, block_count) +# block_model.validate() + +# @pytest.mark.parametrize( +# "block_size", ([2.0, 2.0], [2.0, 2.0, 2.0, 2.0], [-1.0, 2, 2], [0.0, 2, 2]) +# ) +# def test_bad_block_size(self, block_size): +# """Test mismatched block_size""" +# block_model = self.bm_class(parent_block_count=[2, 2, 2]) +# with pytest.raises(properties.ValidationError): +# block_model.parent_block_size = block_size +# block_model.validate() + +# def test_uninstantiated(self): +# """Test all attributes are None on instantiation""" +# block_model = self.bm_class() +# assert block_model.parent_block_count is None +# assert block_model.sub_block_count is None +# assert block_model.parent_block_size is None +# assert block_model.sub_block_size is None +# assert block_model.cbc is None +# assert block_model.cbi is None +# assert block_model.num_cells is None +# with pytest.raises(ValueError): +# block_model.reset_cbc() +# with pytest.raises(ValueError): +# block_model.refine([0, 0, 0]) +# block_model.validate_cbc({"value": MockArray()}) + +# def test_num_cells(self): +# """Test num_cells calculation is correct""" +# block_model = self.bm_class( +# parent_block_count=[2, 2, 2], +# sub_block_count=[2, 2, 2], +# parent_block_size=[1.0, 2.0, 3.0], +# ) +# block_model.reset_cbc() +# assert block_model.num_cells == 8 +# block_model.cbc = np.array([0, 0, 0, 0, 1, 1, 1, 1]) +# assert block_model.num_cells == 4 +# block_model.refine([1, 1, 1]) +# assert block_model.num_cells == 11 + +# def test_cbc(self): +# """Test cbc access and validation is correct""" +# block_model = self.bm_class( +# parent_block_count=[2, 2, 2], +# sub_block_count=[3, 4, 5], +# parent_block_size=[1.0, 2.0, 3.0], +# ) +# block_model.reset_cbc() +# assert block_model.validate() +# assert np.all(block_model.cbc == np.ones(8)) +# block_model.cbc.array[0] = 0 +# assert block_model.validate() +# block_model.cbc.array[0] = 60 +# assert block_model.validate() +# with pytest.raises(properties.ValidationError): +# block_model.cbc = np.ones(7, dtype="int8") +# block_model.cbc = np.ones(8, dtype="uint8") +# with pytest.raises(properties.ValidationError): +# block_model.cbc.array[0] = 2 +# block_model.validate() +# with pytest.raises(properties.ValidationError): +# block_model.cbc.array[0] = -1 +# block_model.validate() + +# def test_cbi(self): +# """Test cbi access and validation is correct""" +# block_model = self.bm_class() +# assert block_model.cbi is None +# block_model.parent_block_count = [2, 2, 2] +# block_model.sub_block_count = [3, 4, 5] +# block_model.parent_block_size = [1.0, 2.0, 3.0] +# block_model.reset_cbc() +# assert np.all(block_model.cbi == np.array(range(9), dtype="int8")) +# block_model.cbc.array[0] = 0 +# assert np.all( +# block_model.cbi +# == np.r_[np.array([0], dtype="int8"), np.array(range(8), dtype="int8")] +# ) +# block_model.refine([1, 0, 0]) +# assert np.all( +# block_model.cbi +# == np.r_[ +# np.array([0, 0], dtype="int8"), np.array(range(60, 67), dtype="int8") +# ] +# ) + +# def test_location_length(self): +# """Ensure location length updates as expected with block refinement""" +# block_model = self.bm_class( +# parent_block_count=[2, 2, 2], +# sub_block_count=[3, 4, 5], +# parent_block_size=[1.0, 2.0, 3.0], +# ) +# block_model.reset_cbc() +# assert block_model.location_length("parent_blocks") == 8 +# assert block_model.location_length("sub_blocks") == 8 +# block_model.refine([0, 0, 0]) +# assert block_model.location_length("parent_blocks") == 8 +# assert block_model.location_length("sub_blocks") == 67 + + +# class TestOctreeSubBlockModel: +# """Test class for octree sub block model""" + +# bm_class = omf.OctreeSubBlockModel + +# @pytest.mark.parametrize( +# "block_count", ([2, 2], [2, 2, 2, 2], [0, 2, 2], [2, 2, 0.5]) +# ) +# def test_bad_block_count(self, block_count): +# """Test mismatched block_count""" +# block_model = self.bm_class(parent_block_size=[1.0, 2.0, 3.0]) +# with pytest.raises(properties.ValidationError): +# block_model.parent_block_size = block_count +# block_model.validate() + +# @pytest.mark.parametrize( +# "block_size", ([2.0, 2.0], [2.0, 2.0, 2.0, 2.0], [-1.0, 2, 2], [0.0, 2, 2]) +# ) +# def test_bad_block_size(self, block_size): +# """Test mismatched block_size""" +# block_model = self.bm_class(parent_block_count=[2, 2, 2]) +# with pytest.raises(properties.ValidationError): +# block_model.parent_block_count = block_size +# block_model.validate() + +# def test_uninstantiated(self): +# """Test all attributes are None on instantiation""" +# block_model = self.bm_class() +# assert block_model.parent_block_count is None +# assert block_model.parent_block_size is None +# assert block_model.cbc is None +# assert block_model.cbi is None +# assert block_model.zoc is None +# assert block_model.num_cells is None +# block_model.validate_cbc({"value": MockArray()}) +# block_model.validate_zoc({"value": MockArray()}) +# with pytest.raises(ValueError): +# block_model.reset_cbc() +# with pytest.raises(ValueError): +# block_model.reset_zoc() + +# def test_num_cells(self): +# """Test num_cells calculation is correct""" +# block_model = self.bm_class( +# parent_block_count=[2, 2, 2], +# parent_block_size=[1.0, 2.0, 3.0], +# ) +# block_model.reset_cbc() +# assert block_model.num_cells == 8 +# block_model.cbc = np.array([0, 0, 0, 0, 1, 1, 1, 1]) +# assert block_model.num_cells == 4 + +# def test_cbc(self): +# """Test cbc access and validation is correct""" +# block_model = self.bm_class( +# parent_block_count=[2, 2, 2], +# parent_block_size=[1.0, 2.0, 3.0], +# ) +# block_model.reset_cbc() +# block_model.reset_zoc() +# assert block_model.validate() +# assert np.all(block_model.cbc == np.ones(8)) +# block_model.cbc.array[0] = 0 +# block_model.zoc = block_model.zoc[1:] +# assert block_model.validate() +# with pytest.raises(properties.ValidationError): +# block_model.cbc = np.ones(7, dtype="int8") +# block_model.cbc = np.ones(8, dtype="uint8") +# block_model.zoc = np.zeros(8, dtype="uint8") +# assert block_model.validate() +# with pytest.raises(properties.ValidationError): +# block_model.cbc.array[0] = 2 +# block_model.validate() +# with pytest.raises(properties.ValidationError): +# block_model.cbc.array[0] = -1 +# block_model.validate() + +# def test_cbi(self): +# """Test cbi access and validation is correct""" +# block_model = self.bm_class() +# assert block_model.cbi is None +# block_model.parent_block_count = [2, 2, 2] +# block_model.parent_block_size = [1.0, 2.0, 3.0] +# block_model.reset_cbc() +# assert np.all(block_model.cbi == np.array(range(9), dtype=np.uint64)) +# block_model.cbc.array[0] = 0 +# assert np.all( +# block_model.cbi +# == np.r_[ +# np.array([0], dtype=np.uint64), np.array(range(8), dtype=np.uint64) +# ] +# ) + +# def test_zoc(self): +# """Test z-order curves""" +# block_model = self.bm_class( +# parent_block_count=[2, 2, 2], +# parent_block_size=[1.0, 2.0, 3.0], +# ) +# block_model.reset_cbc() +# block_model.reset_zoc() +# assert np.all(block_model.zoc == np.zeros(8)) +# with pytest.raises(properties.ValidationError): +# block_model.zoc = np.zeros(7, dtype=np.uint64) +# with pytest.raises(properties.ValidationError): +# block_model.zoc = np.r_[np.zeros(7), -1.0].astype(np.uint64) +# with pytest.raises(properties.ValidationError): +# block_model.zoc = np.r_[np.zeros(7), 268435448 + 1].astype(np.uint64) +# block_model.zoc = np.r_[np.zeros(7), 268435448].astype(np.uint64) +# assert block_model.validate() + +# @pytest.mark.parametrize( +# ("pointer", "level", "curve_value"), +# [ +# ([1, 16, 0], 7, 131095), +# ([0, 0, 0], 0, 0), +# ([255, 255, 255], 8, 268435448), +# ], +# ) +# def test_curve_values(self, pointer, level, curve_value): +# """Test curve value functions""" +# assert self.bm_class.get_curve_value(pointer, level) == curve_value +# assert self.bm_class.get_level(curve_value) == level +# assert self.bm_class.get_pointer(curve_value) == pointer + +# def test_level_width(self): +# """Test level width function""" +# with pytest.raises(ValueError): +# self.bm_class.level_width(9) + +# def test_refinement(self): +# """Test refinement method""" +# block_model = self.bm_class( +# parent_block_count=[2, 2, 2], +# parent_block_size=[5.0, 5.0, 5.0], +# ) +# block_model.reset_cbc() +# block_model.reset_zoc() +# assert len(block_model.zoc) == 8 +# assert all(zoc == 0 for zoc in block_model.zoc) +# block_model.refine(0) +# assert len(block_model.zoc) == 15 +# assert block_model.location_length("parent_blocks") == 8 +# assert block_model.location_length("") == 15 +# assert np.array_equal(block_model.cbc, [8] + [1] * 7) +# assert np.array_equal(block_model.cbi, [0] + list(range(8, 16))) +# assert np.array_equal( +# block_model.zoc, +# [ +# block_model.get_curve_value([0, 0, 0], 1), +# block_model.get_curve_value([128, 0, 0], 1), +# block_model.get_curve_value([0, 128, 0], 1), +# block_model.get_curve_value([128, 128, 0], 1), +# block_model.get_curve_value([0, 0, 128], 1), +# block_model.get_curve_value([128, 0, 128], 1), +# block_model.get_curve_value([0, 128, 128], 1), +# block_model.get_curve_value([128, 128, 128], 1), +# ] +# + [0] * 7, +# ) +# block_model.refine(2, refinements=2) +# assert len(block_model.zoc) == 78 +# assert np.array_equal(block_model.cbc, [71] + [1] * 7) +# assert np.array_equal(block_model.cbi, [0] + list(range(71, 79))) +# assert block_model.zoc[2] == block_model.get_curve_value([0, 128, 0], 3) +# assert block_model.zoc[3] == block_model.get_curve_value([32, 128, 0], 3) +# assert block_model.zoc[4] == block_model.get_curve_value([0, 160, 0], 3) +# assert block_model.zoc[5] == block_model.get_curve_value([32, 160, 0], 3) +# assert block_model.zoc[6] == block_model.get_curve_value([0, 128, 32], 3) +# assert block_model.zoc[64] == block_model.get_curve_value([64, 224, 96], 3) +# assert block_model.zoc[65] == block_model.get_curve_value([96, 224, 96], 3) +# assert block_model.zoc[66] == block_model.get_curve_value([128, 128, 0], 1) +# block_model.refine(0, [1, 0, 0]) +# assert len(block_model.zoc) == 85 +# assert np.array_equal(block_model.cbc, [71, 8] + [1] * 6) +# with pytest.raises(ValueError): +# block_model.refine(85) +# with pytest.raises(ValueError): +# block_model.refine(-1) +# with pytest.raises(ValueError): +# block_model.refine(1, [1, 1, 1]) +# with pytest.raises(ValueError): +# block_model.refine(2, refinements=-1) +# with pytest.raises(ValueError): +# block_model.refine(2, refinements=6) + + +# class TestArbitrarySubBlockModel: +# """Test class for ArbitrarySubBlockModel""" + +# bm_class = omf.ArbitrarySubBlockModel + +# @pytest.mark.parametrize( +# "block_count", ([2, 2], [2, 2, 2, 2], [0, 2, 2], [2, 2, 0.5]) +# ) +# def test_bad_block_count(self, block_count): +# """Test mismatched block_count""" +# block_model = self.bm_class(parent_block_size=[1.0, 2.0, 3.0]) +# with pytest.raises(properties.ValidationError): +# block_model.parent_block_size = block_count +# block_model.validate() + +# @pytest.mark.parametrize( +# "block_size", ([2.0, 2.0], [2.0, 2.0, 2.0, 2.0], [-1.0, 2, 2], [0.0, 2, 2]) +# ) +# def test_bad_block_size(self, block_size): +# """Test mismatched block_size""" +# block_model = self.bm_class(parent_block_count=[2, 2, 2]) +# with pytest.raises(properties.ValidationError): +# block_model.parent_block_count = block_size +# block_model.validate() + +# def test_uninstantiated(self): +# """Test all attributes are None on instantiation""" +# block_model = self.bm_class() +# assert block_model.parent_block_count is None +# assert block_model.parent_block_size is None +# assert block_model.cbc is None +# assert block_model.cbi is None +# assert block_model.sub_block_corners is None +# assert block_model.sub_block_sizes is None +# assert block_model.sub_block_centroids is None +# assert block_model.sub_block_corners_absolute is None +# assert block_model.sub_block_sizes_absolute is None +# assert block_model.sub_block_centroids_absolute is None +# assert block_model.num_cells is None +# block_model.validate_cbc({"value": MockArray()}) +# with pytest.raises(ValueError): +# block_model.reset_cbc() + +# def test_num_cells(self): +# """Test num_cells calculation is correct""" +# block_model = self.bm_class( +# parent_block_count=[2, 2, 2], +# parent_block_size=[1.0, 2.0, 3.0], +# ) +# block_model.reset_cbc() +# assert block_model.num_cells == 8 +# block_model.cbc = np.array([0, 0, 0, 0, 1, 1, 1, 1]) +# assert block_model.num_cells == 4 + +# def test_cbc(self): +# """Test cbc access and validation is correct""" +# block_model = self.bm_class( +# parent_block_count=[2, 2, 2], +# parent_block_size=[1.0, 2.0, 3.0], +# ) +# with pytest.raises(properties.ValidationError): +# block_model.validate() +# block_model.sub_block_corners = np.zeros((8, 3)) +# block_model.sub_block_sizes = np.ones((8, 3)) +# block_model.reset_cbc() +# assert block_model.validate() +# assert np.all(block_model.cbc == np.ones(8)) +# block_model.cbc.array[0] = 0 +# with pytest.raises(properties.ValidationError): +# block_model.validate() +# block_model.sub_block_corners = np.zeros((7, 3)) +# block_model.sub_block_sizes = np.ones((7, 3)) +# assert block_model.validate() +# with pytest.raises(properties.ValidationError): +# block_model.cbc = np.ones(7, dtype="int8") +# block_model.cbc = np.ones(8, dtype="uint8") +# block_model.sub_block_corners = np.zeros((8, 3)) +# block_model.sub_block_sizes = np.ones((8, 3)) +# with pytest.raises(properties.ValidationError): +# block_model.cbc.array[0] = 2 +# block_model.validate() +# with pytest.raises(properties.ValidationError): +# block_model.cbc.array[0] = -1 +# block_model.validate() + +# def test_cbi(self): +# """Test cbi access and validation is correct""" +# block_model = self.bm_class() +# assert block_model.cbi is None +# block_model.parent_block_count = [2, 2, 2] +# block_model.parent_block_size = [1.0, 2.0, 3.0] +# block_model.reset_cbc() +# assert np.all(block_model.cbi == np.array(range(9), dtype=np.uint64)) +# block_model.cbc.array[0] = 0 +# assert np.all( +# block_model.cbi +# == np.r_[ +# np.array([0], dtype=np.uint64), np.array(range(8), dtype=np.uint64) +# ] +# ) + +# def test_validate_sub_block_attrs(self): +# """Test sub block attribute validation""" +# block_model = self.bm_class() +# value = [1, 2, 3] +# assert block_model.validate_sub_block_attributes(value, "") is value +# block_model.parent_block_count = [2, 2, 2] +# block_model.parent_block_size = [1.0, 2.0, 3.0] +# block_model.reset_cbc() +# with pytest.raises(properties.ValidationError): +# block_model.validate_sub_block_attributes(value, "") + +# def test_validate_sub_block_sizes(self): +# """Test sub block size validation""" +# block_model = self.bm_class() +# block_model.sub_block_sizes = [[1.0, 2, 3]] +# with pytest.raises(properties.ValidationError): +# block_model.sub_block_sizes = [[0.0, 1, 2]] + +# def test_sub_block_attributes(self): +# """Test sub block attributes""" +# block_model = self.bm_class( +# parent_block_count=[2, 2, 2], +# parent_block_size=[1.0, 2.0, 3.0], +# ) +# block_model.reset_cbc() +# with pytest.raises(properties.ValidationError): +# block_model.sub_block_sizes = np.ones((3, 3)) +# with pytest.raises(properties.ValidationError): +# block_model.sub_block_sizes = np.r_[np.ones((7, 3)), [[1.0, 1.0, 0]]] +# block_model.sub_block_sizes = np.ones((8, 3)) +# assert np.array_equal( +# block_model.sub_block_sizes_absolute, np.array([[1.0, 2.0, 3.0]] * 8) +# ) +# assert block_model.sub_block_centroids is None +# assert block_model.sub_block_centroids_absolute is None +# with pytest.raises(properties.ValidationError): +# block_model.sub_block_corners = np.zeros((3, 3)) +# block_model.sub_block_corners = np.zeros((8, 3)) +# assert np.array_equal( +# block_model.sub_block_corners_absolute, +# np.array( +# [ +# [0.0, 0, 0], +# [1.0, 0, 0], +# [0.0, 2, 0], +# [1.0, 2, 0], +# [0.0, 0, 3], +# [1.0, 0, 3], +# [0.0, 2, 3], +# [1.0, 2, 3], +# ] +# ), +# ) +# assert np.array_equal(block_model.sub_block_centroids, np.ones((8, 3)) * 0.5) +# assert np.array_equal( +# block_model.sub_block_centroids_absolute, +# np.array( +# [ +# [0.5, 1, 1.5], +# [1.5, 1, 1.5], +# [0.5, 3, 1.5], +# [1.5, 3, 1.5], +# [0.5, 1, 4.5], +# [1.5, 1, 4.5], +# [0.5, 3, 4.5], +# [1.5, 3, 4.5], +# ] +# ), +# ) +# assert block_model.validate() +# assert block_model.location_length("parent_blocks") == 8 +# assert block_model.location_length("") == 8 +# block_model.cbc = np.array([1] + [0] * 7, dtype=int) +# with pytest.raises(properties.ValidationError): +# block_model.validate() +# block_model.sub_block_corners = np.array([[-0.5, 2, 0]]) +# block_model.sub_block_sizes = np.array([[0.5, 0.5, 2]]) +# assert block_model.validate() +# assert block_model.location_length("parent_blocks") == 1 +# assert block_model.location_length("") == 1 +# assert np.array_equal( +# block_model.sub_block_centroids, np.array([[-0.25, 2.25, 1]]) +# ) +# assert np.array_equal( +# block_model.sub_block_corners_absolute, np.array([[-0.5, 4, 0]]) +# ) +# assert np.array_equal( +# block_model.sub_block_sizes_absolute, np.array([[0.5, 1, 6]]) +# ) +# assert np.array_equal( +# block_model.sub_block_centroids_absolute, np.array([[-0.25, 4.5, 3]]) +# ) +# assert block_model.validate() # pylint: enable=W0143 From 01a856d53e4e7bba3376268b4c083cd77bd5d8d1 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Tue, 28 Feb 2023 12:25:11 +1300 Subject: [PATCH 04/42] Moved to sub-package. --- omf/blockmodel/__init__.py | 10 ++++++++++ omf/{ => blockmodel}/blockmodel.py | 2 +- omf/blockmodel/models.py | 0 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 omf/blockmodel/__init__.py rename omf/{ => blockmodel}/blockmodel.py (99%) create mode 100644 omf/blockmodel/models.py diff --git a/omf/blockmodel/__init__.py b/omf/blockmodel/__init__.py new file mode 100644 index 00000000..f4e9490f --- /dev/null +++ b/omf/blockmodel/__init__.py @@ -0,0 +1,10 @@ +from .blockmodel import ( + FreeformSubblockDefinition, + FreeformSubblockedModel, + OctreeSubblockDefinition, + RegularBlockModel, + RegularSubblockDefinition, + SubblockedModel, + TensorGridBlockModel, + VariableZSubblockDefinition, +) diff --git a/omf/blockmodel.py b/omf/blockmodel/blockmodel.py similarity index 99% rename from omf/blockmodel.py rename to omf/blockmodel/blockmodel.py index e15d6d1d..07d4d44a 100644 --- a/omf/blockmodel.py +++ b/omf/blockmodel/blockmodel.py @@ -2,7 +2,7 @@ import numpy as np import properties -from .base import ProjectElement +from ..base import ProjectElement def _shrink_uint(obj, attr): diff --git a/omf/blockmodel/models.py b/omf/blockmodel/models.py new file mode 100644 index 00000000..e69de29b From 8448b1665e738a6679df58185a87866ee4d4fe53 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Tue, 28 Feb 2023 12:41:42 +1300 Subject: [PATCH 05/42] Split up. --- omf/__init__.py | 26 +- omf/blockmodel/__init__.py | 10 - omf/blockmodel/_properties.py | 53 ++++ omf/blockmodel/blockmodel.py | 410 -------------------------- omf/blockmodel/regular.py | 111 +++++++ omf/blockmodel/subblock_definition.py | 53 ++++ omf/blockmodel/subblocked.py | 128 ++++++++ omf/blockmodel/tensor.py | 75 +++++ omf/composite.py | 9 +- tests/test_blockmodel.py | 4 +- 10 files changed, 436 insertions(+), 443 deletions(-) create mode 100644 omf/blockmodel/_properties.py delete mode 100644 omf/blockmodel/blockmodel.py create mode 100644 omf/blockmodel/regular.py create mode 100644 omf/blockmodel/subblock_definition.py create mode 100644 omf/blockmodel/subblocked.py create mode 100644 omf/blockmodel/tensor.py diff --git a/omf/__init__.py b/omf/__init__.py index fdc629f0..9d5b4d71 100644 --- a/omf/__init__.py +++ b/omf/__init__.py @@ -1,16 +1,4 @@ """omf: API library for Open Mining Format file interchange format""" -from .base import Project -from .blockmodel import ( - FreeformSubblockDefinition, - FreeformSubblockedModel, - OctreeSubblockDefinition, - RegularBlockModel, - RegularSubblockDefinition, - SubblockedModel, - TensorGridBlockModel, - VariableZSubblockDefinition, -) -from .composite import Composite from .attribute import ( Array, CategoryAttribute, @@ -21,13 +9,23 @@ StringAttribute, VectorAttribute, ) +from .base import Project +from .blockmodel.regular import RegularBlockModel +from .blockmodel.subblocked import SubblockedModel, FreeformSubblockedModel +from .blockmodel.subblock_definition import ( + FreeformSubblockDefinition, + OctreeSubblockDefinition, + RegularSubblockDefinition, + VariableZSubblockDefinition, +) +from .blockmodel.tensor import TensorGridBlockModel +from .composite import Composite +from .fileio import load, save, __version__ from .lineset import LineSet from .pointset import PointSet from .surface import Surface, TensorGridSurface from .texture import ProjectedTexture, UVMappedTexture -from .fileio import load, save, __version__ - __author__ = "Global Mining Guidelines Group" __license__ = "MIT License" __copyright__ = "Copyright 2021 Global Mining Guidelines Group" diff --git a/omf/blockmodel/__init__.py b/omf/blockmodel/__init__.py index f4e9490f..e69de29b 100644 --- a/omf/blockmodel/__init__.py +++ b/omf/blockmodel/__init__.py @@ -1,10 +0,0 @@ -from .blockmodel import ( - FreeformSubblockDefinition, - FreeformSubblockedModel, - OctreeSubblockDefinition, - RegularBlockModel, - RegularSubblockDefinition, - SubblockedModel, - TensorGridBlockModel, - VariableZSubblockDefinition, -) diff --git a/omf/blockmodel/_properties.py b/omf/blockmodel/_properties.py new file mode 100644 index 00000000..4974dc1b --- /dev/null +++ b/omf/blockmodel/_properties.py @@ -0,0 +1,53 @@ +import numpy as np +import properties + + +class BlockCount(properties.Array): + def __init__(self, doc, **kw): + super().__init__(doc, **kw, dtype=int, shape=(3,)) + + def validate(self, instance, value): + """Check shape and dtype of the count and that items are >= min.""" + value = super().validate(instance, value) + for item in value: + if item < 1: + if instance is None: + msg = f"block counts must be >= 1" + else: + cls = instance.__class__.__name__ + msg = f"{cls}.{self.name} counts must be >= 1" + raise properties.ValidationError(msg, prop=self.name, instance=instance) + return value + + +class OctreeSubblockCount(BlockCount): + def validate(self, instance, value): + """Check shape and dtype of the count and that items are >= min.""" + value = super().validate(instance, value) + for item in value: + l = np.log2(item) + if np.trunc(l) != l: + if instance is None: + msg = f"octree block counts must be powers of two" + else: + cls = instance.__class__.__name__ + msg = f"{cls}.{self.name} octree counts must be powers of two" + raise properties.ValidationError(msg, prop=self.name, instance=instance) + return value + + +class BlockSize(properties.Array): + def __init__(self, doc, **kw): + super().__init__(doc, **kw, dtype=float, shape=(3,)) + + def validate(self, instance, value): + """Check shape and dtype of the count and that items are >= min.""" + value = super().validate(instance, value) + for item in value: + if item <= 0.0: + if instance is None: + msg = f"block size elements must be > 0.0" + else: + msg = f"{instance.__class__.__name__}.{self.name} elements must be > 0.0" + raise properties.ValidationError(msg, prop=self.name, instance=instance) + return value diff --git a/omf/blockmodel/blockmodel.py b/omf/blockmodel/blockmodel.py deleted file mode 100644 index 07d4d44a..00000000 --- a/omf/blockmodel/blockmodel.py +++ /dev/null @@ -1,410 +0,0 @@ -"""blockmodel2.py: New Block Model element definitions""" -import numpy as np -import properties - -from ..base import ProjectElement - - -def _shrink_uint(obj, attr): - arr = getattr(obj, attr) - assert arr.min() >= 0 - t = np.min_scalar_type(arr.max()) - setattr(obj, attr, arr.astype(t)) - - -class _BlockCount(properties.Array): - def __init__(self, doc, **kw): - super().__init__(doc, **kw, dtype=int, shape=(3,)) - - def validate(self, instance, value): - """Check shape and dtype of the count and that items are >= min.""" - value = super().validate(instance, value) - for item in value: - if item < 1: - if instance is None: - msg = f"block counts must be >= 1" - else: - cls = instance.__class__.__name__ - msg = f"{cls}.{self.name} counts must be >= 1" - raise properties.ValidationError(msg, prop=self.name, instance=instance) - return value - - -class _OctreeSubblockCount(_BlockCount): - def validate(self, instance, value): - """Check shape and dtype of the count and that items are >= min.""" - value = super().validate(instance, value) - for item in value: - l = np.log2(item) - if np.trunc(l) != l: - if instance is None: - msg = f"octree block counts must be powers of two" - else: - cls = instance.__class__.__name__ - msg = f"{cls}.{self.name} octree counts must be powers of two" - raise properties.ValidationError(msg, prop=self.name, instance=instance) - return value - - -class _BlockSize(properties.Array): - def __init__(self, doc, **kw): - super().__init__(doc, **kw, dtype=float, shape=(3,)) - - def validate(self, instance, value): - """Check shape and dtype of the count and that items are >= min.""" - value = super().validate(instance, value) - for item in value: - if item <= 0.0: - if instance is None: - msg = f"block size elements must be > 0.0" - else: - msg = f"{instance.__class__.__name__}.{self.name} elements must be > 0.0" - raise properties.ValidationError(msg, prop=self.name, instance=instance) - return value - - -class _BaseBlockModel(ProjectElement): - """Basic orientation properties and indexing for all block models""" - - 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, - ) - axis_w = properties.Vector3( - "Vector orientation of w-direction", - default="Z", - length=1, - ) - corner = properties.Vector3( - "Corner of the block model relative to Project coordinate reference system", - default="zero", - ) - _valid_locations = ("cells", "parent_blocks") - - @properties.validator - def _validate_axes(self): - """Check if mesh content is built correctly""" - if not ( - np.abs(self.axis_u.dot(self.axis_v) < 1e-6) - and np.abs(self.axis_v.dot(self.axis_w) < 1e-6) - and np.abs(self.axis_w.dot(self.axis_u) < 1e-6) - ): - raise ValueError("axis_u, axis_v, and axis_w must be orthogonal") - return True - - @property - def parent_block_count(self): - """Computed property for number of parent blocks - - This is required for ijk indexing. For tensor and regular block - models, all the blocks are considered parent blocks. - """ - raise NotImplementedError() - - def ijk_to_index(self, ijk): - """Map IJK triples to flat indices for a singoe triple or an array, preseving shape.""" - arr = np.asarray(ijk) - if arr.dtype.kind not in "ui": - raise TypeError(f"'ijk' must be integer typed, found {arr.dtype}") - match arr.shape: - case (*output_shape, 3): - shaped = arr.reshape(-1, 3) - case _: - raise ValueError( - "'ijk' must have 3 elements or be an array with shape (*_, 3)" - ) - count = self.parent_block_count - if (shaped < 0).any() or (shaped >= count).any(): - raise IndexError(f"0 <= ijk < ({count[0]}, {count[1]}, {count[2]}) failed") - indices = np.ravel_multi_index(multi_index=shaped.T, dims=count, order="F") - if output_shape == (): - return indices[0] - else: - return indices.reshape(output_shape) - - def index_to_ijk(self, index): - """Map flat indices to IJK triples for a singoe index or an array, preserving shape.""" - arr = np.asarray(index) - if arr.dtype.kind not in "ui": - raise TypeError(f"'index' must be integer typed, found {arr.dtype}") - output_shape = arr.shape + (3,) - shaped = arr.reshape(-1) - count = self.parent_block_count - if (shaped < 0).any() or (shaped >= np.prod(count)).any(): - raise IndexError(f"0 <= index < {np.prod(count)} failed") - ijk = np.unravel_index(indices=shaped, shape=count, order="F") - return np.c_[ijk[0], ijk[1], ijk[2]].reshape(output_shape) - - -class TensorGridBlockModel(_BaseBlockModel): - """Block model with variable spacing in each dimension. - - Unlike the rest of the block models attributes here can also be on the block vertices. - """ - - schema = "org.omf.v2.element.blockmodel.tensorgrid" - - _valid_locations = ("vertices",) + _BaseBlockModel._valid_locations - - tensor_u = properties.Array( - "Tensor cell widths, u-direction", - shape=("*",), - dtype=float, - ) - tensor_v = properties.Array( - "Tensor cell widths, v-direction", - shape=("*",), - dtype=float, - ) - tensor_w = properties.Array( - "Tensor cell widths, w-direction", - shape=("*",), - dtype=float, - ) - - @properties.validator("tensor_u") - @properties.validator("tensor_v") - @properties.validator("tensor_w") - def _validate_tensor(self, change): - tensor = change["value"] - if (tensor <= 0.0).any(): - raise properties.ValidationError( - "Tensor spacings must all be greater than zero", - prop=change["name"], - instance=self, - reason="invalid", - ) - - def _require_tensors(self): - if self.tensor_u is None or self.tensor_v is None or self.tensor_w is None: - raise ValueError("tensors haven't been set yet") - - @property - def parent_block_count(self): - self._require_tensors() - return np.array( - (len(self.tensor_u), len(self.tensor_v), len(self.tensor_w)), dtype=int - ) - - @property - def num_nodes(self): - """Number of nodes (vertices)""" - return np.prod(self.parent_block_count + 1) - - @property - def num_cells(self): - """Number of cells""" - return np.prod(self.parent_block_count) - - def location_length(self, location): - """Return correct attribute length for 'location'.""" - match location: - case "vertices": - return self.num_nodes - case "cells" | "parent_blocks" | "": - return self.num_cells - case _: - raise ValueError(f"unknown location type: {location!r}") - - -class RegularBlockModel(_BaseBlockModel): - """Block model with constant spacing in each dimension.""" - - schema = "org.omf.v2.elements.blockmodel.regular" - - block_count = _BlockCount("Number of blocks along u, v, and w axes") - block_size = _BlockSize("Size of blocks in the u, v, and w directions") - - @property - def num_cells(self): - """The number of cells, which in this case are always parent blocks.""" - return np.prod(self.parent_block_count) - - @property - def parent_block_count(self): - """Number of parent blocks equals number of blocks""" - return self.block_count - - def location_length(self, location): - """Return correct attribute length for 'location'.""" - match location: - case "cells" | "parent_blocks" | "": - return self.num_cells - case _: - raise ValueError(f"unknown location type: {location!r}") - - -class RegularSubblockDefinition: - """The simplest gridded sub-block definition.""" - - count = _BlockCount( - "The maximum number of sub-blocks inside a parent in each direction." - ) - - def validate_subblocks(self, _corners): - """Checks the sub-blocks within one parent block.""" - # XXX can we check for overlaps efficiently? - - -class OctreeSubblockDefinition(RegularSubblockDefinition): - """Sub-blocks form an octree inside the parent block. - - Cut the parent block in half in all directions to create eight sub-blocks. Repeat that - division for some or all of those new sub-blocks. Continue doing that until the limit - on sub-block count is reached or until the sub-blocks accurately model the inputs. - - This definition also allows the lower level cuts to be omitted in one or two axes, - giving a maximum sub-block count of (16, 16, 4) for example rather than requiring - all axes to be equal. - """ - - count = _OctreeSubblockCount( - "The maximum number of sub-blocks inside a parent in each direction." - ) - - def validate_subblocks(self, corners): - """Checks the sub-blocks within one parent block.""" - super().validate_subblocks(corners) - # TODO check that blocks lie on the octree - - -class SubblockedModel(_BaseBlockModel): - """A model where regular parent blocks are divided into sub-blocks that align with a grid. - - The sub-blocks here must align with a regular grid within the parent block. What that grid - is and how blocks are generated within it is defined by the sub-block definition. - """ - - schema = "org.omf.v2.elements.blockmodel.subblocked" - - parent_block_count = _BlockCount("Number of parent blocks along u, v, and w axes") - parent_block_size = _BlockSize( - "Size of parent blocks in the u, v, and w directions" - ) - subblock_parent_indices = properties.Array( - "The parent block IJK index of each sub-block", - shape=("*", 3), - dtype=int, - ) - subblock_corners = properties.Array( - """The positions of the sub-block corners on the grid within their parent block. - - The columns are (min_i, min_j, min_k, max_i, max_j, max_k). Values must be - greater than or equal to zero and less than or equal to the maximum number of - sub-blocks in that axis. - - Sub-blocks must stay within the parent block and should not overlap. Gaps are - allowed but it will be impossible for 'cell' attributes to assign values to - those areas. - """, - shape=("*", 6), - dtype=int, - ) - subblock_definition = properties.Instance( - "Defines the structure of sub-blocks within each parent block for this model.", - RegularSubblockDefinition, - ) - - @property - def subblock_count(self): - return self.subblock_definition.count - - # XXX add num_cells property and implement location_length - - def _check_lengths(self): - if len(self.subblock_parent_indices) != len(self.subblock_corners): - raise ValueError( - "'subblock_parent_indices' and 'subblock_corners' arrays must be the same length" - ) - - def _check_parent_indices(self): - indices = self.subblock_parent_indices - count = self.parent_block_count - if (indices < 0).any() or (indices >= count).any(): - raise IndexError( - f"0 <= subblock_parent_indices < ({count[0]}, {count[1]}, {count[2]}) failed" - ) - - def _check_inside_parent(self): - min_corner = self.subblock_corners[:, :3] - max_corner = self.subblock_corners[:, 3:] - count = self.subblock_count - if min_corner.dtype.kind != "u" and not (0 <= min_corner).all(): - raise IndexError("0 <= min_corner failed") - if not (min_corner < max_corner).all(): - raise IndexError("min_corner < max_corner failed") - if not (max_corner <= count).all(): - raise IndexError( - f"max_corner <= ({count[0]}, {count[1]}, {count[2]}) failed" - ) - - @properties.validator - def _validate_subblocks(self): - self._check_lengths() - self._check_parent_indices() - self._check_inside_parent() - # Check corners against the definition. - # TODO check that sub-blocks in each parent are adjacent or remove that requirement - indices = self.ijk_to_index(self.subblock_parent_indices) - for index in np.unique(indices): - corners = self.subblock_corners[indices == index, :] # XXX slow - self.subblock_definition.validate_subblocks(corners) - # Cast to the smallest unsigned integer type. - _shrink_uint(self, "subblock_parent_indices") - _shrink_uint(self, "subblock_corners") - - -class FreeformSubblockDefinition: - """Unconstrained free-form sub-block definition. - - Doesn't provide any limitations on or explanation of sub-block positions. - """ - - def validate_subblocks(self, _corners): - """Checks the sub-blocks within one parent block.""" - # XXX can we check for overlaps efficiently? - - -class VariableZSubblockDefinition(FreeformSubblockDefinition): - def validate_subblocks(self, corners): - """Checks the sub-blocks within one parent block.""" - super().validate_subblocks(corners) - # TODO check that blocks lie on the octree - - -class FreeformSubblockedModel(_BaseBlockModel): - """A model where regular parent blocks are divided into free-form sub-blocks.""" - - schema = "org.omf.v2.elements.blockmodel.freeform_subblocked" - - parent_block_count = _BlockCount("Number of parent blocks along u, v, and w axes") - parent_block_size = _BlockSize( - "Size of parent blocks in the u, v, and w directions" - ) - subblock_parent_indices = properties.Array( - "The parent block IJK index of each sub-block", - shape=("*", 3), - dtype=int, - ) - subblock_corners = properties.Array( - """The positions of the sub-block corners within their parent block. - - Positions are relative to the parent block, with 0.0 being the minimum side and 1.0 the - maximum side. - - Sub-blocks must stay within the parent block and should not overlap. Gaps are allowed - but it will be impossible for 'cell' attributes to assign values to those areas. - """, - shape=("*", 6), - dtype=float, - ) - subblock_definition = properties.Instance( - "Defines the structure of sub-blocks within each parent block for this model.", - FreeformSubblockDefinition, - ) diff --git a/omf/blockmodel/regular.py b/omf/blockmodel/regular.py new file mode 100644 index 00000000..67e8dcd0 --- /dev/null +++ b/omf/blockmodel/regular.py @@ -0,0 +1,111 @@ +import numpy as np +import properties + +from ..base import ProjectElement +from ._properties import BlockCount, BlockSize + + +class BaseBlockModel(ProjectElement): + """Basic orientation properties and indexing for all block models""" + + 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, + ) + axis_w = properties.Vector3( + "Vector orientation of w-direction", + default="Z", + length=1, + ) + corner = properties.Vector3( + "Corner of the block model relative to Project coordinate reference system", + default="zero", + ) + _valid_locations = ("cells", "parent_blocks") + + @properties.validator + def _validate_axes(self): + """Check if mesh content is built correctly""" + if not ( + np.abs(self.axis_u.dot(self.axis_v) < 1e-6) + and np.abs(self.axis_v.dot(self.axis_w) < 1e-6) + and np.abs(self.axis_w.dot(self.axis_u) < 1e-6) + ): + raise ValueError("axis_u, axis_v, and axis_w must be orthogonal") + return True + + @property + def parent_block_count(self): + """Computed property for number of parent blocks + + This is required for ijk indexing. For tensor and regular block + models, all the blocks are considered parent blocks. + """ + raise NotImplementedError() + + def ijk_to_index(self, ijk): + """Map IJK triples to flat indices for a singoe triple or an array, preseving shape.""" + arr = np.asarray(ijk) + if arr.dtype.kind not in "ui": + raise TypeError(f"'ijk' must be integer typed, found {arr.dtype}") + match arr.shape: + case (*output_shape, 3): + shaped = arr.reshape(-1, 3) + case _: + raise ValueError( + "'ijk' must have 3 elements or be an array with shape (*_, 3)" + ) + count = self.parent_block_count + if (shaped < 0).any() or (shaped >= count).any(): + raise IndexError(f"0 <= ijk < ({count[0]}, {count[1]}, {count[2]}) failed") + indices = np.ravel_multi_index(multi_index=shaped.T, dims=count, order="F") + if output_shape == (): + return indices[0] + else: + return indices.reshape(output_shape) + + def index_to_ijk(self, index): + """Map flat indices to IJK triples for a singoe index or an array, preserving shape.""" + arr = np.asarray(index) + if arr.dtype.kind not in "ui": + raise TypeError(f"'index' must be integer typed, found {arr.dtype}") + output_shape = arr.shape + (3,) + shaped = arr.reshape(-1) + count = self.parent_block_count + if (shaped < 0).any() or (shaped >= np.prod(count)).any(): + raise IndexError(f"0 <= index < {np.prod(count)} failed") + ijk = np.unravel_index(indices=shaped, shape=count, order="F") + return np.c_[ijk[0], ijk[1], ijk[2]].reshape(output_shape) + + +class RegularBlockModel(BaseBlockModel): + """Block model with constant spacing in each dimension.""" + + schema = "org.omf.v2.elements.blockmodel.regular" + + block_count = BlockCount("Number of blocks along u, v, and w axes") + block_size = BlockSize("Size of blocks in the u, v, and w directions") + + @property + def num_cells(self): + """The number of cells, which in this case are always parent blocks.""" + return np.prod(self.parent_block_count) + + @property + def parent_block_count(self): + """Number of parent blocks equals number of blocks""" + return self.block_count + + def location_length(self, location): + """Return correct attribute length for 'location'.""" + match location: + case "cells" | "parent_blocks" | "": + return self.num_cells + case _: + raise ValueError(f"unknown location type: {location!r}") diff --git a/omf/blockmodel/subblock_definition.py b/omf/blockmodel/subblock_definition.py new file mode 100644 index 00000000..4b9f029c --- /dev/null +++ b/omf/blockmodel/subblock_definition.py @@ -0,0 +1,53 @@ +from ._properties import BlockCount, OctreeSubblockCount + + +class RegularSubblockDefinition: + """The simplest gridded sub-block definition.""" + + count = BlockCount( + "The maximum number of sub-blocks inside a parent in each direction." + ) + + def validate_subblocks(self, _corners): + """Checks the sub-blocks within one parent block.""" + # XXX can we check for overlaps efficiently? + + +class OctreeSubblockDefinition(RegularSubblockDefinition): + """Sub-blocks form an octree inside the parent block. + + Cut the parent block in half in all directions to create eight sub-blocks. Repeat that + division for some or all of those new sub-blocks. Continue doing that until the limit + on sub-block count is reached or until the sub-blocks accurately model the inputs. + + This definition also allows the lower level cuts to be omitted in one or two axes, + giving a maximum sub-block count of (16, 16, 4) for example rather than requiring + all axes to be equal. + """ + + count = OctreeSubblockCount( + "The maximum number of sub-blocks inside a parent in each direction." + ) + + def validate_subblocks(self, corners): + """Checks the sub-blocks within one parent block.""" + super().validate_subblocks(corners) + # TODO check that blocks lie on the octree + + +class FreeformSubblockDefinition: + """Unconstrained free-form sub-block definition. + + Doesn't provide any limitations on or explanation of sub-block positions. + """ + + def validate_subblocks(self, _corners): + """Checks the sub-blocks within one parent block.""" + # XXX can we check for overlaps efficiently? + + +class VariableZSubblockDefinition(FreeformSubblockDefinition): + def validate_subblocks(self, corners): + """Checks the sub-blocks within one parent block.""" + super().validate_subblocks(corners) + # TODO check that blocks lie on the octree diff --git a/omf/blockmodel/subblocked.py b/omf/blockmodel/subblocked.py new file mode 100644 index 00000000..855acd2b --- /dev/null +++ b/omf/blockmodel/subblocked.py @@ -0,0 +1,128 @@ +"""blockmodel2.py: New Block Model element definitions""" +import numpy as np +import properties + +from ._properties import BlockCount, BlockSize +from .regular import BaseBlockModel +from .subblock_definition import FreeformSubblockDefinition, RegularSubblockDefinition + + +def _shrink_uint(obj, attr): + arr = getattr(obj, attr) + assert arr.min() >= 0 + t = np.min_scalar_type(arr.max()) + setattr(obj, attr, arr.astype(t)) + + +class SubblockedModel(BaseBlockModel): + """A model where regular parent blocks are divided into sub-blocks that align with a grid. + + The sub-blocks here must align with a regular grid within the parent block. What that grid + is and how blocks are generated within it is defined by the sub-block definition. + """ + + schema = "org.omf.v2.elements.blockmodel.subblocked" + + parent_block_count = BlockCount("Number of parent blocks along u, v, and w axes") + parent_block_size = BlockSize("Size of parent blocks in the u, v, and w directions") + subblock_parent_indices = properties.Array( + "The parent block IJK index of each sub-block", + shape=("*", 3), + dtype=int, + ) + subblock_corners = properties.Array( + """The positions of the sub-block corners on the grid within their parent block. + + The columns are (min_i, min_j, min_k, max_i, max_j, max_k). Values must be + greater than or equal to zero and less than or equal to the maximum number of + sub-blocks in that axis. + + Sub-blocks must stay within the parent block and should not overlap. Gaps are + allowed but it will be impossible for 'cell' attributes to assign values to + those areas. + """, + shape=("*", 6), + dtype=int, + ) + subblock_definition = properties.Instance( + "Defines the structure of sub-blocks within each parent block for this model.", + RegularSubblockDefinition, + ) + + @property + def subblock_count(self): + return self.subblock_definition.count + + # XXX add num_cells property and implement location_length + + def _check_lengths(self): + if len(self.subblock_parent_indices) != len(self.subblock_corners): + raise ValueError( + "'subblock_parent_indices' and 'subblock_corners' arrays must be the same length" + ) + + def _check_parent_indices(self): + indices = self.subblock_parent_indices + count = self.parent_block_count + if (indices < 0).any() or (indices >= count).any(): + raise IndexError( + f"0 <= subblock_parent_indices < ({count[0]}, {count[1]}, {count[2]}) failed" + ) + + def _check_inside_parent(self): + min_corner = self.subblock_corners[:, :3] + max_corner = self.subblock_corners[:, 3:] + count = self.subblock_count + if min_corner.dtype.kind != "u" and not (0 <= min_corner).all(): + raise IndexError("0 <= min_corner failed") + if not (min_corner < max_corner).all(): + raise IndexError("min_corner < max_corner failed") + if not (max_corner <= count).all(): + raise IndexError( + f"max_corner <= ({count[0]}, {count[1]}, {count[2]}) failed" + ) + + @properties.validator + def _validate_subblocks(self): + self._check_lengths() + self._check_parent_indices() + self._check_inside_parent() + # Check corners against the definition. + # TODO check that sub-blocks in each parent are adjacent or remove that requirement + indices = self.ijk_to_index(self.subblock_parent_indices) + for index in np.unique(indices): + corners = self.subblock_corners[indices == index, :] # XXX slow + self.subblock_definition.validate_subblocks(corners) + # Cast to the smallest unsigned integer type. + _shrink_uint(self, "subblock_parent_indices") + _shrink_uint(self, "subblock_corners") + + +class FreeformSubblockedModel(BaseBlockModel): + """A model where regular parent blocks are divided into free-form sub-blocks.""" + + schema = "org.omf.v2.elements.blockmodel.freeform_subblocked" + + parent_block_count = BlockCount("Number of parent blocks along u, v, and w axes") + parent_block_size = BlockSize("Size of parent blocks in the u, v, and w directions") + subblock_parent_indices = properties.Array( + "The parent block IJK index of each sub-block", + shape=("*", 3), + dtype=int, + ) + subblock_corners = properties.Array( + """The positions of the sub-block corners within their parent block. + + Positions are relative to the parent block, with 0.0 being the minimum side and 1.0 the + maximum side. + + Sub-blocks must stay within the parent block and should not overlap. Gaps are allowed + but it will be impossible for 'cell' attributes to assign values to those areas. + """, + shape=("*", 6), + dtype=float, + ) + subblock_definition = properties.Instance( + "Defines the structure of sub-blocks within each parent block for this model.", + FreeformSubblockDefinition, + ) diff --git a/omf/blockmodel/tensor.py b/omf/blockmodel/tensor.py new file mode 100644 index 00000000..e274262c --- /dev/null +++ b/omf/blockmodel/tensor.py @@ -0,0 +1,75 @@ +import numpy as np +import properties + +from .regular import BaseBlockModel + + +class TensorGridBlockModel(BaseBlockModel): + """Block model with variable spacing in each dimension. + + Unlike the rest of the block models attributes here can also be on the block vertices. + """ + + schema = "org.omf.v2.element.blockmodel.tensorgrid" + + _valid_locations = ("vertices",) + BaseBlockModel._valid_locations + + tensor_u = properties.Array( + "Tensor cell widths, u-direction", + shape=("*",), + dtype=float, + ) + tensor_v = properties.Array( + "Tensor cell widths, v-direction", + shape=("*",), + dtype=float, + ) + tensor_w = properties.Array( + "Tensor cell widths, w-direction", + shape=("*",), + dtype=float, + ) + + @properties.validator("tensor_u") + @properties.validator("tensor_v") + @properties.validator("tensor_w") + def _validate_tensor(self, change): + tensor = change["value"] + if (tensor <= 0.0).any(): + raise properties.ValidationError( + "Tensor spacings must all be greater than zero", + prop=change["name"], + instance=self, + reason="invalid", + ) + + def _require_tensors(self): + if self.tensor_u is None or self.tensor_v is None or self.tensor_w is None: + raise ValueError("tensors haven't been set yet") + + @property + def parent_block_count(self): + self._require_tensors() + return np.array( + (len(self.tensor_u), len(self.tensor_v), len(self.tensor_w)), dtype=int + ) + + @property + def num_nodes(self): + """Number of nodes (vertices)""" + return np.prod(self.parent_block_count + 1) + + @property + def num_cells(self): + """Number of cells""" + return np.prod(self.parent_block_count) + + def location_length(self, location): + """Return correct attribute length for 'location'.""" + match location: + case "vertices": + return self.num_nodes + case "cells" | "parent_blocks" | "": + return self.num_cells + case _: + raise ValueError(f"unknown location type: {location!r}") diff --git a/omf/composite.py b/omf/composite.py index d50bc067..2588fabf 100644 --- a/omf/composite.py +++ b/omf/composite.py @@ -2,12 +2,9 @@ import properties from .base import ProjectElement -from .blockmodel import ( - FreeformSubblockedModel, - RegularBlockModel, - SubblockedModel, - TensorGridBlockModel, -) +from .blockmodel.regular import RegularBlockModel +from .blockmodel.subblocked import FreeformSubblockedModel, SubblockedModel +from .blockmodel.tensor import TensorGridBlockModel from .lineset import LineSet from .pointset import PointSet from .surface import Surface, TensorGridSurface diff --git a/tests/test_blockmodel.py b/tests/test_blockmodel.py index cb50ce5f..a51593a3 100644 --- a/tests/test_blockmodel.py +++ b/tests/test_blockmodel.py @@ -7,9 +7,7 @@ def _make_regular(count): - return omf.blockmodel.RegularBlockModel( - block_count=count, block_size=[1.0, 1.0, 1.0] - ) + return omf.RegularBlockModel(block_count=count, block_size=[1.0, 1.0, 1.0]) class MockArray(omf.base.BaseModel): From 3c2f800349b7b9a9f84248a64e4275237f8202b9 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Tue, 28 Feb 2023 14:02:43 +1300 Subject: [PATCH 06/42] Added more sub-block checks. --- omf/blockmodel/subblock_definition.py | 4 +- omf/blockmodel/subblocked.py | 62 +++++++++++++++++---------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/omf/blockmodel/subblock_definition.py b/omf/blockmodel/subblock_definition.py index 4b9f029c..135d24ee 100644 --- a/omf/blockmodel/subblock_definition.py +++ b/omf/blockmodel/subblock_definition.py @@ -1,7 +1,9 @@ +import properties + from ._properties import BlockCount, OctreeSubblockCount -class RegularSubblockDefinition: +class RegularSubblockDefinition(properties.HasProperties): """The simplest gridded sub-block definition.""" count = BlockCount( diff --git a/omf/blockmodel/subblocked.py b/omf/blockmodel/subblocked.py index 855acd2b..d0dfdfce 100644 --- a/omf/blockmodel/subblocked.py +++ b/omf/blockmodel/subblocked.py @@ -1,4 +1,6 @@ """blockmodel2.py: New Block Model element definitions""" +import itertools + import numpy as np import properties @@ -8,6 +10,7 @@ def _shrink_uint(obj, attr): + """Cast an attribute to the smallest unsigned integer type it will fit in.""" arr = getattr(obj, attr) assert arr.min() >= 0 t = np.min_scalar_type(arr.max()) @@ -55,20 +58,6 @@ def subblock_count(self): # XXX add num_cells property and implement location_length - def _check_lengths(self): - if len(self.subblock_parent_indices) != len(self.subblock_corners): - raise ValueError( - "'subblock_parent_indices' and 'subblock_corners' arrays must be the same length" - ) - - def _check_parent_indices(self): - indices = self.subblock_parent_indices - count = self.parent_block_count - if (indices < 0).any() or (indices >= count).any(): - raise IndexError( - f"0 <= subblock_parent_indices < ({count[0]}, {count[1]}, {count[2]}) failed" - ) - def _check_inside_parent(self): min_corner = self.subblock_corners[:, :3] max_corner = self.subblock_corners[:, 3:] @@ -84,16 +73,8 @@ def _check_inside_parent(self): @properties.validator def _validate_subblocks(self): - self._check_lengths() - self._check_parent_indices() self._check_inside_parent() - # Check corners against the definition. - # TODO check that sub-blocks in each parent are adjacent or remove that requirement - indices = self.ijk_to_index(self.subblock_parent_indices) - for index in np.unique(indices): - corners = self.subblock_corners[indices == index, :] # XXX slow - self.subblock_definition.validate_subblocks(corners) - # Cast to the smallest unsigned integer type. + _check_subblocks(self) _shrink_uint(self, "subblock_parent_indices") _shrink_uint(self, "subblock_corners") @@ -126,3 +107,38 @@ class FreeformSubblockedModel(BaseBlockModel): "Defines the structure of sub-blocks within each parent block for this model.", FreeformSubblockDefinition, ) + + @properties.validator + def _validate_subblocks(self): + self._check_inside_parent() + _check_subblocks(self) + _shrink_uint(self, "subblock_parent_indices") + + +def _check_subblocks(model): + indices = model.subblock_parent_indices + corners = model.subblock_corners + # Check arrays are the same length. + if len(indices) != len(corners): + raise ValueError( + "'subblock_parent_indices' and 'subblock_corners' arrays must be the same length" + ) + # Check parent indices are valid. + count = model.parent_block_count + if (indices < 0).any() or (indices >= count).any(): + raise IndexError( + f"0 <= subblock_parent_indices < ({count[0]}, {count[1]}, {count[2]}) failed" + ) + # Check corners. + seen = np.zeros(np.prod(count), dtype=bool) + validate = model.subblock_definition.validate_subblocks + flat = model.ijk_to_index(indices) + diff = np.flatnonzero(flat[1:] != flat[:-1]) + 1 + for start, end in itertools.pairwise(itertools.chain([0], diff, [len(corners)])): + parent = flat[start] + if seen[parent]: + raise ValueError( + "all sub-blocks inside one parent block must be adjacent in the arrays" + ) + seen[parent] = True + validate(corners[start:end]) From 1a961904b5d96105e8f0de3e42aff03ae173e149 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Tue, 28 Feb 2023 15:37:53 +1300 Subject: [PATCH 07/42] Split definitions from models to make things simpler. --- omf/__init__.py | 13 +- omf/blockmodel/_properties.py | 18 +++ omf/blockmodel/_subblock_check.py | 62 ++++++++ omf/blockmodel/{regular.py => definition.py} | 96 ++++++------ omf/blockmodel/models.py | 130 ++++++++++++++++ omf/blockmodel/subblocked.py | 144 ------------------ .../{subblock_definition.py => subblocks.py} | 26 ++-- omf/blockmodel/tensor.py | 75 --------- omf/composite.py | 5 +- tests/test_blockmodel.py | 96 ++++++------ tests/test_doc_example.py | 11 +- 11 files changed, 335 insertions(+), 341 deletions(-) create mode 100644 omf/blockmodel/_subblock_check.py rename omf/blockmodel/{regular.py => definition.py} (53%) delete mode 100644 omf/blockmodel/subblocked.py rename omf/blockmodel/{subblock_definition.py => subblocks.py} (67%) delete mode 100644 omf/blockmodel/tensor.py diff --git a/omf/__init__.py b/omf/__init__.py index 9d5b4d71..b792503f 100644 --- a/omf/__init__.py +++ b/omf/__init__.py @@ -10,15 +10,12 @@ VectorAttribute, ) from .base import Project -from .blockmodel.regular import RegularBlockModel -from .blockmodel.subblocked import SubblockedModel, FreeformSubblockedModel -from .blockmodel.subblock_definition import ( - FreeformSubblockDefinition, - OctreeSubblockDefinition, - RegularSubblockDefinition, - VariableZSubblockDefinition, +from .blockmodel.definition import ( + RegularBlockModelDefinition, + TensorBlockModelDefinition, ) -from .blockmodel.tensor import TensorGridBlockModel +from .blockmodel.models import RegularBlockModel, SubblockedModel, TensorGridBlockModel +from .blockmodel.subblocks import OctreeSubblockDefinition, RegularSubblockDefinition from .composite import Composite from .fileio import load, save, __version__ from .lineset import LineSet diff --git a/omf/blockmodel/_properties.py b/omf/blockmodel/_properties.py index 4974dc1b..6d37ec24 100644 --- a/omf/blockmodel/_properties.py +++ b/omf/blockmodel/_properties.py @@ -51,3 +51,21 @@ def validate(self, instance, value): msg = f"{instance.__class__.__name__}.{self.name} elements must be > 0.0" raise properties.ValidationError(msg, prop=self.name, instance=instance) return value + + +class TensorArray(properties.Array): + def __init__(self, doc, **kw): + super().__init__(doc, **kw, dtype=float, shape=("*",)) + + def validate(self, instance, value): + """Check that tensor spacings are all > 0.""" + super().validate(instance, value) + value = np.asarray(value) + if (value <= 0.0).any(): + raise properties.ValidationError( + "Tensor spacings must be greater than zero", + prop=self.name, + instance=instance, + reason="invalid", + ) + return value diff --git a/omf/blockmodel/_subblock_check.py b/omf/blockmodel/_subblock_check.py new file mode 100644 index 00000000..9b6d4e8e --- /dev/null +++ b/omf/blockmodel/_subblock_check.py @@ -0,0 +1,62 @@ +import itertools + +import numpy as np + +from .subblocks import RegularSubblockDefinition + + +def _group_by(arr): + assert len(arr) > 0 + diff = np.flatnonzero(arr[1:] != arr[:-1]) + diff += 1 + if len(diff) == 0: + yield 0, len(arr), arr[0] + else: + yield 0, diff[0], arr[0] + for start, end in itertools.pairwise(diff): + yield start, end, arr[start] + yield diff[-1], len(arr), arr[-1] + + +def _check_parent_indices(definition, parent_indices): + count = definition.block_count + if (parent_indices < 0).any() or (parent_indices >= count).any(): + raise IndexError( + f"0 <= subblock_parent_indices < ({count[0]}, {count[1]}, {count[2]}) failed" + ) + + +def _check_inside_parent(subblock_definition, corners): + if isinstance(subblock_definition, RegularSubblockDefinition): + upper = subblock_definition.count + upper_str = f"({upper[0]}, {upper[1]}, {upper[2]})" + else: + upper = 1.0 + upper_str = "1" + mn = corners[:, :3] + mx = corners[:, 3:] + if mn.dtype.kind != "u" and not (0 <= mn).all(): + raise IndexError("0 <= min_corner failed") + if not (mn < mx).all(): + raise IndexError("min_corner < max_corner failed") + if not (mx <= upper).all(): + raise IndexError(f"max_corner <= ({upper_str}) failed") + + +def check_subblocks(definition, subblock_definition, parent_indices, corners): + if len(parent_indices) != len(corners): + raise ValueError( + "'subblock_parent_indices' and 'subblock_corners' arrays must be the same length" + ) + _check_inside_parent(subblock_definition, corners) + _check_parent_indices(definition, parent_indices) + # Check order and pass groups to the sub-block definition to check further. + seen = np.zeros(np.prod(definition.block_count), dtype=bool) + validate = subblock_definition.validate_subblocks + for start, end, parent in _group_by(definition.ijk_to_index(parent_indices)): + if seen[parent]: + raise ValueError( + "all sub-blocks inside one parent block must be adjacent in the arrays" + ) + seen[parent] = True + validate(corners[start:end]) diff --git a/omf/blockmodel/regular.py b/omf/blockmodel/definition.py similarity index 53% rename from omf/blockmodel/regular.py rename to omf/blockmodel/definition.py index 67e8dcd0..5f9c9a73 100644 --- a/omf/blockmodel/regular.py +++ b/omf/blockmodel/definition.py @@ -1,33 +1,24 @@ import numpy as np import properties -from ..base import ProjectElement -from ._properties import BlockCount, BlockSize +from ._properties import BlockCount, BlockSize, TensorArray -class BaseBlockModel(ProjectElement): - """Basic orientation properties and indexing for all block models""" - +class _BaseBlockModelDefinition(properties.HasProperties): axis_u = properties.Vector3( - "Vector orientation of u-direction", - default="X", - length=1, + "Vector orientation of u-direction", default="X", length=1 ) axis_v = properties.Vector3( - "Vector orientation of v-direction", - default="Y", - length=1, + "Vector orientation of v-direction", default="Y", length=1 ) axis_w = properties.Vector3( - "Vector orientation of w-direction", - default="Z", - length=1, + "Vector orientation of w-direction", default="Z", length=1 ) corner = properties.Vector3( "Corner of the block model relative to Project coordinate reference system", default="zero", ) - _valid_locations = ("cells", "parent_blocks") + block_count = None @properties.validator def _validate_axes(self): @@ -38,19 +29,11 @@ def _validate_axes(self): and np.abs(self.axis_w.dot(self.axis_u) < 1e-6) ): raise ValueError("axis_u, axis_v, and axis_w must be orthogonal") - return True - - @property - def parent_block_count(self): - """Computed property for number of parent blocks - - This is required for ijk indexing. For tensor and regular block - models, all the blocks are considered parent blocks. - """ - raise NotImplementedError() def ijk_to_index(self, ijk): """Map IJK triples to flat indices for a singoe triple or an array, preseving shape.""" + if self.block_count is None: + raise ValueError("block_count is not set") arr = np.asarray(ijk) if arr.dtype.kind not in "ui": raise TypeError(f"'ijk' must be integer typed, found {arr.dtype}") @@ -61,7 +44,7 @@ def ijk_to_index(self, ijk): raise ValueError( "'ijk' must have 3 elements or be an array with shape (*_, 3)" ) - count = self.parent_block_count + count = self.block_count if (shaped < 0).any() or (shaped >= count).any(): raise IndexError(f"0 <= ijk < ({count[0]}, {count[1]}, {count[2]}) failed") indices = np.ravel_multi_index(multi_index=shaped.T, dims=count, order="F") @@ -72,40 +55,59 @@ def ijk_to_index(self, ijk): def index_to_ijk(self, index): """Map flat indices to IJK triples for a singoe index or an array, preserving shape.""" + if self.block_count is None: + raise ValueError("block_count is not set") arr = np.asarray(index) if arr.dtype.kind not in "ui": raise TypeError(f"'index' must be integer typed, found {arr.dtype}") output_shape = arr.shape + (3,) shaped = arr.reshape(-1) - count = self.parent_block_count + count = self.block_count if (shaped < 0).any() or (shaped >= np.prod(count)).any(): raise IndexError(f"0 <= index < {np.prod(count)} failed") ijk = np.unravel_index(indices=shaped, shape=count, order="F") return np.c_[ijk[0], ijk[1], ijk[2]].reshape(output_shape) -class RegularBlockModel(BaseBlockModel): - """Block model with constant spacing in each dimension.""" +class RegularBlockModelDefinition(_BaseBlockModelDefinition): + """Defines the block structure of a regular block model. - schema = "org.omf.v2.elements.blockmodel.regular" + If used on a sub-blocked model then everything here applies to the parent blocks only. + """ - block_count = BlockCount("Number of blocks along u, v, and w axes") - block_size = BlockSize("Size of blocks in the u, v, and w directions") + block_count = BlockCount("Number of blocks in each of the u, v, and w directions.") + block_size = BlockSize("Size of blocks in the u, v, and w directions.") - @property - def num_cells(self): - """The number of cells, which in this case are always parent blocks.""" - return np.prod(self.parent_block_count) + +class TensorBlockModelDefinition(_BaseBlockModelDefinition): + """Defines the block structure of a tensor grid block model.""" + + tensor_u = TensorArray("Tensor cell widths, u-direction") + tensor_v = TensorArray("Tensor cell widths, v-direction") + tensor_w = TensorArray("Tensor cell widths, w-direction") + + def _tensors(self): + yield self.tensor_u + yield self.tensor_v + yield self.tensor_w @property - def parent_block_count(self): - """Number of parent blocks equals number of blocks""" - return self.block_count - - def location_length(self, location): - """Return correct attribute length for 'location'.""" - match location: - case "cells" | "parent_blocks" | "": - return self.num_cells - case _: - raise ValueError(f"unknown location type: {location!r}") + def block_count(self): + """The block count is derived from the tensors here.""" + c = tuple(None if t is None else len(t) for t in self._tensors()) + if None in c: + return None + return np.array(c, dtype=int) + + @properties.validator("tensor_u") + @properties.validator("tensor_v") + @properties.validator("tensor_w") + def _validate_tensor(self, change): + tensor = change["value"] + if (tensor <= 0.0).any(): + raise properties.ValidationError( + "Tensor spacings must be greater than zero", + prop=change["name"], + instance=self, + reason="invalid", + ) diff --git a/omf/blockmodel/models.py b/omf/blockmodel/models.py index e69de29b..a3f52581 100644 --- a/omf/blockmodel/models.py +++ b/omf/blockmodel/models.py @@ -0,0 +1,130 @@ +import numpy as np +import properties + +from ..base import ProjectElement +from .definition import RegularBlockModelDefinition, TensorBlockModelDefinition +from .subblocks import RegularSubblockDefinition +from ._subblock_check import check_subblocks + + +def _shrink_uint(arr): + assert arr.min() >= 0 + t = np.min_scalar_type(arr.max()) + return arr.astype(t) + + +class RegularBlockModel(ProjectElement): + schema = "org.omf.v2.elements.blockmodel.regular" + _valid_locations = ("cells", "parent_blocks") + + definition = properties.Instance( + "Block model definition", + RegularBlockModelDefinition, + default=RegularBlockModelDefinition, + ) + + @property + def num_cells(self): + """The number of cells, which in this case are always parent blocks.""" + return np.prod(self.definition.block_count) + + def location_length(self, location): + """Return correct attribute length for 'location'.""" + match location: + case "cells" | "parent_blocks" | "": + return self.num_cells + case _: + raise ValueError(f"unknown location type: {location!r}") + + +class TensorGridBlockModel(ProjectElement): + schema = "org.omf.v2.elements.blockmodel.tensor" + _valid_locations = ("vertices", "cells", "parent_blocks") + + definition = properties.Instance( + "Block model definition, including the tensor arrays", + TensorBlockModelDefinition, + default=TensorBlockModelDefinition, + ) + + @property + def num_cells(self): + """The number of cells.""" + return np.prod(self.definition.block_count) + + @property + def num_nodes(self): + """Number of nodes or vertices.""" + bc = self.definition.block_count + return None if bc is None else np.prod(bc + 1) + + def location_length(self, location): + """Return correct attribute length for 'location'.""" + match location: + case "cells" | "parent_blocks" | "": + return self.num_cells + case "vertices": + return self.num_nodes + case _: + raise ValueError(f"unknown location type: {location!r}") + + +class SubblockedModel(ProjectElement): + schema = "org.omf.v2.elements.blockmodel.subblocked" + _valid_locations = ("cells", "parent_blocks") + + definition = properties.Instance( + "Block model definition, for the parent blocks", + RegularBlockModelDefinition, + default=RegularBlockModelDefinition, + ) + subblock_parent_indices = properties.Array( + "The parent block IJK index of each sub-block", + shape=("*", 3), + dtype=int, + ) + subblock_corners = properties.Array( + """The positions of the sub-block corners on the grid within their parent block. + + The columns are (min_i, min_j, min_k, max_i, max_j, max_k). Values must be + greater than or equal to zero and less than or equal to the maximum number of + sub-blocks in that axis. + + Sub-blocks must stay within the parent block and should not overlap. Gaps are + allowed but it will be impossible for 'cell' attributes to assign values to + those areas. + """, + shape=("*", 6), + dtype=int, + ) + subblock_definition = properties.Instance( + "Defines the structure of sub-blocks within each parent block for this model.", + RegularSubblockDefinition, + default=RegularSubblockDefinition, + ) + + @property + def num_cells(self): + """The number of cells, which in this case are always parent blocks.""" + return None if self.subblock_corners is None else len(self.subblock_corners) + + def location_length(self, location): + """Return correct attribute length for 'location'.""" + match location: + case "cells" | "": + return self.num_cells + case "parent_blocks": + return np.prod(self.definition.block_count) + case _: + raise ValueError(f"unknown location type: {location!r}") + + @properties.validator + def _validate_subblocks(self): + check_subblocks( + self.definition, + self.subblock_definition, + self.subblock_parent_indices, + self.subblock_corners, + ) + self.subblock_parent_indices = _shrink_uint(self.subblock_parent_indices) + self.subblock_corners = _shrink_uint(self.subblock_corners) diff --git a/omf/blockmodel/subblocked.py b/omf/blockmodel/subblocked.py deleted file mode 100644 index d0dfdfce..00000000 --- a/omf/blockmodel/subblocked.py +++ /dev/null @@ -1,144 +0,0 @@ -"""blockmodel2.py: New Block Model element definitions""" -import itertools - -import numpy as np -import properties - -from ._properties import BlockCount, BlockSize -from .regular import BaseBlockModel -from .subblock_definition import FreeformSubblockDefinition, RegularSubblockDefinition - - -def _shrink_uint(obj, attr): - """Cast an attribute to the smallest unsigned integer type it will fit in.""" - arr = getattr(obj, attr) - assert arr.min() >= 0 - t = np.min_scalar_type(arr.max()) - setattr(obj, attr, arr.astype(t)) - - -class SubblockedModel(BaseBlockModel): - """A model where regular parent blocks are divided into sub-blocks that align with a grid. - - The sub-blocks here must align with a regular grid within the parent block. What that grid - is and how blocks are generated within it is defined by the sub-block definition. - """ - - schema = "org.omf.v2.elements.blockmodel.subblocked" - - parent_block_count = BlockCount("Number of parent blocks along u, v, and w axes") - parent_block_size = BlockSize("Size of parent blocks in the u, v, and w directions") - subblock_parent_indices = properties.Array( - "The parent block IJK index of each sub-block", - shape=("*", 3), - dtype=int, - ) - subblock_corners = properties.Array( - """The positions of the sub-block corners on the grid within their parent block. - - The columns are (min_i, min_j, min_k, max_i, max_j, max_k). Values must be - greater than or equal to zero and less than or equal to the maximum number of - sub-blocks in that axis. - - Sub-blocks must stay within the parent block and should not overlap. Gaps are - allowed but it will be impossible for 'cell' attributes to assign values to - those areas. - """, - shape=("*", 6), - dtype=int, - ) - subblock_definition = properties.Instance( - "Defines the structure of sub-blocks within each parent block for this model.", - RegularSubblockDefinition, - ) - - @property - def subblock_count(self): - return self.subblock_definition.count - - # XXX add num_cells property and implement location_length - - def _check_inside_parent(self): - min_corner = self.subblock_corners[:, :3] - max_corner = self.subblock_corners[:, 3:] - count = self.subblock_count - if min_corner.dtype.kind != "u" and not (0 <= min_corner).all(): - raise IndexError("0 <= min_corner failed") - if not (min_corner < max_corner).all(): - raise IndexError("min_corner < max_corner failed") - if not (max_corner <= count).all(): - raise IndexError( - f"max_corner <= ({count[0]}, {count[1]}, {count[2]}) failed" - ) - - @properties.validator - def _validate_subblocks(self): - self._check_inside_parent() - _check_subblocks(self) - _shrink_uint(self, "subblock_parent_indices") - _shrink_uint(self, "subblock_corners") - - -class FreeformSubblockedModel(BaseBlockModel): - """A model where regular parent blocks are divided into free-form sub-blocks.""" - - schema = "org.omf.v2.elements.blockmodel.freeform_subblocked" - - parent_block_count = BlockCount("Number of parent blocks along u, v, and w axes") - parent_block_size = BlockSize("Size of parent blocks in the u, v, and w directions") - subblock_parent_indices = properties.Array( - "The parent block IJK index of each sub-block", - shape=("*", 3), - dtype=int, - ) - subblock_corners = properties.Array( - """The positions of the sub-block corners within their parent block. - - Positions are relative to the parent block, with 0.0 being the minimum side and 1.0 the - maximum side. - - Sub-blocks must stay within the parent block and should not overlap. Gaps are allowed - but it will be impossible for 'cell' attributes to assign values to those areas. - """, - shape=("*", 6), - dtype=float, - ) - subblock_definition = properties.Instance( - "Defines the structure of sub-blocks within each parent block for this model.", - FreeformSubblockDefinition, - ) - - @properties.validator - def _validate_subblocks(self): - self._check_inside_parent() - _check_subblocks(self) - _shrink_uint(self, "subblock_parent_indices") - - -def _check_subblocks(model): - indices = model.subblock_parent_indices - corners = model.subblock_corners - # Check arrays are the same length. - if len(indices) != len(corners): - raise ValueError( - "'subblock_parent_indices' and 'subblock_corners' arrays must be the same length" - ) - # Check parent indices are valid. - count = model.parent_block_count - if (indices < 0).any() or (indices >= count).any(): - raise IndexError( - f"0 <= subblock_parent_indices < ({count[0]}, {count[1]}, {count[2]}) failed" - ) - # Check corners. - seen = np.zeros(np.prod(count), dtype=bool) - validate = model.subblock_definition.validate_subblocks - flat = model.ijk_to_index(indices) - diff = np.flatnonzero(flat[1:] != flat[:-1]) + 1 - for start, end in itertools.pairwise(itertools.chain([0], diff, [len(corners)])): - parent = flat[start] - if seen[parent]: - raise ValueError( - "all sub-blocks inside one parent block must be adjacent in the arrays" - ) - seen[parent] = True - validate(corners[start:end]) diff --git a/omf/blockmodel/subblock_definition.py b/omf/blockmodel/subblocks.py similarity index 67% rename from omf/blockmodel/subblock_definition.py rename to omf/blockmodel/subblocks.py index 135d24ee..6047a8b0 100644 --- a/omf/blockmodel/subblock_definition.py +++ b/omf/blockmodel/subblocks.py @@ -12,7 +12,7 @@ class RegularSubblockDefinition(properties.HasProperties): def validate_subblocks(self, _corners): """Checks the sub-blocks within one parent block.""" - # XXX can we check for overlaps efficiently? + # TODO check for overlaps class OctreeSubblockDefinition(RegularSubblockDefinition): @@ -37,19 +37,19 @@ def validate_subblocks(self, corners): # TODO check that blocks lie on the octree -class FreeformSubblockDefinition: - """Unconstrained free-form sub-block definition. +# class FreeformSubblockDefinition: +# """Unconstrained free-form sub-block definition. - Doesn't provide any limitations on or explanation of sub-block positions. - """ +# Doesn't provide any limitations on or explanation of sub-block positions. +# """ - def validate_subblocks(self, _corners): - """Checks the sub-blocks within one parent block.""" - # XXX can we check for overlaps efficiently? +# def validate_subblocks(self, _corners): +# """Checks the sub-blocks within one parent block.""" +# # XXX can we check for overlaps efficiently? -class VariableZSubblockDefinition(FreeformSubblockDefinition): - def validate_subblocks(self, corners): - """Checks the sub-blocks within one parent block.""" - super().validate_subblocks(corners) - # TODO check that blocks lie on the octree +# class VariableZSubblockDefinition(FreeformSubblockDefinition): +# def validate_subblocks(self, corners): +# """Checks the sub-blocks within one parent block.""" +# super().validate_subblocks(corners) +# # TODO check that blocks lie on the octree diff --git a/omf/blockmodel/tensor.py b/omf/blockmodel/tensor.py deleted file mode 100644 index e274262c..00000000 --- a/omf/blockmodel/tensor.py +++ /dev/null @@ -1,75 +0,0 @@ -import numpy as np -import properties - -from .regular import BaseBlockModel - - -class TensorGridBlockModel(BaseBlockModel): - """Block model with variable spacing in each dimension. - - Unlike the rest of the block models attributes here can also be on the block vertices. - """ - - schema = "org.omf.v2.element.blockmodel.tensorgrid" - - _valid_locations = ("vertices",) + BaseBlockModel._valid_locations - - tensor_u = properties.Array( - "Tensor cell widths, u-direction", - shape=("*",), - dtype=float, - ) - tensor_v = properties.Array( - "Tensor cell widths, v-direction", - shape=("*",), - dtype=float, - ) - tensor_w = properties.Array( - "Tensor cell widths, w-direction", - shape=("*",), - dtype=float, - ) - - @properties.validator("tensor_u") - @properties.validator("tensor_v") - @properties.validator("tensor_w") - def _validate_tensor(self, change): - tensor = change["value"] - if (tensor <= 0.0).any(): - raise properties.ValidationError( - "Tensor spacings must all be greater than zero", - prop=change["name"], - instance=self, - reason="invalid", - ) - - def _require_tensors(self): - if self.tensor_u is None or self.tensor_v is None or self.tensor_w is None: - raise ValueError("tensors haven't been set yet") - - @property - def parent_block_count(self): - self._require_tensors() - return np.array( - (len(self.tensor_u), len(self.tensor_v), len(self.tensor_w)), dtype=int - ) - - @property - def num_nodes(self): - """Number of nodes (vertices)""" - return np.prod(self.parent_block_count + 1) - - @property - def num_cells(self): - """Number of cells""" - return np.prod(self.parent_block_count) - - def location_length(self, location): - """Return correct attribute length for 'location'.""" - match location: - case "vertices": - return self.num_nodes - case "cells" | "parent_blocks" | "": - return self.num_cells - case _: - raise ValueError(f"unknown location type: {location!r}") diff --git a/omf/composite.py b/omf/composite.py index 2588fabf..405209ea 100644 --- a/omf/composite.py +++ b/omf/composite.py @@ -2,9 +2,7 @@ import properties from .base import ProjectElement -from .blockmodel.regular import RegularBlockModel -from .blockmodel.subblocked import FreeformSubblockedModel, SubblockedModel -from .blockmodel.tensor import TensorGridBlockModel +from .blockmodel.models import RegularBlockModel, SubblockedModel, TensorGridBlockModel from .lineset import LineSet from .pointset import PointSet from .surface import Surface, TensorGridSurface @@ -29,7 +27,6 @@ class is created, then create an identical subclass so the docs prop=properties.Union( "", ( - FreeformSubblockedModel, LineSet, PointSet, RegularBlockModel, diff --git a/tests/test_blockmodel.py b/tests/test_blockmodel.py index a51593a3..dba6d056 100644 --- a/tests/test_blockmodel.py +++ b/tests/test_blockmodel.py @@ -7,7 +7,10 @@ def _make_regular(count): - return omf.RegularBlockModel(block_count=count, block_size=[1.0, 1.0, 1.0]) + bm = omf.RegularBlockModel() + bm.definition.block_count = count + bm.definition.block_size = [1.0, 1.0, 1.0] + return bm class MockArray(omf.base.BaseModel): @@ -21,23 +24,23 @@ def test_ijk_index_errors(): block_model = _make_regular([3, 4, 5]) with pytest.raises(TypeError): - block_model.ijk_to_index("a") + block_model.definition.ijk_to_index("a") with pytest.raises(TypeError): - block_model.index_to_ijk("a") + block_model.definition.index_to_ijk("a") with pytest.raises(ValueError): - block_model.ijk_to_index([0, 0]) + block_model.definition.ijk_to_index([0, 0]) with pytest.raises(TypeError): - block_model.ijk_to_index([0, 0, 0.5]) + block_model.definition.ijk_to_index([0, 0, 0.5]) with pytest.raises(TypeError): - block_model.index_to_ijk(0.5) + block_model.definition.index_to_ijk(0.5) with pytest.raises(IndexError): - block_model.ijk_to_index([0, 0, 5]) + block_model.definition.ijk_to_index([0, 0, 5]) with pytest.raises(IndexError): - block_model.index_to_ijk(60) + block_model.definition.index_to_ijk(60) with pytest.raises(IndexError): - block_model.ijk_to_index([[0, 0, 5], [0, 0, 3]]) + block_model.definition.ijk_to_index([[0, 0, 5], [0, 0, 3]]) with pytest.raises(IndexError): - block_model.index_to_ijk([0, 1, 60]) + block_model.definition.index_to_ijk([0, 1, 60]) def test_ijk_index_arrays(): @@ -45,12 +48,12 @@ def test_ijk_index_arrays(): block_model = _make_regular([3, 4, 5]) ijk = [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1), (2, 3, 4)] index = [0, 1, 3, 12, 59] - assert np.array_equal(block_model.ijk_to_index(ijk), index) - assert np.array_equal(block_model.index_to_ijk(index), ijk) + assert np.array_equal(block_model.definition.ijk_to_index(ijk), index) + assert np.array_equal(block_model.definition.index_to_ijk(index), ijk) ijk = [[(0, 0, 0), (1, 0, 0)], [(0, 1, 0), (0, 0, 1)]] index = [(0, 1), (3, 12)] - assert np.array_equal(block_model.ijk_to_index(ijk), index) - assert np.array_equal(block_model.index_to_ijk(index), ijk) + assert np.array_equal(block_model.definition.ijk_to_index(ijk), index) + assert np.array_equal(block_model.definition.index_to_ijk(index), ijk) @pytest.mark.parametrize( @@ -60,27 +63,27 @@ def test_ijk_index_arrays(): def test_ijk_index(ijk, index): """Test ijk indexing into parent blocks works as expected""" block_model = _make_regular([3, 4, 5]) - assert block_model.ijk_to_index(ijk) == index - assert np.array_equal(block_model.index_to_ijk(index), ijk) - - -# def test_tensorblockmodel(): -# """Test volume grid geometry validation""" -# elem = omf.TensorGridBlockModel() -# assert elem.num_nodes is None -# assert elem.num_cells is None -# assert elem.parent_block_count is None -# elem.tensor_u = [1.0, 1.0] -# elem.tensor_v = [2.0, 2.0, 2.0] -# elem.tensor_w = [3.0] -# assert elem.parent_block_count == [2, 3, 1] -# assert elem.validate() -# assert elem.location_length("vertices") == 24 -# assert elem.location_length("cells") == 6 -# elem.axis_v = [1.0, 1.0, 0] -# with pytest.raises(ValueError): -# elem.validate() -# elem.axis_v = "Y" + assert block_model.definition.ijk_to_index(ijk) == index + assert np.array_equal(block_model.definition.index_to_ijk(index), ijk) + + +def test_tensorblockmodel(): + """Test volume grid geometry validation""" + elem = omf.TensorGridBlockModel() + assert elem.num_nodes is None + assert elem.num_cells is None + assert elem.definition.block_count is None + elem.definition.tensor_u = [1.0, 1.0] + elem.definition.tensor_v = [2.0, 2.0, 2.0] + elem.definition.tensor_w = [3.0] + np.testing.assert_array_equal(elem.definition.block_count, [2, 3, 1]) + assert elem.validate() + assert elem.location_length("vertices") == 24 + assert elem.location_length("cells") == 6 + elem.definition.axis_v = [1.0, 1.0, 0] + with pytest.raises(ValueError): + elem.validate() + elem.axis_v = "Y" # pylint: disable=W0143 @@ -92,9 +95,10 @@ class TestRegularBlockModel: ) def test_bad_block_count(self, block_count): """Test mismatched block_count""" - block_model = omf.RegularBlockModel(block_size=[1.0, 2.0, 3.0]) + block_model = omf.RegularBlockModel() + block_model.definition.block_size = [1.0, 2.0, 3.0] with pytest.raises(properties.ValidationError): - block_model.block_count = block_count + block_model.definition.block_count = block_count block_model.validate() @pytest.mark.parametrize( @@ -102,25 +106,25 @@ def test_bad_block_count(self, block_count): ) def test_bad_block_size(self, block_size): """Test mismatched block_size""" - block_model = omf.RegularBlockModel(block_count=[2, 2, 2]) + block_model = omf.RegularBlockModel() + block_model.definition.block_count = [2, 2, 2] with pytest.raises(properties.ValidationError): - block_model.block_size = block_size + block_model.definition.block_size = block_size block_model.validate() def test_uninstantiated(self): """Test all attributes are None on instantiation""" block_model = omf.RegularBlockModel() - assert block_model.block_count is None - assert block_model.block_size is None + assert block_model.definition.block_count is None + assert block_model.definition.block_size is None assert block_model.num_cells is None def test_num_cells(self): """Test num_cells calculation is correct""" - block_model = omf.RegularBlockModel( - block_count=[2, 2, 2], - block_size=[1.0, 2.0, 3.0], - ) - np.testing.assert_array_equal(block_model.parent_block_count, [2, 2, 2]) + block_model = omf.RegularBlockModel() + block_model.definition.block_count = [2, 2, 2] + block_model.definition.block_size = [1.0, 2.0, 3.0] + np.testing.assert_array_equal(block_model.definition.block_count, [2, 2, 2]) assert block_model.num_cells == 8 assert block_model.location_length("cells") == 8 assert block_model.location_length("parent_blocks") == 8 diff --git a/tests/test_doc_example.py b/tests/test_doc_example.py index 11cbbda9..57cfd82e 100644 --- a/tests/test_doc_example.py +++ b/tests/test_doc_example.py @@ -132,12 +132,15 @@ def test_doc_ex(): ), ], ) + defn = omf.TensorBlockModelDefinition( + tensor_u=np.ones(10, dtype=float), + tensor_v=np.ones(15, dtype=float), + tensor_w=np.ones(20, dtype=float), + corner=[10.0, 10.0, -10], + ) vol = omf.TensorGridBlockModel( name="vol", - tensor_u=np.ones(10).astype(float), - tensor_v=np.ones(15).astype(float), - tensor_w=np.ones(20).astype(float), - corner=[10.0, 10.0, -10], + definition=defn, attributes=[ omf.NumericAttribute( name="random attr", location="cells", array=np.random.rand(10 * 15 * 20) From 8f3335db9d6ad1aac566ead8753491893952c0a4 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Tue, 28 Feb 2023 15:54:34 +1300 Subject: [PATCH 08/42] Test that uint array packing works. --- tests/test_blockmodel.py | 48 ++++++++++++---------------------------- 1 file changed, 14 insertions(+), 34 deletions(-) diff --git a/tests/test_blockmodel.py b/tests/test_blockmodel.py index dba6d056..9dc545f6 100644 --- a/tests/test_blockmodel.py +++ b/tests/test_blockmodel.py @@ -129,40 +129,20 @@ def test_num_cells(self): assert block_model.location_length("cells") == 8 assert block_model.location_length("parent_blocks") == 8 - # def test_cbc(self): - # """Test cbc access and validation is correct""" - # block_model = omf.RegularBlockModel( - # block_count=[2, 2, 2], - # block_size=[1.0, 2.0, 3.0], - # ) - # block_model.reset_cbc() - # assert block_model.validate() - # assert np.all(block_model.cbc == np.ones(8)) - # block_model.cbc.array[0] = 0 - # assert block_model.validate() - # with pytest.raises(properties.ValidationError): - # block_model.cbc = np.ones(7, dtype="int8") - # block_model.cbc = np.ones(8, dtype="uint8") - # with pytest.raises(properties.ValidationError): - # block_model.cbc.array[0] = 2 - # block_model.validate() - # with pytest.raises(properties.ValidationError): - # block_model.cbc.array[0] = -1 - # block_model.validate() - - # def test_cbi(self): - # """Test cbi access and validation is correct""" - # block_model = omf.RegularBlockModel() - # assert block_model.cbi is None - # block_model.block_count = [2, 2, 2] - # block_model.block_size = [1.0, 2.0, 3.0] - # block_model.reset_cbc() - # assert np.all(block_model.cbi == np.array(range(9), dtype="int8")) - # block_model.cbc.array[0] = 0 - # assert np.all( - # block_model.cbi - # == np.r_[np.array([0], dtype="int8"), np.array(range(8), dtype="int8")] - # ) + +class TestSubblockedModel: + def test_pack_uints(self): + block_model = omf.SubblockedModel() + block_model.subblock_definition.count = [2, 2, 2] + block_model.definition.block_size = [1.0, 1.0, 1.0] + block_model.definition.block_count = [10, 10, 10] + block_model.subblock_parent_indices = np.array([(0, 0, 0)]) + block_model.subblock_corners = np.array([(0, 0, 0, 2, 2, 2)]) + # We set this as uint32 + assert block_model.subblock_corners.dtype == np.int32 + block_model.validate() + # Validate should have packed it down to uint8 + assert block_model.subblock_corners.dtype == np.uint8 # class TestRegularSubBlockModel: From e2c7ab679a8826864b9f31a16e5c4eb2547c2415 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Tue, 28 Feb 2023 16:01:21 +1300 Subject: [PATCH 09/42] Raise ValidationError from sub-block checks. --- omf/blockmodel/_subblock_check.py | 43 +++++++++++++++++++++---------- omf/blockmodel/models.py | 1 + 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/omf/blockmodel/_subblock_check.py b/omf/blockmodel/_subblock_check.py index 9b6d4e8e..b8c5e8b9 100644 --- a/omf/blockmodel/_subblock_check.py +++ b/omf/blockmodel/_subblock_check.py @@ -1,6 +1,7 @@ import itertools import numpy as np +import properties from .subblocks import RegularSubblockDefinition @@ -18,15 +19,17 @@ def _group_by(arr): yield diff[-1], len(arr), arr[-1] -def _check_parent_indices(definition, parent_indices): +def _check_parent_indices(definition, parent_indices, instance): count = definition.block_count if (parent_indices < 0).any() or (parent_indices >= count).any(): - raise IndexError( - f"0 <= subblock_parent_indices < ({count[0]}, {count[1]}, {count[2]}) failed" + raise properties.ValidationError( + f"0 <= subblock_parent_indices < ({count[0]}, {count[1]}, {count[2]}) failed", + prop="subblock_parent_indices", + instance=instance, ) -def _check_inside_parent(subblock_definition, corners): +def _check_inside_parent(subblock_definition, corners, instance): if isinstance(subblock_definition, RegularSubblockDefinition): upper = subblock_definition.count upper_str = f"({upper[0]}, {upper[1]}, {upper[2]})" @@ -36,27 +39,39 @@ def _check_inside_parent(subblock_definition, corners): mn = corners[:, :3] mx = corners[:, 3:] if mn.dtype.kind != "u" and not (0 <= mn).all(): - raise IndexError("0 <= min_corner failed") + raise properties.ValidationError( + "0 <= min_corner failed", prop="subblock_corners", instance=instance + ) if not (mn < mx).all(): - raise IndexError("min_corner < max_corner failed") + raise properties.ValidationError( + "min_corner < max_corner failed", prop="subblock_corners", instance=instance + ) if not (mx <= upper).all(): - raise IndexError(f"max_corner <= ({upper_str}) failed") + raise properties.ValidationError( + f"max_corner <= ({upper_str}) failed", + prop="subblock_corners", + instance=instance, + ) -def check_subblocks(definition, subblock_definition, parent_indices, corners): +def check_subblocks(definition, subblock_definition, parent_indices, corners, instance): if len(parent_indices) != len(corners): - raise ValueError( - "'subblock_parent_indices' and 'subblock_corners' arrays must be the same length" + raise properties.ValidationError( + "'subblock_parent_indices' and 'subblock_corners' arrays must be the same length", + prop="subblock_corners", + instance=instance, ) - _check_inside_parent(subblock_definition, corners) - _check_parent_indices(definition, parent_indices) + _check_inside_parent(subblock_definition, corners, instance) + _check_parent_indices(definition, parent_indices, instance) # Check order and pass groups to the sub-block definition to check further. seen = np.zeros(np.prod(definition.block_count), dtype=bool) validate = subblock_definition.validate_subblocks for start, end, parent in _group_by(definition.ijk_to_index(parent_indices)): if seen[parent]: - raise ValueError( - "all sub-blocks inside one parent block must be adjacent in the arrays" + raise properties.ValidationError( + "all sub-blocks inside one parent block must be adjacent in the arrays", + prop="subblock_parent_indices", + instance=instance, ) seen[parent] = True validate(corners[start:end]) diff --git a/omf/blockmodel/models.py b/omf/blockmodel/models.py index a3f52581..1ffa40ee 100644 --- a/omf/blockmodel/models.py +++ b/omf/blockmodel/models.py @@ -125,6 +125,7 @@ def _validate_subblocks(self): self.subblock_definition, self.subblock_parent_indices, self.subblock_corners, + instance=self, ) self.subblock_parent_indices = _shrink_uint(self.subblock_parent_indices) self.subblock_corners = _shrink_uint(self.subblock_corners) From 3a8901b47426df5486afb6980a174a65cf4ce790 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Tue, 28 Feb 2023 16:13:14 +1300 Subject: [PATCH 10/42] Added free-form sub-blocked models. --- omf/blockmodel/models.py | 64 +++++++++++++++++++++++++++++++++++-- omf/blockmodel/subblocks.py | 24 +++++++------- 2 files changed, 74 insertions(+), 14 deletions(-) diff --git a/omf/blockmodel/models.py b/omf/blockmodel/models.py index 1ffa40ee..1614dffa 100644 --- a/omf/blockmodel/models.py +++ b/omf/blockmodel/models.py @@ -3,7 +3,7 @@ from ..base import ProjectElement from .definition import RegularBlockModelDefinition, TensorBlockModelDefinition -from .subblocks import RegularSubblockDefinition +from .subblocks import FreeformSubblockDefinition, RegularSubblockDefinition from ._subblock_check import check_subblocks @@ -98,7 +98,7 @@ class SubblockedModel(ProjectElement): dtype=int, ) subblock_definition = properties.Instance( - "Defines the structure of sub-blocks within each parent block for this model.", + "Defines the structure of sub-blocks within each parent block.", RegularSubblockDefinition, default=RegularSubblockDefinition, ) @@ -129,3 +129,63 @@ def _validate_subblocks(self): ) self.subblock_parent_indices = _shrink_uint(self.subblock_parent_indices) self.subblock_corners = _shrink_uint(self.subblock_corners) + + +class FreeformSubblockedModel(ProjectElement): + schema = "org.omf.v2.elements.blockmodel.freeform_subblocked" + _valid_locations = ("cells", "parent_blocks") + + definition = properties.Instance( + "Block model definition, for the parent blocks", + RegularBlockModelDefinition, + default=RegularBlockModelDefinition, + ) + subblock_parent_indices = properties.Array( + "The parent block IJK index of each sub-block", + shape=("*", 3), + dtype=int, + ) + subblock_corners = properties.Array( + """The positions of the sub-block corners on the grid within their parent block. + + The columns are (min_i, min_j, min_k, max_i, max_j, max_k). Values must be + between 0.0 and 1.0 inclusive. + + Sub-blocks must stay within the parent block and should not overlap. Gaps are + allowed but it will be impossible for 'cell' attributes to assign values to + those areas. + """, + shape=("*", 6), + dtype=float, + ) + subblock_definition = properties.Instance( + "Defines the structure of sub-blocks within each parent block.", + FreeformSubblockDefinition, + default=FreeformSubblockDefinition, + ) + + @property + def num_cells(self): + """The number of cells, which in this case are always parent blocks.""" + return None if self.subblock_corners is None else len(self.subblock_corners) + + def location_length(self, location): + """Return correct attribute length for 'location'.""" + match location: + case "cells" | "": + return self.num_cells + case "parent_blocks": + return np.prod(self.definition.block_count) + case _: + raise ValueError(f"unknown location type: {location!r}") + + @properties.validator + def _validate_subblocks(self): + check_subblocks( + self.definition, + self.subblock_definition, + self.subblock_parent_indices, + self.subblock_corners, + instance=self, + ) + self.subblock_parent_indices = _shrink_uint(self.subblock_parent_indices) diff --git a/omf/blockmodel/subblocks.py b/omf/blockmodel/subblocks.py index 6047a8b0..20c48307 100644 --- a/omf/blockmodel/subblocks.py +++ b/omf/blockmodel/subblocks.py @@ -37,19 +37,19 @@ def validate_subblocks(self, corners): # TODO check that blocks lie on the octree -# class FreeformSubblockDefinition: -# """Unconstrained free-form sub-block definition. +class FreeformSubblockDefinition: + """Unconstrained free-form sub-block definition. -# Doesn't provide any limitations on or explanation of sub-block positions. -# """ + Provide np limitations on, or explanation of, sub-block positions. + """ -# def validate_subblocks(self, _corners): -# """Checks the sub-blocks within one parent block.""" -# # XXX can we check for overlaps efficiently? + def validate_subblocks(self, _corners): + """Checks the sub-blocks within one parent block.""" + # XXX can we check for overlaps efficiently? -# class VariableZSubblockDefinition(FreeformSubblockDefinition): -# def validate_subblocks(self, corners): -# """Checks the sub-blocks within one parent block.""" -# super().validate_subblocks(corners) -# # TODO check that blocks lie on the octree +class VariableZSubblockDefinition(FreeformSubblockDefinition): + def validate_subblocks(self, corners): + """Checks the sub-blocks within one parent block.""" + super().validate_subblocks(corners) + # TODO check that blocks lie on the octree From 6726a95be2f7865bd6247d4eaa6fdfdbc3d668a3 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Thu, 2 Mar 2023 12:30:10 +1300 Subject: [PATCH 11/42] Finished sub-block checks and tests for them. --- omf/blockmodel/_subblock_check.py | 73 +++++++++++++++++--- omf/blockmodel/models.py | 9 +-- omf/blockmodel/subblocks.py | 30 ++++----- tests/test_blockmodel.py | 2 +- tests/test_subblocks.py | 107 ++++++++++++++++++++++++++++++ 5 files changed, 187 insertions(+), 34 deletions(-) create mode 100644 tests/test_subblocks.py diff --git a/omf/blockmodel/_subblock_check.py b/omf/blockmodel/_subblock_check.py index b8c5e8b9..582c0659 100644 --- a/omf/blockmodel/_subblock_check.py +++ b/omf/blockmodel/_subblock_check.py @@ -1,13 +1,15 @@ import itertools +import sys import numpy as np import properties -from .subblocks import RegularSubblockDefinition +from .subblocks import RegularSubblockDefinition, OctreeSubblockDefinition def _group_by(arr): - assert len(arr) > 0 + if len(arr) == 0: + return diff = np.flatnonzero(arr[1:] != arr[:-1]) diff += 1 if len(diff) == 0: @@ -31,7 +33,7 @@ def _check_parent_indices(definition, parent_indices, instance): def _check_inside_parent(subblock_definition, corners, instance): if isinstance(subblock_definition, RegularSubblockDefinition): - upper = subblock_definition.count + upper = subblock_definition.subblock_count upper_str = f"({upper[0]}, {upper[1]}, {upper[2]})" else: upper = 1.0 @@ -48,13 +50,61 @@ def _check_inside_parent(subblock_definition, corners, instance): ) if not (mx <= upper).all(): raise properties.ValidationError( - f"max_corner <= ({upper_str}) failed", + f"max_corner <= {upper_str} failed", prop="subblock_corners", instance=instance, ) -def check_subblocks(definition, subblock_definition, parent_indices, corners, instance): +def _check_for_overlaps(subblock_definition, one_parent_corners, instance): + # This won't be very fast but there doesn't seem to be a better option. + tracker = np.zeros(subblock_definition.subblock_count[::-1], dtype=int) + for min_i, min_j, min_k, max_i, max_j, max_k in one_parent_corners: + tracker[min_k:max_k, min_j:max_j, min_i:max_i] += 1 + if (tracker > 1).any(): + raise properties.ValidationError( + "found overlapping sub-blocks", prop="subblock_corners", instance=instance + ) + + +def _sizes_to_ints(sizes): + sizes = np.array(sizes, dtype=np.uint64) + assert len(sizes.shape) == 2 and sizes.shape[1] == 3 + sizes[:, 0] *= 2**32 + sizes[:, 1] *= 2**16 + return sizes.sum(axis=1) + + +def _check_octree(subblock_definition, corners, instance): + mn = corners[:, :3] + mx = corners[:, 3:] + # Sizes. + count = subblock_definition.subblock_count + valid_sizes = [count.copy()] + while (count > 1).any(): + count[count > 1] //= 2 + valid_sizes.append(count.copy()) + valid_sizes = _sizes_to_ints(valid_sizes) + sizes = _sizes_to_ints(mx - mn) + if not np.isin(sizes, valid_sizes, kind="table").all(): + raise properties.ValidationError( + "found non-octree sub-block sizes", + prop="subblock_corners", + instance=instance, + ) + # Positions. Octree blocks always start at a multiple of their size. + r = np.remainder(mn, mx - mn) + if (r != 0).any(): + raise properties.ValidationError( + "found non-octree sub-block positions", + prop="subblock_corners", + instance=instance, + ) + + +def check_subblocks( + definition, subblock_definition, parent_indices, corners, instance=None +): if len(parent_indices) != len(corners): raise properties.ValidationError( "'subblock_parent_indices' and 'subblock_corners' arrays must be the same length", @@ -63,15 +113,16 @@ def check_subblocks(definition, subblock_definition, parent_indices, corners, in ) _check_inside_parent(subblock_definition, corners, instance) _check_parent_indices(definition, parent_indices, instance) - # Check order and pass groups to the sub-block definition to check further. + if isinstance(subblock_definition, OctreeSubblockDefinition): + _check_octree(subblock_definition, corners, instance) seen = np.zeros(np.prod(definition.block_count), dtype=bool) - validate = subblock_definition.validate_subblocks - for start, end, parent in _group_by(definition.ijk_to_index(parent_indices)): - if seen[parent]: + for start, end, value in _group_by(definition.ijk_to_index(parent_indices)): + if seen[value]: raise properties.ValidationError( "all sub-blocks inside one parent block must be adjacent in the arrays", prop="subblock_parent_indices", instance=instance, ) - seen[parent] = True - validate(corners[start:end]) + seen[value] = True + if end - start > 1: + _check_for_overlaps(subblock_definition, corners[start:end], instance) diff --git a/omf/blockmodel/models.py b/omf/blockmodel/models.py index 1614dffa..85eb214f 100644 --- a/omf/blockmodel/models.py +++ b/omf/blockmodel/models.py @@ -8,7 +8,8 @@ def _shrink_uint(arr): - assert arr.min() >= 0 + if arr.min() < 0: + return arr t = np.min_scalar_type(arr.max()) return arr.astype(t) @@ -120,6 +121,8 @@ def location_length(self, location): @properties.validator def _validate_subblocks(self): + self.subblock_parent_indices = _shrink_uint(self.subblock_parent_indices) + self.subblock_corners = _shrink_uint(self.subblock_corners) check_subblocks( self.definition, self.subblock_definition, @@ -127,8 +130,6 @@ def _validate_subblocks(self): self.subblock_corners, instance=self, ) - self.subblock_parent_indices = _shrink_uint(self.subblock_parent_indices) - self.subblock_corners = _shrink_uint(self.subblock_corners) class FreeformSubblockedModel(ProjectElement): @@ -181,6 +182,7 @@ def location_length(self, location): @properties.validator def _validate_subblocks(self): + self.subblock_parent_indices = _shrink_uint(self.subblock_parent_indices) check_subblocks( self.definition, self.subblock_definition, @@ -188,4 +190,3 @@ def _validate_subblocks(self): self.subblock_corners, instance=self, ) - self.subblock_parent_indices = _shrink_uint(self.subblock_parent_indices) diff --git a/omf/blockmodel/subblocks.py b/omf/blockmodel/subblocks.py index 20c48307..2675272b 100644 --- a/omf/blockmodel/subblocks.py +++ b/omf/blockmodel/subblocks.py @@ -1,3 +1,4 @@ +import numpy as np import properties from ._properties import BlockCount, OctreeSubblockCount @@ -6,13 +7,16 @@ class RegularSubblockDefinition(properties.HasProperties): """The simplest gridded sub-block definition.""" - count = BlockCount( + subblock_count = BlockCount( "The maximum number of sub-blocks inside a parent in each direction." ) - def validate_subblocks(self, _corners): - """Checks the sub-blocks within one parent block.""" - # TODO check for overlaps + @properties.validator("subblock_count") + def _validate_subblock_count(self, change): + if (change["value"] > 65535).any(): + raise properties.ValidationError( + "sub-block count is limited to 65535 in each direction" + ) class OctreeSubblockDefinition(RegularSubblockDefinition): @@ -27,15 +31,10 @@ class OctreeSubblockDefinition(RegularSubblockDefinition): all axes to be equal. """ - count = OctreeSubblockCount( + subblock_count = OctreeSubblockCount( "The maximum number of sub-blocks inside a parent in each direction." ) - def validate_subblocks(self, corners): - """Checks the sub-blocks within one parent block.""" - super().validate_subblocks(corners) - # TODO check that blocks lie on the octree - class FreeformSubblockDefinition: """Unconstrained free-form sub-block definition. @@ -43,13 +42,8 @@ class FreeformSubblockDefinition: Provide np limitations on, or explanation of, sub-block positions. """ - def validate_subblocks(self, _corners): - """Checks the sub-blocks within one parent block.""" - # XXX can we check for overlaps efficiently? - class VariableZSubblockDefinition(FreeformSubblockDefinition): - def validate_subblocks(self, corners): - """Checks the sub-blocks within one parent block.""" - super().validate_subblocks(corners) - # TODO check that blocks lie on the octree + """Sub-blocks will be contrained to be on an XY grid with variable Z.""" + + # FIXME add var-z properties diff --git a/tests/test_blockmodel.py b/tests/test_blockmodel.py index 9dc545f6..a041a8ca 100644 --- a/tests/test_blockmodel.py +++ b/tests/test_blockmodel.py @@ -133,7 +133,7 @@ def test_num_cells(self): class TestSubblockedModel: def test_pack_uints(self): block_model = omf.SubblockedModel() - block_model.subblock_definition.count = [2, 2, 2] + block_model.subblock_definition.subblock_count = [2, 2, 2] block_model.definition.block_size = [1.0, 1.0, 1.0] block_model.definition.block_count = [10, 10, 10] block_model.subblock_parent_indices = np.array([(0, 0, 0)]) diff --git a/tests/test_subblocks.py b/tests/test_subblocks.py new file mode 100644 index 00000000..dc7c27fe --- /dev/null +++ b/tests/test_subblocks.py @@ -0,0 +1,107 @@ +"""Tests for block models""" +import numpy as np +import properties +import pytest + +import omf +from omf.blockmodel import _subblock_check + + +def test_group_by(): + a = np.array([0, 0, 1, 1, 1, 2]) + assert list(_subblock_check._group_by(a)) == [(0, 2, 0), (2, 5, 1), (5, 6, 2)] + a = np.ones(1, dtype=int) + assert list(_subblock_check._group_by(a)) == [(0, 1, 1)] + a = np.zeros(0, dtype=int) + assert list(_subblock_check._group_by(a)) == [] + + +def _bm_def(): + return omf.RegularBlockModelDefinition( + block_size=(1.0, 1.0, 1.0), + block_count=(1, 1, 1), + ) + + +def _test_regular(*corners): + block_model = omf.SubblockedModel() + block_model.definition = _bm_def() + block_model.subblock_definition = omf.RegularSubblockDefinition( + subblock_count=(5, 4, 3) + ) + block_model.subblock_corners = np.array(corners) + block_model.subblock_parent_indices = np.zeros((len(corners), 3), dtype=int) + block_model.validate() + + +def test_overlap(): + with pytest.raises(properties.ValidationError, match="overlapping sub-blocks"): + _test_regular((0, 0, 0, 2, 2, 1), (0, 0, 0, 4, 4, 2)) + + +def test_outside_parent(): + with pytest.raises(properties.ValidationError, match="0 <= min_corner"): + _test_regular((0, 0, -1, 4, 4, 1)) + with pytest.raises(properties.ValidationError, match="min_corner < max_corner"): + _test_regular((4, 0, 0, 0, 4, 2)) + with pytest.raises(properties.ValidationError, match=r"max_corner <= \(5, 4, 3\)"): + _test_regular((0, 0, 0, 4, 5, 2)) + + +def test_invalid_parent_indices(): + block_model = omf.SubblockedModel() + block_model.definition = _bm_def() + block_model.subblock_definition = omf.RegularSubblockDefinition( + subblock_count=(5, 4, 3) + ) + block_model.subblock_corners = np.array([(0, 0, 0, 5, 4, 3), (0, 0, 0, 5, 4, 3)]) + block_model.subblock_parent_indices = np.array([(0, 0, 0), (1, 0, 0)]) + with pytest.raises( + properties.ValidationError, match=r"subblock_parent_indices < \(1, 1, 1\)" + ): + block_model.validate() + block_model.subblock_parent_indices = np.array([(0, 0, -1), (0, 0, 0)]) + with pytest.raises( + properties.ValidationError, match="0 <= subblock_parent_indices" + ): + block_model.validate() + + +def _test_octree(*corners): + block_model = omf.SubblockedModel() + block_model.definition = _bm_def() + block_model.subblock_definition = omf.OctreeSubblockDefinition( + subblock_count=(4, 4, 2) + ) + block_model.subblock_corners = np.array(corners) + block_model.subblock_parent_indices = np.zeros((len(corners), 3), dtype=int) + block_model.validate() + + +def test_one_full_block(): + _test_octree((0, 0, 0, 4, 4, 2)) + + +def test_eight_blocks(): + _test_octree( + (0, 0, 0, 2, 2, 1), + (2, 0, 0, 4, 2, 1), + (0, 2, 0, 2, 4, 1), + (2, 2, 0, 4, 4, 1), + (0, 0, 1, 2, 2, 2), + (2, 0, 1, 4, 2, 2), + (0, 2, 1, 2, 4, 2), + (2, 2, 1, 4, 4, 2), + ) + + +def test_bad_size(): + with pytest.raises(properties.ValidationError, match="non-octree sub-block sizes"): + _test_octree((0, 0, 0, 3, 4, 2)) + + +def test_bad_position(): + with pytest.raises( + properties.ValidationError, match="non-octree sub-block positions" + ): + _test_octree((0, 1, 0, 2, 3, 1)) From 002b96325779b8878345bbde7e575f5a847b8721 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Thu, 2 Mar 2023 12:53:56 +1300 Subject: [PATCH 12/42] Removed dead test code, added a few more tests. --- omf/blockmodel/models.py | 9 +- tests/test_blockmodel.py | 636 ++---------------- ...t_subblocks.py => test_subblockedmodel.py} | 45 ++ 3 files changed, 106 insertions(+), 584 deletions(-) rename tests/{test_subblocks.py => test_subblockedmodel.py} (61%) diff --git a/omf/blockmodel/models.py b/omf/blockmodel/models.py index 85eb214f..f6523993 100644 --- a/omf/blockmodel/models.py +++ b/omf/blockmodel/models.py @@ -106,16 +106,21 @@ class SubblockedModel(ProjectElement): @property def num_cells(self): - """The number of cells, which in this case are always parent blocks.""" + """The number of cells, which in this case are sub-blocks.""" return None if self.subblock_corners is None else len(self.subblock_corners) + @property + def num_parent_blocks(self): + """The number of parent blocks.""" + return np.prod(self.definition.block_count) + def location_length(self, location): """Return correct attribute length for 'location'.""" match location: case "cells" | "": return self.num_cells case "parent_blocks": - return np.prod(self.definition.block_count) + return self.num_parent_blocks case _: raise ValueError(f"unknown location type: {location!r}") diff --git a/tests/test_blockmodel.py b/tests/test_blockmodel.py index a041a8ca..24b152fb 100644 --- a/tests/test_blockmodel.py +++ b/tests/test_blockmodel.py @@ -6,54 +6,47 @@ import omf -def _make_regular(count): - bm = omf.RegularBlockModel() - bm.definition.block_count = count - bm.definition.block_size = [1.0, 1.0, 1.0] - return bm - - -class MockArray(omf.base.BaseModel): - """Test array class""" - - array = np.array([1, 2, 3]) +def _make_regular_definition(count): + return omf.RegularBlockModelDefinition( + block_count=count, block_size=(1.0, 1.0, 1.0) + ) def test_ijk_index_errors(): """Test ijk indexing into parent blocks errors as expected""" - block_model = _make_regular([3, 4, 5]) + defn = _make_regular_definition([3, 4, 5]) with pytest.raises(TypeError): - block_model.definition.ijk_to_index("a") + defn.ijk_to_index("a") with pytest.raises(TypeError): - block_model.definition.index_to_ijk("a") + defn.index_to_ijk("a") with pytest.raises(ValueError): - block_model.definition.ijk_to_index([0, 0]) + defn.ijk_to_index([0, 0]) with pytest.raises(TypeError): - block_model.definition.ijk_to_index([0, 0, 0.5]) + defn.ijk_to_index([0, 0, 0.5]) with pytest.raises(TypeError): - block_model.definition.index_to_ijk(0.5) + defn.index_to_ijk(0.5) with pytest.raises(IndexError): - block_model.definition.ijk_to_index([0, 0, 5]) + defn.ijk_to_index([0, 0, 5]) with pytest.raises(IndexError): - block_model.definition.index_to_ijk(60) + defn.index_to_ijk(60) with pytest.raises(IndexError): - block_model.definition.ijk_to_index([[0, 0, 5], [0, 0, 3]]) + defn.ijk_to_index([[0, 0, 5], [0, 0, 3]]) with pytest.raises(IndexError): - block_model.definition.index_to_ijk([0, 1, 60]) + defn.index_to_ijk([0, 1, 60]) def test_ijk_index_arrays(): """Test ijk array indexing into parent blocks works as expected""" - block_model = _make_regular([3, 4, 5]) + defn = _make_regular_definition([3, 4, 5]) ijk = [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1), (2, 3, 4)] index = [0, 1, 3, 12, 59] - assert np.array_equal(block_model.definition.ijk_to_index(ijk), index) - assert np.array_equal(block_model.definition.index_to_ijk(index), ijk) + assert np.array_equal(defn.ijk_to_index(ijk), index) + assert np.array_equal(defn.index_to_ijk(index), ijk) ijk = [[(0, 0, 0), (1, 0, 0)], [(0, 1, 0), (0, 0, 1)]] index = [(0, 1), (3, 12)] - assert np.array_equal(block_model.definition.ijk_to_index(ijk), index) - assert np.array_equal(block_model.definition.index_to_ijk(index), ijk) + assert np.array_equal(defn.ijk_to_index(ijk), index) + assert np.array_equal(defn.index_to_ijk(index), ijk) @pytest.mark.parametrize( @@ -62,9 +55,9 @@ def test_ijk_index_arrays(): ) def test_ijk_index(ijk, index): """Test ijk indexing into parent blocks works as expected""" - block_model = _make_regular([3, 4, 5]) - assert block_model.definition.ijk_to_index(ijk) == index - assert np.array_equal(block_model.definition.index_to_ijk(index), ijk) + defn = _make_regular_definition([3, 4, 5]) + assert defn.ijk_to_index(ijk) == index + assert np.array_equal(defn.index_to_ijk(index), ijk) def test_tensorblockmodel(): @@ -86,563 +79,42 @@ def test_tensorblockmodel(): elem.axis_v = "Y" -# pylint: disable=W0143 -class TestRegularBlockModel: - """Test class for regular block model functionality""" - - @pytest.mark.parametrize( - "block_count", ([2, 2], [2, 2, 2, 2], [0, 2, 2], [2, 2, 0.5]) - ) - def test_bad_block_count(self, block_count): - """Test mismatched block_count""" - block_model = omf.RegularBlockModel() - block_model.definition.block_size = [1.0, 2.0, 3.0] - with pytest.raises(properties.ValidationError): - block_model.definition.block_count = block_count - block_model.validate() - - @pytest.mark.parametrize( - "block_size", ([2.0, 2.0], [2.0, 2.0, 2.0, 2.0], [-1.0, 2, 2], [0.0, 2, 2]) - ) - def test_bad_block_size(self, block_size): - """Test mismatched block_size""" - block_model = omf.RegularBlockModel() - block_model.definition.block_count = [2, 2, 2] - with pytest.raises(properties.ValidationError): - block_model.definition.block_size = block_size - block_model.validate() - - def test_uninstantiated(self): - """Test all attributes are None on instantiation""" - block_model = omf.RegularBlockModel() - assert block_model.definition.block_count is None - assert block_model.definition.block_size is None - assert block_model.num_cells is None - - def test_num_cells(self): - """Test num_cells calculation is correct""" - block_model = omf.RegularBlockModel() - block_model.definition.block_count = [2, 2, 2] - block_model.definition.block_size = [1.0, 2.0, 3.0] - np.testing.assert_array_equal(block_model.definition.block_count, [2, 2, 2]) - assert block_model.num_cells == 8 - assert block_model.location_length("cells") == 8 - assert block_model.location_length("parent_blocks") == 8 - - -class TestSubblockedModel: - def test_pack_uints(self): - block_model = omf.SubblockedModel() - block_model.subblock_definition.subblock_count = [2, 2, 2] - block_model.definition.block_size = [1.0, 1.0, 1.0] - block_model.definition.block_count = [10, 10, 10] - block_model.subblock_parent_indices = np.array([(0, 0, 0)]) - block_model.subblock_corners = np.array([(0, 0, 0, 2, 2, 2)]) - # We set this as uint32 - assert block_model.subblock_corners.dtype == np.int32 +@pytest.mark.parametrize("block_count", ([2, 2], [2, 2, 2, 2], [0, 2, 2], [2, 2, 0.5])) +def test_bad_block_count(block_count): + """Test mismatched block_count""" + block_model = omf.RegularBlockModel() + block_model.definition.block_size = [1.0, 2.0, 3.0] + with pytest.raises(properties.ValidationError): + block_model.definition.block_count = block_count block_model.validate() - # Validate should have packed it down to uint8 - assert block_model.subblock_corners.dtype == np.uint8 - - -# class TestRegularSubBlockModel: -# """Test class for regular sub block model functionality""" - -# bm_class = omf.RegularSubBlockModel - -# @pytest.mark.parametrize( -# "block_count", ([2, 2], [2, 2, 2, 2], [0, 2, 2], [2, 2, 0.5]) -# ) -# @pytest.mark.parametrize("attr", ("parent_block_count", "sub_block_count")) -# def test_bad_block_count(self, block_count, attr): -# """Test mismatched block_count""" -# block_model = self.bm_class(parent_block_size=[1.0, 2.0, 3.0]) -# with pytest.raises(properties.ValidationError): -# setattr(block_model, attr, block_count) -# block_model.validate() - -# @pytest.mark.parametrize( -# "block_size", ([2.0, 2.0], [2.0, 2.0, 2.0, 2.0], [-1.0, 2, 2], [0.0, 2, 2]) -# ) -# def test_bad_block_size(self, block_size): -# """Test mismatched block_size""" -# block_model = self.bm_class(parent_block_count=[2, 2, 2]) -# with pytest.raises(properties.ValidationError): -# block_model.parent_block_size = block_size -# block_model.validate() - -# def test_uninstantiated(self): -# """Test all attributes are None on instantiation""" -# block_model = self.bm_class() -# assert block_model.parent_block_count is None -# assert block_model.sub_block_count is None -# assert block_model.parent_block_size is None -# assert block_model.sub_block_size is None -# assert block_model.cbc is None -# assert block_model.cbi is None -# assert block_model.num_cells is None -# with pytest.raises(ValueError): -# block_model.reset_cbc() -# with pytest.raises(ValueError): -# block_model.refine([0, 0, 0]) -# block_model.validate_cbc({"value": MockArray()}) - -# def test_num_cells(self): -# """Test num_cells calculation is correct""" -# block_model = self.bm_class( -# parent_block_count=[2, 2, 2], -# sub_block_count=[2, 2, 2], -# parent_block_size=[1.0, 2.0, 3.0], -# ) -# block_model.reset_cbc() -# assert block_model.num_cells == 8 -# block_model.cbc = np.array([0, 0, 0, 0, 1, 1, 1, 1]) -# assert block_model.num_cells == 4 -# block_model.refine([1, 1, 1]) -# assert block_model.num_cells == 11 - -# def test_cbc(self): -# """Test cbc access and validation is correct""" -# block_model = self.bm_class( -# parent_block_count=[2, 2, 2], -# sub_block_count=[3, 4, 5], -# parent_block_size=[1.0, 2.0, 3.0], -# ) -# block_model.reset_cbc() -# assert block_model.validate() -# assert np.all(block_model.cbc == np.ones(8)) -# block_model.cbc.array[0] = 0 -# assert block_model.validate() -# block_model.cbc.array[0] = 60 -# assert block_model.validate() -# with pytest.raises(properties.ValidationError): -# block_model.cbc = np.ones(7, dtype="int8") -# block_model.cbc = np.ones(8, dtype="uint8") -# with pytest.raises(properties.ValidationError): -# block_model.cbc.array[0] = 2 -# block_model.validate() -# with pytest.raises(properties.ValidationError): -# block_model.cbc.array[0] = -1 -# block_model.validate() - -# def test_cbi(self): -# """Test cbi access and validation is correct""" -# block_model = self.bm_class() -# assert block_model.cbi is None -# block_model.parent_block_count = [2, 2, 2] -# block_model.sub_block_count = [3, 4, 5] -# block_model.parent_block_size = [1.0, 2.0, 3.0] -# block_model.reset_cbc() -# assert np.all(block_model.cbi == np.array(range(9), dtype="int8")) -# block_model.cbc.array[0] = 0 -# assert np.all( -# block_model.cbi -# == np.r_[np.array([0], dtype="int8"), np.array(range(8), dtype="int8")] -# ) -# block_model.refine([1, 0, 0]) -# assert np.all( -# block_model.cbi -# == np.r_[ -# np.array([0, 0], dtype="int8"), np.array(range(60, 67), dtype="int8") -# ] -# ) - -# def test_location_length(self): -# """Ensure location length updates as expected with block refinement""" -# block_model = self.bm_class( -# parent_block_count=[2, 2, 2], -# sub_block_count=[3, 4, 5], -# parent_block_size=[1.0, 2.0, 3.0], -# ) -# block_model.reset_cbc() -# assert block_model.location_length("parent_blocks") == 8 -# assert block_model.location_length("sub_blocks") == 8 -# block_model.refine([0, 0, 0]) -# assert block_model.location_length("parent_blocks") == 8 -# assert block_model.location_length("sub_blocks") == 67 - -# class TestOctreeSubBlockModel: -# """Test class for octree sub block model""" -# bm_class = omf.OctreeSubBlockModel - -# @pytest.mark.parametrize( -# "block_count", ([2, 2], [2, 2, 2, 2], [0, 2, 2], [2, 2, 0.5]) -# ) -# def test_bad_block_count(self, block_count): -# """Test mismatched block_count""" -# block_model = self.bm_class(parent_block_size=[1.0, 2.0, 3.0]) -# with pytest.raises(properties.ValidationError): -# block_model.parent_block_size = block_count -# block_model.validate() - -# @pytest.mark.parametrize( -# "block_size", ([2.0, 2.0], [2.0, 2.0, 2.0, 2.0], [-1.0, 2, 2], [0.0, 2, 2]) -# ) -# def test_bad_block_size(self, block_size): -# """Test mismatched block_size""" -# block_model = self.bm_class(parent_block_count=[2, 2, 2]) -# with pytest.raises(properties.ValidationError): -# block_model.parent_block_count = block_size -# block_model.validate() - -# def test_uninstantiated(self): -# """Test all attributes are None on instantiation""" -# block_model = self.bm_class() -# assert block_model.parent_block_count is None -# assert block_model.parent_block_size is None -# assert block_model.cbc is None -# assert block_model.cbi is None -# assert block_model.zoc is None -# assert block_model.num_cells is None -# block_model.validate_cbc({"value": MockArray()}) -# block_model.validate_zoc({"value": MockArray()}) -# with pytest.raises(ValueError): -# block_model.reset_cbc() -# with pytest.raises(ValueError): -# block_model.reset_zoc() - -# def test_num_cells(self): -# """Test num_cells calculation is correct""" -# block_model = self.bm_class( -# parent_block_count=[2, 2, 2], -# parent_block_size=[1.0, 2.0, 3.0], -# ) -# block_model.reset_cbc() -# assert block_model.num_cells == 8 -# block_model.cbc = np.array([0, 0, 0, 0, 1, 1, 1, 1]) -# assert block_model.num_cells == 4 - -# def test_cbc(self): -# """Test cbc access and validation is correct""" -# block_model = self.bm_class( -# parent_block_count=[2, 2, 2], -# parent_block_size=[1.0, 2.0, 3.0], -# ) -# block_model.reset_cbc() -# block_model.reset_zoc() -# assert block_model.validate() -# assert np.all(block_model.cbc == np.ones(8)) -# block_model.cbc.array[0] = 0 -# block_model.zoc = block_model.zoc[1:] -# assert block_model.validate() -# with pytest.raises(properties.ValidationError): -# block_model.cbc = np.ones(7, dtype="int8") -# block_model.cbc = np.ones(8, dtype="uint8") -# block_model.zoc = np.zeros(8, dtype="uint8") -# assert block_model.validate() -# with pytest.raises(properties.ValidationError): -# block_model.cbc.array[0] = 2 -# block_model.validate() -# with pytest.raises(properties.ValidationError): -# block_model.cbc.array[0] = -1 -# block_model.validate() - -# def test_cbi(self): -# """Test cbi access and validation is correct""" -# block_model = self.bm_class() -# assert block_model.cbi is None -# block_model.parent_block_count = [2, 2, 2] -# block_model.parent_block_size = [1.0, 2.0, 3.0] -# block_model.reset_cbc() -# assert np.all(block_model.cbi == np.array(range(9), dtype=np.uint64)) -# block_model.cbc.array[0] = 0 -# assert np.all( -# block_model.cbi -# == np.r_[ -# np.array([0], dtype=np.uint64), np.array(range(8), dtype=np.uint64) -# ] -# ) - -# def test_zoc(self): -# """Test z-order curves""" -# block_model = self.bm_class( -# parent_block_count=[2, 2, 2], -# parent_block_size=[1.0, 2.0, 3.0], -# ) -# block_model.reset_cbc() -# block_model.reset_zoc() -# assert np.all(block_model.zoc == np.zeros(8)) -# with pytest.raises(properties.ValidationError): -# block_model.zoc = np.zeros(7, dtype=np.uint64) -# with pytest.raises(properties.ValidationError): -# block_model.zoc = np.r_[np.zeros(7), -1.0].astype(np.uint64) -# with pytest.raises(properties.ValidationError): -# block_model.zoc = np.r_[np.zeros(7), 268435448 + 1].astype(np.uint64) -# block_model.zoc = np.r_[np.zeros(7), 268435448].astype(np.uint64) -# assert block_model.validate() - -# @pytest.mark.parametrize( -# ("pointer", "level", "curve_value"), -# [ -# ([1, 16, 0], 7, 131095), -# ([0, 0, 0], 0, 0), -# ([255, 255, 255], 8, 268435448), -# ], -# ) -# def test_curve_values(self, pointer, level, curve_value): -# """Test curve value functions""" -# assert self.bm_class.get_curve_value(pointer, level) == curve_value -# assert self.bm_class.get_level(curve_value) == level -# assert self.bm_class.get_pointer(curve_value) == pointer - -# def test_level_width(self): -# """Test level width function""" -# with pytest.raises(ValueError): -# self.bm_class.level_width(9) - -# def test_refinement(self): -# """Test refinement method""" -# block_model = self.bm_class( -# parent_block_count=[2, 2, 2], -# parent_block_size=[5.0, 5.0, 5.0], -# ) -# block_model.reset_cbc() -# block_model.reset_zoc() -# assert len(block_model.zoc) == 8 -# assert all(zoc == 0 for zoc in block_model.zoc) -# block_model.refine(0) -# assert len(block_model.zoc) == 15 -# assert block_model.location_length("parent_blocks") == 8 -# assert block_model.location_length("") == 15 -# assert np.array_equal(block_model.cbc, [8] + [1] * 7) -# assert np.array_equal(block_model.cbi, [0] + list(range(8, 16))) -# assert np.array_equal( -# block_model.zoc, -# [ -# block_model.get_curve_value([0, 0, 0], 1), -# block_model.get_curve_value([128, 0, 0], 1), -# block_model.get_curve_value([0, 128, 0], 1), -# block_model.get_curve_value([128, 128, 0], 1), -# block_model.get_curve_value([0, 0, 128], 1), -# block_model.get_curve_value([128, 0, 128], 1), -# block_model.get_curve_value([0, 128, 128], 1), -# block_model.get_curve_value([128, 128, 128], 1), -# ] -# + [0] * 7, -# ) -# block_model.refine(2, refinements=2) -# assert len(block_model.zoc) == 78 -# assert np.array_equal(block_model.cbc, [71] + [1] * 7) -# assert np.array_equal(block_model.cbi, [0] + list(range(71, 79))) -# assert block_model.zoc[2] == block_model.get_curve_value([0, 128, 0], 3) -# assert block_model.zoc[3] == block_model.get_curve_value([32, 128, 0], 3) -# assert block_model.zoc[4] == block_model.get_curve_value([0, 160, 0], 3) -# assert block_model.zoc[5] == block_model.get_curve_value([32, 160, 0], 3) -# assert block_model.zoc[6] == block_model.get_curve_value([0, 128, 32], 3) -# assert block_model.zoc[64] == block_model.get_curve_value([64, 224, 96], 3) -# assert block_model.zoc[65] == block_model.get_curve_value([96, 224, 96], 3) -# assert block_model.zoc[66] == block_model.get_curve_value([128, 128, 0], 1) -# block_model.refine(0, [1, 0, 0]) -# assert len(block_model.zoc) == 85 -# assert np.array_equal(block_model.cbc, [71, 8] + [1] * 6) -# with pytest.raises(ValueError): -# block_model.refine(85) -# with pytest.raises(ValueError): -# block_model.refine(-1) -# with pytest.raises(ValueError): -# block_model.refine(1, [1, 1, 1]) -# with pytest.raises(ValueError): -# block_model.refine(2, refinements=-1) -# with pytest.raises(ValueError): -# block_model.refine(2, refinements=6) - - -# class TestArbitrarySubBlockModel: -# """Test class for ArbitrarySubBlockModel""" - -# bm_class = omf.ArbitrarySubBlockModel - -# @pytest.mark.parametrize( -# "block_count", ([2, 2], [2, 2, 2, 2], [0, 2, 2], [2, 2, 0.5]) -# ) -# def test_bad_block_count(self, block_count): -# """Test mismatched block_count""" -# block_model = self.bm_class(parent_block_size=[1.0, 2.0, 3.0]) -# with pytest.raises(properties.ValidationError): -# block_model.parent_block_size = block_count -# block_model.validate() - -# @pytest.mark.parametrize( -# "block_size", ([2.0, 2.0], [2.0, 2.0, 2.0, 2.0], [-1.0, 2, 2], [0.0, 2, 2]) -# ) -# def test_bad_block_size(self, block_size): -# """Test mismatched block_size""" -# block_model = self.bm_class(parent_block_count=[2, 2, 2]) -# with pytest.raises(properties.ValidationError): -# block_model.parent_block_count = block_size -# block_model.validate() - -# def test_uninstantiated(self): -# """Test all attributes are None on instantiation""" -# block_model = self.bm_class() -# assert block_model.parent_block_count is None -# assert block_model.parent_block_size is None -# assert block_model.cbc is None -# assert block_model.cbi is None -# assert block_model.sub_block_corners is None -# assert block_model.sub_block_sizes is None -# assert block_model.sub_block_centroids is None -# assert block_model.sub_block_corners_absolute is None -# assert block_model.sub_block_sizes_absolute is None -# assert block_model.sub_block_centroids_absolute is None -# assert block_model.num_cells is None -# block_model.validate_cbc({"value": MockArray()}) -# with pytest.raises(ValueError): -# block_model.reset_cbc() - -# def test_num_cells(self): -# """Test num_cells calculation is correct""" -# block_model = self.bm_class( -# parent_block_count=[2, 2, 2], -# parent_block_size=[1.0, 2.0, 3.0], -# ) -# block_model.reset_cbc() -# assert block_model.num_cells == 8 -# block_model.cbc = np.array([0, 0, 0, 0, 1, 1, 1, 1]) -# assert block_model.num_cells == 4 - -# def test_cbc(self): -# """Test cbc access and validation is correct""" -# block_model = self.bm_class( -# parent_block_count=[2, 2, 2], -# parent_block_size=[1.0, 2.0, 3.0], -# ) -# with pytest.raises(properties.ValidationError): -# block_model.validate() -# block_model.sub_block_corners = np.zeros((8, 3)) -# block_model.sub_block_sizes = np.ones((8, 3)) -# block_model.reset_cbc() -# assert block_model.validate() -# assert np.all(block_model.cbc == np.ones(8)) -# block_model.cbc.array[0] = 0 -# with pytest.raises(properties.ValidationError): -# block_model.validate() -# block_model.sub_block_corners = np.zeros((7, 3)) -# block_model.sub_block_sizes = np.ones((7, 3)) -# assert block_model.validate() -# with pytest.raises(properties.ValidationError): -# block_model.cbc = np.ones(7, dtype="int8") -# block_model.cbc = np.ones(8, dtype="uint8") -# block_model.sub_block_corners = np.zeros((8, 3)) -# block_model.sub_block_sizes = np.ones((8, 3)) -# with pytest.raises(properties.ValidationError): -# block_model.cbc.array[0] = 2 -# block_model.validate() -# with pytest.raises(properties.ValidationError): -# block_model.cbc.array[0] = -1 -# block_model.validate() - -# def test_cbi(self): -# """Test cbi access and validation is correct""" -# block_model = self.bm_class() -# assert block_model.cbi is None -# block_model.parent_block_count = [2, 2, 2] -# block_model.parent_block_size = [1.0, 2.0, 3.0] -# block_model.reset_cbc() -# assert np.all(block_model.cbi == np.array(range(9), dtype=np.uint64)) -# block_model.cbc.array[0] = 0 -# assert np.all( -# block_model.cbi -# == np.r_[ -# np.array([0], dtype=np.uint64), np.array(range(8), dtype=np.uint64) -# ] -# ) - -# def test_validate_sub_block_attrs(self): -# """Test sub block attribute validation""" -# block_model = self.bm_class() -# value = [1, 2, 3] -# assert block_model.validate_sub_block_attributes(value, "") is value -# block_model.parent_block_count = [2, 2, 2] -# block_model.parent_block_size = [1.0, 2.0, 3.0] -# block_model.reset_cbc() -# with pytest.raises(properties.ValidationError): -# block_model.validate_sub_block_attributes(value, "") +@pytest.mark.parametrize( + "block_size", ([2.0, 2.0], [2.0, 2.0, 2.0, 2.0], [-1.0, 2, 2], [0.0, 2, 2]) +) +def test_bad_block_size(block_size): + """Test mismatched block_size""" + block_model = omf.RegularBlockModel() + block_model.definition.block_count = [2, 2, 2] + with pytest.raises(properties.ValidationError): + block_model.definition.block_size = block_size + block_model.validate() -# def test_validate_sub_block_sizes(self): -# """Test sub block size validation""" -# block_model = self.bm_class() -# block_model.sub_block_sizes = [[1.0, 2, 3]] -# with pytest.raises(properties.ValidationError): -# block_model.sub_block_sizes = [[0.0, 1, 2]] -# def test_sub_block_attributes(self): -# """Test sub block attributes""" -# block_model = self.bm_class( -# parent_block_count=[2, 2, 2], -# parent_block_size=[1.0, 2.0, 3.0], -# ) -# block_model.reset_cbc() -# with pytest.raises(properties.ValidationError): -# block_model.sub_block_sizes = np.ones((3, 3)) -# with pytest.raises(properties.ValidationError): -# block_model.sub_block_sizes = np.r_[np.ones((7, 3)), [[1.0, 1.0, 0]]] -# block_model.sub_block_sizes = np.ones((8, 3)) -# assert np.array_equal( -# block_model.sub_block_sizes_absolute, np.array([[1.0, 2.0, 3.0]] * 8) -# ) -# assert block_model.sub_block_centroids is None -# assert block_model.sub_block_centroids_absolute is None -# with pytest.raises(properties.ValidationError): -# block_model.sub_block_corners = np.zeros((3, 3)) -# block_model.sub_block_corners = np.zeros((8, 3)) -# assert np.array_equal( -# block_model.sub_block_corners_absolute, -# np.array( -# [ -# [0.0, 0, 0], -# [1.0, 0, 0], -# [0.0, 2, 0], -# [1.0, 2, 0], -# [0.0, 0, 3], -# [1.0, 0, 3], -# [0.0, 2, 3], -# [1.0, 2, 3], -# ] -# ), -# ) -# assert np.array_equal(block_model.sub_block_centroids, np.ones((8, 3)) * 0.5) -# assert np.array_equal( -# block_model.sub_block_centroids_absolute, -# np.array( -# [ -# [0.5, 1, 1.5], -# [1.5, 1, 1.5], -# [0.5, 3, 1.5], -# [1.5, 3, 1.5], -# [0.5, 1, 4.5], -# [1.5, 1, 4.5], -# [0.5, 3, 4.5], -# [1.5, 3, 4.5], -# ] -# ), -# ) -# assert block_model.validate() -# assert block_model.location_length("parent_blocks") == 8 -# assert block_model.location_length("") == 8 -# block_model.cbc = np.array([1] + [0] * 7, dtype=int) -# with pytest.raises(properties.ValidationError): -# block_model.validate() -# block_model.sub_block_corners = np.array([[-0.5, 2, 0]]) -# block_model.sub_block_sizes = np.array([[0.5, 0.5, 2]]) -# assert block_model.validate() -# assert block_model.location_length("parent_blocks") == 1 -# assert block_model.location_length("") == 1 -# assert np.array_equal( -# block_model.sub_block_centroids, np.array([[-0.25, 2.25, 1]]) -# ) -# assert np.array_equal( -# block_model.sub_block_corners_absolute, np.array([[-0.5, 4, 0]]) -# ) -# assert np.array_equal( -# block_model.sub_block_sizes_absolute, np.array([[0.5, 1, 6]]) -# ) -# assert np.array_equal( -# block_model.sub_block_centroids_absolute, np.array([[-0.25, 4.5, 3]]) -# ) -# assert block_model.validate() +def test_uninstantiated(): + """Test all attributes are None on instantiation""" + block_model = omf.RegularBlockModel() + assert block_model.definition.block_count is None + assert block_model.definition.block_size is None + assert block_model.num_cells is None -# pylint: enable=W0143 +def test_num_cells(): + """Test num_cells calculation is correct""" + block_model = omf.RegularBlockModel() + block_model.definition.block_count = [2, 2, 2] + block_model.definition.block_size = [1.0, 2.0, 3.0] + np.testing.assert_array_equal(block_model.definition.block_count, [2, 2, 2]) + assert block_model.num_cells == 8 + assert block_model.location_length("cells") == 8 + assert block_model.location_length("parent_blocks") == 8 diff --git a/tests/test_subblocks.py b/tests/test_subblockedmodel.py similarity index 61% rename from tests/test_subblocks.py rename to tests/test_subblockedmodel.py index dc7c27fe..a0e42b45 100644 --- a/tests/test_subblocks.py +++ b/tests/test_subblockedmodel.py @@ -105,3 +105,48 @@ def test_bad_position(): properties.ValidationError, match="non-octree sub-block positions" ): _test_octree((0, 1, 0, 2, 3, 1)) + + +def test_pack_subblock_arrays(): + block_model = omf.SubblockedModel() + block_model.subblock_definition.subblock_count = [2, 2, 2] + block_model.definition.block_size = [1.0, 1.0, 1.0] + block_model.definition.block_count = [10, 10, 10] + block_model.subblock_parent_indices = np.array([(0, 0, 0)]) + block_model.subblock_corners = np.array([(0, 0, 0, 2, 2, 2)]) + # We set this as default ints. + assert block_model.subblock_corners.dtype == np.int32 + block_model.validate() + # Validate should have packed it down to uint8. + assert block_model.subblock_corners.dtype == np.uint8 + + +def test_uninstantiated(): + """Test definitions are default and attributes are None on instantiation""" + block_model = omf.SubblockedModel() + assert isinstance(block_model.definition, omf.RegularBlockModelDefinition) + assert isinstance(block_model.subblock_definition, omf.RegularSubblockDefinition) + assert block_model.definition.block_count is None + assert block_model.definition.block_size is None + assert block_model.subblock_definition.subblock_count is None + assert block_model.num_cells is None + assert block_model.subblock_parent_indices is None + assert block_model.subblock_corners is None + + +def test_num_cells(): + """Test num_cells calculation is correct""" + block_model = omf.SubblockedModel() + block_model.definition.block_count = [2, 2, 2] + block_model.definition.block_size = [1.0, 2.0, 3.0] + block_model.subblock_definition.subblock_count = [5, 5, 5] + np.testing.assert_array_equal(block_model.definition.block_count, [2, 2, 2]) + np.testing.assert_array_equal( + block_model.subblock_definition.subblock_count, [5, 5, 5] + ) + block_model.subblock_parent_indices = np.array([(0, 0, 0), (1, 0, 0)]) + block_model.subblock_corners = np.array([(0, 0, 0, 5, 5, 5), (1, 1, 1, 4, 4, 4)]) + assert block_model.num_cells == 2 + assert block_model.num_parent_blocks == 8 + assert block_model.location_length("cells") == 2 + assert block_model.location_length("parent_blocks") == 8 From d2e6850668fda531b7284a96759c4678945c7aa0 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Thu, 2 Mar 2023 13:16:02 +1300 Subject: [PATCH 13/42] Cleaned up code and added docstrings. --- omf/__init__.py | 3 +- omf/blockmodel/_properties.py | 18 ++++++-- omf/blockmodel/_subblock_check.py | 3 +- omf/blockmodel/definition.py | 71 +++++++++++++++++++++++++------ omf/blockmodel/models.py | 17 +++++++- omf/blockmodel/subblocks.py | 49 --------------------- 6 files changed, 91 insertions(+), 70 deletions(-) delete mode 100644 omf/blockmodel/subblocks.py diff --git a/omf/__init__.py b/omf/__init__.py index b792503f..2f610d49 100644 --- a/omf/__init__.py +++ b/omf/__init__.py @@ -11,11 +11,12 @@ ) from .base import Project from .blockmodel.definition import ( + OctreeSubblockDefinition, RegularBlockModelDefinition, + RegularSubblockDefinition, TensorBlockModelDefinition, ) from .blockmodel.models import RegularBlockModel, SubblockedModel, TensorGridBlockModel -from .blockmodel.subblocks import OctreeSubblockDefinition, RegularSubblockDefinition from .composite import Composite from .fileio import load, save, __version__ from .lineset import LineSet diff --git a/omf/blockmodel/_properties.py b/omf/blockmodel/_properties.py index 6d37ec24..9fa03b6f 100644 --- a/omf/blockmodel/_properties.py +++ b/omf/blockmodel/_properties.py @@ -15,12 +15,24 @@ def validate(self, instance, value): msg = f"block counts must be >= 1" else: cls = instance.__class__.__name__ - msg = f"{cls}.{self.name} counts must be >= 1" + msg = f"{cls}.{self.name} must be >= 1" raise properties.ValidationError(msg, prop=self.name, instance=instance) return value -class OctreeSubblockCount(BlockCount): +class SubBlockCount(BlockCount): + def validate(self, instance, value): + value = super().validate(instance, value) + if (value > 65535).any(): + raise properties.ValidationError( + "sub-block counts are limited to 65535 in each direction", + prop=self.name, + instance=instance, + ) + return value + + +class OctreeSubblockCount(SubBlockCount): def validate(self, instance, value): """Check shape and dtype of the count and that items are >= min.""" value = super().validate(instance, value) @@ -28,7 +40,7 @@ def validate(self, instance, value): l = np.log2(item) if np.trunc(l) != l: if instance is None: - msg = f"octree block counts must be powers of two" + msg = f"octree sub-block counts must be powers of two" else: cls = instance.__class__.__name__ msg = f"{cls}.{self.name} octree counts must be powers of two" diff --git a/omf/blockmodel/_subblock_check.py b/omf/blockmodel/_subblock_check.py index 582c0659..8ed14229 100644 --- a/omf/blockmodel/_subblock_check.py +++ b/omf/blockmodel/_subblock_check.py @@ -1,10 +1,9 @@ import itertools -import sys import numpy as np import properties -from .subblocks import RegularSubblockDefinition, OctreeSubblockDefinition +from .definition import RegularSubblockDefinition, OctreeSubblockDefinition def _group_by(arr): diff --git a/omf/blockmodel/definition.py b/omf/blockmodel/definition.py index 5f9c9a73..8424b398 100644 --- a/omf/blockmodel/definition.py +++ b/omf/blockmodel/definition.py @@ -1,7 +1,13 @@ import numpy as np import properties -from ._properties import BlockCount, BlockSize, TensorArray +from ._properties import ( + BlockCount, + BlockSize, + OctreeSubblockCount, + SubBlockCount, + TensorArray, +) class _BaseBlockModelDefinition(properties.HasProperties): @@ -99,15 +105,54 @@ def block_count(self): return None return np.array(c, dtype=int) - @properties.validator("tensor_u") - @properties.validator("tensor_v") - @properties.validator("tensor_w") - def _validate_tensor(self, change): - tensor = change["value"] - if (tensor <= 0.0).any(): - raise properties.ValidationError( - "Tensor spacings must be greater than zero", - prop=change["name"], - instance=self, - reason="invalid", - ) + +class RegularSubblockDefinition(properties.HasProperties): + """The simplest gridded sub-block definition.""" + + subblock_count = SubBlockCount( + "The maximum number of sub-blocks inside a parent in each direction." + ) + + +class OctreeSubblockDefinition(RegularSubblockDefinition): + """Sub-blocks form an octree inside the parent block. + + Cut the parent block in half in all directions to create eight sub-blocks. Repeat that + division for some or all of those new sub-blocks. Continue doing that until the limit + on sub-block count is reached or until the sub-blocks accurately model the inputs. + + This definition also allows the lower level cuts to be omitted in one or two axes, + giving a maximum sub-block count of (16, 16, 4) for example rather than requiring + all axes to be equal. + """ + + subblock_count = OctreeSubblockCount( + "The maximum number of sub-blocks inside a parent in each direction." + ) + + +class FreeformSubblockDefinition: + """Unconstrained free-form sub-block definition. + + Provides no limitations on or explanation of sub-block positions. + """ + + +class VariableHeightSubblockDefinition(FreeformSubblockDefinition): + """Defines sub-blocks on a grid in the U and V directions but variable in the W direction. + + A single sub-block covering the whole parent block is also valid. Sub-blocks should not + overlap. + + Note: these constraints on sub-blocks are not checked during validation. + """ + + subblock_count_u = properties.Integer( + "Number of sub-blocks in the u-direction", min=1, max=65535 + ) + subblock_count_v = properties.Integer( + "Number of sub-blocks in the v-direction", min=1, max=65535 + ) + minimum_size_w = properties.Float( + "Minimum size of sub-blocks in the z-direction", min=0.0 + ) diff --git a/omf/blockmodel/models.py b/omf/blockmodel/models.py index f6523993..91076427 100644 --- a/omf/blockmodel/models.py +++ b/omf/blockmodel/models.py @@ -2,12 +2,17 @@ import properties from ..base import ProjectElement -from .definition import RegularBlockModelDefinition, TensorBlockModelDefinition -from .subblocks import FreeformSubblockDefinition, RegularSubblockDefinition +from .definition import ( + FreeformSubblockDefinition, + RegularBlockModelDefinition, + RegularSubblockDefinition, + TensorBlockModelDefinition, +) from ._subblock_check import check_subblocks def _shrink_uint(arr): + assert arr.dtype.kind in "ui" if arr.min() < 0: return arr t = np.min_scalar_type(arr.max()) @@ -15,6 +20,8 @@ def _shrink_uint(arr): class RegularBlockModel(ProjectElement): + """A block model with fixed size blocks on a regular grid and no sub-blocks.""" + schema = "org.omf.v2.elements.blockmodel.regular" _valid_locations = ("cells", "parent_blocks") @@ -39,6 +46,8 @@ def location_length(self, location): class TensorGridBlockModel(ProjectElement): + """A block model with variable spacing in all directions and no sub-blocks.""" + schema = "org.omf.v2.elements.blockmodel.tensor" _valid_locations = ("vertices", "cells", "parent_blocks") @@ -71,6 +80,8 @@ def location_length(self, location): class SubblockedModel(ProjectElement): + """A regular block model with sub-blocks that align with a lower-level grid.""" + schema = "org.omf.v2.elements.blockmodel.subblocked" _valid_locations = ("cells", "parent_blocks") @@ -138,6 +149,8 @@ def _validate_subblocks(self): class FreeformSubblockedModel(ProjectElement): + """A regular block model with sub-blocks can be anywhere within the parent.""" + schema = "org.omf.v2.elements.blockmodel.freeform_subblocked" _valid_locations = ("cells", "parent_blocks") diff --git a/omf/blockmodel/subblocks.py b/omf/blockmodel/subblocks.py deleted file mode 100644 index 2675272b..00000000 --- a/omf/blockmodel/subblocks.py +++ /dev/null @@ -1,49 +0,0 @@ -import numpy as np -import properties - -from ._properties import BlockCount, OctreeSubblockCount - - -class RegularSubblockDefinition(properties.HasProperties): - """The simplest gridded sub-block definition.""" - - subblock_count = BlockCount( - "The maximum number of sub-blocks inside a parent in each direction." - ) - - @properties.validator("subblock_count") - def _validate_subblock_count(self, change): - if (change["value"] > 65535).any(): - raise properties.ValidationError( - "sub-block count is limited to 65535 in each direction" - ) - - -class OctreeSubblockDefinition(RegularSubblockDefinition): - """Sub-blocks form an octree inside the parent block. - - Cut the parent block in half in all directions to create eight sub-blocks. Repeat that - division for some or all of those new sub-blocks. Continue doing that until the limit - on sub-block count is reached or until the sub-blocks accurately model the inputs. - - This definition also allows the lower level cuts to be omitted in one or two axes, - giving a maximum sub-block count of (16, 16, 4) for example rather than requiring - all axes to be equal. - """ - - subblock_count = OctreeSubblockCount( - "The maximum number of sub-blocks inside a parent in each direction." - ) - - -class FreeformSubblockDefinition: - """Unconstrained free-form sub-block definition. - - Provide np limitations on, or explanation of, sub-block positions. - """ - - -class VariableZSubblockDefinition(FreeformSubblockDefinition): - """Sub-blocks will be contrained to be on an XY grid with variable Z.""" - - # FIXME add var-z properties From f00f3baddad7dd518c260d0c77a4d1552a31e370 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Thu, 2 Mar 2023 14:11:12 +1300 Subject: [PATCH 14/42] Cleaned up merge issues. --- omf/blockmodel/definition.py | 2 +- omf/blockmodel/models.py | 49 ++++++++++++++---------------- omf/compat/omf_v1.py | 58 ++++++++++++++++++++++-------------- 3 files changed, 60 insertions(+), 49 deletions(-) diff --git a/omf/blockmodel/definition.py b/omf/blockmodel/definition.py index 5a6209e4..9f8dc109 100644 --- a/omf/blockmodel/definition.py +++ b/omf/blockmodel/definition.py @@ -119,7 +119,7 @@ class OctreeSubblockDefinition(RegularSubblockDefinition): subblock_count = OctreeSubblockCount("The maximum number of sub-blocks inside a parent in each direction.") -class FreeformSubblockDefinition: +class FreeformSubblockDefinition(properties.HasProperties): """Unconstrained free-form sub-block definition. Provides no limitations on or explanation of sub-block positions. diff --git a/omf/blockmodel/models.py b/omf/blockmodel/models.py index 91076427..d610494c 100644 --- a/omf/blockmodel/models.py +++ b/omf/blockmodel/models.py @@ -38,11 +38,9 @@ def num_cells(self): def location_length(self, location): """Return correct attribute length for 'location'.""" - match location: - case "cells" | "parent_blocks" | "": - return self.num_cells - case _: - raise ValueError(f"unknown location type: {location!r}") + if location in ("cells", "parent_blocks", ""): + return self.num_cells + raise ValueError(f"unknown location type: {location!r}") class TensorGridBlockModel(ProjectElement): @@ -70,13 +68,11 @@ def num_nodes(self): def location_length(self, location): """Return correct attribute length for 'location'.""" - match location: - case "cells" | "parent_blocks" | "": - return self.num_cells - case "vertices": - return self.num_nodes - case _: - raise ValueError(f"unknown location type: {location!r}") + if location in ("cells", "parent_blocks", ""): + return self.num_cells + if location == "vertices": + return self.num_nodes + raise ValueError(f"unknown location type: {location!r}") class SubblockedModel(ProjectElement): @@ -127,13 +123,11 @@ def num_parent_blocks(self): def location_length(self, location): """Return correct attribute length for 'location'.""" - match location: - case "cells" | "": - return self.num_cells - case "parent_blocks": - return self.num_parent_blocks - case _: - raise ValueError(f"unknown location type: {location!r}") + if location in ("cells", ""): + return self.num_cells + if location == "parent_blocks": + return self.num_parent_blocks + raise ValueError(f"unknown location type: {location!r}") @properties.validator def _validate_subblocks(self): @@ -188,15 +182,18 @@ def num_cells(self): """The number of cells, which in this case are always parent blocks.""" return None if self.subblock_corners is None else len(self.subblock_corners) + @property + def num_parent_blocks(self): + """The number of parent blocks.""" + return np.prod(self.definition.block_count) + def location_length(self, location): """Return correct attribute length for 'location'.""" - match location: - case "cells" | "": - return self.num_cells - case "parent_blocks": - return np.prod(self.definition.block_count) - case _: - raise ValueError(f"unknown location type: {location!r}") + if location in ("cells", ""): + return self.num_cells + if location == "parent_blocks": + return self.num_parent_blocks + raise ValueError(f"unknown location type: {location!r}") @properties.validator def _validate_subblocks(self): diff --git a/omf/compat/omf_v1.py b/omf/compat/omf_v1.py index 256c348e..2e6cb740 100644 --- a/omf/compat/omf_v1.py +++ b/omf/compat/omf_v1.py @@ -9,9 +9,23 @@ import numpy as np import properties +from ..attribute import ( + CategoryAttribute, + CategoryColormap, + ContinuousColormap, + NumericAttribute, + StringAttribute, + VectorAttribute, +) +from ..base import Project +from ..blockmodel.models import TensorGridBlockModel +from ..lineset import LineSet +from ..pointset import PointSet +from ..surface import Surface, TensorGridSurface +from ..texture import Image, ProjectedTexture from .interface import IOMFReader, InvalidOMFFile, WrongVersionError -from .. import attribute, base, blockmodel, lineset, pointset, surface, texture +# from .. import attribute, base, blockmodel, lineset, pointset, surface, texture COMPATIBILITY_VERSION = b"OMF-v0.9.0" _default = object() @@ -184,7 +198,7 @@ def _convert_image(self, image_png): img = io.BytesIO() img.write(zlib.decompress(self._f.read(length))) img.seek(0, 0) - return texture.Image(img) + return Image(img) def _copy_image_png(self, src, src_attr, dst, dst_attr=None): if not self._include_binary: @@ -211,7 +225,7 @@ def _copy_project_element_geometry(self, src, dst): def _convert_texture(self, texture_uuid): texture_v1 = self.__get_attr(self._project, texture_uuid) - texture_ = texture.ProjectedTexture() + texture_ = ProjectedTexture() self.__require_attr(texture_v1, "__class__", "ImageTexture") self.__copy_attr(texture_v1, "origin", texture_) @@ -232,14 +246,14 @@ def _convert_colormap(self, colormap_uuid): self.__require_attr(colormap_v1, "__class__", "ScalarColormap") gradient_uuid = self.__get_attr(colormap_v1, "gradient") - colormap = attribute.ContinuousColormap() + colormap = ContinuousColormap() colormap.gradient = self._load_gradient(gradient_uuid) self.__copy_attr(colormap_v1, "limits", colormap) self._copy_content_model(colormap_v1, colormap) return colormap def _convert_scalar_data(self, data_v1): - data = attribute.NumericAttribute() + data = NumericAttribute() self._copy_scalar_array(data_v1, "array", data) colormap_uuid = self.__get_attr(data_v1, "colormap", optional=True) if colormap_uuid is not None: @@ -247,17 +261,17 @@ def _convert_scalar_data(self, data_v1): return [data] def _convert_vector_data(self, data_v1): - data = attribute.VectorAttribute() + data = VectorAttribute() self._copy_scalar_array(data_v1, "array", data) return [data] def _convert_string_data(self, data_v1): - data = attribute.StringAttribute() + data = StringAttribute() self._copy_scalar_array(data_v1, "array", data) return [data] def _mapped_column_to_category(self, legend_v1, data_v1, data_column, color_column): - colormap = attribute.CategoryColormap() + colormap = CategoryColormap() length = len(data_column) colormap.indices = list(range(length)) @@ -266,7 +280,7 @@ def _mapped_column_to_category(self, legend_v1, data_v1, data_column, color_colu colormap.colors = color_column self._copy_content_model(legend_v1, colormap) - catgory_attribute = attribute.CategoryAttribute() + catgory_attribute = CategoryAttribute() self._copy_scalar_array(data_v1, "array", catgory_attribute) catgory_attribute.categories = colormap @@ -358,7 +372,7 @@ def _convert_pointset_element(self, points_v1): geometry_v1 = self.__get_attr(self._project, geometry_uuid) self.__require_attr(geometry_v1, "__class__", "PointSetGeometry") - points = pointset.PointSet() + points = PointSet() self._copy_textures(points_v1, points) self.__copy_attr(points_v1, "subtype", points.metadata) self._copy_project_element_geometry(geometry_v1, points) @@ -373,7 +387,7 @@ def _convert_lineset_element(self, lines_v1): geometry_v1 = self.__get_attr(self._project, geometry_uuid) self.__require_attr(geometry_v1, "__class__", "LineSetGeometry") - lines = lineset.LineSet() + lines = LineSet() self.__copy_attr(lines_v1, "subtype", lines.metadata) self._copy_project_element_geometry(geometry_v1, lines) self._copy_scalar_array(geometry_v1, "vertices", lines) @@ -384,7 +398,7 @@ def _convert_lineset_element(self, lines_v1): # surfaces - triangulated or gridded def _convert_surface_geometry(self, geometry_v1): - surface_ = surface.Surface() + surface_ = Surface() self._copy_project_element_geometry(geometry_v1, surface_) self._copy_scalar_array(geometry_v1, "vertices", surface_) self._copy_scalar_array(geometry_v1, "triangles", surface_) @@ -393,7 +407,7 @@ def _convert_surface_geometry(self, geometry_v1): return surface_, valid_locations def _convert_surface_grid_geometry(self, geometry_v1): - surface_ = surface.TensorGridSurface() + surface_ = TensorGridSurface() self._copy_project_element_geometry(geometry_v1, surface_) self.__copy_attr(geometry_v1, "tensor_u", surface_) self.__copy_attr(geometry_v1, "tensor_v", surface_) @@ -423,15 +437,15 @@ def _convert_volume_element(self, volume_v1): geometry_uuid = self.__get_attr(volume_v1, "geometry") geometry_v1 = self.__get_attr(self._project, geometry_uuid) self.__require_attr(geometry_v1, "__class__", "VolumeGridGeometry") - volume = blockmodel.TensorGridBlockModel() + volume = TensorGridBlockModel() self.__copy_attr(volume_v1, "subtype", volume.metadata) - self._copy_project_element_geometry(geometry_v1, volume) - self.__copy_attr(geometry_v1, "tensor_u", volume) - self.__copy_attr(geometry_v1, "tensor_v", volume) - self.__copy_attr(geometry_v1, "tensor_w", volume) - self.__copy_attr(geometry_v1, "axis_u", volume) - self.__copy_attr(geometry_v1, "axis_v", volume) - self.__copy_attr(geometry_v1, "axis_w", volume) + self._copy_project_element_geometry(geometry_v1, volume.definition) + self.__copy_attr(geometry_v1, "tensor_u", volume.definition) + self.__copy_attr(geometry_v1, "tensor_v", volume.definition) + self.__copy_attr(geometry_v1, "tensor_w", volume.definition) + self.__copy_attr(geometry_v1, "axis_u", volume.definition) + self.__copy_attr(geometry_v1, "axis_v", volume.definition) + self.__copy_attr(geometry_v1, "axis_w", volume.definition) valid_locations = ("vertices", "cells") return volume, valid_locations @@ -458,7 +472,7 @@ def _convert_project_element(self, element_uuid): # main project def _convert_project(self, project_uuid): project_v1 = self.__get_attr(self._project, project_uuid) - project = base.Project() + project = Project() self._copy_content_model(project_v1, project) self.__copy_attr(project_v1, "author", project.metadata, optional_dst=True, default="") From 90243cd321df1a0b14ae7d0abce055933527efe6 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Thu, 2 Mar 2023 14:31:24 +1300 Subject: [PATCH 15/42] Added schemas to objects without them, and cleaned up code. --- omf/blockmodel/definition.py | 12 ++++++++++++ omf/blockmodel/models.py | 22 ++++------------------ 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/omf/blockmodel/definition.py b/omf/blockmodel/definition.py index 9f8dc109..38c27836 100644 --- a/omf/blockmodel/definition.py +++ b/omf/blockmodel/definition.py @@ -73,6 +73,8 @@ class RegularBlockModelDefinition(_BaseBlockModelDefinition): If used on a sub-blocked model then everything here applies to the parent blocks only. """ + schema = "org.omf.v2.blockmodeldefinition.regular" + block_count = BlockCount("Number of blocks in each of the u, v, and w directions.") block_size = BlockSize("Size of blocks in the u, v, and w directions.") @@ -80,6 +82,8 @@ class RegularBlockModelDefinition(_BaseBlockModelDefinition): class TensorBlockModelDefinition(_BaseBlockModelDefinition): """Defines the block structure of a tensor grid block model.""" + schema = "org.omf.v2.blockmodeldefinition.tensor" + tensor_u = TensorArray("Tensor cell widths, u-direction") tensor_v = TensorArray("Tensor cell widths, v-direction") tensor_w = TensorArray("Tensor cell widths, w-direction") @@ -101,6 +105,8 @@ def block_count(self): class RegularSubblockDefinition(properties.HasProperties): """The simplest gridded sub-block definition.""" + schema = "org.omf.v2.subblockdefinition.regular" + subblock_count = SubBlockCount("The maximum number of sub-blocks inside a parent in each direction.") @@ -116,6 +122,8 @@ class OctreeSubblockDefinition(RegularSubblockDefinition): all axes to be equal. """ + schema = "org.omf.v2.subblockdefinition.octree" + subblock_count = OctreeSubblockCount("The maximum number of sub-blocks inside a parent in each direction.") @@ -125,6 +133,8 @@ class FreeformSubblockDefinition(properties.HasProperties): Provides no limitations on or explanation of sub-block positions. """ + schema = "org.omf.v2.subblockdefinition.freeform" + class VariableHeightSubblockDefinition(FreeformSubblockDefinition): """Defines sub-blocks on a grid in the U and V directions but variable in the W direction. @@ -135,6 +145,8 @@ class VariableHeightSubblockDefinition(FreeformSubblockDefinition): Note: these constraints on sub-blocks are not checked during validation. """ + schema = "org.omf.v2.subblockdefinition.variableheight" + subblock_count_u = properties.Integer("Number of sub-blocks in the u-direction", min=1, max=65535) subblock_count_v = properties.Integer("Number of sub-blocks in the v-direction", min=1, max=65535) minimum_size_w = properties.Float("Minimum size of sub-blocks in the z-direction", min=0.0) diff --git a/omf/blockmodel/models.py b/omf/blockmodel/models.py index d610494c..980fde90 100644 --- a/omf/blockmodel/models.py +++ b/omf/blockmodel/models.py @@ -38,9 +38,7 @@ def num_cells(self): def location_length(self, location): """Return correct attribute length for 'location'.""" - if location in ("cells", "parent_blocks", ""): - return self.num_cells - raise ValueError(f"unknown location type: {location!r}") + return self.num_cells class TensorGridBlockModel(ProjectElement): @@ -68,11 +66,7 @@ def num_nodes(self): def location_length(self, location): """Return correct attribute length for 'location'.""" - if location in ("cells", "parent_blocks", ""): - return self.num_cells - if location == "vertices": - return self.num_nodes - raise ValueError(f"unknown location type: {location!r}") + return self.num_nodes if location == "vertices" else self.num_cells class SubblockedModel(ProjectElement): @@ -123,11 +117,7 @@ def num_parent_blocks(self): def location_length(self, location): """Return correct attribute length for 'location'.""" - if location in ("cells", ""): - return self.num_cells - if location == "parent_blocks": - return self.num_parent_blocks - raise ValueError(f"unknown location type: {location!r}") + return self.num_parent_blocks if location == "parent_blocks" else self.num_cells @properties.validator def _validate_subblocks(self): @@ -189,11 +179,7 @@ def num_parent_blocks(self): def location_length(self, location): """Return correct attribute length for 'location'.""" - if location in ("cells", ""): - return self.num_cells - if location == "parent_blocks": - return self.num_parent_blocks - raise ValueError(f"unknown location type: {location!r}") + return self.num_parent_blocks if location == "parent_blocks" else self.num_cells @properties.validator def _validate_subblocks(self): From 8f3b49555e48cd674e552d69759f959f7b5aa940 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Thu, 2 Mar 2023 15:25:29 +1300 Subject: [PATCH 16/42] Fixed pylint complaints. --- omf/__init__.py | 9 ++++-- omf/blockmodel/__init__.py | 8 +++++ omf/blockmodel/_properties.py | 39 +++++++++++++---------- omf/blockmodel/_subblock_check.py | 39 ++++++++++------------- omf/blockmodel/definition.py | 14 ++++----- omf/blockmodel/models.py | 8 ++--- omf/compat/omf_v1.py | 2 +- omf/composite.py | 2 +- pyproject.toml | 6 ++-- tests/test_subblockedmodel.py | 52 ++++++++++++++----------------- 10 files changed, 93 insertions(+), 86 deletions(-) diff --git a/omf/__init__.py b/omf/__init__.py index 2f610d49..aab34da9 100644 --- a/omf/__init__.py +++ b/omf/__init__.py @@ -10,15 +10,18 @@ VectorAttribute, ) from .base import Project -from .blockmodel.definition import ( +from .blockmodel import ( + FreeformSubblockedModel, OctreeSubblockDefinition, + RegularBlockModel, RegularBlockModelDefinition, RegularSubblockDefinition, + SubblockedModel, TensorBlockModelDefinition, + TensorGridBlockModel, ) -from .blockmodel.models import RegularBlockModel, SubblockedModel, TensorGridBlockModel from .composite import Composite -from .fileio import load, save, __version__ +from .fileio import __version__, load, save from .lineset import LineSet from .pointset import PointSet from .surface import Surface, TensorGridSurface diff --git a/omf/blockmodel/__init__.py b/omf/blockmodel/__init__.py index e69de29b..2f11ef85 100644 --- a/omf/blockmodel/__init__.py +++ b/omf/blockmodel/__init__.py @@ -0,0 +1,8 @@ +"""blockmodel/__init__.py: sub-package for block models.""" +from .definition import ( + OctreeSubblockDefinition, + RegularBlockModelDefinition, + RegularSubblockDefinition, + TensorBlockModelDefinition, +) +from .models import FreeformSubblockedModel, RegularBlockModel, SubblockedModel, TensorGridBlockModel diff --git a/omf/blockmodel/_properties.py b/omf/blockmodel/_properties.py index 9fa03b6f..ada2f017 100644 --- a/omf/blockmodel/_properties.py +++ b/omf/blockmodel/_properties.py @@ -1,8 +1,11 @@ +"""blockmodel/_properties.py: block model specific property classes.""" import numpy as np import properties class BlockCount(properties.Array): + """Number of blocks in three axes. Must be greater than 1.""" + def __init__(self, doc, **kw): super().__init__(doc, **kw, dtype=int, shape=(3,)) @@ -11,36 +14,36 @@ def validate(self, instance, value): value = super().validate(instance, value) for item in value: if item < 1: - if instance is None: - msg = f"block counts must be >= 1" - else: - cls = instance.__class__.__name__ - msg = f"{cls}.{self.name} must be >= 1" - raise properties.ValidationError(msg, prop=self.name, instance=instance) + raise properties.ValidationError("block counts must be >= 1", prop=self.name, instance=instance) return value class SubBlockCount(BlockCount): + """Number of sub-blocks in three axes. Must be between 1 and 65535.""" + def validate(self, instance, value): value = super().validate(instance, value) - if (value > 65535).any(): - raise properties.ValidationError( - "sub-block counts are limited to 65535 in each direction", - prop=self.name, - instance=instance, - ) + for item in value: + if item > 65535: + raise properties.ValidationError( + "sub-block counts are limited to 65535 in each direction", + prop=self.name, + instance=instance, + ) return value class OctreeSubblockCount(SubBlockCount): + """Number of octree sub-blocks in three axes. Must be between 1 and 65535 and a power of 2.""" + def validate(self, instance, value): """Check shape and dtype of the count and that items are >= min.""" value = super().validate(instance, value) for item in value: - l = np.log2(item) - if np.trunc(l) != l: + log = np.log2(item) + if np.trunc(log) != log: if instance is None: - msg = f"octree sub-block counts must be powers of two" + msg = "octree sub-block counts must be powers of two" else: cls = instance.__class__.__name__ msg = f"{cls}.{self.name} octree counts must be powers of two" @@ -49,6 +52,8 @@ def validate(self, instance, value): class BlockSize(properties.Array): + """Block size in three axes. Must be greater than zero.""" + def __init__(self, doc, **kw): super().__init__(doc, **kw, dtype=float, shape=(3,)) @@ -58,7 +63,7 @@ def validate(self, instance, value): for item in value: if item <= 0.0: if instance is None: - msg = f"block size elements must be > 0.0" + msg = "block size elements must be > 0.0" else: msg = f"{instance.__class__.__name__}.{self.name} elements must be > 0.0" raise properties.ValidationError(msg, prop=self.name, instance=instance) @@ -66,6 +71,8 @@ def validate(self, instance, value): class TensorArray(properties.Array): + """Arrays of block spacings in one axis. All spacings must be greater than zero.""" + def __init__(self, doc, **kw): super().__init__(doc, **kw, dtype=float, shape=("*",)) diff --git a/omf/blockmodel/_subblock_check.py b/omf/blockmodel/_subblock_check.py index 8ed14229..53315c36 100644 --- a/omf/blockmodel/_subblock_check.py +++ b/omf/blockmodel/_subblock_check.py @@ -1,3 +1,4 @@ +"""blockmodel/_subblock_check.py: functions for checking sub-block constraints.""" import itertools import numpy as np @@ -37,17 +38,13 @@ def _check_inside_parent(subblock_definition, corners, instance): else: upper = 1.0 upper_str = "1" - mn = corners[:, :3] - mx = corners[:, 3:] - if mn.dtype.kind != "u" and not (0 <= mn).all(): - raise properties.ValidationError( - "0 <= min_corner failed", prop="subblock_corners", instance=instance - ) - if not (mn < mx).all(): - raise properties.ValidationError( - "min_corner < max_corner failed", prop="subblock_corners", instance=instance - ) - if not (mx <= upper).all(): + min_corners = corners[:, :3] + max_corners = corners[:, 3:] + if min_corners.dtype.kind != "u" and not (0 <= min_corners).all(): + raise properties.ValidationError("0 <= min_corner failed", prop="subblock_corners", instance=instance) + if not (min_corners < max_corners).all(): + raise properties.ValidationError("min_corner < max_corner failed", prop="subblock_corners", instance=instance) + if not (max_corners <= upper).all(): raise properties.ValidationError( f"max_corner <= {upper_str} failed", prop="subblock_corners", @@ -61,9 +58,7 @@ def _check_for_overlaps(subblock_definition, one_parent_corners, instance): for min_i, min_j, min_k, max_i, max_j, max_k in one_parent_corners: tracker[min_k:max_k, min_j:max_j, min_i:max_i] += 1 if (tracker > 1).any(): - raise properties.ValidationError( - "found overlapping sub-blocks", prop="subblock_corners", instance=instance - ) + raise properties.ValidationError("found overlapping sub-blocks", prop="subblock_corners", instance=instance) def _sizes_to_ints(sizes): @@ -75,8 +70,9 @@ def _sizes_to_ints(sizes): def _check_octree(subblock_definition, corners, instance): - mn = corners[:, :3] - mx = corners[:, 3:] + min_corners = corners[:, :3] + max_corners = corners[:, 3:] + sizes = max_corners - min_corners # Sizes. count = subblock_definition.subblock_count valid_sizes = [count.copy()] @@ -84,16 +80,14 @@ def _check_octree(subblock_definition, corners, instance): count[count > 1] //= 2 valid_sizes.append(count.copy()) valid_sizes = _sizes_to_ints(valid_sizes) - sizes = _sizes_to_ints(mx - mn) - if not np.isin(sizes, valid_sizes, kind="table").all(): + if not np.isin(_sizes_to_ints(sizes), valid_sizes, kind="table").all(): raise properties.ValidationError( "found non-octree sub-block sizes", prop="subblock_corners", instance=instance, ) # Positions. Octree blocks always start at a multiple of their size. - r = np.remainder(mn, mx - mn) - if (r != 0).any(): + if (np.remainder(min_corners, sizes) != 0).any(): raise properties.ValidationError( "found non-octree sub-block positions", prop="subblock_corners", @@ -101,9 +95,8 @@ def _check_octree(subblock_definition, corners, instance): ) -def check_subblocks( - definition, subblock_definition, parent_indices, corners, instance=None -): +def check_subblocks(definition, subblock_definition, parent_indices, corners, instance=None): + """Run all checks on the given defintions and sub-blocks.""" if len(parent_indices) != len(corners): raise properties.ValidationError( "'subblock_parent_indices' and 'subblock_corners' arrays must be the same length", diff --git a/omf/blockmodel/definition.py b/omf/blockmodel/definition.py index 38c27836..3b0385e3 100644 --- a/omf/blockmodel/definition.py +++ b/omf/blockmodel/definition.py @@ -1,3 +1,4 @@ +"""blockmodel/definition.py: various block model and sub-block definition structures.""" import numpy as np import properties @@ -18,7 +19,7 @@ class _BaseBlockModelDefinition(properties.HasProperties): "Minimum corner of the block model relative to Project coordinate reference system", default="zero", ) - block_count = None + block_count = () @properties.validator def _validate_axes(self): @@ -46,10 +47,7 @@ def ijk_to_index(self, ijk): if (shaped < 0).any() or (shaped >= count).any(): raise IndexError(f"0 <= ijk < ({count[0]}, {count[1]}, {count[2]}) failed") indices = np.ravel_multi_index(multi_index=shaped.T, dims=count, order="F") - if output_shape == (): - return indices[0] - else: - return indices.reshape(output_shape) + return indices[0] if output_shape == () else indices.reshape(output_shape) def index_to_ijk(self, index): """Map flat indices to IJK triples for a singoe index or an array, preserving shape.""" @@ -96,10 +94,10 @@ def _tensors(self): @property def block_count(self): """The block count is derived from the tensors here.""" - c = tuple(None if t is None else len(t) for t in self._tensors()) - if None in c: + count = tuple(None if t is None else len(t) for t in self._tensors()) + if None in count: return None - return np.array(c, dtype=int) + return np.array(count, dtype=int) class RegularSubblockDefinition(properties.HasProperties): diff --git a/omf/blockmodel/models.py b/omf/blockmodel/models.py index 980fde90..59f8ba93 100644 --- a/omf/blockmodel/models.py +++ b/omf/blockmodel/models.py @@ -1,3 +1,4 @@ +"""blockmodel/models.py: block model elements.""" import numpy as np import properties @@ -15,8 +16,7 @@ def _shrink_uint(arr): assert arr.dtype.kind in "ui" if arr.min() < 0: return arr - t = np.min_scalar_type(arr.max()) - return arr.astype(t) + return arr.astype(np.min_scalar_type(arr.max())) class RegularBlockModel(ProjectElement): @@ -61,8 +61,8 @@ def num_cells(self): @property def num_nodes(self): """Number of nodes or vertices.""" - bc = self.definition.block_count - return None if bc is None else np.prod(bc + 1) + count = self.definition.block_count + return None if count is None else np.prod(count + 1) def location_length(self, location): """Return correct attribute length for 'location'.""" diff --git a/omf/compat/omf_v1.py b/omf/compat/omf_v1.py index 2e6cb740..989ac385 100644 --- a/omf/compat/omf_v1.py +++ b/omf/compat/omf_v1.py @@ -18,7 +18,7 @@ VectorAttribute, ) from ..base import Project -from ..blockmodel.models import TensorGridBlockModel +from ..blockmodel import TensorGridBlockModel from ..lineset import LineSet from ..pointset import PointSet from ..surface import Surface, TensorGridSurface diff --git a/omf/composite.py b/omf/composite.py index ea603c46..cbf20244 100644 --- a/omf/composite.py +++ b/omf/composite.py @@ -2,7 +2,7 @@ import properties from .base import ProjectElement -from .blockmodel.models import RegularBlockModel, SubblockedModel, TensorGridBlockModel +from .blockmodel import RegularBlockModel, SubblockedModel, TensorGridBlockModel from .lineset import LineSet from .pointset import PointSet from .surface import Surface, TensorGridSurface diff --git a/pyproject.toml b/pyproject.toml index e92f47ef..70ffae38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,10 @@ exclude-protected = "_asdict,_fields,_replace,_source,_make,_props,_backend" max-line-length = 120 [tool.pylint.'MESSAGES CONTROL'] -disable = "consider-using-f-string" +disable = [ + "consider-using-f-string", + "no-name-in-module", +] [tool.pylint.'SIMILARITIES'] min-similarity-lines = 20 @@ -87,4 +90,3 @@ generated-members = "_backend,array" minversion = "7.2" required_plugins = "pytest-rst" testpaths = ["docs", "tests"] - diff --git a/tests/test_subblockedmodel.py b/tests/test_subblockedmodel.py index a0e42b45..b90b6159 100644 --- a/tests/test_subblockedmodel.py +++ b/tests/test_subblockedmodel.py @@ -8,12 +8,14 @@ def test_group_by(): - a = np.array([0, 0, 1, 1, 1, 2]) - assert list(_subblock_check._group_by(a)) == [(0, 2, 0), (2, 5, 1), (5, 6, 2)] - a = np.ones(1, dtype=int) - assert list(_subblock_check._group_by(a)) == [(0, 1, 1)] - a = np.zeros(0, dtype=int) - assert list(_subblock_check._group_by(a)) == [] + """Test the array grouping function used by sub-block checks.""" + group_by = _subblock_check._group_by # pylint: disable=W0212 + arr = np.array([0, 0, 1, 1, 1, 2]) + assert list(group_by(arr)) == [(0, 2, 0), (2, 5, 1), (5, 6, 2)] + arr = np.ones(1, dtype=int) + assert list(group_by(arr)) == [(0, 1, 1)] + arr = np.zeros(0, dtype=int) + assert not list(group_by(arr)) def _bm_def(): @@ -26,20 +28,20 @@ def _bm_def(): def _test_regular(*corners): block_model = omf.SubblockedModel() block_model.definition = _bm_def() - block_model.subblock_definition = omf.RegularSubblockDefinition( - subblock_count=(5, 4, 3) - ) + block_model.subblock_definition = omf.RegularSubblockDefinition(subblock_count=(5, 4, 3)) block_model.subblock_corners = np.array(corners) block_model.subblock_parent_indices = np.zeros((len(corners), 3), dtype=int) block_model.validate() def test_overlap(): + """Test that overlapping sub-blocks are rejected.""" with pytest.raises(properties.ValidationError, match="overlapping sub-blocks"): _test_regular((0, 0, 0, 2, 2, 1), (0, 0, 0, 4, 4, 2)) def test_outside_parent(): + """Test that sub-blocks outside the parent block are rejected.""" with pytest.raises(properties.ValidationError, match="0 <= min_corner"): _test_regular((0, 0, -1, 4, 4, 1)) with pytest.raises(properties.ValidationError, match="min_corner < max_corner"): @@ -49,40 +51,35 @@ def test_outside_parent(): def test_invalid_parent_indices(): + """Test invalid parent block indices are rejected.""" block_model = omf.SubblockedModel() block_model.definition = _bm_def() - block_model.subblock_definition = omf.RegularSubblockDefinition( - subblock_count=(5, 4, 3) - ) + block_model.subblock_definition = omf.RegularSubblockDefinition(subblock_count=(5, 4, 3)) block_model.subblock_corners = np.array([(0, 0, 0, 5, 4, 3), (0, 0, 0, 5, 4, 3)]) block_model.subblock_parent_indices = np.array([(0, 0, 0), (1, 0, 0)]) - with pytest.raises( - properties.ValidationError, match=r"subblock_parent_indices < \(1, 1, 1\)" - ): + with pytest.raises(properties.ValidationError, match=r"subblock_parent_indices < \(1, 1, 1\)"): block_model.validate() block_model.subblock_parent_indices = np.array([(0, 0, -1), (0, 0, 0)]) - with pytest.raises( - properties.ValidationError, match="0 <= subblock_parent_indices" - ): + with pytest.raises(properties.ValidationError, match="0 <= subblock_parent_indices"): block_model.validate() def _test_octree(*corners): block_model = omf.SubblockedModel() block_model.definition = _bm_def() - block_model.subblock_definition = omf.OctreeSubblockDefinition( - subblock_count=(4, 4, 2) - ) + block_model.subblock_definition = omf.OctreeSubblockDefinition(subblock_count=(4, 4, 2)) block_model.subblock_corners = np.array(corners) block_model.subblock_parent_indices = np.zeros((len(corners), 3), dtype=int) block_model.validate() def test_one_full_block(): + """Test a single sub-block covering the parent.""" _test_octree((0, 0, 0, 4, 4, 2)) def test_eight_blocks(): + """Test eight sub-blocks covering the parent.""" _test_octree( (0, 0, 0, 2, 2, 1), (2, 0, 0, 4, 2, 1), @@ -96,18 +93,19 @@ def test_eight_blocks(): def test_bad_size(): + """Test that non-octree sub-blocks sizes are rejected.""" with pytest.raises(properties.ValidationError, match="non-octree sub-block sizes"): _test_octree((0, 0, 0, 3, 4, 2)) def test_bad_position(): - with pytest.raises( - properties.ValidationError, match="non-octree sub-block positions" - ): + """Test that non-octree sub-blocks positions are rejected.""" + with pytest.raises(properties.ValidationError, match="non-octree sub-block positions"): _test_octree((0, 1, 0, 2, 3, 1)) def test_pack_subblock_arrays(): + """Test that packing of uint arrays during validation works.""" block_model = omf.SubblockedModel() block_model.subblock_definition.subblock_count = [2, 2, 2] block_model.definition.block_size = [1.0, 1.0, 1.0] @@ -122,7 +120,7 @@ def test_pack_subblock_arrays(): def test_uninstantiated(): - """Test definitions are default and attributes are None on instantiation""" + """Test that definitions are default and attributes are None on instantiation""" block_model = omf.SubblockedModel() assert isinstance(block_model.definition, omf.RegularBlockModelDefinition) assert isinstance(block_model.subblock_definition, omf.RegularSubblockDefinition) @@ -141,9 +139,7 @@ def test_num_cells(): block_model.definition.block_size = [1.0, 2.0, 3.0] block_model.subblock_definition.subblock_count = [5, 5, 5] np.testing.assert_array_equal(block_model.definition.block_count, [2, 2, 2]) - np.testing.assert_array_equal( - block_model.subblock_definition.subblock_count, [5, 5, 5] - ) + np.testing.assert_array_equal(block_model.subblock_definition.subblock_count, [5, 5, 5]) block_model.subblock_parent_indices = np.array([(0, 0, 0), (1, 0, 0)]) block_model.subblock_corners = np.array([(0, 0, 0, 5, 5, 5), (1, 1, 1, 4, 4, 4)]) assert block_model.num_cells == 2 From 04fbe72be0151a46cbe038261c7fab05c1f60372 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Thu, 2 Mar 2023 16:32:06 +1300 Subject: [PATCH 17/42] Fixed failing schema lookups giving bad error messages. --- omf/base.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/omf/base.py b/omf/base.py index 8ab1f940..0bcae3ca 100644 --- a/omf/base.py +++ b/omf/base.py @@ -20,14 +20,17 @@ def serialize(self, include_class=True, save_dynamic=False, **kwargs): return output @classmethod - def deserialize(cls, value, trusted=False, strict=False, assert_valid=False, **kwargs): - schema = value.pop("schema", "") + def __lookup_class(cls, schema): for class_name, class_value in cls._REGISTRY.items(): - if not hasattr(class_value, "schema"): - continue - if class_value.schema == schema: - value.update({"__class__": class_name}) - break + if hasattr(class_value, "schema") and class_value.schema == schema: + return class_name + raise ValueError(f"schema not found: {schema}") + + @classmethod + def deserialize(cls, value, trusted=False, strict=False, assert_valid=False, **kwargs): + schema = value.pop("schema", None) + if schema is not None: + value["__class__"] = cls.__lookup_class(schema) return super().deserialize(value, trusted, strict, assert_valid, **kwargs) From e4a148f130ace6b3d76d286cfbecc0d550e785ae Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Thu, 2 Mar 2023 16:32:48 +1300 Subject: [PATCH 18/42] Fixed v2 asset test. --- assets/v2/test_file.omf | Bin 42171 -> 42211 bytes docs/content/examples.rst | 12 +++++++----- omf/blockmodel/models.py | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/assets/v2/test_file.omf b/assets/v2/test_file.omf index 4ca71f50c247aea0a7b3509e9b313c2b4787ce1b..da038ae871dd9aa1aa9898b5aaf8359c93ae6173 100644 GIT binary patch literal 42211 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

R*LMY342KpDRXLS7DJ7 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 diff --git a/docs/content/examples.rst b/docs/content/examples.rst index b26d5c35..76a9c8c0 100644 --- a/docs/content/examples.rst +++ b/docs/content/examples.rst @@ -9,7 +9,7 @@ Also, this example builds elements all at once. They can also be initialized with no arguments, and properties can be set one-by-one (see code snippet at bottom of page). -.. code:: python +.. code-block:: python :name: test_doc import datetime @@ -143,10 +143,12 @@ bottom of page). ) vol = omf.TensorGridBlockModel( name="vol", - tensor_u=np.ones(10).astype(float), - tensor_v=np.ones(15).astype(float), - tensor_w=np.ones(20).astype(float), - origin=[10.0, 10.0, -10], + definition=omf.TensorBlockModelDefinition( + tensor_u=np.ones(10, dtype=float), + tensor_v=np.ones(15, dtype=float), + tensor_w=np.ones(20, dtype=float), + origin=[10.0, 10.0, -10], + ), attributes=[ omf.NumericAttribute( name="random attr", location="cells", array=np.random.rand(10 * 15 * 20) diff --git a/omf/blockmodel/models.py b/omf/blockmodel/models.py index 59f8ba93..a70ea8d1 100644 --- a/omf/blockmodel/models.py +++ b/omf/blockmodel/models.py @@ -44,7 +44,7 @@ def location_length(self, location): class TensorGridBlockModel(ProjectElement): """A block model with variable spacing in all directions and no sub-blocks.""" - schema = "org.omf.v2.elements.blockmodel.tensor" + schema = "org.omf.v2.element.blockmodel.tensorgrid" _valid_locations = ("vertices", "cells", "parent_blocks") definition = properties.Instance( From 2d288c4cb419183cb190d4682cd84b2516e92da2 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Thu, 2 Mar 2023 16:58:26 +1300 Subject: [PATCH 19/42] Copy origin correctly for VolumeGridGeometry. --- omf/compat/omf_v1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/omf/compat/omf_v1.py b/omf/compat/omf_v1.py index 989ac385..9065070b 100644 --- a/omf/compat/omf_v1.py +++ b/omf/compat/omf_v1.py @@ -439,7 +439,7 @@ def _convert_volume_element(self, volume_v1): self.__require_attr(geometry_v1, "__class__", "VolumeGridGeometry") volume = TensorGridBlockModel() self.__copy_attr(volume_v1, "subtype", volume.metadata) - self._copy_project_element_geometry(geometry_v1, volume.definition) + self.__copy_attr(geometry_v1, "origin", volume.definition) self.__copy_attr(geometry_v1, "tensor_u", volume.definition) self.__copy_attr(geometry_v1, "tensor_v", volume.definition) self.__copy_attr(geometry_v1, "tensor_w", volume.definition) From 63cc16b859e10345d018f29d09b68768a1d16400 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Thu, 2 Mar 2023 17:02:34 +1300 Subject: [PATCH 20/42] Removed match statement. --- omf/blockmodel/definition.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/omf/blockmodel/definition.py b/omf/blockmodel/definition.py index 3b0385e3..a622671a 100644 --- a/omf/blockmodel/definition.py +++ b/omf/blockmodel/definition.py @@ -38,11 +38,10 @@ def ijk_to_index(self, ijk): arr = np.asarray(ijk) if arr.dtype.kind not in "ui": raise TypeError(f"'ijk' must be integer typed, found {arr.dtype}") - match arr.shape: - case (*output_shape, 3): - shaped = arr.reshape(-1, 3) - case _: - raise ValueError("'ijk' must have 3 elements or be an array with shape (*_, 3)") + if not arr.shape or arr.shape[-1] != 3: + raise ValueError("'ijk' must have 3 elements or be an array with shape (*_, 3)") + output_shape = arr.shape[:-1] + shaped = arr.reshape(-1, 3) count = self.block_count if (shaped < 0).any() or (shaped >= count).any(): raise IndexError(f"0 <= ijk < ({count[0]}, {count[1]}, {count[2]}) failed") From e7f8e073ef9a267732cf7ab57b7837a4808e4548 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Thu, 2 Mar 2023 17:06:20 +1300 Subject: [PATCH 21/42] Don't assert things that are different on Linux. --- tests/test_subblockedmodel.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/test_subblockedmodel.py b/tests/test_subblockedmodel.py index b90b6159..fd64cbb4 100644 --- a/tests/test_subblockedmodel.py +++ b/tests/test_subblockedmodel.py @@ -110,12 +110,10 @@ def test_pack_subblock_arrays(): block_model.subblock_definition.subblock_count = [2, 2, 2] block_model.definition.block_size = [1.0, 1.0, 1.0] block_model.definition.block_count = [10, 10, 10] - block_model.subblock_parent_indices = np.array([(0, 0, 0)]) - block_model.subblock_corners = np.array([(0, 0, 0, 2, 2, 2)]) - # We set this as default ints. - assert block_model.subblock_corners.dtype == np.int32 + block_model.subblock_parent_indices = np.array([(0, 0, 0)], dtype=int) + block_model.subblock_corners = np.array([(0, 0, 0, 2, 2, 2)], dtype=int) block_model.validate() - # Validate should have packed it down to uint8. + # Arrays were set as int, validate should have packed it down to uint8. assert block_model.subblock_corners.dtype == np.uint8 From b75a6536eb1aa0b52d03b6d59aea29bcd89a9e26 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Thu, 2 Mar 2023 17:10:02 +1300 Subject: [PATCH 22/42] Use the lower-memory version of numpy.isin. --- omf/blockmodel/_subblock_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/omf/blockmodel/_subblock_check.py b/omf/blockmodel/_subblock_check.py index 53315c36..0d0897b6 100644 --- a/omf/blockmodel/_subblock_check.py +++ b/omf/blockmodel/_subblock_check.py @@ -80,7 +80,7 @@ def _check_octree(subblock_definition, corners, instance): count[count > 1] //= 2 valid_sizes.append(count.copy()) valid_sizes = _sizes_to_ints(valid_sizes) - if not np.isin(_sizes_to_ints(sizes), valid_sizes, kind="table").all(): + if not np.isin(_sizes_to_ints(sizes), valid_sizes, kind="sort").all(): raise properties.ValidationError( "found non-octree sub-block sizes", prop="subblock_corners", From 63856727e410eb552a41845b14d98a6e1a68b632 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Thu, 2 Mar 2023 17:24:48 +1300 Subject: [PATCH 23/42] Updated docs and fixed tests. --- docs/content/blockmodel.rst | 32 +++++++++++++++++++++++++------ omf/blockmodel/__init__.py | 2 ++ omf/blockmodel/_subblock_check.py | 4 ++-- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/docs/content/blockmodel.rst b/docs/content/blockmodel.rst index 76898818..47699655 100644 --- a/docs/content/blockmodel.rst +++ b/docs/content/blockmodel.rst @@ -6,22 +6,42 @@ Block Models .. image:: /images/VolumeGrid.png :scale: 80% -Element -------- +Elements +-------- .. image:: /images/VolumeGridGeometry.png :width: 80% :align: center +.. autoclass:: omf.blockmodel.RegularBlockModel + .. autoclass:: omf.blockmodel.TensorGridBlockModel -.. autoclass:: omf.blockmodel.RegularBlockModel +.. autoclass:: omf.blockmodel.SubblockedModel + +.. autoclass:: omf.blockmodel.FreeformSubblockedModel + + +Block Model Definitions +----------------------- + +These classes are used as part of the block model elements to define the position +and size of the model. + +.. autoclass:: omf.blockmodel.RegularBlockModelDefinition + +.. autoclass:: omf.blockmodel.TensorBlockModelDefinition + +Sub-block Definitions +--------------------- + +These classes are used to define the structure of sub-blocks within a parent block. -.. autoclass:: omf.blockmodel.RegularSubBlockModel +.. autoclass:: omf.blockmodel.RegularSubblockDefinition -.. autoclass:: omf.blockmodel.OctreeSubBlockModel +.. autoclass:: omf.blockmodel.FreeformSubblockDefinition -.. autoclass:: omf.blockmodel.ArbitrarySubBlockModel +.. autoclass:: omf.blockmodel.VariableHeightSubblockDefinition Attributes ---------- diff --git a/omf/blockmodel/__init__.py b/omf/blockmodel/__init__.py index 2f11ef85..587110cd 100644 --- a/omf/blockmodel/__init__.py +++ b/omf/blockmodel/__init__.py @@ -1,8 +1,10 @@ """blockmodel/__init__.py: sub-package for block models.""" from .definition import ( + FreeformSubblockDefinition, OctreeSubblockDefinition, RegularBlockModelDefinition, RegularSubblockDefinition, TensorBlockModelDefinition, + VariableHeightSubblockDefinition, ) from .models import FreeformSubblockedModel, RegularBlockModel, SubblockedModel, TensorGridBlockModel diff --git a/omf/blockmodel/_subblock_check.py b/omf/blockmodel/_subblock_check.py index 0d0897b6..24219d5b 100644 --- a/omf/blockmodel/_subblock_check.py +++ b/omf/blockmodel/_subblock_check.py @@ -16,7 +16,7 @@ def _group_by(arr): yield 0, len(arr), arr[0] else: yield 0, diff[0], arr[0] - for start, end in itertools.pairwise(diff): + for start, end in zip(diff[:-1], diff[1:]): yield start, end, arr[start] yield diff[-1], len(arr), arr[-1] @@ -80,7 +80,7 @@ def _check_octree(subblock_definition, corners, instance): count[count > 1] //= 2 valid_sizes.append(count.copy()) valid_sizes = _sizes_to_ints(valid_sizes) - if not np.isin(_sizes_to_ints(sizes), valid_sizes, kind="sort").all(): + if not np.isin(_sizes_to_ints(sizes), valid_sizes).all(): raise properties.ValidationError( "found non-octree sub-block sizes", prop="subblock_corners", From 5a3009e188941b3797c4c36d069dea5e8f144dee Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Thu, 2 Mar 2023 17:48:58 +1300 Subject: [PATCH 24/42] Fixed remaining tests. --- omf/blockmodel/_properties.py | 90 ----------------------------------- omf/blockmodel/definition.py | 68 +++++++++++++++++++------- tests/test_blockmodel.py | 4 +- 3 files changed, 52 insertions(+), 110 deletions(-) delete mode 100644 omf/blockmodel/_properties.py diff --git a/omf/blockmodel/_properties.py b/omf/blockmodel/_properties.py deleted file mode 100644 index ada2f017..00000000 --- a/omf/blockmodel/_properties.py +++ /dev/null @@ -1,90 +0,0 @@ -"""blockmodel/_properties.py: block model specific property classes.""" -import numpy as np -import properties - - -class BlockCount(properties.Array): - """Number of blocks in three axes. Must be greater than 1.""" - - def __init__(self, doc, **kw): - super().__init__(doc, **kw, dtype=int, shape=(3,)) - - def validate(self, instance, value): - """Check shape and dtype of the count and that items are >= min.""" - value = super().validate(instance, value) - for item in value: - if item < 1: - raise properties.ValidationError("block counts must be >= 1", prop=self.name, instance=instance) - return value - - -class SubBlockCount(BlockCount): - """Number of sub-blocks in three axes. Must be between 1 and 65535.""" - - def validate(self, instance, value): - value = super().validate(instance, value) - for item in value: - if item > 65535: - raise properties.ValidationError( - "sub-block counts are limited to 65535 in each direction", - prop=self.name, - instance=instance, - ) - return value - - -class OctreeSubblockCount(SubBlockCount): - """Number of octree sub-blocks in three axes. Must be between 1 and 65535 and a power of 2.""" - - def validate(self, instance, value): - """Check shape and dtype of the count and that items are >= min.""" - value = super().validate(instance, value) - for item in value: - log = np.log2(item) - if np.trunc(log) != log: - if instance is None: - msg = "octree sub-block counts must be powers of two" - else: - cls = instance.__class__.__name__ - msg = f"{cls}.{self.name} octree counts must be powers of two" - raise properties.ValidationError(msg, prop=self.name, instance=instance) - return value - - -class BlockSize(properties.Array): - """Block size in three axes. Must be greater than zero.""" - - def __init__(self, doc, **kw): - super().__init__(doc, **kw, dtype=float, shape=(3,)) - - def validate(self, instance, value): - """Check shape and dtype of the count and that items are >= min.""" - value = super().validate(instance, value) - for item in value: - if item <= 0.0: - if instance is None: - msg = "block size elements must be > 0.0" - else: - msg = f"{instance.__class__.__name__}.{self.name} elements must be > 0.0" - raise properties.ValidationError(msg, prop=self.name, instance=instance) - return value - - -class TensorArray(properties.Array): - """Arrays of block spacings in one axis. All spacings must be greater than zero.""" - - def __init__(self, doc, **kw): - super().__init__(doc, **kw, dtype=float, shape=("*",)) - - def validate(self, instance, value): - """Check that tensor spacings are all > 0.""" - super().validate(instance, value) - value = np.asarray(value) - if (value <= 0.0).any(): - raise properties.ValidationError( - "Tensor spacings must be greater than zero", - prop=self.name, - instance=instance, - reason="invalid", - ) - return value diff --git a/omf/blockmodel/definition.py b/omf/blockmodel/definition.py index a622671a..c40049fe 100644 --- a/omf/blockmodel/definition.py +++ b/omf/blockmodel/definition.py @@ -2,14 +2,6 @@ import numpy as np import properties -from ._properties import ( - BlockCount, - BlockSize, - OctreeSubblockCount, - SubBlockCount, - TensorArray, -) - class _BaseBlockModelDefinition(properties.HasProperties): axis_u = properties.Vector3("Vector orientation of u-direction", default="X", length=1) @@ -72,8 +64,21 @@ class RegularBlockModelDefinition(_BaseBlockModelDefinition): schema = "org.omf.v2.blockmodeldefinition.regular" - block_count = BlockCount("Number of blocks in each of the u, v, and w directions.") - block_size = BlockSize("Size of blocks in the u, v, and w directions.") + block_count = properties.Array("Number of blocks in each of the u, v, and w directions.", dtype=int, shape=(3,)) + block_size = properties.Vector3("Size of blocks in the u, v, and w directions.") + + @properties.validator("block_count") + def _validate_block_count(self, change): + print(">>>", change) + for item in change["value"]: + if item < 1: + raise properties.ValidationError("block counts must be >= 1", prop=change["name"], instance=self) + + @properties.validator("block_size") + def _validate_block_size(self, change): + for item in change["value"]: + if item <= 0.0: + raise properties.ValidationError("block sizes must be > 0.0", prop=change["name"], instance=self) class TensorBlockModelDefinition(_BaseBlockModelDefinition): @@ -81,14 +86,20 @@ class TensorBlockModelDefinition(_BaseBlockModelDefinition): schema = "org.omf.v2.blockmodeldefinition.tensor" - tensor_u = TensorArray("Tensor cell widths, u-direction") - tensor_v = TensorArray("Tensor cell widths, v-direction") - tensor_w = TensorArray("Tensor cell widths, w-direction") + tensor_u = properties.Array("Tensor cell widths, u-direction", dtype=float, shape=("*",)) + tensor_v = properties.Array("Tensor cell widths, v-direction", dtype=float, shape=("*",)) + tensor_w = properties.Array("Tensor cell widths, w-direction", dtype=float, shape=("*",)) + + @properties.validator("tensor_u") + @properties.validator("tensor_v") + @properties.validator("tensor_w") + def _validate_tensor(self, change): + for item in change["value"]: + if item <= 0.0: + raise properties.ValidationError("tensor sizes must be > 0.0", prop=change["name"], instance=self) def _tensors(self): - yield self.tensor_u - yield self.tensor_v - yield self.tensor_w + return (self.tensor_u, self.tensor_v, self.tensor_w) @property def block_count(self): @@ -104,7 +115,15 @@ class RegularSubblockDefinition(properties.HasProperties): schema = "org.omf.v2.subblockdefinition.regular" - subblock_count = SubBlockCount("The maximum number of sub-blocks inside a parent in each direction.") + subblock_count = properties.Array( + "The maximum number of sub-blocks inside a parent in each direction.", dtype=int, shape=(3,) + ) + + @properties.validator("subblock_count") + def _validate_subblock_count(self, change): + for item in change["value"]: + if item < 1: + raise properties.ValidationError("sub-block counts must be >= 1", prop=change["name"], instance=self) class OctreeSubblockDefinition(RegularSubblockDefinition): @@ -121,7 +140,20 @@ class OctreeSubblockDefinition(RegularSubblockDefinition): schema = "org.omf.v2.subblockdefinition.octree" - subblock_count = OctreeSubblockCount("The maximum number of sub-blocks inside a parent in each direction.") + subblock_count = properties.Array( + "The maximum number of sub-blocks inside a parent in each direction.", dtype=int, shape=(3,) + ) + + @properties.validator("subblock_count") + def _validate_subblock_count(self, change): + for item in change["value"]: + if item < 1: + raise properties.ValidationError("sub-block counts must be >= 1", prop=change["name"], instance=self) + log = np.log2(item) + if np.trunc(log) != log: + raise properties.ValidationError( + "octree sub-block counts must be powers of two", prop=change["name"], instance=self + ) class FreeformSubblockDefinition(properties.HasProperties): diff --git a/tests/test_blockmodel.py b/tests/test_blockmodel.py index 4e4e0c0b..73d2ab20 100644 --- a/tests/test_blockmodel.py +++ b/tests/test_blockmodel.py @@ -82,7 +82,7 @@ def test_bad_block_count(block_count): """Test mismatched block_count""" block_model = omf.RegularBlockModel() block_model.definition.block_size = [1.0, 2.0, 3.0] - with pytest.raises(properties.ValidationError): + with pytest.raises((ValueError, properties.ValidationError)): block_model.definition.block_count = block_count block_model.validate() @@ -92,7 +92,7 @@ def test_bad_block_size(block_size): """Test mismatched block_size""" block_model = omf.RegularBlockModel() block_model.definition.block_count = [2, 2, 2] - with pytest.raises(properties.ValidationError): + with pytest.raises((ValueError, properties.ValidationError)): block_model.definition.block_size = block_size block_model.validate() From 14df0118a0bfd462304f468d5c6b1176a17b80e1 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Thu, 2 Mar 2023 17:56:27 +1300 Subject: [PATCH 25/42] Remove unused import. --- omf/blockmodel/_subblock_check.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/omf/blockmodel/_subblock_check.py b/omf/blockmodel/_subblock_check.py index 24219d5b..24974426 100644 --- a/omf/blockmodel/_subblock_check.py +++ b/omf/blockmodel/_subblock_check.py @@ -1,6 +1,4 @@ """blockmodel/_subblock_check.py: functions for checking sub-block constraints.""" -import itertools - import numpy as np import properties From 67eac276d8a8dd6c8e12922369f311d78f8b40d3 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Fri, 3 Mar 2023 14:45:26 +1300 Subject: [PATCH 26/42] Removed dead code and outdated notebooks. --- notebooks/cbi.py | 225 ---------- notebooks/cbi_plot.py | 122 ------ notebooks/omf_cbi.ipynb | 656 ---------------------------- notebooks/z_order_utils.py | 85 ---- notebooks/zordercurve.png | Bin 7531 -> 0 bytes omf/blockmodel.py | 863 ------------------------------------- 6 files changed, 1951 deletions(-) delete mode 100644 notebooks/cbi.py delete mode 100644 notebooks/cbi_plot.py delete mode 100644 notebooks/omf_cbi.ipynb delete mode 100644 notebooks/z_order_utils.py delete mode 100644 notebooks/zordercurve.png delete mode 100644 omf/blockmodel.py diff --git a/notebooks/cbi.py b/notebooks/cbi.py deleted file mode 100644 index b057713d..00000000 --- a/notebooks/cbi.py +++ /dev/null @@ -1,225 +0,0 @@ -import numpy as np -import properties -import z_order_utils - - -class BaseMetadata(properties.HasProperties): - name = properties.String("Name of the block model", default="") - description = properties.String("Description of the block model", default="") - # Other named metadata? - - -class BaseOrientation(properties.HasProperties): - origin = properties.Vector3( - "Origin of the block model, where axes extend from", - default="ZERO", - ) - axis_u = properties.Vector3("Vector orientation of u-direction", default="X") - axis_v = properties.Vector3("Vector orientation of v-direction", default="Y") - axis_w = properties.Vector3("Vector orientation of w-direction", default="Z") - - -class RegularBlockModel(BaseMetadata, BaseOrientation): - block_size = properties.Vector3( - "Size of each block", - ) - block_count = properties.List( - "Number of blocks in each dimension", - min_length=3, - max_length=3, - prop=properties.Integer("", min=1), - ) - - -class TensorBlockModel(BaseMetadata, BaseOrientation): - tensor_u = properties.Array("Tensor cell widths, u-direction", shape=("*",), dtype=float) - tensor_v = properties.Array("Tensor cell widths, v-direction", shape=("*",), dtype=float) - tensor_w = properties.Array("Tensor cell widths, w-direction", shape=("*",), dtype=float) - - @property - def block_count(self): - return [ - len(self.tensor_u), - len(self.tensor_v), - len(self.tensor_w), - ] - - @property - def num_blocks(self): - return np.prod(self.block_count) - - -class BaseCompressedBlockStorage(properties.HasProperties): - - parent_block_size = properties.Vector3( - "Size of each parent block", - ) - parent_block_count = properties.List( - "Number of parent blocks in each dimension", - min_length=3, - max_length=3, - prop=properties.Integer("", min=1), - ) - - @property - def num_parent_blocks(self): - return np.prod(self.parent_block_count) - - @property - def num_blocks(self): - return self.compressed_block_index[-1] - - @property - def is_sub_blocked(self): - self.compressed_block_index # assert that _cbi exists - return (self._cbi[1:] - self._cbi[:-1]) > 1 - - def _get_starting_cbi(self): - return np.arange(self.num_parent_blocks + 1, dtype="uint32") - - @property - def compressed_block_index(self): - # Need the block counts to exist - assert self._props["parent_block_count"].assert_valid(self, self.parent_block_count) - if "sub_block_count" in self._props: - assert self._props["sub_block_count"].assert_valid(self, self.sub_block_count) - # Note: We could have some warnings here, if the above change - # It is probably less relevant as these are not targeted - # to be used in a dynamic context? - - # If the sub block storage does not exist, create it - if not hasattr(self, "_cbi"): - # Each parent cell has a single attribute before refinement - self._cbi = self._get_starting_cbi() - return self._cbi - - def _get_parent_index(self, ijk): - pbc = self.parent_block_count - assert len(ijk) == 3 # Should be a 3 length integer tuple/list - assert (0 <= ijk[0] < pbc[0]) & (0 <= ijk[1] < pbc[1]) & (0 <= ijk[2] < pbc[2]), "Must be valid ijk index" - - (parent_index,) = np.ravel_multi_index( - [[ijk[0]], [ijk[1]], [ijk[2]]], # Index into the block model - self.parent_block_count, # shape of the parent - order="F", # Explicit column major ordering, "i moves fastest" - ) - return parent_index - - -class RegularSubBlockModel(BaseMetadata, BaseOrientation, BaseCompressedBlockStorage): - - sub_block_count = properties.List( - "Number of sub blocks in each sub-blocked parent", - min_length=3, - max_length=3, - prop=properties.Integer("", min=1), - ) - - @property - def sub_block_size(self): - return self.parent_block_size / np.array(self.sub_block_count) - - def refine(self, ijk): - self.compressed_block_index # assert that _cbi exists - parent_index = self._get_parent_index(ijk) - # Adding "num_sub_blocks" - 1, because the parent was already counted - self._cbi[parent_index + 1 :] += np.prod(self.sub_block_count) - 1 - # Attribute index is where to insert into attribute arrays - attribute_index = tuple(self._cbi[parent_index : parent_index + 2]) - return parent_index, attribute_index - - # Note: Perhaps if there is an unrefined RSBM, - # then OMF should serialize as a RBM? - - -class OctreeSubBlockModel(BaseMetadata, BaseOrientation, BaseCompressedBlockStorage): - @property - def z_order_curves(self): - forest = self._get_forest() - cbi = self.compressed_block_index - curves = np.zeros(self.num_blocks, dtype="uint32") - for i, tree in enumerate(forest): - curves[cbi[i] : cbi[i + 1]] = sorted(tree) - return curves - - def _get_forest(self): - """Want a set before we create the array. - This may not be useful for less dynamic implementations. - """ - if not hasattr(self, "_forest"): - # Do your part for the planet: - # Plant trees in every parent block. - self._forest = [{0} for _ in range(self.num_parent_blocks)] - return self._forest - - def _refine_child(self, ijk, ind): - - self.compressed_block_index # assert that _cbi exists - parent_index = self._get_parent_index(ijk) - tree = self._get_forest()[parent_index] - - if ind not in tree: - raise IndexError(ind) - - p, lvl = z_order_utils.get_pointer(ind) - w = z_order_utils.level_width(lvl + 1) - - children = [ - [p[0], p[1], p[2], lvl + 1], - [p[0] + w, p[1], p[2], lvl + 1], - [p[0], p[1] + w, p[2], lvl + 1], - [p[0] + w, p[1] + w, p[2], lvl + 1], - [p[0], p[1], p[2] + w, lvl + 1], - [p[0] + w, p[1], p[2] + w, lvl + 1], - [p[0], p[1] + w, p[2] + w, lvl + 1], - [p[0] + w, p[1] + w, p[2] + w, lvl + 1], - ] - - for child in children: - tree.add(z_order_utils.get_index(child[:3], child[3])) - tree.remove(ind) - - # Adding "num_sub_blocks" - 1, because the parent was already counted - self._cbi[parent_index + 1 :] += 7 - - return children - - -class ArbitrarySubBlockModel(BaseMetadata, BaseOrientation, BaseCompressedBlockStorage): - def _get_starting_cbi(self): - """Unlike octree and rsbm, this has zero sub-blocks to start with.""" - return np.zeros(self.num_parent_blocks + 1, dtype="uint32") - - def _get_lists(self): - """Want a set before we create the array. - This may not be useful for less dynamic implementations. - """ - if not hasattr(self, "_lists"): - # Do your part for the planet: - # Plant trees in every parent block. - self._lists = [(np.zeros((0, 3)), np.zeros((0, 3))) for _ in range(self.num_parent_blocks)] - return self._lists - - def _add_sub_blocks(self, ijk, new_centroids, new_sizes): - self.compressed_block_index # assert that _cbi exists - parent_index = self._get_parent_index(ijk) - centroids, sizes = self._get_lists()[parent_index] - - if not isinstance(new_centroids, np.ndarray): - new_centroids = np.array(new_centroids) - new_centroids = new_centroids.reshape((-1, 3)) - - if not isinstance(new_sizes, np.ndarray): - new_sizes = np.array(new_sizes) - new_sizes = new_sizes.reshape((-1, 3)) - - assert (new_centroids.size % 3 == 0) & (new_sizes.size % 3 == 0) & (new_centroids.size == new_sizes.size) - - # TODO: Check that the centroid exists in the block - - self._lists[parent_index] = ( - np.r_[centroids, new_centroids], - np.r_[sizes, new_sizes], - ) - - self._cbi[parent_index + 1 :] += new_sizes.size // 3 diff --git a/notebooks/cbi_plot.py b/notebooks/cbi_plot.py deleted file mode 100644 index e79f8218..00000000 --- a/notebooks/cbi_plot.py +++ /dev/null @@ -1,122 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import z_order_utils -from mpl_toolkits.mplot3d import axes3d, Axes3D - - -def _get_vecs(bc, bs): - - vec_x = np.arange(bc[0] + 1) * np.repeat(bs[0], bc[0] + 1) - vec_y = np.arange(bc[1] + 1) * np.repeat(bs[1], bc[1] + 1) - vec_z = np.arange(bc[2] + 1) * np.repeat(bs[2], bc[2] + 1) - return vec_x, vec_y, vec_z - - -def _plot_rbm(bc, bs, corner, vecs=None, ax=None): - if ax is None: - plt.figure() - ax = plt.subplot(111, projection="3d") - - if vecs is None: - vec_x, vec_y, vec_z = _get_vecs(bc, bs) - else: - vec_x, vec_y, vec_z = vecs - - x_lines = [] - for z in vec_z: - for y in vec_y: - x_lines += [[vec_x[0], y, z], [vec_x[-1], y, z], [np.nan] * 3] - x_lines = np.array(x_lines) - - y_lines = [] - for z in vec_z: - for x in vec_x: - y_lines += [[x, vec_y[0], z], [x, vec_y[-1], z], [np.nan] * 3] - - z_lines = [] - for x in vec_x: - for y in vec_y: - z_lines += [[x, y, vec_z[0]], [x, y, vec_z[-1]], [np.nan] * 3] - - X, Y, Z = np.array(x_lines), np.array(y_lines), np.array(z_lines) - XS = np.r_[X[:, 0], Y[:, 0], Z[:, 0]] + corner[0] - YS = np.r_[X[:, 1], Y[:, 1], Z[:, 1]] + corner[1] - ZS = np.r_[X[:, 2], Y[:, 2], Z[:, 2]] + corner[2] - plt.plot(XS, YS, zs=ZS) - - return ax - - -def plot_rbm(rbm): - _plot_rbm(rbm.block_count, rbm.block_size, rbm.corner) - - -def plot_tbm(tbm): - vecs = ( - np.r_[0, np.cumsum(tbm.tensor_u)], - np.r_[0, np.cumsum(tbm.tensor_v)], - np.r_[0, np.cumsum(tbm.tensor_w)], - ) - _plot_rbm(tbm.block_count, np.nan, tbm.corner, vecs=vecs) - - -def plot_rsbm(rsbm, ax=None): - pbc = rsbm.parent_block_count - pbs = rsbm.parent_block_size - isb = rsbm.is_sub_blocked - sbc = rsbm.sub_block_count - sbs = rsbm.sub_block_size - corner = rsbm.corner - - ax = _plot_rbm(pbc, pbs, corner, ax=ax) - - for k in range(0, pbc[2]): - for j in range(0, pbc[1]): - for i in range(0, pbc[0]): - ind = rsbm._get_parent_index([i, j, k]) - if isb[ind]: - sub_corner = corner + pbs * np.array([i, j, k]) - _plot_rbm(sbc, sbs, sub_corner, ax=ax) - - -def plot_osbm(osbm, ax=None): - pbc = osbm.parent_block_count - pbs = osbm.parent_block_size - isb = osbm.is_sub_blocked - corner = osbm.corner - - max_lvl = z_order_utils.level_width(0) - - ax = _plot_rbm(pbc, pbs, corner, ax=ax) - - vec_x, vec_y, vec_z = _get_vecs(pbc, pbs) - - def plot_block(index, corner): - pnt, lvl = z_order_utils.get_pointer(index) - bs = [s * (z_order_utils.level_width(lvl) / max_lvl) for s in pbs] - cnr = [c + s * (p / max_lvl) for c, p, s in zip(corner, pnt, pbs)] - _plot_rbm([1, 1, 1], bs, cnr, ax=ax) - - for parent_index, tree in enumerate(osbm._get_forest()): - i, j, k = np.unravel_index(parent_index, osbm.parent_block_count, order="F") - parent_corner = vec_x[i], vec_y[j], vec_z[k] - - for block in tree: - if block == 0: - # plotted as a parent above - continue - plot_block(block, parent_corner) - - -def plot_asbm(asbm, ax=None): - if ax is None: - plt.figure() - ax = plt.subplot(111, projection="3d") - - def plot_block(centroid, size): - cnr = [c - s / 2.0 for c, s in zip(centroid, size)] - _plot_rbm([1, 1, 1], size, cnr, ax=ax) - - for centroids, sizes in asbm._get_lists(): - for i in range(centroids.shape[0]): - plot_block(centroids[i, :], sizes[i, :]) diff --git a/notebooks/omf_cbi.ipynb b/notebooks/omf_cbi.ipynb deleted file mode 100644 index 39b3ab6d..00000000 --- a/notebooks/omf_cbi.ipynb +++ /dev/null @@ -1,656 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# OMF.v2 Block Model Storage\n", - "\n", - "**Authors:** Rowan Cockett, Franklin Koch
\n", - "**Company:** Seequent
\n", - "**Date:** March 3, 2019\n", - "\n", - "## Overview\n", - "\n", - "The proposal below defines a storage algorithm for all sub block model formats in OMF.v2.\n", - "The storage & access algorithm is based on [sparse matrix/array storage](https://en.wikipedia.org/wiki/Sparse_matrix#Storing_a_sparse_matrix) in linear algebra.\n", - "The algorithm for the _Compressed Block Index_ format is largely similar between the various block model formats supported by OMF.v2:\n", - "\n", - "* _Regular Block Model_: No aditional storage information necessary.\n", - "* _Tensor Block Model_: No aditional storage information necessary.\n", - "* _Regular Sub Block Model_: Single storage array required to record sub-blocking and provide indexing into attribute arrays.\n", - "* _Octree Sub Block Model_: Storage array required as well as storage for each Z-Order Curve per octree (discussed in detail below).\n", - "* _Arbitrary Sub Block Model_: Storage array required as well as storage of sub-block centroids and sizes.\n", - "\n", - "## Summary\n", - "\n", - "* The compression format for a _Regular Sub Block Model_ scales with parent block count rather than sub block count.\n", - "* Storing an _Octree Sub Block Model_ is 12 times more efficient than an _Arbitrary Sub Block Model_ for the same structure. For example, an _Octree Sub Block Model_ with 10M sub-blocks would save 3.52 GB of space.\n", - "* Attributes for all sub-block types are stored on-disk in contiguous chunks per parent block, allowing for easy memory mapping of attributes, if necessary." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import cbi\n", - "import cbi_plot\n", - "import z_order_utils\n", - "import numpy as np\n", - "%matplotlib inline" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Compressed Block Index\n", - "\n", - "The _Compressed Block Index_ format (or `cbi` in code) is a monotonically increasing integer array, which starts at 0 (`cbi[0] := 0`) and ends at the total number of blocks `cbi[i * j * k] := num_blocks`. For the n-th parent block, `cbi[n+1] - cbi[n]` will always be the number of sub-blocks per parent (`prod(sub_block_count)` for a _Regular Sub Block Model_). This can be used to determine if the n-th parent block is sub-blocked (i.e. `is_sub_blocked[n]`), as well as the index into any attribute array to retrive all of the attribute data for that parent block. That is, `attribute[cbi[n] : cbi[n+1]]` will always return the attributes for the n-th parent block, regardless of if the parent block is sub-blocked or not. The `cbi` indexing is also useful for the Octree and Arbitrary Sub Block Models, allowing additional topology information about the tree structure or arbitrary sub-blocks, respectively, to be stored in a single array.\n", - "\n", - "The _Compressed Block Index_ format means the total size for storage is a fixed length `UInt32` array plus a small amount of metadata (i.e. nine extra numbers, name, description, etc.). That is, this compression format **scales with the parent block count** rather than the sub-block count. All other information can be derived from the `cbi` array (e.g. `is_sub_blocked` as a boolean and all indexing into the attribute arrays). **Note:** `cbi` could instead use Int64, for the index depending on the upper limit required for number of blocks.\n", - "\n", - "The technique is to be used as underlying storage for _Regular Sub Block Model_, _Octree Sub Block Model_, and _Arbitrary Sub Block Model_. This index is not required for _Tensor Block Model_ or _Regular Block Model_; however, it could be used as an optional property to have null-blocks (e.g. above the topography) that would decrease the storage of all array attributes. In this case, `cbi[n] == cbi[n+1]`.\n", - "\n", - "**Note** - For the final implementation, we may store _compressed block count_, so like `[1, 1, 32, 1]` instead of `[0, 1, 2, 34, 35]`, and _compressed block index_ is a computed sum. This has slight performance advantages, i.e. refining a parent block into sub-blocks only is O(1) rather than O(n), and storage size advantages, since we can likely use `UInt16`, constrained by number of sub-blocks per parent, not total sub-blocks." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# All Block Models\n", - "\n", - "All block models have been decided to be defined inside of a rotated coordinate frame. The implementation of this orientation uses three `axis` vectors (named `axis_u`, `axis_v` and `axis_w`) and a `corner` that defines a bounding box in the project coordinate reference system. These axis vectors must be orthogonal but are not opinionated about \"handed-ness\". The implementation is explicitly not (a) rotation matrices, which may have skew and or (b) defined as three rotations, which may be applied in other orders (e.g. `ZYX` vs `YXZ`) and not be consistent. The unwrapping of attributes and the `ijk` block index is relative to these axis, respectively, in the rotated coordinate frame. By convention, the `axis` vectors are normalized since their length does not have meaning. Total size of the block model is determined by summing parent block sizes on each dimension. However, it is not absolutely necessary for normalized lengths to be enforced by OMF.\n", - "\n", - "**Stored Properties**\n", - "\n", - "* `name` - Name of the block model\n", - "* `description` - Description of the block model\n", - "* `attributes` - list of standard [OMF.v1 attributes](https://omf.readthedocs.io/en/stable/content/data.html#scalardata)\n", - "* `axis_u` (Vector3) Orientation of the i-axis in the project CRS\n", - "* `axis_v` (Vector3) Orientation of the j-axis in the project CRS\n", - "* `axis_w` (Vector3) Orientation of the k-axis in the project CRS\n", - "* `corner` (Vector3) Minimum x/y/z in the project CRS\n", - "* `location` - String representation of where attributes are defiend on the block model. Either `\"parent_blocks\"` or `\"sub_blocks\"` (if sub blocks are present in the block model class). This could be extended to `\"faces\"`, `\"edges\"`, and `\"Nodes\"` for Regular and Tensor Block Models\n", - "\n", - "**Attributes**\n", - "\n", - "All block models are stored with flat attribute arrays, allowing for efficient storage and access, as well as adhering to existing standards set out for all other Elements in the OMF.v1 format. The standard counting is _column major ordering_, following \"Fortran\" style indexing -- in `numpy` (Python) this uses `array.flatten(order='F')` where array is the 3D attribute array. To be explicit, inside a for-loop the `i` index always moves the fastest:\n", - "\n", - "```python\n", - "count = regular_block_model.block_count\n", - "index = 0\n", - "for k in range(count[2]):\n", - " for j in range(count[1]):\n", - " for i in range(count[0]):\n", - " print(index, (i, j, k))\n", - " index += 1\n", - "```\n", - "\n", - "# Regular Block Model\n", - "\n", - "**Stored Properties**\n", - "\n", - "* `block_size`: a Vector3 (Float) that describes how large each block is\n", - "* `block_count`: a Vector3 (Int) that describes how many blocks in each dimension\n", - "\n", - "**Note**\n", - "\n", - "For the final implementation we will use property names `size_blocks`/`size_parent_blocks`/`size_sub_blocks` and equivalently `num_*` - this enables slightly easier discoverability of properties across different element types." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "

" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "rbm = cbi.RegularBlockModel()\n", - "rbm.block_size = [1.5, 2.5, 10.]\n", - "rbm.block_count = [3, 2, 1]\n", - "rbm.validate()\n", - "\n", - "cbi_plot.plot_rbm(rbm)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Tensor Block Model\n", - "\n", - "**Stored Properties**\n", - "\n", - "* `tensor_u`: a Float64 array of spacings along `axis_u`\n", - "* `tensor_v`: a Float64 array of spacings along `axis_v`\n", - "* `tensor_w`: a Float64 array of spacings along `axis_w`\n", - "\n", - "**Note:** `block_size[0]` for the i-th block is `tensor_u[i]` and `block_count[0]` is `len(tensor_u)`. Counting for attributes is the same as _Regular Block Model_." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "block_count: [3, 2, 1]\n", - "num_blocks: 6\n" - ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "tbm = cbi.TensorBlockModel()\n", - "tbm.tensor_u = [2.5, 1.0, 1.0]\n", - "tbm.tensor_v = [3.5, 1.5]\n", - "tbm.tensor_w = [10.0]\n", - "tbm.validate()\n", - "\n", - "print(\"block_count:\", tbm.block_count)\n", - "print(\"num_blocks:\", tbm.num_blocks)\n", - "cbi_plot.plot_tbm(tbm)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Regular Sub Block Model\n", - "\n", - "The `RegularSubBlockModel` requires storage of information to store the parent and sub block counts as well as the parent block sizes. Attribute ordering for sub-blocks within each parent block is also _column-major ordering_.\n", - "\n", - "**Stored Properties**\n", - "\n", - "* `parent_block_size`: a Vector3 (Float) that describes how large each parent block is\n", - "* `parent_block_count`: a Vector3 (Int) that describes how many parent blocks in each dimension\n", - "* `sub_block_count`: a Vector3 (Int) that describes how many sub blocks in each dimension are contained within each parent block\n", - "* `compressed_block_index`: a UInt32 array of length (`i * j * k + 1`) that defines the sub block topology" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "cbi: [0 1 2 3 4 5 6]\n", - "num_blocks: 6\n", - "is_sub_blocked: [False False False False False False]\n", - "sub_block_size: [0.75 1.25 5. ]\n" - ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "rsbm = cbi.RegularSubBlockModel()\n", - "rsbm.parent_block_size = [1.5, 2.5, 10.]\n", - "rsbm.parent_block_count = [3, 2, 1]\n", - "rsbm.sub_block_count = [2, 2, 2]\n", - "rsbm.validate()\n", - "\n", - "print(\"cbi:\", rsbm.compressed_block_index)\n", - "print(\"num_blocks:\", rsbm.num_blocks)\n", - "print(\"is_sub_blocked:\", rsbm.is_sub_blocked)\n", - "print(\"sub_block_size:\", rsbm.sub_block_size)\n", - "cbi_plot.plot_rsbm(rsbm)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "cbi: [ 0 1 2 3 11 12 13]\n", - "num_blocks: 13\n", - "is_sub_blocked: [False False False True False False]\n", - "sub_block_size: [0.75 1.25 5. ]\n" - ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "rsbm.refine((0, 1, 0))\n", - "\n", - "print(\"cbi:\", rsbm.compressed_block_index)\n", - "print(\"num_blocks:\", rsbm.num_blocks)\n", - "print(\"is_sub_blocked:\", rsbm.is_sub_blocked)\n", - "print(\"sub_block_size:\", rsbm.sub_block_size)\n", - "cbi_plot.plot_rsbm(rsbm)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Octree Sub Block Model\n", - "\n", - "The _Octree Sub Block Model_ is a \"forest\" of individual octrees, with the \"root\" of every octree positioined at the center of each parent block within a _Regular Block Model_. Each octree is stored as a [Linear Octree](https://en.wikipedia.org/wiki/Linear_octree) with the space-filling curve chosen to be a [Z-Order Curve](https://en.wikipedia.org/wiki/Z-order_curve) (also known as a Morton curve). The Z-Order curve was chosen based on the efficient properties of bit-interleaving to produce a sorted integer array that defines both the attribute ordering and the topology of the sub blocks; this has been used successfully in HPC algorithms for \"forests of octrees\" (e.g. [Parallel Forset of Octree's](https://epubs.siam.org/doi/abs/10.1137/100791634), [PDF](http://p4est.github.io/papers/BursteddeWilcoxGhattas11.pdf)). Note, that the maximum level necessary for each octree must be decided upon in OMF.v2, the industry standard is up to eight refinements, and that has been proposed. The `level` information is stored in this integer through a left-shift binary operation (i.e. `(z_order << 3) + level`). For efficient access to the attributes, the _Compressed Block Index_ is also stored.\n", - "\n", - "**Stored Properties**\n", - "\n", - "* `parent_block_size`: a Vector3 (Float64) that describes how large each parent block is\n", - "* `parent_block_count`: a Vector3 (Int16) that describes how many parent blocks in each dimension\n", - "* `compressed_block_index`: a UInt32 array of length (`i * j * k + 1`) that defines delineation between octrees in the forest\n", - "* `z_order_curves`: a UInt32 array of length `num_blocks` containing the Z-Order curves for all octrees. Unrefined parents have z-order curve of `0`\n", - "\n", - "\n", - "See first three functions of [discretize tree mesh](https://github.com/simpeg/discretize/blob/1721a8626682cf7df0083f8401fff9d0c643b999/discretize/TreeUtils.pyx) for an implementation of z-order curve." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "cbi: [0 1 2 3 4 5 6]\n", - "z_order_curves: [0 0 0 0 0 0]\n", - "num_blocks: 6\n" - ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "osbm = cbi.OctreeSubBlockModel()\n", - "osbm.parent_block_size = [1.5, 2.5, 10.]\n", - "osbm.parent_block_count = [3, 2, 1]\n", - "osbm.validate();\n", - "print('cbi: ', osbm.compressed_block_index)\n", - "print('z_order_curves: ', osbm.z_order_curves)\n", - "print('num_blocks: ', osbm.num_blocks)\n", - "cbi_plot.plot_osbm(osbm)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "cbi: [ 0 1 2 3 18 19 20]\n", - "z_order_curves: [ 0 0 0 2 2097154 4194306 6291458\n", - " 8388610 10485762 12582914 14680066 16777217 33554433 50331649\n", - " 67108865 83886081 100663297 117440513 0 0]\n", - "num_blocks: 20\n" - ] - }, - { - "data": { - "image/png": "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KF8GvjqiO5PU19dzwQUXhEV3igiCwOHDh2WTn88//xxRFINM4PPy8o7K7Ebymw3sHxYEf7Q7f/58TDFx1Pa4g/LDjWPsp8tNMvC7bZX8+zMLZ+fG815FF/kpRu69MC/IwGeiNDc384tf/IK5c+eyefNmzOaJ71w7ltTX13Paaadx8ODBafe/mKWERfd4EBbd6UVKGezdu5eSkhKKi4upqKggJiZG7pQoLCwkNTV1alsYhoYckpOTiYyMDBpekNYimUwmxIhIytoHR+0f9gyFxhqVkuvPnB/SXHw8fD4f//znP3niiSd48MEHWbt27QkR3QL09/dz+umnc8cdd3DppZfO9HG+LIRF93gQFt1jjyiKdHZ2BpnAt7a2Mn/+/CATeJPJNKpoud1uKioqZG+H4T7FkthbrdaQa5FMJhN2QS3nh/++y1+83XrdSZMyF5doamri+uuvJzMzk02bNp1QkaTH4+GCCy7g3HPP5aabbprp43yZCIvu8SAsujODZAIvTdPt3bsXp9M5wgRerVbz6aefolKpWLBgAYmJiRN+jkCXMZvNJueHzWYzeoORmGgz+kmuA/L5fDz99NP87W9/46GHHuLss88+YaJb8H/4XHXVVcTGxvLwww/P9HG+bIRF93gQFt0TB5fLJZvAS0tCbTYbWVlZXHXVVRQUFDB//vyjWjc+3lokk8k06j60xsZG1q9fT05ODps2bZqyHeSx5OOPP+bUU09lyZIl8vt03333sW7duhk+2ZeCsOgea773ve+xc+dOurq6SEpKYuPGjfz4xz8+Ls8d7qccm9LSUn72s59x9913o9FoZCGuq6sjNTVVjoYLCgqIi4s7qra1sdYitbe3k5eXx7/+9S+eeuopHnroIc4666wTKroNM22ERXc2E+6nHBvJUGf4Akufz0djY6OcligtLcVqtZKXlzfCBH6qiKIoeyr85je/YdeuXTidTi688ELWrFnD97///SltBw5zwhMW3a8S4X7KqePxeEaYwCuVSlasWCEPcuTk5Iw7pRaIIAg8+eSTPP300zz88MMUFhayf/9+9uzZw/r1648qxRHmhCUsul8Vwv2U04soivT397N3715ZiCsrK0lISJBFuLCwkKSkpJBpgrq6Oq6//nqWLFnCfffdR1RU6DXzx4rwpOSMERbdrwLhfsrjg2T1GGgC39HRIZvAr1q1imXLlvH888/zzDPP8Mgjj3DqqafOSO42PCk5Y4RFd7YT7qecWQRBoKKiQu4f3rZtG0VFRTz99NPo9ZPv3Z1Owl01M0JYdGcz4X7KEw9RFE+YroSw6M4Io37zwxn8WcAnn3zCM888w44dO1i+fDnLly9n69atx+35nU4nRUVFLFu2jMWLF3PXXXcdt+c+UTlRBDfMices8tP9qnLKKaeENHA/Xmi1Wnbs2IHBYMDj8XDKKadw/vnns3r16hk7U5gwJyrhSDfMUaNQKOSJKo/Hg8fjCUd6YcKMQlh0w0wLgiCwfPlyEhMTWbt2LSeddNJMHykM/knJk08+mYqKCtLS0njyySdn+khfecKFtDDTSl9fH5dccgmPPvoo+fn5M32cMGFminAhLczxITo6mjPOOINt27bN9FHChDkhCYtumKOms7OTvr4+wL+y/N133yUvL2+GTxUmzIlJWHTDHDUWi4UzzzyTpUuXUlhYyNq1a7nggguO+zkEQWDFihUz8tzHkm3btpGbm0tWVha///3vZ/o4YY6ScE43zKxhy5Yt7NmzB5vNxhtvvDHTx5kWBEEgJyeH7du3k5aWRmFhIc8//3zYQe7EJ5zTDTO7aW5u5s033+Saa66Z6aNMKyUlJWRlZZGZmYlGo+Hyyy/n1VdfneljhTkKwqIbZlZw4403smnTpllnk9jS0sLcuXPl36elpdHS0jKDJwpztMyun9AwX0kk68KCgoKZPsq0Eyr9Fx48+XIzXk43TJgTHoVCcT/wQ8AL6AAT8LIoij+Y0YNNAwqF4mTgblEUzx36/W0AoijeP6MHCzNlwqIbZlahUCjOAG4RRXFWtDAoFAo1UAmcDbQApcAVoigemtGDhZkyYcObMGFOYERR9CoUivXA24AK+HtYcL/chCPdMGHChDmOhAtpYcKECXMcCYtumDBhwhxHwqIbJkyYMMeR/x/+uFhrRax6xwAAAABJRU5ErkJggg==\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "# This part needs work in the implementation for a high level wrapper\n", - "osbm._refine_child((0, 1, 0), 0)\n", - "osbm._refine_child((0, 1, 0), 1)\n", - "\n", - "print('cbi: ', osbm.compressed_block_index)\n", - "print('z_order_curves: ', osbm.z_order_curves)\n", - "print('num_blocks: ', osbm.num_blocks)\n", - "cbi_plot.plot_osbm(osbm)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Octree Pointers and Level\n", - "\n", - "A Z-Order curve is used to encode each octree into a linear array. The below example shows visually how the pointer and level information is encoded into a single 32 bit integer. The key pieces are to decide how many levels are possible within each tree. Choosing the current industry standard of 8 levels, allows for 256 sub-blocks in each dimension. This can accomodate 16.7 million sub-blocks within each parent block. Note that the actual block model may have many more blocks than those in a single parent block.\n", - "\n", - "The `pointer` of an octree sub-block has an `ijk` index, which is the sub-block corner relative to the parent block corner, the max dimension of each is 256. There is also a `level` that corresponds to the level of the octree -- 0 corresponds to the largest block size (i.e. same as the parent block) and 7 corresponds to the smallest block size.\n", - "\n", - "The sub-blocks must be refined as an octree. That is, the root block has `level=0` and `width=256`, and can be refined into 8 children - each with `level=1` and `width=128`." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Refine the (0, 0, 0) parent block.\n", - "The children are:\n", - "[[0, 0, 0, 1], [128, 0, 0, 1], [0, 128, 0, 1], [128, 128, 0, 1], [0, 0, 128, 1], [128, 0, 128, 1], [0, 128, 128, 1], [128, 128, 128, 1]]\n" - ] - } - ], - "source": [ - "osbm = cbi.OctreeSubBlockModel()\n", - "osbm.parent_block_size = [1.5, 2.5, 10.]\n", - "osbm.parent_block_count = [3, 2, 1]\n", - "print('Refine the (0, 0, 0) parent block.')\n", - "children = osbm._refine_child((0, 0, 0), 0)\n", - "print('The children are:')\n", - "print(children)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Refine the (0, 0, 0) parent block, sub-block (0, 0, 0, 1).\n", - "The children are:\n", - "[[0, 0, 0, 2], [64, 0, 0, 2], [0, 64, 0, 2], [64, 64, 0, 2], [0, 0, 64, 2], [64, 0, 64, 2], [0, 64, 64, 2], [64, 64, 64, 2]]\n" - ] - } - ], - "source": [ - "print('Refine the (0, 0, 0) parent block, sub-block (0, 0, 0, 1).')\n", - "children = osbm._refine_child((0, 0, 0), 1)\n", - "print('The children are:')\n", - "print(children)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Linear Octree Encoding\n", - "\n", - "The encoding into a linear octree is done through bit-interleaving of each location integer. This produces a [Z-Order Curve](https://en.wikipedia.org/wiki/Z-order_curve), which is a space filling curve - it guarantees a unique index for each block, and has the nice property that blocks close together are stored close together in the attribute arrays.\n", - "\n", - "

Visualization of the space filling Z-Order Curve
\n" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "33554433\n", - "[0, 128, 0] 1\n" - ] - } - ], - "source": [ - "pointer = [0, 128, 0]\n", - "level = 1\n", - "ind = z_order_utils.get_index(pointer, level)\n", - "pnt, lvl = z_order_utils.get_pointer(ind)\n", - "\n", - "# assert that you get back what you put in:\n", - "assert (pointer == pnt) & (level == lvl)\n", - "\n", - "print(ind)\n", - "print(pnt, lvl)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The actual encoding is completed through bit-interleaving of the three ijk-indices and then adding the level via left-shifting the integer. This is visualized in text as: " - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " 001 = 1\n", - " 0 0 0 0 0 0 0 0 0 0 = 0\n", - " 0 0 1 0 0 0 0 0 0 0 = 128\n", - "0 0 0 0 0 0 0 0 0 0 = 0\n", - "000000010000000000000000000000001 = 33554433\n" - ] - } - ], - "source": [ - "z_order_utils._print_example(pointer, level);" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Octree Storage Summary\n", - "\n", - "The overall storage format reduces to two arrays, (1) `csb` has length equal to the number of parent blocks; (2) `z_order_curves` has length equal to the total number of sub-blocks. This parallels standard storage formats for sparse matrices as well as standard octree storage formats. The outcome is a storage format that is compact and allows for efficient access of, for example, all sub-blocks in a parent block. The contiguous data access allows for memory-mapped arrays, among other efficiencies. The format is also **twelve times more efficient** than the equivalent storage of an _Arbtrary Sub Block Model_ (one UInt32 vs six Float64 arrays). For example, a 10M cell block model **saves 3.52 GB of space** stored in this format. The format also enforces consistency on the indexing of the attributes. These efficiencies, as well as classic algorithms possible for searching octree, can be taken advantage of in vendor applications both for visualization and for evaluation of other attributes." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Arbitrary Sub Block Model\n", - "\n", - "The _Arbitrary Sub Block Model_ is the most flexible and also least efficient storage format. The format allows for storage of arbitrary blocks that are contained within the parent block. The _Arbitrary Sub Block Model_ does not enforce that sub-blocks fill the entire space of the parent block.\n", - "\n", - "**Stored Properties**\n", - "\n", - "* `parent_block_size`: a Vector3 (Float64) that describes how large each parent block is\n", - "* `parent_block_count`: a Vector3 (Int16) that describes how many parent blocks in each dimension\n", - "* `compressed_block_index`: a UInt32 array of length (`i * j * k + 1`) that defines the sub block count\n", - "* `sub_block_centroids`: a Float64 array containing the sub block centroids for all parent blocks - there are no assumptions about how the sub-blocks are ordered within each parent block\n", - "* `sub_block_sizes`: a Float64 array containing the sub block sizes for all parent blocks\n", - "\n", - "**Centroids and Sizes**\n", - "\n", - "These are stored as a two Float64 arrays as `[x_1, y_1, z_1, x_2, y_2, z_2, ...]` to ensure centroids can easily be accessed through the `cbi` indexing as well as memory mapped per parent block. The sizes and centroids are **normalized** within the parent block, that is, `0 < centroid < ` and `0 < size <= 1`. This has 2 advantages: (1) it's easy to tell if values are outside the parent block, and (2) given a large offset, this may allow smaller storage size.\n", - "\n", - "**Parent blocks without sub blocks**\n", - "\n", - "Since the `cbi` is used to index into sub block centroid/size arrays, non-sub-blocked parents require an entry in these arrays. Likely this is centroid `[.5, .5, .5]` and size `[1, 1, 1]`.\n", - "\n", - "**Question: Centroid vs. Corner**\n", - "\n", - "Should we store the `corner` instead to be consistent with the orientation of the block model storage? Storing the corner means three less operations per centroid for checking if it is contained by the parent (although one more for access, as centroid seems to be the industry standard). We could even store opposing corners, instead of corner and size, which would enable exact comparisons to determine adjacent sub blocks.\n", - "\n", - "There is no _storage_ advantage to corners/corners vs. corners/sizes vs. centroids/sizes, especially if these are all normalized. Corners/sizes gives the most API consistency, since we store block model corner and block size. Regardless of which we store, all these properties should be exposed in the client libraries." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "cbi: [0 0 0 0 0 0 0]\n", - "num_blocks: 0\n", - "num_parent_blocks: 6\n" - ] - } - ], - "source": [ - "asbm = cbi.ArbitrarySubBlockModel()\n", - "asbm.parent_block_size = [1.5, 2.5, 10.]\n", - "asbm.parent_block_count = [3, 2, 1]\n", - "asbm.validate();\n", - "print('cbi: ', asbm.compressed_block_index)\n", - "print('num_blocks: ', asbm.num_blocks)\n", - "print('num_parent_blocks: ', asbm.num_parent_blocks)\n", - "# Nothing to plot to start with" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "def add_parent_block(asbm, ijk):\n", - " \"\"\"Nothing special about these, they are just sub-blocks.\"\"\"\n", - " pbs = np.array(asbm.parent_block_size)\n", - " half = pbs / 2.0\n", - " offset = half + pbs * ijk\n", - " asbm._add_sub_blocks(ijk, offset, half*2)" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "cbi: [0 3 4 5 6 7 8]\n", - "num_blocks: 8\n", - "num_parent_blocks: 6\n" - ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "# Something special for the first ones\n", - "asbm._add_sub_blocks(\n", - " (0, 0, 0), [0.75, 1.25, 2.5], [1.5, 2.5, 5.]\n", - ")\n", - "asbm._add_sub_blocks(\n", - " (0, 0, 0), [0.375, 1.25, 7.5], [0.75, 2.5, 5.]\n", - ")\n", - "asbm._add_sub_blocks(\n", - " (0, 0, 0), [1.175, 1.25, 7.5], [0.75, 2.5, 5.]\n", - ")\n", - "add_parent_block(asbm, (1, 0, 0))\n", - "add_parent_block(asbm, (2, 0, 0))\n", - "add_parent_block(asbm, (0, 1, 0))\n", - "add_parent_block(asbm, (1, 1, 0))\n", - "add_parent_block(asbm, (2, 1, 0))\n", - "\n", - "print('cbi: ', asbm.compressed_block_index)\n", - "print('num_blocks: ', asbm.num_blocks)\n", - "print('num_parent_blocks: ', asbm.num_parent_blocks)\n", - "cbi_plot.plot_asbm(asbm)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.8" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/notebooks/z_order_utils.py b/notebooks/z_order_utils.py deleted file mode 100644 index 7c7cc174..00000000 --- a/notebooks/z_order_utils.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -Given a `pointer` and a `level` get_index interleaves the pointer bits -and left-shits the resulting index and adds the level information. - -The resulting index is a Z-Order Curve that can store a linear octree. - -Example encoding: - -level of block | 0111 = 7 -corner of sub-block, |u 0 0 0 0 0 0 0 1 = 1 -indexed by smallest |v 0 0 0 1 0 0 0 0 = 16 -possible level |w 0 0 0 0 0 0 0 0 = 0 - | 0000000000100000000000010111 = 131095 -""" - -import numpy as np - -dimension = 3 # Always in 3D -level_bits = 4 # Enough for eight refinements -max_bits = 8 # max necessary per integer, enough for UInt32 -total_bits = max_bits * dimension + level_bits - - -# Below code is a bit general. For this implementation, -# we can remove/hard-code dimension. And probably use -# `0b01010101` like integers instead of doing for-loops. -# See https://en.wikipedia.org/wiki/Z-order_curve - - -def bitrange(x, width, start, end): - """ - Extract a bit range as an integer. - (start, end) is inclusive lower bound, exclusive upper bound. - """ - return x >> (width - end) & ((2 ** (end - start)) - 1) - - -def get_index(pointer, level): - idx = 0 - iwidth = max_bits * dimension - for i in range(iwidth): - bitoff = max_bits - (i // dimension) - 1 - poff = dimension - (i % dimension) - 1 - b = bitrange(pointer[dimension - 1 - poff], max_bits, bitoff, bitoff + 1) << i - idx |= b - return (idx << level_bits) + level - - -def get_pointer(index): - level = index & (2**level_bits - 1) - index = index >> level_bits - - pointer = [0] * dimension - iwidth = max_bits * dimension - for i in range(iwidth): - b = bitrange(index, iwidth, i, i + 1) << (iwidth - i - 1) // dimension - pointer[i % dimension] |= b - pointer.reverse() - return pointer, level - - -def level_width(level): - total_levels = 8 - # Remove assert to be more efficient? - assert 0 <= level < total_levels - return 2 ** (total_levels - level) - - -def _print_example(pointer, level): - - ind = get_index(pointer, level) - pnt, lvl = get_pointer(ind) - assert (pointer == pnt) & (level == lvl) - - def print_binary(num, frm): - bstr = "{0:b}".format(num).rjust(max_bits, "0") - print("".join([frm(b) for b in bstr]) + " = " + str(num)) - - print("{0:b}".format(level).rjust(level_bits, "0").rjust(total_bits, " ") + " = " + str(level)) - print_binary(pointer[0], lambda b: " " + b + "") - print_binary(pointer[1], lambda b: " " + b + " ") - print_binary(pointer[2], lambda b: "" + b + " ") - print("{0:b}".format(ind).rjust(total_bits, "0") + " = " + str(ind)) - - return ind diff --git a/notebooks/zordercurve.png b/notebooks/zordercurve.png deleted file mode 100644 index b0b110a41407db204049b36416357543ef17bd60..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7531 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 blocks): - raise ValueError("ijk must be less than parent_block_count in each dimension") - index = np.ravel_multi_index( - multi_index=ijk_array.T, - dims=blocks, - order="F", - ) - return index - - def index_to_ijk(self, index): - """Return ijk triple for single index""" - return self.indices_to_ijk_array([index])[0] - - def indices_to_ijk_array(self, indices): - """Return an array of ijk triples for an array of indices""" - blocks = self.parent_block_count - if not blocks: - raise AttributeError("parent_block_count is required to calculate ijk values") - if not isinstance(indices, (list, tuple, np.ndarray)): - raise ValueError("indices must be a list of index values") - indices = np.array(indices) - if len(indices.shape) != 1: - raise ValueError("indices must be 1D array") - if not np.array_equal(indices, indices.astype(np.uint64)): - raise ValueError("indices values must be non-negative integers") - if np.max(indices) >= np.prod(blocks): - raise ValueError("indices must be less than total number of parent blocks") - ijk = np.unravel_index( - indices=indices, - shape=blocks, - order="F", - ) - ijk_array = np.c_[ijk[0], ijk[1], ijk[2]] - return ijk_array - - -class TensorGridBlockModel(BaseBlockModel): - """Block model with variable spacing in each dimension""" - - schema = "org.omf.v2.element.blockmodel.tensorgrid" - - tensor_u = properties.Array( - "Tensor cell widths, u-direction", - shape=("*",), - dtype=float, - ) - tensor_v = properties.Array( - "Tensor cell widths, v-direction", - shape=("*",), - dtype=float, - ) - tensor_w = properties.Array( - "Tensor cell widths, w-direction", - shape=("*",), - dtype=float, - ) - - _valid_locations = ("vertices", "cells", "parent_blocks") - - def location_length(self, location): - """Return correct attribute length based on location""" - if location == "vertices": - return self.num_nodes - return self.num_cells - - def _tensors_defined(self): - """Check if all tensors are defined""" - tensors = [self.tensor_u, self.tensor_v, self.tensor_w] - return all((tensor is not None for tensor in tensors)) - - @property - def num_nodes(self): - """Number of nodes (vertices)""" - if not self._tensors_defined(): - return None - nodes = (len(self.tensor_u) + 1) * (len(self.tensor_v) + 1) * (len(self.tensor_w) + 1) - return nodes - - @property - def num_cells(self): - """Number of cells""" - if not self._tensors_defined(): - return None - cells = len(self.tensor_u) * len(self.tensor_v) * len(self.tensor_w) - return cells - - @property - def parent_block_count(self): - """Number of parent blocks equals number of blocks""" - if not self._tensors_defined(): - return None - blocks = [len(self.tensor_u), len(self.tensor_v), len(self.tensor_w)] - return blocks - - -class RegularBlockModel(BaseBlockModel): - """Block model with constant spacing in each dimension""" - - schema = "org.omf.v2.elements.blockmodel.regular" - - block_count = properties.List( - "Number of blocks along u, v, and w axes", - properties.Integer("", min=1), - min_length=3, - max_length=3, - ) - block_size = properties.List( - "Size of blocks in the u, v, and w dimensions", - properties.Float("", min=0), - min_length=3, - max_length=3, - ) - cbc = ArrayInstanceProperty( - "Compressed block count - for regular block models this must " - "have length equal to the product of block_count and all values " - "must be 1 (if attributes exist on the block) or 0; the default " - "is an array of 1s", - shape=("*",), - dtype=(int, bool), - ) - - _valid_locations = ("cells", "parent_blocks") - - @properties.Array( - "Compressed block index - used for indexing attributes " - "into the block model; must have length equal to the " - "product of block_count plus 1 and monotonically increasing", - shape=("*",), - dtype=int, - coerce=False, - ) - def cbi(self): - """Compressed block index""" - if self.cbc is None: - return None - # Recalculating the sum on the fly is faster than checking md5 - cbi = np.concatenate( - [ - np.array([0], dtype=np.uint32), - np.cumsum(self.cbc, dtype=np.uint32), - ] - ) - return cbi - - @properties.validator("block_size") - def _validate_size_is_not_zero(self, change): - """Ensure block sizes are non-zero""" - if 0 in change["value"]: - raise properties.ValidationError( - "Block size cannot be 0", - prop="block_size", - instance=self, - reason="invalid", - ) - - @properties.validator("cbc") - def validate_cbc(self, change): - """Ensure cbc is correct size and values""" - value = change["value"] - if self.block_count and len(value.array) != np.prod(self.block_count): - raise properties.ValidationError( - "cbc must have length equal to the product of block_count", - prop="cbc", - instance=self, - reason="invalid", - ) - if np.max(value.array) > 1 or np.min(value.array) < 0: - raise properties.ValidationError( - "cbc must have only values 0 or 1", - prop="cbc", - instance=self, - reason="invalid", - ) - - @property - def num_cells(self): - """Number of cells from last value in the compressed block index""" - cbi = self.cbi - if cbi is None: - return None - return cbi[-1] # pylint: disable=E1136 - - def location_length(self, location): - """Return correct attribute length based on location""" - return self.num_cells - - @property - def parent_block_count(self): - """Number of parent blocks equals number of blocks""" - return self.block_count - - def reset_cbc(self): - """Reset cbc to no sub-blocks""" - if not self.block_count: - raise ValueError("cannot reset cbc until block_count is set") - cbc_len = np.prod(self.block_count) - self.cbc = np.ones(cbc_len, dtype=bool) - - -class RegularSubBlockModel(BaseBlockModel): - """Block model with one level of sub-blocking possible in each parent block""" - - schema = "org.omf.v2.elements.blockmodel.sub" - - parent_block_count = properties.List( - "Number of parent blocks along u, v, and w axes", - properties.Integer("", min=1), - min_length=3, - max_length=3, - ) - sub_block_count = properties.List( - "Number of sub blocks in each parent block, along u, v, and w axes", - properties.Integer("", min=1), - min_length=3, - max_length=3, - ) - parent_block_size = properties.List( - "Size of parent blocks in the u, v, and w dimensions", - properties.Float("", min=0), - min_length=3, - max_length=3, - ) - cbc = ArrayInstanceProperty( - "Compressed block count - for regular sub block models this must " - "have length equal to the product of parent_block_count and all " - "values must be the product of sub_block_count (if attributes " - "exist on the sub blocks), 1 (if attributes exist on the parent " - "block) or 0; the default is an array of 1s", - shape=("*",), - dtype=(int, bool), - ) - - _valid_locations = ("parent_blocks", "sub_blocks") - - @properties.Array( - "Compressed block index - used for indexing attributes " - "into the sub block model; must have length equal to the " - "product of parent_block_count plus 1 and monotonically increasing", - shape=("*",), - dtype=int, - coerce=False, - ) - def cbi(self): - """Compressed block index""" - if self.cbc is None: - return None - cbi = np.concatenate( - [ - np.array([0], dtype=np.uint64), - np.cumsum(self.cbc, dtype=np.uint64), - ] - ) - return cbi - - @properties.List( - "Size of sub blocks in the u, v, and w dimensions", - properties.Float("", min=0), - min_length=3, - max_length=3, - ) - def sub_block_size(self): - """Computed sub block size""" - if not self.sub_block_count or not self.parent_block_size: - return None - return self.parent_block_size / np.array(self.sub_block_count) - - @properties.validator("parent_block_size") - def _validate_size_is_not_zero(self, change): - """Ensure block sizes are non-zero""" - if 0 in change["value"]: - raise properties.ValidationError( - "Block size cannot be 0", - prop="parent_block_size", - instance=self, - reason="invalid", - ) - - @properties.validator("cbc") - def validate_cbc(self, change): - """Ensure cbc is correct size and values""" - value = change["value"] - if not self.parent_block_count: - pass - elif len(value.array) != np.prod(self.parent_block_count): - raise properties.ValidationError( - "cbc must have length equal to the product of parent_block_count", - prop="cbc", - instance=self, - reason="invalid", - ) - if not self.sub_block_count: - pass - elif np.any((value.array != 1) & (value.array != 0) & (value.array != np.prod(self.sub_block_count))): - raise properties.ValidationError( - "cbc must have only values of prod(sub_block_count), 1, or 0", - prop="cbc", - instance=self, - reason="invalid", - ) - - @property - def num_cells(self): - """Number of cells from last value in the compressed block index""" - cbi = self.cbi - if cbi is None: - return None - return cbi[-1] # pylint: disable=E1136 - - def location_length(self, location): - """Return correct attribute length based on location""" - if location == "parent_blocks": - return np.sum(self.cbc.array.astype(bool)) - return self.num_cells - - def reset_cbc(self): - """Reset cbc to no sub-blocks""" - if not self.parent_block_count: - raise ValueError("cannot reset cbc until parent_block_count is set") - cbc_len = np.prod(self.parent_block_count) - self.cbc = np.ones(cbc_len, dtype=np.uint32) - - def refine(self, ijk): - """Refine parent blocks at a single ijk or a list of multiple ijks""" - if self.cbc is None or not self.sub_block_count: - raise ValueError("Cannot refine sub block model without specifying number of parent and sub blocks") - try: - inds = self.ijk_array_to_indices(ijk) - except ValueError: - inds = self.ijk_to_index(ijk) - self.cbc.array[inds] = np.prod(self.sub_block_count) # pylint: disable=E1137 - - -class OctreeSubBlockModel(BaseBlockModel): - """Block model where sub-blocks follow an octree pattern in each parent""" - - schema = "org.omf.v2.elements.blockmodel.octree" - - max_level = 8 # Maximum times blocks can be subdivided - level_bits = 4 # Enough for 0 to 8 refinements - - parent_block_count = properties.List( - "Number of parent blocks along u, v, and w axes", - properties.Integer("", min=1), - min_length=3, - max_length=3, - ) - parent_block_size = properties.List( - "Size of parent blocks in the u, v, and w dimensions", - properties.Float("", min=0), - min_length=3, - max_length=3, - ) - cbc = ArrayInstanceProperty( - "Compressed block count - for octree sub block models this must " - "have length equal to the product of parent_block_count and each " - "value must be equal to the number of octree sub blocks within " - "the corresponding parent block (since max level is 8 in each " - "dimension, the max number of sub blocks in a parent is (2^8)^3), " - "1 (if parent block is not subdivided) or 0 (if parent block is " - "unused); the default is an array of 1s", - shape=("*",), - dtype=(int, bool), - ) - zoc = ArrayInstanceProperty( - "Z-order curves - sub block location pointer and level, encoded as bits", - shape=("*",), - dtype=int, - ) - - _valid_locations = ("parent_blocks", "sub_blocks") - - @properties.Array( - "Compressed block index - used for indexing attributes " - "into the sub block model; must have length equal to the " - "product of parent_block_count plus 1 and monotonically increasing", - shape=("*",), - dtype=int, - coerce=False, - ) - def cbi(self): - """Compressed block index""" - if self.cbc is None: - return None - cbi = np.concatenate( - [ - np.array([0], dtype=np.uint64), - np.cumsum(self.cbc, dtype=np.uint64), - ] - ) - return cbi - - @properties.validator("cbc") - def validate_cbc(self, change): - """Ensure cbc is correct size and values""" - value = change["value"] - if not self.parent_block_count: - pass - elif len(value.array) != np.prod(self.parent_block_count): - raise properties.ValidationError( - "cbc must have length equal to the product of parent_block_count", - prop="cbc", - instance=self, - reason="invalid", - ) - if np.max(value.array) > 8**8 or np.min(value.array) < 0: - raise properties.ValidationError( - "cbc must have values between 0 and 8^8", - prop="cbc", - instance=self, - reason="invalid", - ) - - @properties.validator("zoc") - def validate_zoc(self, change): - """Ensure Z-order curve array is correct length and valid values""" - value = change["value"] - cbi = self.cbi - if cbi is None: - pass - elif len(value.array) != cbi[-1]: - raise properties.ValidationError( - "zoc must have length equal to maximum compressed block index value", - prop="zoc", - instance=self, - reason="invalid", - ) - max_curve_value = 268435448 # -> 0b1111111111111111111111111000 - if np.max(value.array) > max_curve_value or np.min(value.array) < 0: - raise properties.ValidationError( - "zoc must have values between 0 and 8^8", - prop="cbc", - instance=self, - reason="invalid", - ) - - @property - def num_cells(self): - """Number of cells from last value in the compressed block index""" - cbi = self.cbi - if cbi is None: - return None - return cbi[-1] # pylint: disable=E1136 - - def location_length(self, location): - """Return correct attribute length based on location""" - if location == "parent_blocks": - return np.sum(self.cbc.array.astype(bool)) - return self.num_cells - - def reset_cbc(self): - """Reset cbc to no sub-blocks""" - if not self.parent_block_count: - raise ValueError("cannot reset cbc until parent_block_count is set") - cbc_len = np.prod(self.parent_block_count) - self.cbc = np.ones(cbc_len, dtype=np.uint32) - - def reset_zoc(self): - """Reset zoc to no sub-blocks""" - if not self.parent_block_count: - raise ValueError("cannot reset zoc until parent_block_count is set") - zoc_len = np.prod(self.parent_block_count) - self.zoc = np.zeros(zoc_len, dtype=np.int32) - - @staticmethod - def bitrange(index, width, start, end): - """Extract a bit range as an integer - - [start, end) is inclusive lower bound, exclusive upper bound. - """ - return index >> (width - end) & ((2 ** (end - start)) - 1) - - @classmethod - def get_curve_value(cls, pointer, level): - """Get Z-order curve value from pointer and level - - Values range from 0 (pointer=[0, 0, 0], level=0) to - 268435448 (pointer=[255, 255, 255], level=8). - """ - idx = 0 - iwidth = cls.max_level * 3 - for i in range(iwidth): - bitoff = cls.max_level - (i // 3) - 1 - poff = 3 - (i % 3) - 1 - bitrange = ( - cls.bitrange( - index=pointer[3 - 1 - poff], - width=cls.max_level, - start=bitoff, - end=bitoff + 1, - ) - << i - ) - idx |= bitrange - return (idx << cls.level_bits) + level - - @classmethod - def get_pointer(cls, curve_value): - """Get pointer value from Z-order curve value - - Pointer values are length-3 with values between 0 and 255 - """ - index = curve_value >> cls.level_bits - pointer = [0] * 3 - iwidth = cls.max_level * 3 - for i in range(iwidth): - bitrange = ( - cls.bitrange( - index=index, - width=iwidth, - start=i, - end=i + 1, - ) - << (iwidth - i - 1) // 3 - ) - pointer[i % 3] |= bitrange - pointer.reverse() - return pointer - - @classmethod - def get_level(cls, curve_value): - """Get level value from Z-order curve value - - Level comes from the last 4 bits, with values between 0 and 8 - """ - return curve_value & (2**cls.level_bits - 1) - - @classmethod - def level_width(cls, level): - """Width of a level, in bits - - Max level of 8 has level width of 1; min level of 0 has level - width of 256. - """ - if not 0 <= level <= cls.max_level: - raise ValueError("level must be between 0 and {}".format(cls.max_level)) - return 2 ** (cls.max_level - level) - - def refine(self, index, ijk=None, refinements=1): - """Subdivide at the given index - - .. note:: - This method is for demonstration only. - It is impractical and not intended to build an octree blockmodel - using this method alone. - - If ijk is provided, index is relative to ijk parent block. - Otherwise, index is relative to the entire block model. - - By default, blocks are refined a single level, from 1 sub-block - to 8 sub-blocks. However, a greater number of refinements may be - specified, where the final number of sub-blocks equals - (2**refinements)**3. - """ - cbi = self.cbi - if ijk is not None: - index += int(cbi[self.ijk_to_index(ijk)]) - parent_index = np.sum(index >= cbi) - 1 # pylint: disable=W0143 - if not 0 <= index < len(self.zoc): - raise ValueError("index must be between 0 and {}".format(len(self.zoc))) - - curve_value = self.zoc[index] - level = self.get_level(curve_value) - if not 0 <= refinements <= self.max_level - level: - raise ValueError("refinements must be between 0 and {}".format(self.max_level - level)) - new_width = self.level_width(level + refinements) - - new_pointers = np.indices([2**refinements] * 3) - new_pointers = new_pointers.reshape(3, (2**refinements) ** 3).T - new_pointers = new_pointers * new_width - - pointer = self.get_pointer(curve_value) - new_pointers = new_pointers + pointer - - new_curve_values = sorted([self.get_curve_value(pointer, level + refinements) for pointer in new_pointers]) - - self.cbc.array[parent_index] += len(new_curve_values) - 1 - self.zoc = np.concatenate( - [ - self.zoc[:index], - new_curve_values, - self.zoc[index + 1 :], - ] - ) - - -class ArbitrarySubBlockModel(BaseBlockModel): - """Block model with arbitrary, variable sub-blocks""" - - schema = "org.omf.v2.elements.blockmodel.arbitrary" - - parent_block_count = properties.List( - "Number of parent blocks along u, v, and w axes", - properties.Integer("", min=1), - min_length=3, - max_length=3, - ) - parent_block_size = properties.List( - "Size of parent blocks in the u, v, and w dimensions", - properties.Float("", min=0), - min_length=3, - max_length=3, - ) - cbc = ArrayInstanceProperty( - "Compressed block count - for arbitrary sub block models this must " - "have length equal to the product of parent_block_count and each " - "value must be equal to the number of sub blocks within the " - "corresponding parent block, 1 (if attributes exist on the parent " - "block) or 0; the default is an array of 1s", - shape=("*",), - dtype=(int, bool), - ) - sub_block_corners = ArrayInstanceProperty( - "Block corners normalized 0-1 relative to parent block", - shape=("*", 3), - dtype=float, - ) - sub_block_sizes = ArrayInstanceProperty( - "Block widths normalized 0-1 relative to parent block", - shape=("*", 3), - dtype=float, - ) - - _valid_locations = ("parent_blocks", "sub_blocks") - - @properties.Array( - "Compressed block index - used for indexing attributes " - "into the sub block model; must have length equal to the " - "product of parent_block_count plus 1 and monotonically increasing", - shape=("*",), - dtype=int, - coerce=False, - ) - def cbi(self): - """Compressed block index""" - if self.cbc is None: - return None - cbi = np.r_[ - np.array([0], dtype=np.uint64), - np.cumsum(self.cbc, dtype=np.uint64), - ] - return cbi - - @properties.Array( - "Block centroids normalized 0-1 relative to parent block", - shape=("*", 3), - dtype=float, - coerce=False, - ) - def sub_block_centroids(self): - """Block centroids normalized 0-1 relative to parent block - - Computed from sub_block_corners and sub_block_sizes - """ - if self.sub_block_corners is None or self.sub_block_sizes is None: - return None - return self.sub_block_corners.array + self.sub_block_sizes.array / 2 - - @properties.Array( - "Block corners relative to parent block", - shape=("*", 3), - dtype=float, - coerce=False, - ) - def sub_block_corners_absolute(self): - """Block corners relative to parent block - - Computed from sub_block_corners and sub_block_sizes - """ - if self.sub_block_corners is None or self.parent_block_size is None: - return None - cbc = self.cbc - all_indices = np.array(range(len(cbc)), dtype=np.uint64) - unique_parent_ijks = self.indices_to_ijk_array(all_indices) - parent_ijks = np.repeat(unique_parent_ijks, cbc, axis=0) - corners = parent_ijks + self.sub_block_corners - return corners * self.parent_block_size - - @properties.Array( - "Block centroids relative to parent block", - shape=("*", 3), - dtype=float, - coerce=False, - ) - def sub_block_centroids_absolute(self): - """Block centroids relative to parent block - - Computed from sub_block_corners and sub_block_sizes - """ - if self.sub_block_centroids is None or self.parent_block_size is None: - return None - cbc = self.cbc - all_indices = np.array(range(len(cbc)), dtype=np.uint64) - unique_parent_ijks = self.indices_to_ijk_array(all_indices) - parent_ijks = np.repeat(unique_parent_ijks, cbc, axis=0) - centroids = parent_ijks + self.sub_block_centroids - return centroids * self.parent_block_size - - @properties.Array( - "Block widths relative to parent block", - shape=("*", 3), - dtype=float, - coerce=False, - ) - def sub_block_sizes_absolute(self): - """Block widths relative to parent block - - Computed from sub_block_corners and sub_block_sizes - """ - if self.sub_block_sizes is None or self.parent_block_size is None: - return None - return self.sub_block_sizes.array * self.parent_block_size - - @properties.validator("parent_block_size") - def _validate_size_is_not_zero(self, change): - """Ensure parent blocks are non-zero""" - if 0 in change["value"]: - raise properties.ValidationError( - "Block size cannot be 0", - prop="parent_block_size", - instance=self, - reason="invalid", - ) - - @properties.validator("cbc") - def validate_cbc(self, change): - """Ensure cbc is correct size and values""" - value = change["value"] - if not self.parent_block_count: - pass - elif len(value.array) != np.prod(self.parent_block_count): - raise properties.ValidationError( - "cbc must have length equal to the product of parent_block_count", - prop="cbc", - instance=self, - reason="invalid", - ) - if np.min(value.array) < 0: - raise properties.ValidationError( - "cbc values must be non-negative", - prop="cbc", - instance=self, - reason="invalid", - ) - return value - - def validate_sub_block_attributes(self, value, prop_name): - """Ensure value is correct length""" - cbi = self.cbi - if cbi is None: - return value - if len(value) != cbi[-1]: - raise properties.ValidationError( - "{} attributes must have length equal to " "total number of sub blocks".format(prop_name), - prop=prop_name, - instance=self, - reason="invalid", - ) - return value - - @properties.validator("sub_block_corners") - def _validate_sub_block_corners(self, change): - """Validate sub block corners array is correct length""" - change["value"] = self.validate_sub_block_attributes(change["value"], "sub_block_corners") - - @properties.validator("sub_block_sizes") - def _validate_sub_block_sizes(self, change): - """Validate sub block size array is correct length and positive""" - value = self.validate_sub_block_attributes(change["value"], "sub_block_sizes") - if np.min(value.array) <= 0: - raise properties.ValidationError( - "sub block sizes must be positive", - prop="sub_block_sizes", - instance=self, - reason="invalid", - ) - - @property - def num_cells(self): - """Number of cells from last value in the compressed block index""" - cbi = self.cbi - if cbi is None: - return None - return cbi[-1] # pylint: disable=E1136 - - def location_length(self, location): - """Return correct attribute length based on location""" - if location == "parent_blocks": - return np.sum(self.cbc.array.astype(bool)) - return self.num_cells - - def reset_cbc(self): - """Reset cbc to no sub-blocks""" - if not self.parent_block_count: - raise ValueError("cannot reset cbc until parent_block_count is set") - cbc_len = np.prod(self.parent_block_count) - self.cbc = np.ones(cbc_len, dtype=np.uint32) From b65d863c41a35ed90990f493d856d13eec26ea68 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Fri, 3 Mar 2023 15:57:45 +1300 Subject: [PATCH 27/42] Code review fixes. --- omf/blockmodel/definition.py | 5 ++--- omf/blockmodel/models.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/omf/blockmodel/definition.py b/omf/blockmodel/definition.py index c40049fe..747d208e 100644 --- a/omf/blockmodel/definition.py +++ b/omf/blockmodel/definition.py @@ -24,7 +24,7 @@ def _validate_axes(self): raise ValueError("axis_u, axis_v, and axis_w must be orthogonal") def ijk_to_index(self, ijk): - """Map IJK triples to flat indices for a singoe triple or an array, preseving shape.""" + """Map IJK triples to flat indices for a single triple or an array, preserving shape.""" if self.block_count is None: raise ValueError("block_count is not set") arr = np.asarray(ijk) @@ -41,7 +41,7 @@ def ijk_to_index(self, ijk): return indices[0] if output_shape == () else indices.reshape(output_shape) def index_to_ijk(self, index): - """Map flat indices to IJK triples for a singoe index or an array, preserving shape.""" + """Map flat indices to IJK triples for a single index or an array, preserving shape.""" if self.block_count is None: raise ValueError("block_count is not set") arr = np.asarray(index) @@ -69,7 +69,6 @@ class RegularBlockModelDefinition(_BaseBlockModelDefinition): @properties.validator("block_count") def _validate_block_count(self, change): - print(">>>", change) for item in change["value"]: if item < 1: raise properties.ValidationError("block counts must be >= 1", prop=change["name"], instance=self) diff --git a/omf/blockmodel/models.py b/omf/blockmodel/models.py index a70ea8d1..a21c9742 100644 --- a/omf/blockmodel/models.py +++ b/omf/blockmodel/models.py @@ -133,7 +133,7 @@ def _validate_subblocks(self): class FreeformSubblockedModel(ProjectElement): - """A regular block model with sub-blocks can be anywhere within the parent.""" + """A regular block model where sub-blocks can be anywhere within the parent.""" schema = "org.omf.v2.elements.blockmodel.freeform_subblocked" _valid_locations = ("cells", "parent_blocks") From 4d276719a7f64dab6a3a5ffa511c4113e0e1890e Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Tue, 14 Mar 2023 12:50:31 +1300 Subject: [PATCH 28/42] Code review changes and some tidying up. --- omf/blockmodel/_subblock_check.py | 2 +- omf/blockmodel/definition.py | 26 +++++++++++--- omf/blockmodel/models.py | 58 ++++++++++++++++++------------- tests/test_subblockedmodel.py | 2 +- 4 files changed, 56 insertions(+), 32 deletions(-) diff --git a/omf/blockmodel/_subblock_check.py b/omf/blockmodel/_subblock_check.py index 24974426..770650a2 100644 --- a/omf/blockmodel/_subblock_check.py +++ b/omf/blockmodel/_subblock_check.py @@ -30,7 +30,7 @@ def _check_parent_indices(definition, parent_indices, instance): def _check_inside_parent(subblock_definition, corners, instance): - if isinstance(subblock_definition, RegularSubblockDefinition): + if subblock_definition.regular: upper = subblock_definition.subblock_count upper_str = f"({upper[0]}, {upper[1]}, {upper[2]})" else: diff --git a/omf/blockmodel/definition.py b/omf/blockmodel/definition.py index 747d208e..d0395f32 100644 --- a/omf/blockmodel/definition.py +++ b/omf/blockmodel/definition.py @@ -103,10 +103,10 @@ def _tensors(self): @property def block_count(self): """The block count is derived from the tensors here.""" - count = tuple(None if t is None else len(t) for t in self._tensors()) - if None in count: + counts = tuple(None if t is None else len(t) for t in self._tensors()) + if None in counts: return None - return np.array(count, dtype=int) + return np.array(counts, dtype=int) class RegularSubblockDefinition(properties.HasProperties): @@ -118,6 +118,10 @@ class RegularSubblockDefinition(properties.HasProperties): "The maximum number of sub-blocks inside a parent in each direction.", dtype=int, shape=(3,) ) + @property + def regular(self): + return True + @properties.validator("subblock_count") def _validate_subblock_count(self, change): for item in change["value"]: @@ -125,7 +129,7 @@ def _validate_subblock_count(self, change): raise properties.ValidationError("sub-block counts must be >= 1", prop=change["name"], instance=self) -class OctreeSubblockDefinition(RegularSubblockDefinition): +class OctreeSubblockDefinition(properties.HasProperties): """Sub-blocks form an octree inside the parent block. Cut the parent block in half in all directions to create eight sub-blocks. Repeat that @@ -143,6 +147,10 @@ class OctreeSubblockDefinition(RegularSubblockDefinition): "The maximum number of sub-blocks inside a parent in each direction.", dtype=int, shape=(3,) ) + @property + def regular(self): + return True + @properties.validator("subblock_count") def _validate_subblock_count(self, change): for item in change["value"]: @@ -163,8 +171,12 @@ class FreeformSubblockDefinition(properties.HasProperties): schema = "org.omf.v2.subblockdefinition.freeform" + @property + def regular(self): + return False + -class VariableHeightSubblockDefinition(FreeformSubblockDefinition): +class VariableHeightSubblockDefinition(properties.HasProperties): """Defines sub-blocks on a grid in the U and V directions but variable in the W direction. A single sub-block covering the whole parent block is also valid. Sub-blocks should not @@ -178,3 +190,7 @@ class VariableHeightSubblockDefinition(FreeformSubblockDefinition): subblock_count_u = properties.Integer("Number of sub-blocks in the u-direction", min=1, max=65535) subblock_count_v = properties.Integer("Number of sub-blocks in the v-direction", min=1, max=65535) minimum_size_w = properties.Float("Minimum size of sub-blocks in the z-direction", min=0.0) + + @property + def regular(self): + return False diff --git a/omf/blockmodel/models.py b/omf/blockmodel/models.py index a21c9742..533bd6b8 100644 --- a/omf/blockmodel/models.py +++ b/omf/blockmodel/models.py @@ -2,21 +2,23 @@ import numpy as np import properties +from ..attribute import Array, ArrayInstanceProperty from ..base import ProjectElement from .definition import ( FreeformSubblockDefinition, + OctreeSubblockDefinition, RegularBlockModelDefinition, RegularSubblockDefinition, TensorBlockModelDefinition, + VariableHeightSubblockDefinition, ) from ._subblock_check import check_subblocks def _shrink_uint(arr): - assert arr.dtype.kind in "ui" - if arr.min() < 0: - return arr - return arr.astype(np.min_scalar_type(arr.max())) + assert arr.array.dtype.kind in "ui" + if arr.array.min() >= 0: + arr.array = arr.array.astype(np.min_scalar_type(arr.array.max())) class RegularBlockModel(ProjectElement): @@ -80,12 +82,20 @@ class SubblockedModel(ProjectElement): RegularBlockModelDefinition, default=RegularBlockModelDefinition, ) - subblock_parent_indices = properties.Array( + subblock_definition = properties.Union( + "Defines the structure of sub-blocks within each parent block.", + props=[ + properties.Instance("", RegularSubblockDefinition), + properties.Instance("", OctreeSubblockDefinition), + ], + default=RegularSubblockDefinition, + ) + subblock_parent_indices = ArrayInstanceProperty( "The parent block IJK index of each sub-block", shape=("*", 3), dtype=int, ) - subblock_corners = properties.Array( + subblock_corners = ArrayInstanceProperty( """The positions of the sub-block corners on the grid within their parent block. The columns are (min_i, min_j, min_k, max_i, max_j, max_k). Values must be @@ -99,11 +109,6 @@ class SubblockedModel(ProjectElement): shape=("*", 6), dtype=int, ) - subblock_definition = properties.Instance( - "Defines the structure of sub-blocks within each parent block.", - RegularSubblockDefinition, - default=RegularSubblockDefinition, - ) @property def num_cells(self): @@ -121,13 +126,13 @@ def location_length(self, location): @properties.validator def _validate_subblocks(self): - self.subblock_parent_indices = _shrink_uint(self.subblock_parent_indices) - self.subblock_corners = _shrink_uint(self.subblock_corners) + _shrink_uint(self.subblock_parent_indices) + _shrink_uint(self.subblock_corners) check_subblocks( self.definition, self.subblock_definition, - self.subblock_parent_indices, - self.subblock_corners, + self.subblock_parent_indices.array, + self.subblock_corners.array, instance=self, ) @@ -143,12 +148,20 @@ class FreeformSubblockedModel(ProjectElement): RegularBlockModelDefinition, default=RegularBlockModelDefinition, ) - subblock_parent_indices = properties.Array( + subblock_definition = properties.Union( + "Defines the structure of sub-blocks within each parent block.", + props=[ + properties.Instance("", FreeformSubblockDefinition), + properties.Instance("", VariableHeightSubblockDefinition), + ], + default=FreeformSubblockDefinition, + ) + subblock_parent_indices = ArrayInstanceProperty( "The parent block IJK index of each sub-block", shape=("*", 3), dtype=int, ) - subblock_corners = properties.Array( + subblock_corners = ArrayInstanceProperty( """The positions of the sub-block corners on the grid within their parent block. The columns are (min_i, min_j, min_k, max_i, max_j, max_k). Values must be @@ -161,11 +174,6 @@ class FreeformSubblockedModel(ProjectElement): shape=("*", 6), dtype=float, ) - subblock_definition = properties.Instance( - "Defines the structure of sub-blocks within each parent block.", - FreeformSubblockDefinition, - default=FreeformSubblockDefinition, - ) @property def num_cells(self): @@ -183,11 +191,11 @@ def location_length(self, location): @properties.validator def _validate_subblocks(self): - self.subblock_parent_indices = _shrink_uint(self.subblock_parent_indices) + _shrink_uint(self.subblock_parent_indices) check_subblocks( self.definition, self.subblock_definition, - self.subblock_parent_indices, - self.subblock_corners, + self.subblock_parent_indices.array, + self.subblock_corners.array, instance=self, ) diff --git a/tests/test_subblockedmodel.py b/tests/test_subblockedmodel.py index fd64cbb4..53e66782 100644 --- a/tests/test_subblockedmodel.py +++ b/tests/test_subblockedmodel.py @@ -114,7 +114,7 @@ def test_pack_subblock_arrays(): block_model.subblock_corners = np.array([(0, 0, 0, 2, 2, 2)], dtype=int) block_model.validate() # Arrays were set as int, validate should have packed it down to uint8. - assert block_model.subblock_corners.dtype == np.uint8 + assert block_model.subblock_corners.array.dtype == np.uint8 def test_uninstantiated(): From 93bd53b376d8b128bfaf597b25eece1f56eefa38 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Tue, 14 Mar 2023 13:05:03 +1300 Subject: [PATCH 29/42] Fixed unions and improved docs a little. --- omf/blockmodel/definition.py | 7 ++++++- omf/blockmodel/models.py | 10 ++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/omf/blockmodel/definition.py b/omf/blockmodel/definition.py index d0395f32..104d7d4b 100644 --- a/omf/blockmodel/definition.py +++ b/omf/blockmodel/definition.py @@ -110,7 +110,12 @@ def block_count(self): class RegularSubblockDefinition(properties.HasProperties): - """The simplest gridded sub-block definition.""" + """The simplest gridded sub-block definition. + + Divide the parent block into a regular grid of `subblock_count` cells. Each block covers + a cuboid region within that grid. If a parent block is not sub-blocked then it will still + contain a single block that covers the entire grid. + """ schema = "org.omf.v2.subblockdefinition.regular" diff --git a/omf/blockmodel/models.py b/omf/blockmodel/models.py index 533bd6b8..75a9d968 100644 --- a/omf/blockmodel/models.py +++ b/omf/blockmodel/models.py @@ -84,10 +84,7 @@ class SubblockedModel(ProjectElement): ) subblock_definition = properties.Union( "Defines the structure of sub-blocks within each parent block.", - props=[ - properties.Instance("", RegularSubblockDefinition), - properties.Instance("", OctreeSubblockDefinition), - ], + props=[RegularSubblockDefinition, OctreeSubblockDefinition], default=RegularSubblockDefinition, ) subblock_parent_indices = ArrayInstanceProperty( @@ -150,10 +147,7 @@ class FreeformSubblockedModel(ProjectElement): ) subblock_definition = properties.Union( "Defines the structure of sub-blocks within each parent block.", - props=[ - properties.Instance("", FreeformSubblockDefinition), - properties.Instance("", VariableHeightSubblockDefinition), - ], + props=[FreeformSubblockDefinition, VariableHeightSubblockDefinition], default=FreeformSubblockDefinition, ) subblock_parent_indices = ArrayInstanceProperty( From f7c8b5c8b06a86cea3b3710b6e2381eb50564273 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Tue, 14 Mar 2023 13:15:32 +1300 Subject: [PATCH 30/42] Added missing doc link. --- docs/content/blockmodel.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/content/blockmodel.rst b/docs/content/blockmodel.rst index 47699655..9d67e1e0 100644 --- a/docs/content/blockmodel.rst +++ b/docs/content/blockmodel.rst @@ -39,6 +39,8 @@ These classes are used to define the structure of sub-blocks within a parent blo .. autoclass:: omf.blockmodel.RegularSubblockDefinition +.. autoclass:: omf.blockmodel.OctreeSubblockDefinition + .. autoclass:: omf.blockmodel.FreeformSubblockDefinition .. autoclass:: omf.blockmodel.VariableHeightSubblockDefinition From 15c59252cd24bf3a9f2fc2154947c5ea8fb3d425 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Tue, 14 Mar 2023 13:23:04 +1300 Subject: [PATCH 31/42] Removed unnecessary methods. --- omf/blockmodel/_subblock_check.py | 14 +++++++++++--- omf/blockmodel/definition.py | 16 ---------------- omf/blockmodel/models.py | 2 +- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/omf/blockmodel/_subblock_check.py b/omf/blockmodel/_subblock_check.py index 770650a2..5ddf0927 100644 --- a/omf/blockmodel/_subblock_check.py +++ b/omf/blockmodel/_subblock_check.py @@ -2,7 +2,15 @@ import numpy as np import properties -from .definition import RegularSubblockDefinition, OctreeSubblockDefinition +from .definition import OctreeSubblockDefinition, RegularSubblockDefinition + + +def _is_regular(defn): + return isinstance(defn, (OctreeSubblockDefinition, RegularSubblockDefinition)) + + +def _is_octree(defn): + return isinstance(defn, OctreeSubblockDefinition) def _group_by(arr): @@ -30,7 +38,7 @@ def _check_parent_indices(definition, parent_indices, instance): def _check_inside_parent(subblock_definition, corners, instance): - if subblock_definition.regular: + if _is_regular(subblock_definition): upper = subblock_definition.subblock_count upper_str = f"({upper[0]}, {upper[1]}, {upper[2]})" else: @@ -103,7 +111,7 @@ def check_subblocks(definition, subblock_definition, parent_indices, corners, in ) _check_inside_parent(subblock_definition, corners, instance) _check_parent_indices(definition, parent_indices, instance) - if isinstance(subblock_definition, OctreeSubblockDefinition): + if _is_octree(subblock_definition): _check_octree(subblock_definition, corners, instance) seen = np.zeros(np.prod(definition.block_count), dtype=bool) for start, end, value in _group_by(definition.ijk_to_index(parent_indices)): diff --git a/omf/blockmodel/definition.py b/omf/blockmodel/definition.py index 104d7d4b..79fcc35c 100644 --- a/omf/blockmodel/definition.py +++ b/omf/blockmodel/definition.py @@ -123,10 +123,6 @@ class RegularSubblockDefinition(properties.HasProperties): "The maximum number of sub-blocks inside a parent in each direction.", dtype=int, shape=(3,) ) - @property - def regular(self): - return True - @properties.validator("subblock_count") def _validate_subblock_count(self, change): for item in change["value"]: @@ -152,10 +148,6 @@ class OctreeSubblockDefinition(properties.HasProperties): "The maximum number of sub-blocks inside a parent in each direction.", dtype=int, shape=(3,) ) - @property - def regular(self): - return True - @properties.validator("subblock_count") def _validate_subblock_count(self, change): for item in change["value"]: @@ -176,10 +168,6 @@ class FreeformSubblockDefinition(properties.HasProperties): schema = "org.omf.v2.subblockdefinition.freeform" - @property - def regular(self): - return False - class VariableHeightSubblockDefinition(properties.HasProperties): """Defines sub-blocks on a grid in the U and V directions but variable in the W direction. @@ -195,7 +183,3 @@ class VariableHeightSubblockDefinition(properties.HasProperties): subblock_count_u = properties.Integer("Number of sub-blocks in the u-direction", min=1, max=65535) subblock_count_v = properties.Integer("Number of sub-blocks in the v-direction", min=1, max=65535) minimum_size_w = properties.Float("Minimum size of sub-blocks in the z-direction", min=0.0) - - @property - def regular(self): - return False diff --git a/omf/blockmodel/models.py b/omf/blockmodel/models.py index 75a9d968..a3be01ea 100644 --- a/omf/blockmodel/models.py +++ b/omf/blockmodel/models.py @@ -2,7 +2,7 @@ import numpy as np import properties -from ..attribute import Array, ArrayInstanceProperty +from ..attribute import ArrayInstanceProperty from ..base import ProjectElement from .definition import ( FreeformSubblockDefinition, From e8f4aa4d9417426cb5865dfc7b5f9f17589c974a Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Tue, 14 Mar 2023 16:23:08 +1300 Subject: [PATCH 32/42] Changed to a single top-level block model element with optional sub-blocks. --- assets/v2/test_file.omf | Bin 42211 -> 42189 bytes docs/content/blockmodel.rst | 62 +++++-- docs/content/examples.rst | 2 +- omf/__init__.py | 10 +- omf/blockmodel/__init__.py | 12 +- omf/blockmodel/_subblock_check.py | 36 ++-- omf/blockmodel/freeform_subblocks.py | 78 +++++++++ omf/blockmodel/{definition.py => model.py} | 121 ++++++------- omf/blockmodel/models.py | 195 --------------------- omf/blockmodel/regular_subblocks.py | 107 +++++++++++ omf/compat/omf_v1.py | 24 ++- omf/composite.py | 6 +- tests/test_blockmodel.py | 16 +- tests/test_subblockedmodel.py | 60 ++++--- 14 files changed, 360 insertions(+), 369 deletions(-) create mode 100644 omf/blockmodel/freeform_subblocks.py rename omf/blockmodel/{definition.py => model.py} (58%) delete mode 100644 omf/blockmodel/models.py create mode 100644 omf/blockmodel/regular_subblocks.py diff --git a/assets/v2/test_file.omf b/assets/v2/test_file.omf index da038ae871dd9aa1aa9898b5aaf8359c93ae6173..4f47c3e84cd627dd059d32bc19d3c6b76648d936 100644 GIT binary patch literal 42189 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

    R*LMY342KpDRXLS7DJ7 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. For block models, -:code:`location='parent_blocks'` and :code:`location='sub_blocks'` are valid. +:code:`location='parent_blocks'`, :code:`location='vertices'`, and :code:`location='cells'` +are valid. diff --git a/docs/content/examples.rst b/docs/content/examples.rst index 76a9c8c0..6bd06f4a 100644 --- a/docs/content/examples.rst +++ b/docs/content/examples.rst @@ -141,7 +141,7 @@ bottom of page). ), ], ) - vol = omf.TensorGridBlockModel( + vol = omf.BlockModel( name="vol", definition=omf.TensorBlockModelDefinition( tensor_u=np.ones(10, dtype=float), diff --git a/omf/__init__.py b/omf/__init__.py index aab34da9..0bff6e88 100644 --- a/omf/__init__.py +++ b/omf/__init__.py @@ -11,14 +11,14 @@ ) from .base import Project from .blockmodel import ( - FreeformSubblockedModel, + BlockModel, + FreeformSubblockDefinition, + FreeformSubblocks, OctreeSubblockDefinition, - RegularBlockModel, - RegularBlockModelDefinition, RegularSubblockDefinition, - SubblockedModel, + RegularBlockModelDefinition, + RegularSubblocks, TensorBlockModelDefinition, - TensorGridBlockModel, ) from .composite import Composite from .fileio import __version__, load, save diff --git a/omf/blockmodel/__init__.py b/omf/blockmodel/__init__.py index 587110cd..27c0a5b1 100644 --- a/omf/blockmodel/__init__.py +++ b/omf/blockmodel/__init__.py @@ -1,10 +1,4 @@ """blockmodel/__init__.py: sub-package for block models.""" -from .definition import ( - FreeformSubblockDefinition, - OctreeSubblockDefinition, - RegularBlockModelDefinition, - RegularSubblockDefinition, - TensorBlockModelDefinition, - VariableHeightSubblockDefinition, -) -from .models import FreeformSubblockedModel, RegularBlockModel, SubblockedModel, TensorGridBlockModel +from .freeform_subblocks import * +from .model import * +from .regular_subblocks import * diff --git a/omf/blockmodel/_subblock_check.py b/omf/blockmodel/_subblock_check.py index 5ddf0927..10dd93d3 100644 --- a/omf/blockmodel/_subblock_check.py +++ b/omf/blockmodel/_subblock_check.py @@ -2,16 +2,6 @@ import numpy as np import properties -from .definition import OctreeSubblockDefinition, RegularSubblockDefinition - - -def _is_regular(defn): - return isinstance(defn, (OctreeSubblockDefinition, RegularSubblockDefinition)) - - -def _is_octree(defn): - return isinstance(defn, OctreeSubblockDefinition) - def _group_by(arr): if len(arr) == 0: @@ -37,8 +27,8 @@ def _check_parent_indices(definition, parent_indices, instance): ) -def _check_inside_parent(subblock_definition, corners, instance): - if _is_regular(subblock_definition): +def _check_inside_parent(subblock_definition, corners, instance, regular): + if regular: upper = subblock_definition.subblock_count upper_str = f"({upper[0]}, {upper[1]}, {upper[2]})" else: @@ -101,18 +91,20 @@ def _check_octree(subblock_definition, corners, instance): ) -def check_subblocks(definition, subblock_definition, parent_indices, corners, instance=None): +def check_subblocks(definition, subblocks, instance=None, regular=False, octree=False): """Run all checks on the given defintions and sub-blocks.""" + parent_indices = subblocks.parent_indices.array + corners = subblocks.corners.array if len(parent_indices) != len(corners): raise properties.ValidationError( "'subblock_parent_indices' and 'subblock_corners' arrays must be the same length", prop="subblock_corners", instance=instance, ) - _check_inside_parent(subblock_definition, corners, instance) + _check_inside_parent(subblocks.definition, corners, instance, regular) _check_parent_indices(definition, parent_indices, instance) - if _is_octree(subblock_definition): - _check_octree(subblock_definition, corners, instance) + if octree: + _check_octree(subblocks.definition, corners, instance) seen = np.zeros(np.prod(definition.block_count), dtype=bool) for start, end, value in _group_by(definition.ijk_to_index(parent_indices)): if seen[value]: @@ -123,4 +115,14 @@ def check_subblocks(definition, subblock_definition, parent_indices, corners, in ) seen[value] = True if end - start > 1: - _check_for_overlaps(subblock_definition, corners[start:end], instance) + _check_for_overlaps(subblocks.definition, corners[start:end], instance) + + +def shrink_uint(arr): + """Takes an ArrayInstanceProperty containing unsigned integers and shrinks it. + + The type after this call will be the smallest uint type that can represent the data. + """ + kind = arr.array.dtype.kind + if kind == "u" or (kind == "i" and arr.array.min() >= 0): + arr.array = arr.array.astype(np.min_scalar_type(arr.array.max())) diff --git a/omf/blockmodel/freeform_subblocks.py b/omf/blockmodel/freeform_subblocks.py new file mode 100644 index 00000000..3ab647f0 --- /dev/null +++ b/omf/blockmodel/freeform_subblocks.py @@ -0,0 +1,78 @@ +"""blockmodel/subblocks.py: sub-block definitions and containers.""" +import properties + +from ..attribute import ArrayInstanceProperty +from ..base import BaseModel +from ._subblock_check import check_subblocks, shrink_uint + +__all__ = ["FreeformSubblockDefinition", "FreeformSubblocks", "VariableHeightSubblockDefinition"] + + +class FreeformSubblockDefinition(BaseModel): + """Unconstrained free-form sub-block definition. + + Provides no limitations on or explanation of sub-block positions. + """ + + schema = "org.omf.v2.blockmodel.subblocks.definition.freeform" + + +class VariableHeightSubblockDefinition(BaseModel): + """Defines sub-blocks on a grid in the U and V directions but variable in the W direction. + + A single sub-block covering the whole parent block is also valid. Sub-blocks should not + overlap. + + Note: these constraints on sub-blocks are not checked during validation. + """ + + schema = "org.omf.v2.blockmodel.subblocks.definition.varheight" + + subblock_count_u = properties.Integer("Number of sub-blocks in the u-direction", min=1, max=65535) + subblock_count_v = properties.Integer("Number of sub-blocks in the v-direction", min=1, max=65535) + minimum_size_w = properties.Float("Minimum size of sub-blocks in the z-direction", min=0.0) + + +class FreeformSubblocks(BaseModel): + """Defines free-form sub-blocks for a block model. + + These sub-blocks can exist anywhere without the parent block, subject to any extra + conditions the sub-block definition imposes. + """ + + schema = "org.omf.v2.blockmodel.subblocks.freeform" + + definition = properties.Union( + "Defines the structure of sub-blocks within each parent block.", + props=[FreeformSubblockDefinition, VariableHeightSubblockDefinition], + default=FreeformSubblockDefinition, + ) + parent_indices = ArrayInstanceProperty( + "The parent block IJK index of each sub-block", + shape=("*", 3), + dtype=int, + ) + corners = ArrayInstanceProperty( + """The positions of the sub-block corners on the grid within their parent block. + + The columns are (min_i, min_j, min_k, max_i, max_j, max_k). Values must be + between 0.0 and 1.0 inclusive. + + Sub-blocks must stay within the parent block and should not overlap. Gaps are + allowed but it will be impossible for 'cell' attributes to assign values to + those areas. + """, + shape=("*", 6), + dtype=float, + ) + + def validate_subblocks(self, definition): + """Checks the sub-block data against the given block model definition.""" + shrink_uint(self.parent_indices) + shrink_uint(self.corners) + check_subblocks(definition, self, instance=self) + + @property + def num_subblocks(self): + """The total number of sub-blocks.""" + return None if self.corners is None else len(self.corners) diff --git a/omf/blockmodel/definition.py b/omf/blockmodel/model.py similarity index 58% rename from omf/blockmodel/definition.py rename to omf/blockmodel/model.py index 79fcc35c..5fb3cf8e 100644 --- a/omf/blockmodel/definition.py +++ b/omf/blockmodel/model.py @@ -1,9 +1,15 @@ -"""blockmodel/definition.py: various block model and sub-block definition structures.""" +"""blockmodel/models.py: block model elements.""" import numpy as np import properties +from ..base import BaseModel, ProjectElement +from .freeform_subblocks import FreeformSubblocks +from .regular_subblocks import RegularSubblocks -class _BaseBlockModelDefinition(properties.HasProperties): +__all__ = ["BlockModel", "RegularBlockModelDefinition", "TensorBlockModelDefinition"] + + +class _BaseBlockModelDefinition(BaseModel): 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) axis_w = properties.Vector3("Vector orientation of w-direction", default="Z", length=1) @@ -62,7 +68,7 @@ class RegularBlockModelDefinition(_BaseBlockModelDefinition): If used on a sub-blocked model then everything here applies to the parent blocks only. """ - schema = "org.omf.v2.blockmodeldefinition.regular" + schema = "org.omf.v2.blockmodel.definition.regular" block_count = properties.Array("Number of blocks in each of the u, v, and w directions.", dtype=int, shape=(3,)) block_size = properties.Vector3("Size of blocks in the u, v, and w directions.") @@ -83,7 +89,7 @@ def _validate_block_size(self, change): class TensorBlockModelDefinition(_BaseBlockModelDefinition): """Defines the block structure of a tensor grid block model.""" - schema = "org.omf.v2.blockmodeldefinition.tensor" + schema = "org.omf.v2.blockmodel.definition.tensor" tensor_u = properties.Array("Tensor cell widths, u-direction", dtype=float, shape=("*",)) tensor_v = properties.Array("Tensor cell widths, v-direction", dtype=float, shape=("*",)) @@ -109,77 +115,52 @@ def block_count(self): return np.array(counts, dtype=int) -class RegularSubblockDefinition(properties.HasProperties): - """The simplest gridded sub-block definition. - - Divide the parent block into a regular grid of `subblock_count` cells. Each block covers - a cuboid region within that grid. If a parent block is not sub-blocked then it will still - contain a single block that covers the entire grid. - """ +class BlockModel(ProjectElement): + """A block model, details are in the definition and sub-blocks attributes.""" - schema = "org.omf.v2.subblockdefinition.regular" + schema = "org.omf.v2.elements.blockmodel" + _valid_locations = ("parent_blocks", "vertices", "cells") - subblock_count = properties.Array( - "The maximum number of sub-blocks inside a parent in each direction.", dtype=int, shape=(3,) + definition = properties.Union( + """Block model definition, describing either a regular or tensor-based block layout.""", + props=[RegularBlockModelDefinition, TensorBlockModelDefinition], + default=RegularBlockModelDefinition, ) - - @properties.validator("subblock_count") - def _validate_subblock_count(self, change): - for item in change["value"]: - if item < 1: - raise properties.ValidationError("sub-block counts must be >= 1", prop=change["name"], instance=self) - - -class OctreeSubblockDefinition(properties.HasProperties): - """Sub-blocks form an octree inside the parent block. - - Cut the parent block in half in all directions to create eight sub-blocks. Repeat that - division for some or all of those new sub-blocks. Continue doing that until the limit - on sub-block count is reached or until the sub-blocks accurately model the inputs. - - This definition also allows the lower level cuts to be omitted in one or two axes, - giving a maximum sub-block count of (16, 16, 4) for example rather than requiring - all axes to be equal. - """ - - schema = "org.omf.v2.subblockdefinition.octree" - - subblock_count = properties.Array( - "The maximum number of sub-blocks inside a parent in each direction.", dtype=int, shape=(3,) + subblocks = properties.Union( + """Optional sub-block details. + + If this is `None` then there are no sub-blocks. Otherwise it can be a `FreeformSubblocks` + or `RegularSubblocks` object to define different types of sub-blocks. + """, + props=[FreeformSubblocks, RegularSubblocks], + required=False, ) - @properties.validator("subblock_count") - def _validate_subblock_count(self, change): - for item in change["value"]: - if item < 1: - raise properties.ValidationError("sub-block counts must be >= 1", prop=change["name"], instance=self) - log = np.log2(item) - if np.trunc(log) != log: - raise properties.ValidationError( - "octree sub-block counts must be powers of two", prop=change["name"], instance=self - ) - - -class FreeformSubblockDefinition(properties.HasProperties): - """Unconstrained free-form sub-block definition. - - Provides no limitations on or explanation of sub-block positions. - """ - - schema = "org.omf.v2.subblockdefinition.freeform" - - -class VariableHeightSubblockDefinition(properties.HasProperties): - """Defines sub-blocks on a grid in the U and V directions but variable in the W direction. - - A single sub-block covering the whole parent block is also valid. Sub-blocks should not - overlap. + @properties.validator + def _validate(self): + if self.subblocks is not None: + self.subblocks.validate_subblocks(self.definition) - Note: these constraints on sub-blocks are not checked during validation. - """ + @property + def num_parent_blocks(self): + """The number of cells.""" + return np.prod(self.definition.block_count) - schema = "org.omf.v2.subblockdefinition.variableheight" + @property + def num_parent_vertices(self): + """Number of nodes or vertices.""" + count = self.definition.block_count + return None if count is None else np.prod(count + 1) - subblock_count_u = properties.Integer("Number of sub-blocks in the u-direction", min=1, max=65535) - subblock_count_v = properties.Integer("Number of sub-blocks in the v-direction", min=1, max=65535) - minimum_size_w = properties.Float("Minimum size of sub-blocks in the z-direction", min=0.0) + @property + def num_cells(self): + """The number of cells.""" + return self.num_parent_blocks if self.subblocks is None else self.subblocks.num_subblocks + + def location_length(self, location): + """Return correct attribute length for 'location'.""" + if location == "vertices": + return self.num_parent_vertices + if location == "cells" and self.subblocks is not None: + return self.subblocks.num_subblocks + return self.num_parent_blocks diff --git a/omf/blockmodel/models.py b/omf/blockmodel/models.py deleted file mode 100644 index a3be01ea..00000000 --- a/omf/blockmodel/models.py +++ /dev/null @@ -1,195 +0,0 @@ -"""blockmodel/models.py: block model elements.""" -import numpy as np -import properties - -from ..attribute import ArrayInstanceProperty -from ..base import ProjectElement -from .definition import ( - FreeformSubblockDefinition, - OctreeSubblockDefinition, - RegularBlockModelDefinition, - RegularSubblockDefinition, - TensorBlockModelDefinition, - VariableHeightSubblockDefinition, -) -from ._subblock_check import check_subblocks - - -def _shrink_uint(arr): - assert arr.array.dtype.kind in "ui" - if arr.array.min() >= 0: - arr.array = arr.array.astype(np.min_scalar_type(arr.array.max())) - - -class RegularBlockModel(ProjectElement): - """A block model with fixed size blocks on a regular grid and no sub-blocks.""" - - schema = "org.omf.v2.elements.blockmodel.regular" - _valid_locations = ("cells", "parent_blocks") - - definition = properties.Instance( - "Block model definition", - RegularBlockModelDefinition, - default=RegularBlockModelDefinition, - ) - - @property - def num_cells(self): - """The number of cells, which in this case are always parent blocks.""" - return np.prod(self.definition.block_count) - - def location_length(self, location): - """Return correct attribute length for 'location'.""" - return self.num_cells - - -class TensorGridBlockModel(ProjectElement): - """A block model with variable spacing in all directions and no sub-blocks.""" - - schema = "org.omf.v2.element.blockmodel.tensorgrid" - _valid_locations = ("vertices", "cells", "parent_blocks") - - definition = properties.Instance( - "Block model definition, including the tensor arrays", - TensorBlockModelDefinition, - default=TensorBlockModelDefinition, - ) - - @property - def num_cells(self): - """The number of cells.""" - return np.prod(self.definition.block_count) - - @property - def num_nodes(self): - """Number of nodes or vertices.""" - count = self.definition.block_count - return None if count is None else np.prod(count + 1) - - def location_length(self, location): - """Return correct attribute length for 'location'.""" - return self.num_nodes if location == "vertices" else self.num_cells - - -class SubblockedModel(ProjectElement): - """A regular block model with sub-blocks that align with a lower-level grid.""" - - schema = "org.omf.v2.elements.blockmodel.subblocked" - _valid_locations = ("cells", "parent_blocks") - - definition = properties.Instance( - "Block model definition, for the parent blocks", - RegularBlockModelDefinition, - default=RegularBlockModelDefinition, - ) - subblock_definition = properties.Union( - "Defines the structure of sub-blocks within each parent block.", - props=[RegularSubblockDefinition, OctreeSubblockDefinition], - default=RegularSubblockDefinition, - ) - subblock_parent_indices = ArrayInstanceProperty( - "The parent block IJK index of each sub-block", - shape=("*", 3), - dtype=int, - ) - subblock_corners = ArrayInstanceProperty( - """The positions of the sub-block corners on the grid within their parent block. - - The columns are (min_i, min_j, min_k, max_i, max_j, max_k). Values must be - greater than or equal to zero and less than or equal to the maximum number of - sub-blocks in that axis. - - Sub-blocks must stay within the parent block and should not overlap. Gaps are - allowed but it will be impossible for 'cell' attributes to assign values to - those areas. - """, - shape=("*", 6), - dtype=int, - ) - - @property - def num_cells(self): - """The number of cells, which in this case are sub-blocks.""" - return None if self.subblock_corners is None else len(self.subblock_corners) - - @property - def num_parent_blocks(self): - """The number of parent blocks.""" - return np.prod(self.definition.block_count) - - def location_length(self, location): - """Return correct attribute length for 'location'.""" - return self.num_parent_blocks if location == "parent_blocks" else self.num_cells - - @properties.validator - def _validate_subblocks(self): - _shrink_uint(self.subblock_parent_indices) - _shrink_uint(self.subblock_corners) - check_subblocks( - self.definition, - self.subblock_definition, - self.subblock_parent_indices.array, - self.subblock_corners.array, - instance=self, - ) - - -class FreeformSubblockedModel(ProjectElement): - """A regular block model where sub-blocks can be anywhere within the parent.""" - - schema = "org.omf.v2.elements.blockmodel.freeform_subblocked" - _valid_locations = ("cells", "parent_blocks") - - definition = properties.Instance( - "Block model definition, for the parent blocks", - RegularBlockModelDefinition, - default=RegularBlockModelDefinition, - ) - subblock_definition = properties.Union( - "Defines the structure of sub-blocks within each parent block.", - props=[FreeformSubblockDefinition, VariableHeightSubblockDefinition], - default=FreeformSubblockDefinition, - ) - subblock_parent_indices = ArrayInstanceProperty( - "The parent block IJK index of each sub-block", - shape=("*", 3), - dtype=int, - ) - subblock_corners = ArrayInstanceProperty( - """The positions of the sub-block corners on the grid within their parent block. - - The columns are (min_i, min_j, min_k, max_i, max_j, max_k). Values must be - between 0.0 and 1.0 inclusive. - - Sub-blocks must stay within the parent block and should not overlap. Gaps are - allowed but it will be impossible for 'cell' attributes to assign values to - those areas. - """, - shape=("*", 6), - dtype=float, - ) - - @property - def num_cells(self): - """The number of cells, which in this case are always parent blocks.""" - return None if self.subblock_corners is None else len(self.subblock_corners) - - @property - def num_parent_blocks(self): - """The number of parent blocks.""" - return np.prod(self.definition.block_count) - - def location_length(self, location): - """Return correct attribute length for 'location'.""" - return self.num_parent_blocks if location == "parent_blocks" else self.num_cells - - @properties.validator - def _validate_subblocks(self): - _shrink_uint(self.subblock_parent_indices) - check_subblocks( - self.definition, - self.subblock_definition, - self.subblock_parent_indices.array, - self.subblock_corners.array, - instance=self, - ) diff --git a/omf/blockmodel/regular_subblocks.py b/omf/blockmodel/regular_subblocks.py new file mode 100644 index 00000000..e9d13c47 --- /dev/null +++ b/omf/blockmodel/regular_subblocks.py @@ -0,0 +1,107 @@ +"""blockmodel/subblocks.py: sub-block definitions and containers.""" +import numpy as np +import properties + +from ..attribute import ArrayInstanceProperty +from ..base import BaseModel +from ._subblock_check import check_subblocks, shrink_uint + +__all__ = ["OctreeSubblockDefinition", "RegularSubblockDefinition", "RegularSubblocks"] + + +class RegularSubblockDefinition(BaseModel): + """The simplest gridded sub-block definition. + + Divide the parent block into a regular grid of `subblock_count` cells. Each block covers + a cuboid region within that grid. If a parent block is not sub-blocked then it will still + contain a single block that covers the entire grid. + """ + + schema = "org.omf.v2.blockmodel.subblocks.definition.regular" + + subblock_count = properties.Array( + "The maximum number of sub-blocks inside a parent in each direction.", dtype=int, shape=(3,) + ) + + @properties.validator("subblock_count") + def _validate_subblock_count(self, change): + for item in change["value"]: + if item < 1: + raise properties.ValidationError("sub-block counts must be >= 1", prop=change["name"], instance=self) + + +class OctreeSubblockDefinition(BaseModel): + """Sub-blocks form an octree inside the parent block. + + Cut the parent block in half in all directions to create eight sub-blocks. Repeat that + division for some or all of those new sub-blocks. Continue doing that until the limit + on sub-block count is reached or until the sub-blocks accurately model the inputs. + + This definition also allows the lower level cuts to be omitted in one or two axes, + giving a maximum sub-block count of (16, 16, 4) for example rather than requiring + all axes to be equal. + """ + + schema = "org.omf.v2.blockmodel.subblocks.definition.octree" + + subblock_count = properties.Array( + "The maximum number of sub-blocks inside a parent in each direction.", dtype=int, shape=(3,) + ) + + @properties.validator("subblock_count") + def _validate_subblock_count(self, change): + for item in change["value"]: + if item < 1: + raise properties.ValidationError("sub-block counts must be >= 1", prop=change["name"], instance=self) + log = np.log2(item) + if np.trunc(log) != log: + raise properties.ValidationError( + "octree sub-block counts must be powers of two", prop=change["name"], instance=self + ) + + +class RegularSubblocks(BaseModel): + """Defines regular or octree sub-blocks for a block model. + + These sub-blocks must align with a lower-level grid inside the parent block. + """ + + schema = "org.omf.v2.blockmodel.subblocks.regular" + + definition = properties.Union( + "Defines the structure of sub-blocks within each parent block.", + props=[RegularSubblockDefinition, OctreeSubblockDefinition], + default=RegularSubblockDefinition, + ) + parent_indices = ArrayInstanceProperty( + "The parent block IJK index of each sub-block", + shape=("*", 3), + dtype=int, + ) + corners = ArrayInstanceProperty( + """The positions of the sub-block corners on the grid within their parent block. + + The columns are (min_i, min_j, min_k, max_i, max_j, max_k). Values must be + greater than or equal to zero and less than or equal to the maximum number of + sub-blocks in that axis. + + Sub-blocks must stay within the parent block and should not overlap. Gaps are + allowed but it will be impossible for 'cell' attributes to assign values to + those areas. + """, + shape=("*", 6), + dtype=int, + ) + + def validate_subblocks(self, definition): + """Checks the sub-block data against the given block model definition.""" + shrink_uint(self.parent_indices) + shrink_uint(self.corners) + check_subblocks( + definition, self, instance=self, regular=True, octree=isinstance(self.definition, OctreeSubblockDefinition) + ) + + @property + def num_subblocks(self): + """The total number of sub-blocks.""" + return None if self.corners is None else len(self.corners) diff --git a/omf/compat/omf_v1.py b/omf/compat/omf_v1.py index 9065070b..e906716d 100644 --- a/omf/compat/omf_v1.py +++ b/omf/compat/omf_v1.py @@ -18,15 +18,13 @@ VectorAttribute, ) from ..base import Project -from ..blockmodel import TensorGridBlockModel +from ..blockmodel import BlockModel, TensorBlockModelDefinition from ..lineset import LineSet from ..pointset import PointSet from ..surface import Surface, TensorGridSurface from ..texture import Image, ProjectedTexture from .interface import IOMFReader, InvalidOMFFile, WrongVersionError -# from .. import attribute, base, blockmodel, lineset, pointset, surface, texture - COMPATIBILITY_VERSION = b"OMF-v0.9.0" _default = object() @@ -437,18 +435,18 @@ def _convert_volume_element(self, volume_v1): geometry_uuid = self.__get_attr(volume_v1, "geometry") geometry_v1 = self.__get_attr(self._project, geometry_uuid) self.__require_attr(geometry_v1, "__class__", "VolumeGridGeometry") - volume = TensorGridBlockModel() - self.__copy_attr(volume_v1, "subtype", volume.metadata) - self.__copy_attr(geometry_v1, "origin", volume.definition) - self.__copy_attr(geometry_v1, "tensor_u", volume.definition) - self.__copy_attr(geometry_v1, "tensor_v", volume.definition) - self.__copy_attr(geometry_v1, "tensor_w", volume.definition) - self.__copy_attr(geometry_v1, "axis_u", volume.definition) - self.__copy_attr(geometry_v1, "axis_v", volume.definition) - self.__copy_attr(geometry_v1, "axis_w", volume.definition) + block_model = BlockModel(definition=TensorBlockModelDefinition()) + self.__copy_attr(volume_v1, "subtype", block_model.metadata) + self.__copy_attr(geometry_v1, "origin", block_model.definition) + self.__copy_attr(geometry_v1, "tensor_u", block_model.definition) + self.__copy_attr(geometry_v1, "tensor_v", block_model.definition) + self.__copy_attr(geometry_v1, "tensor_w", block_model.definition) + self.__copy_attr(geometry_v1, "axis_u", block_model.definition) + self.__copy_attr(geometry_v1, "axis_v", block_model.definition) + self.__copy_attr(geometry_v1, "axis_w", block_model.definition) valid_locations = ("vertices", "cells") - return volume, valid_locations + return block_model, valid_locations # element list def _convert_project_element(self, element_uuid): diff --git a/omf/composite.py b/omf/composite.py index cbf20244..e6b2667d 100644 --- a/omf/composite.py +++ b/omf/composite.py @@ -2,7 +2,7 @@ import properties from .base import ProjectElement -from .blockmodel import RegularBlockModel, SubblockedModel, TensorGridBlockModel +from .blockmodel import BlockModel from .lineset import LineSet from .pointset import PointSet from .surface import Surface, TensorGridSurface @@ -25,12 +25,10 @@ class is created, then create an identical subclass so the docs prop=properties.Union( "", ( + BlockModel, LineSet, PointSet, - RegularBlockModel, - SubblockedModel, Surface, - TensorGridBlockModel, TensorGridSurface, ), ), diff --git a/tests/test_blockmodel.py b/tests/test_blockmodel.py index 73d2ab20..e606fabe 100644 --- a/tests/test_blockmodel.py +++ b/tests/test_blockmodel.py @@ -60,9 +60,9 @@ def test_ijk_index(ijk, index): def test_tensorblockmodel(): """Test volume grid geometry validation""" - elem = omf.TensorGridBlockModel() - assert elem.num_nodes is None - assert elem.num_cells is None + elem = omf.BlockModel(definition=omf.TensorBlockModelDefinition()) + assert elem.num_parent_vertices is None + assert elem.num_parent_blocks is None assert elem.definition.block_count is None elem.definition.tensor_u = [1.0, 1.0] elem.definition.tensor_v = [2.0, 2.0, 2.0] @@ -80,7 +80,7 @@ def test_tensorblockmodel(): @pytest.mark.parametrize("block_count", ([2, 2], [2, 2, 2, 2], [0, 2, 2], [2, 2, 0.5])) def test_bad_block_count(block_count): """Test mismatched block_count""" - block_model = omf.RegularBlockModel() + block_model = omf.BlockModel() block_model.definition.block_size = [1.0, 2.0, 3.0] with pytest.raises((ValueError, properties.ValidationError)): block_model.definition.block_count = block_count @@ -90,7 +90,7 @@ def test_bad_block_count(block_count): @pytest.mark.parametrize("block_size", ([2.0, 2.0], [2.0, 2.0, 2.0, 2.0], [-1.0, 2, 2], [0.0, 2, 2])) def test_bad_block_size(block_size): """Test mismatched block_size""" - block_model = omf.RegularBlockModel() + block_model = omf.BlockModel() block_model.definition.block_count = [2, 2, 2] with pytest.raises((ValueError, properties.ValidationError)): block_model.definition.block_size = block_size @@ -99,7 +99,7 @@ def test_bad_block_size(block_size): def test_uninstantiated(): """Test all attributes are None on instantiation""" - block_model = omf.RegularBlockModel() + block_model = omf.BlockModel() assert block_model.definition.block_count is None assert block_model.definition.block_size is None assert block_model.num_cells is None @@ -107,10 +107,12 @@ def test_uninstantiated(): def test_num_cells(): """Test num_cells calculation is correct""" - block_model = omf.RegularBlockModel() + block_model = omf.BlockModel() block_model.definition.block_count = [2, 2, 2] block_model.definition.block_size = [1.0, 2.0, 3.0] np.testing.assert_array_equal(block_model.definition.block_count, [2, 2, 2]) assert block_model.num_cells == 8 assert block_model.location_length("cells") == 8 + assert block_model.location_length("vertices") == 27 assert block_model.location_length("parent_blocks") == 8 + assert block_model.location_length("") == 8 diff --git a/tests/test_subblockedmodel.py b/tests/test_subblockedmodel.py index 53e66782..94c26986 100644 --- a/tests/test_subblockedmodel.py +++ b/tests/test_subblockedmodel.py @@ -26,11 +26,11 @@ def _bm_def(): def _test_regular(*corners): - block_model = omf.SubblockedModel() + block_model = omf.BlockModel(subblocks=omf.RegularSubblocks()) block_model.definition = _bm_def() - block_model.subblock_definition = omf.RegularSubblockDefinition(subblock_count=(5, 4, 3)) - block_model.subblock_corners = np.array(corners) - block_model.subblock_parent_indices = np.zeros((len(corners), 3), dtype=int) + block_model.subblocks.definition = omf.RegularSubblockDefinition(subblock_count=(5, 4, 3)) + block_model.subblocks.corners = np.array(corners) + block_model.subblocks.parent_indices = np.zeros((len(corners), 3), dtype=int) block_model.validate() @@ -52,24 +52,25 @@ def test_outside_parent(): def test_invalid_parent_indices(): """Test invalid parent block indices are rejected.""" - block_model = omf.SubblockedModel() + block_model = omf.BlockModel(subblocks=omf.RegularSubblocks()) block_model.definition = _bm_def() - block_model.subblock_definition = omf.RegularSubblockDefinition(subblock_count=(5, 4, 3)) - block_model.subblock_corners = np.array([(0, 0, 0, 5, 4, 3), (0, 0, 0, 5, 4, 3)]) - block_model.subblock_parent_indices = np.array([(0, 0, 0), (1, 0, 0)]) + block_model.subblocks.definition = omf.RegularSubblockDefinition(subblock_count=(5, 4, 3)) + block_model.subblocks.corners = np.array([(0, 0, 0, 5, 4, 3), (0, 0, 0, 5, 4, 3)]) + block_model.subblocks.parent_indices = np.array([(0, 0, 0), (1, 0, 0)]) with pytest.raises(properties.ValidationError, match=r"subblock_parent_indices < \(1, 1, 1\)"): block_model.validate() - block_model.subblock_parent_indices = np.array([(0, 0, -1), (0, 0, 0)]) + block_model.subblocks.parent_indices = np.array([(0, 0, -1), (0, 0, 0)]) with pytest.raises(properties.ValidationError, match="0 <= subblock_parent_indices"): block_model.validate() def _test_octree(*corners): - block_model = omf.SubblockedModel() - block_model.definition = _bm_def() - block_model.subblock_definition = omf.OctreeSubblockDefinition(subblock_count=(4, 4, 2)) - block_model.subblock_corners = np.array(corners) - block_model.subblock_parent_indices = np.zeros((len(corners), 3), dtype=int) + block_model = omf.BlockModel( + definition=_bm_def(), + subblocks=omf.RegularSubblocks(definition=omf.OctreeSubblockDefinition(subblock_count=(4, 4, 2))), + ) + block_model.subblocks.corners = np.array(corners) + block_model.subblocks.parent_indices = np.zeros((len(corners), 3), dtype=int) block_model.validate() @@ -106,40 +107,41 @@ def test_bad_position(): def test_pack_subblock_arrays(): """Test that packing of uint arrays during validation works.""" - block_model = omf.SubblockedModel() - block_model.subblock_definition.subblock_count = [2, 2, 2] + block_model = omf.BlockModel() + block_model.subblocks = omf.RegularSubblocks() + block_model.subblocks.definition.subblock_count = [2, 2, 2] block_model.definition.block_size = [1.0, 1.0, 1.0] block_model.definition.block_count = [10, 10, 10] - block_model.subblock_parent_indices = np.array([(0, 0, 0)], dtype=int) - block_model.subblock_corners = np.array([(0, 0, 0, 2, 2, 2)], dtype=int) + block_model.subblocks.parent_indices = np.array([(0, 0, 0)], dtype=int) + block_model.subblocks.corners = np.array([(0, 0, 0, 2, 2, 2)], dtype=int) block_model.validate() # Arrays were set as int, validate should have packed it down to uint8. - assert block_model.subblock_corners.array.dtype == np.uint8 + assert block_model.subblocks.corners.array.dtype == np.uint8 def test_uninstantiated(): """Test that definitions are default and attributes are None on instantiation""" - block_model = omf.SubblockedModel() + block_model = omf.BlockModel(subblocks=omf.RegularSubblocks()) assert isinstance(block_model.definition, omf.RegularBlockModelDefinition) - assert isinstance(block_model.subblock_definition, omf.RegularSubblockDefinition) + assert isinstance(block_model.subblocks.definition, omf.RegularSubblockDefinition) assert block_model.definition.block_count is None assert block_model.definition.block_size is None - assert block_model.subblock_definition.subblock_count is None + assert block_model.subblocks.definition.subblock_count is None assert block_model.num_cells is None - assert block_model.subblock_parent_indices is None - assert block_model.subblock_corners is None + assert block_model.subblocks.parent_indices is None + assert block_model.subblocks.corners is None def test_num_cells(): """Test num_cells calculation is correct""" - block_model = omf.SubblockedModel() + block_model = omf.BlockModel(subblocks=omf.RegularSubblocks()) block_model.definition.block_count = [2, 2, 2] block_model.definition.block_size = [1.0, 2.0, 3.0] - block_model.subblock_definition.subblock_count = [5, 5, 5] + block_model.subblocks.definition.subblock_count = [5, 5, 5] np.testing.assert_array_equal(block_model.definition.block_count, [2, 2, 2]) - np.testing.assert_array_equal(block_model.subblock_definition.subblock_count, [5, 5, 5]) - block_model.subblock_parent_indices = np.array([(0, 0, 0), (1, 0, 0)]) - block_model.subblock_corners = np.array([(0, 0, 0, 5, 5, 5), (1, 1, 1, 4, 4, 4)]) + np.testing.assert_array_equal(block_model.subblocks.definition.subblock_count, [5, 5, 5]) + block_model.subblocks.parent_indices = np.array([(0, 0, 0), (1, 0, 0)]) + block_model.subblocks.corners = np.array([(0, 0, 0, 5, 5, 5), (1, 1, 1, 4, 4, 4)]) assert block_model.num_cells == 2 assert block_model.num_parent_blocks == 8 assert block_model.location_length("cells") == 2 From 231d7c2b7906802be26a9cc60c36aca2ecd70e5c Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Tue, 14 Mar 2023 17:01:14 +1300 Subject: [PATCH 33/42] Moved the common parts of the block model definitions back to the block model. --- assets/v2/test_file.omf | Bin 42189 -> 42193 bytes docs/content/examples.rst | 2 +- omf/blockmodel/_subblock_check.py | 15 ++- omf/blockmodel/freeform_subblocks.py | 4 +- omf/blockmodel/model.py | 131 ++++++++++++++------------- omf/blockmodel/regular_subblocks.py | 4 +- omf/compat/omf_v1.py | 11 ++- tests/test_blockmodel.py | 38 ++++---- tests/test_subblockedmodel.py | 5 +- 9 files changed, 106 insertions(+), 104 deletions(-) diff --git a/assets/v2/test_file.omf b/assets/v2/test_file.omf index 4f47c3e84cd627dd059d32bc19d3c6b76648d936..b7925e3429718cf48d18770a2f28b3aa11d8143a 100644 GIT binary patch literal 42193 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 count).any(): +def _check_parent_indices(block_count, parent_indices, instance): + if (parent_indices < 0).any() or (parent_indices >= block_count).any(): raise properties.ValidationError( - f"0 <= subblock_parent_indices < ({count[0]}, {count[1]}, {count[2]}) failed", + f"0 <= subblock_parent_indices < ({block_count[0]}, {block_count[1]}, {block_count[2]}) failed", prop="subblock_parent_indices", instance=instance, ) @@ -91,7 +90,7 @@ def _check_octree(subblock_definition, corners, instance): ) -def check_subblocks(definition, subblocks, instance=None, regular=False, octree=False): +def check_subblocks(model, subblocks, instance=None, regular=False, octree=False): """Run all checks on the given defintions and sub-blocks.""" parent_indices = subblocks.parent_indices.array corners = subblocks.corners.array @@ -102,11 +101,11 @@ def check_subblocks(definition, subblocks, instance=None, regular=False, octree= instance=instance, ) _check_inside_parent(subblocks.definition, corners, instance, regular) - _check_parent_indices(definition, parent_indices, instance) + _check_parent_indices(model.definition.block_count, parent_indices, instance) if octree: _check_octree(subblocks.definition, corners, instance) - seen = np.zeros(np.prod(definition.block_count), dtype=bool) - for start, end, value in _group_by(definition.ijk_to_index(parent_indices)): + seen = np.zeros(np.prod(model.definition.block_count), dtype=bool) + for start, end, value in _group_by(model.ijk_to_index(parent_indices)): if seen[value]: raise properties.ValidationError( "all sub-blocks inside one parent block must be adjacent in the arrays", diff --git a/omf/blockmodel/freeform_subblocks.py b/omf/blockmodel/freeform_subblocks.py index 3ab647f0..cf6dfac5 100644 --- a/omf/blockmodel/freeform_subblocks.py +++ b/omf/blockmodel/freeform_subblocks.py @@ -66,11 +66,11 @@ class FreeformSubblocks(BaseModel): dtype=float, ) - def validate_subblocks(self, definition): + def validate_subblocks(self, model): """Checks the sub-block data against the given block model definition.""" shrink_uint(self.parent_indices) shrink_uint(self.corners) - check_subblocks(definition, self, instance=self) + check_subblocks(model, self, instance=self) @property def num_subblocks(self): diff --git a/omf/blockmodel/model.py b/omf/blockmodel/model.py index 5fb3cf8e..055e7d33 100644 --- a/omf/blockmodel/model.py +++ b/omf/blockmodel/model.py @@ -9,60 +9,33 @@ __all__ = ["BlockModel", "RegularBlockModelDefinition", "TensorBlockModelDefinition"] -class _BaseBlockModelDefinition(BaseModel): - 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) - axis_w = properties.Vector3("Vector orientation of w-direction", default="Z", length=1) - origin = properties.Vector3( - "Minimum corner of the block model relative to Project coordinate reference system", - default="zero", - ) - block_count = () - - @properties.validator - def _validate_axes(self): - """Check if mesh content is built correctly""" - if not ( - np.abs(self.axis_u.dot(self.axis_v) < 1e-6) - and np.abs(self.axis_v.dot(self.axis_w) < 1e-6) - and np.abs(self.axis_w.dot(self.axis_u) < 1e-6) - ): - raise ValueError("axis_u, axis_v, and axis_w must be orthogonal") - - def ijk_to_index(self, ijk): - """Map IJK triples to flat indices for a single triple or an array, preserving shape.""" - if self.block_count is None: - raise ValueError("block_count is not set") - arr = np.asarray(ijk) - if arr.dtype.kind not in "ui": - raise TypeError(f"'ijk' must be integer typed, found {arr.dtype}") - if not arr.shape or arr.shape[-1] != 3: - raise ValueError("'ijk' must have 3 elements or be an array with shape (*_, 3)") - output_shape = arr.shape[:-1] - shaped = arr.reshape(-1, 3) - count = self.block_count - if (shaped < 0).any() or (shaped >= count).any(): - raise IndexError(f"0 <= ijk < ({count[0]}, {count[1]}, {count[2]}) failed") - indices = np.ravel_multi_index(multi_index=shaped.T, dims=count, order="F") - return indices[0] if output_shape == () else indices.reshape(output_shape) - - def index_to_ijk(self, index): - """Map flat indices to IJK triples for a single index or an array, preserving shape.""" - if self.block_count is None: - raise ValueError("block_count is not set") - arr = np.asarray(index) - if arr.dtype.kind not in "ui": - raise TypeError(f"'index' must be integer typed, found {arr.dtype}") - output_shape = arr.shape + (3,) - shaped = arr.reshape(-1) - count = self.block_count - if (shaped < 0).any() or (shaped >= np.prod(count)).any(): - raise IndexError(f"0 <= index < {np.prod(count)} failed") - ijk = np.unravel_index(indices=shaped, shape=count, order="F") - return np.c_[ijk[0], ijk[1], ijk[2]].reshape(output_shape) - - -class RegularBlockModelDefinition(_BaseBlockModelDefinition): +def _ijk_to_index(block_count, ijk): + arr = np.asarray(ijk) + if arr.dtype.kind not in "ui": + raise TypeError(f"'ijk' must be integer typed, found {arr.dtype}") + if not arr.shape or arr.shape[-1] != 3: + raise ValueError("'ijk' must have 3 elements or be an array with shape (*_, 3)") + output_shape = arr.shape[:-1] + shaped = arr.reshape(-1, 3) + if (shaped < 0).any() or (shaped >= block_count).any(): + raise IndexError(f"0 <= ijk < ({block_count[0]}, {block_count[1]}, {block_count[2]}) failed") + indices = np.ravel_multi_index(multi_index=shaped.T, dims=block_count, order="F") + return indices[0] if output_shape == () else indices.reshape(output_shape) + + +def _index_to_ijk(block_count, index): + arr = np.asarray(index) + if arr.dtype.kind not in "ui": + raise TypeError(f"'index' must be integer typed, found {arr.dtype}") + output_shape = arr.shape + (3,) + shaped = arr.reshape(-1) + if (shaped < 0).any() or (shaped >= np.prod(block_count)).any(): + raise IndexError(f"0 <= index < {np.prod(block_count)} failed") + ijk = np.unravel_index(indices=shaped, shape=block_count, order="F") + return np.c_[ijk[0], ijk[1], ijk[2]].reshape(output_shape) + + +class RegularBlockModelDefinition(BaseModel): """Defines the block structure of a regular block model. If used on a sub-blocked model then everything here applies to the parent blocks only. @@ -71,7 +44,7 @@ class RegularBlockModelDefinition(_BaseBlockModelDefinition): schema = "org.omf.v2.blockmodel.definition.regular" block_count = properties.Array("Number of blocks in each of the u, v, and w directions.", dtype=int, shape=(3,)) - block_size = properties.Vector3("Size of blocks in the u, v, and w directions.") + block_size = properties.Vector3("Size of blocks in the u, v, and w directions.", default=lambda: (1.0, 1.0, 1.0)) @properties.validator("block_count") def _validate_block_count(self, change): @@ -86,7 +59,7 @@ def _validate_block_size(self, change): raise properties.ValidationError("block sizes must be > 0.0", prop=change["name"], instance=self) -class TensorBlockModelDefinition(_BaseBlockModelDefinition): +class TensorBlockModelDefinition(BaseModel): """Defines the block structure of a tensor grid block model.""" schema = "org.omf.v2.blockmodel.definition.tensor" @@ -103,15 +76,14 @@ def _validate_tensor(self, change): if item <= 0.0: raise properties.ValidationError("tensor sizes must be > 0.0", prop=change["name"], instance=self) - def _tensors(self): - return (self.tensor_u, self.tensor_v, self.tensor_w) - @property def block_count(self): """The block count is derived from the tensors here.""" - counts = tuple(None if t is None else len(t) for t in self._tensors()) - if None in counts: - return None + counts = [] + for tensor in self.tensor_u, self.tensor_v, self.tensor_w: + if tensor is None: + return None + counts.append(len(tensor)) return np.array(counts, dtype=int) @@ -121,6 +93,13 @@ class BlockModel(ProjectElement): schema = "org.omf.v2.elements.blockmodel" _valid_locations = ("parent_blocks", "vertices", "cells") + origin = properties.Vector3( + "Minimum corner of the block model relative to Project coordinate reference system", + default="zero", + ) + 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) + axis_w = properties.Vector3("Vector orientation of w-direction", default="Z", length=1) definition = properties.Union( """Block model definition, describing either a regular or tensor-based block layout.""", props=[RegularBlockModelDefinition, TensorBlockModelDefinition], @@ -138,8 +117,22 @@ class BlockModel(ProjectElement): @properties.validator def _validate(self): + if not ( + np.abs(self.axis_u.dot(self.axis_v) < 1e-6) + and np.abs(self.axis_v.dot(self.axis_w) < 1e-6) + and np.abs(self.axis_w.dot(self.axis_u) < 1e-6) + ): + raise properties.ValidationError("axis_u, axis_v, and axis_w must be orthogonal", instance=self) if self.subblocks is not None: - self.subblocks.validate_subblocks(self.definition) + self.subblocks.validate_subblocks(self) + + @property + def block_count(self): + """Number of blocks in each of the u, v, and w directions. + + Equivalent to `block_model.definition.block_count`. + """ + return self.definition.block_count @property def num_parent_blocks(self): @@ -164,3 +157,15 @@ def location_length(self, location): if location == "cells" and self.subblocks is not None: return self.subblocks.num_subblocks return self.num_parent_blocks + + def ijk_to_index(self, ijk): + """Map IJK triples to flat indices for a single triple or an array, preserving shape.""" + if self.definition.block_count is None: + raise ValueError("block count is not yet known") + return _ijk_to_index(self.definition.block_count, ijk) + + def index_to_ijk(self, index): + """Map flat indices to IJK triples for a single index or an array, preserving shape.""" + if self.definition.block_count is None: + raise ValueError("block count is not yet known") + return _index_to_ijk(self.definition.block_count, index) diff --git a/omf/blockmodel/regular_subblocks.py b/omf/blockmodel/regular_subblocks.py index e9d13c47..248c494a 100644 --- a/omf/blockmodel/regular_subblocks.py +++ b/omf/blockmodel/regular_subblocks.py @@ -93,12 +93,12 @@ class RegularSubblocks(BaseModel): dtype=int, ) - def validate_subblocks(self, definition): + def validate_subblocks(self, model): """Checks the sub-block data against the given block model definition.""" shrink_uint(self.parent_indices) shrink_uint(self.corners) check_subblocks( - definition, self, instance=self, regular=True, octree=isinstance(self.definition, OctreeSubblockDefinition) + model, self, instance=self, regular=True, octree=isinstance(self.definition, OctreeSubblockDefinition) ) @property diff --git a/omf/compat/omf_v1.py b/omf/compat/omf_v1.py index e906716d..7b5ca12a 100644 --- a/omf/compat/omf_v1.py +++ b/omf/compat/omf_v1.py @@ -435,15 +435,16 @@ def _convert_volume_element(self, volume_v1): geometry_uuid = self.__get_attr(volume_v1, "geometry") geometry_v1 = self.__get_attr(self._project, geometry_uuid) self.__require_attr(geometry_v1, "__class__", "VolumeGridGeometry") - block_model = BlockModel(definition=TensorBlockModelDefinition()) + block_model = BlockModel() self.__copy_attr(volume_v1, "subtype", block_model.metadata) - self.__copy_attr(geometry_v1, "origin", block_model.definition) + self.__copy_attr(geometry_v1, "origin", block_model) + self.__copy_attr(geometry_v1, "axis_u", block_model) + self.__copy_attr(geometry_v1, "axis_v", block_model) + self.__copy_attr(geometry_v1, "axis_w", block_model) + block_model.definition = TensorBlockModelDefinition() self.__copy_attr(geometry_v1, "tensor_u", block_model.definition) self.__copy_attr(geometry_v1, "tensor_v", block_model.definition) self.__copy_attr(geometry_v1, "tensor_w", block_model.definition) - self.__copy_attr(geometry_v1, "axis_u", block_model.definition) - self.__copy_attr(geometry_v1, "axis_v", block_model.definition) - self.__copy_attr(geometry_v1, "axis_w", block_model.definition) valid_locations = ("vertices", "cells") return block_model, valid_locations diff --git a/tests/test_blockmodel.py b/tests/test_blockmodel.py index e606fabe..267e9b18 100644 --- a/tests/test_blockmodel.py +++ b/tests/test_blockmodel.py @@ -4,6 +4,7 @@ import pytest import omf +from omf.blockmodel.model import _ijk_to_index, _index_to_ijk def _make_regular_definition(count): @@ -13,38 +14,36 @@ def _make_regular_definition(count): def test_ijk_index_errors(): """Test ijk indexing into parent blocks errors as expected""" - defn = _make_regular_definition([3, 4, 5]) with pytest.raises(TypeError): - defn.ijk_to_index("a") + _ijk_to_index([3, 4, 5], "a") with pytest.raises(TypeError): - defn.index_to_ijk("a") + _index_to_ijk([3, 4, 5], "a") with pytest.raises(ValueError): - defn.ijk_to_index([0, 0]) + _ijk_to_index([3, 4, 5], [0, 0]) with pytest.raises(TypeError): - defn.ijk_to_index([0, 0, 0.5]) + _ijk_to_index([3, 4, 5], [0, 0, 0.5]) with pytest.raises(TypeError): - defn.index_to_ijk(0.5) + _index_to_ijk([3, 4, 5], 0.5) with pytest.raises(IndexError): - defn.ijk_to_index([0, 0, 5]) + _ijk_to_index([3, 4, 5], [0, 0, 5]) with pytest.raises(IndexError): - defn.index_to_ijk(60) + _index_to_ijk([3, 4, 5], 60) with pytest.raises(IndexError): - defn.ijk_to_index([[0, 0, 5], [0, 0, 3]]) + _ijk_to_index([3, 4, 5], [[0, 0, 5], [0, 0, 3]]) with pytest.raises(IndexError): - defn.index_to_ijk([0, 1, 60]) + _index_to_ijk([3, 4, 5], [0, 1, 60]) def test_ijk_index_arrays(): """Test ijk array indexing into parent blocks works as expected""" - defn = _make_regular_definition([3, 4, 5]) ijk = [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1), (2, 3, 4)] index = [0, 1, 3, 12, 59] - assert np.array_equal(defn.ijk_to_index(ijk), index) - assert np.array_equal(defn.index_to_ijk(index), ijk) + assert np.array_equal(_ijk_to_index([3, 4, 5], ijk), index) + assert np.array_equal(_index_to_ijk([3, 4, 5], index), ijk) ijk = [[(0, 0, 0), (1, 0, 0)], [(0, 1, 0), (0, 0, 1)]] index = [(0, 1), (3, 12)] - assert np.array_equal(defn.ijk_to_index(ijk), index) - assert np.array_equal(defn.index_to_ijk(index), ijk) + assert np.array_equal(_ijk_to_index([3, 4, 5], ijk), index) + assert np.array_equal(_index_to_ijk([3, 4, 5], index), ijk) @pytest.mark.parametrize( @@ -53,9 +52,8 @@ def test_ijk_index_arrays(): ) def test_ijk_index(ijk, index): """Test ijk indexing into parent blocks works as expected""" - defn = _make_regular_definition([3, 4, 5]) - assert defn.ijk_to_index(ijk) == index - assert np.array_equal(defn.index_to_ijk(index), ijk) + assert _ijk_to_index([3, 4, 5], ijk) == index + assert np.array_equal(_index_to_ijk([3, 4, 5], index), ijk) def test_tensorblockmodel(): @@ -71,7 +69,7 @@ def test_tensorblockmodel(): assert elem.validate() assert elem.location_length("vertices") == 24 assert elem.location_length("cells") == 6 - elem.definition.axis_v = [1.0, 1.0, 0] + elem.axis_v = [1.0, 1.0, 0] with pytest.raises(ValueError): elem.validate() elem.axis_v = "Y" @@ -101,8 +99,8 @@ def test_uninstantiated(): """Test all attributes are None on instantiation""" block_model = omf.BlockModel() assert block_model.definition.block_count is None - assert block_model.definition.block_size is None assert block_model.num_cells is None + np.testing.assert_array_equal(block_model.definition.block_size, (1.0, 1.0, 1.0)) def test_num_cells(): diff --git a/tests/test_subblockedmodel.py b/tests/test_subblockedmodel.py index 94c26986..f2215696 100644 --- a/tests/test_subblockedmodel.py +++ b/tests/test_subblockedmodel.py @@ -26,8 +26,7 @@ def _bm_def(): def _test_regular(*corners): - block_model = omf.BlockModel(subblocks=omf.RegularSubblocks()) - block_model.definition = _bm_def() + block_model = omf.BlockModel(subblocks=omf.RegularSubblocks(), definition=_bm_def()) block_model.subblocks.definition = omf.RegularSubblockDefinition(subblock_count=(5, 4, 3)) block_model.subblocks.corners = np.array(corners) block_model.subblocks.parent_indices = np.zeros((len(corners), 3), dtype=int) @@ -125,11 +124,11 @@ def test_uninstantiated(): assert isinstance(block_model.definition, omf.RegularBlockModelDefinition) assert isinstance(block_model.subblocks.definition, omf.RegularSubblockDefinition) assert block_model.definition.block_count is None - assert block_model.definition.block_size is None assert block_model.subblocks.definition.subblock_count is None assert block_model.num_cells is None assert block_model.subblocks.parent_indices is None assert block_model.subblocks.corners is None + np.testing.assert_array_equal(block_model.definition.block_size, (1.0, 1.0, 1.0)) def test_num_cells(): From b0093897614147173a082538237be4449e2712dc Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Tue, 14 Mar 2023 18:45:22 +1300 Subject: [PATCH 34/42] Cleaned up sub-block modes. --- docs/content/blockmodel.rst | 16 ++-- omf/__init__.py | 11 +-- omf/blockmodel/_subblock_check.py | 127 --------------------------- omf/blockmodel/freeform_subblocks.py | 39 +------- omf/blockmodel/model.py | 35 ++------ omf/blockmodel/regular_subblocks.py | 85 +++++++++--------- tests/test_blockmodel.py | 32 +++---- tests/test_subblockedmodel.py | 26 +++--- 8 files changed, 85 insertions(+), 286 deletions(-) delete mode 100644 omf/blockmodel/_subblock_check.py diff --git a/docs/content/blockmodel.rst b/docs/content/blockmodel.rst index a4dd8337..133ff786 100644 --- a/docs/content/blockmodel.rst +++ b/docs/content/blockmodel.rst @@ -11,7 +11,7 @@ Element The `BlockModel` element is used to store all types of block model. Sub-types are described by the block model definition, the presence or absense of sub-blocks, the -type of those sub-blocks, and finally by the inner sub-block definition. +type of those sub-blocks, and finally by the sub-block mode. .. autoclass:: omf.blockmodel.BlockModel @@ -48,26 +48,20 @@ and for the sub-blocks to conform to an octree structure within each parent. .. autoclass:: omf.blockmodel.RegularSubblocks -.. autoclass:: omf.blockmodel.RegularSubblockDefinition +.. autoclass:: omf.blockmodel.SubblockModeOctree -.. autoclass:: omf.blockmodel.OctreeSubblockDefinition +.. autoclass:: omf.blockmodel.SubblockModeFull Free-form Sub-blocks -------------------- Free-form sub-blocks are similar to regular but don't follow any structure or grid within their parent blocks. Sub-blocks must stay within the parent and must have size -greater than zero in all directions. They shouldn't overlap but that is not checked. - -With the `VariableHeightSubblockDefinition` sub-blocks should be on a regular grid -in the X and Y directions but the Z positions are unconstrained. +greater than zero in all directions. They probably shouldn't overlap but that isn't +checked. .. autoclass:: omf.blockmodel.FreeformSubblocks -.. autoclass:: omf.blockmodel.FreeformSubblockDefinition - -.. autoclass:: omf.blockmodel.VariableHeightSubblockDefinition - Attributes ---------- diff --git a/omf/__init__.py b/omf/__init__.py index 0bff6e88..41cae1a3 100644 --- a/omf/__init__.py +++ b/omf/__init__.py @@ -10,16 +10,7 @@ VectorAttribute, ) from .base import Project -from .blockmodel import ( - BlockModel, - FreeformSubblockDefinition, - FreeformSubblocks, - OctreeSubblockDefinition, - RegularSubblockDefinition, - RegularBlockModelDefinition, - RegularSubblocks, - TensorBlockModelDefinition, -) +from .blockmodel import * from .composite import Composite from .fileio import __version__, load, save from .lineset import LineSet diff --git a/omf/blockmodel/_subblock_check.py b/omf/blockmodel/_subblock_check.py deleted file mode 100644 index 7aa0d1a4..00000000 --- a/omf/blockmodel/_subblock_check.py +++ /dev/null @@ -1,127 +0,0 @@ -"""blockmodel/_subblock_check.py: functions for checking sub-block constraints.""" -import numpy as np -import properties - - -def _group_by(arr): - if len(arr) == 0: - return - diff = np.flatnonzero(arr[1:] != arr[:-1]) - diff += 1 - if len(diff) == 0: - yield 0, len(arr), arr[0] - else: - yield 0, diff[0], arr[0] - for start, end in zip(diff[:-1], diff[1:]): - yield start, end, arr[start] - yield diff[-1], len(arr), arr[-1] - - -def _check_parent_indices(block_count, parent_indices, instance): - if (parent_indices < 0).any() or (parent_indices >= block_count).any(): - raise properties.ValidationError( - f"0 <= subblock_parent_indices < ({block_count[0]}, {block_count[1]}, {block_count[2]}) failed", - prop="subblock_parent_indices", - instance=instance, - ) - - -def _check_inside_parent(subblock_definition, corners, instance, regular): - if regular: - upper = subblock_definition.subblock_count - upper_str = f"({upper[0]}, {upper[1]}, {upper[2]})" - else: - upper = 1.0 - upper_str = "1" - min_corners = corners[:, :3] - max_corners = corners[:, 3:] - if min_corners.dtype.kind != "u" and not (0 <= min_corners).all(): - raise properties.ValidationError("0 <= min_corner failed", prop="subblock_corners", instance=instance) - if not (min_corners < max_corners).all(): - raise properties.ValidationError("min_corner < max_corner failed", prop="subblock_corners", instance=instance) - if not (max_corners <= upper).all(): - raise properties.ValidationError( - f"max_corner <= {upper_str} failed", - prop="subblock_corners", - instance=instance, - ) - - -def _check_for_overlaps(subblock_definition, one_parent_corners, instance): - # This won't be very fast but there doesn't seem to be a better option. - tracker = np.zeros(subblock_definition.subblock_count[::-1], dtype=int) - for min_i, min_j, min_k, max_i, max_j, max_k in one_parent_corners: - tracker[min_k:max_k, min_j:max_j, min_i:max_i] += 1 - if (tracker > 1).any(): - raise properties.ValidationError("found overlapping sub-blocks", prop="subblock_corners", instance=instance) - - -def _sizes_to_ints(sizes): - sizes = np.array(sizes, dtype=np.uint64) - assert len(sizes.shape) == 2 and sizes.shape[1] == 3 - sizes[:, 0] *= 2**32 - sizes[:, 1] *= 2**16 - return sizes.sum(axis=1) - - -def _check_octree(subblock_definition, corners, instance): - min_corners = corners[:, :3] - max_corners = corners[:, 3:] - sizes = max_corners - min_corners - # Sizes. - count = subblock_definition.subblock_count - valid_sizes = [count.copy()] - while (count > 1).any(): - count[count > 1] //= 2 - valid_sizes.append(count.copy()) - valid_sizes = _sizes_to_ints(valid_sizes) - if not np.isin(_sizes_to_ints(sizes), valid_sizes).all(): - raise properties.ValidationError( - "found non-octree sub-block sizes", - prop="subblock_corners", - instance=instance, - ) - # Positions. Octree blocks always start at a multiple of their size. - if (np.remainder(min_corners, sizes) != 0).any(): - raise properties.ValidationError( - "found non-octree sub-block positions", - prop="subblock_corners", - instance=instance, - ) - - -def check_subblocks(model, subblocks, instance=None, regular=False, octree=False): - """Run all checks on the given defintions and sub-blocks.""" - parent_indices = subblocks.parent_indices.array - corners = subblocks.corners.array - if len(parent_indices) != len(corners): - raise properties.ValidationError( - "'subblock_parent_indices' and 'subblock_corners' arrays must be the same length", - prop="subblock_corners", - instance=instance, - ) - _check_inside_parent(subblocks.definition, corners, instance, regular) - _check_parent_indices(model.definition.block_count, parent_indices, instance) - if octree: - _check_octree(subblocks.definition, corners, instance) - seen = np.zeros(np.prod(model.definition.block_count), dtype=bool) - for start, end, value in _group_by(model.ijk_to_index(parent_indices)): - if seen[value]: - raise properties.ValidationError( - "all sub-blocks inside one parent block must be adjacent in the arrays", - prop="subblock_parent_indices", - instance=instance, - ) - seen[value] = True - if end - start > 1: - _check_for_overlaps(subblocks.definition, corners[start:end], instance) - - -def shrink_uint(arr): - """Takes an ArrayInstanceProperty containing unsigned integers and shrinks it. - - The type after this call will be the smallest uint type that can represent the data. - """ - kind = arr.array.dtype.kind - if kind == "u" or (kind == "i" and arr.array.min() >= 0): - arr.array = arr.array.astype(np.min_scalar_type(arr.array.max())) diff --git a/omf/blockmodel/freeform_subblocks.py b/omf/blockmodel/freeform_subblocks.py index cf6dfac5..3594f45e 100644 --- a/omf/blockmodel/freeform_subblocks.py +++ b/omf/blockmodel/freeform_subblocks.py @@ -3,34 +3,9 @@ from ..attribute import ArrayInstanceProperty from ..base import BaseModel -from ._subblock_check import check_subblocks, shrink_uint +from ._utils import shrink_uint, SubblockChecker -__all__ = ["FreeformSubblockDefinition", "FreeformSubblocks", "VariableHeightSubblockDefinition"] - - -class FreeformSubblockDefinition(BaseModel): - """Unconstrained free-form sub-block definition. - - Provides no limitations on or explanation of sub-block positions. - """ - - schema = "org.omf.v2.blockmodel.subblocks.definition.freeform" - - -class VariableHeightSubblockDefinition(BaseModel): - """Defines sub-blocks on a grid in the U and V directions but variable in the W direction. - - A single sub-block covering the whole parent block is also valid. Sub-blocks should not - overlap. - - Note: these constraints on sub-blocks are not checked during validation. - """ - - schema = "org.omf.v2.blockmodel.subblocks.definition.varheight" - - subblock_count_u = properties.Integer("Number of sub-blocks in the u-direction", min=1, max=65535) - subblock_count_v = properties.Integer("Number of sub-blocks in the v-direction", min=1, max=65535) - minimum_size_w = properties.Float("Minimum size of sub-blocks in the z-direction", min=0.0) +__all__ = ["FreeformSubblocks"] class FreeformSubblocks(BaseModel): @@ -40,13 +15,8 @@ class FreeformSubblocks(BaseModel): conditions the sub-block definition imposes. """ - schema = "org.omf.v2.blockmodel.subblocks.freeform" + schema = "org.omf.v2.elements.blockmodel.subblocks.freeform" - definition = properties.Union( - "Defines the structure of sub-blocks within each parent block.", - props=[FreeformSubblockDefinition, VariableHeightSubblockDefinition], - default=FreeformSubblockDefinition, - ) parent_indices = ArrayInstanceProperty( "The parent block IJK index of each sub-block", shape=("*", 3), @@ -69,8 +39,7 @@ class FreeformSubblocks(BaseModel): def validate_subblocks(self, model): """Checks the sub-block data against the given block model definition.""" shrink_uint(self.parent_indices) - shrink_uint(self.corners) - check_subblocks(model, self, instance=self) + SubblockChecker.from_freeform(model).check() @property def num_subblocks(self): diff --git a/omf/blockmodel/model.py b/omf/blockmodel/model.py index 055e7d33..6337472d 100644 --- a/omf/blockmodel/model.py +++ b/omf/blockmodel/model.py @@ -3,45 +3,20 @@ import properties from ..base import BaseModel, ProjectElement +from ._utils import ijk_to_index, index_to_ijk from .freeform_subblocks import FreeformSubblocks from .regular_subblocks import RegularSubblocks __all__ = ["BlockModel", "RegularBlockModelDefinition", "TensorBlockModelDefinition"] -def _ijk_to_index(block_count, ijk): - arr = np.asarray(ijk) - if arr.dtype.kind not in "ui": - raise TypeError(f"'ijk' must be integer typed, found {arr.dtype}") - if not arr.shape or arr.shape[-1] != 3: - raise ValueError("'ijk' must have 3 elements or be an array with shape (*_, 3)") - output_shape = arr.shape[:-1] - shaped = arr.reshape(-1, 3) - if (shaped < 0).any() or (shaped >= block_count).any(): - raise IndexError(f"0 <= ijk < ({block_count[0]}, {block_count[1]}, {block_count[2]}) failed") - indices = np.ravel_multi_index(multi_index=shaped.T, dims=block_count, order="F") - return indices[0] if output_shape == () else indices.reshape(output_shape) - - -def _index_to_ijk(block_count, index): - arr = np.asarray(index) - if arr.dtype.kind not in "ui": - raise TypeError(f"'index' must be integer typed, found {arr.dtype}") - output_shape = arr.shape + (3,) - shaped = arr.reshape(-1) - if (shaped < 0).any() or (shaped >= np.prod(block_count)).any(): - raise IndexError(f"0 <= index < {np.prod(block_count)} failed") - ijk = np.unravel_index(indices=shaped, shape=block_count, order="F") - return np.c_[ijk[0], ijk[1], ijk[2]].reshape(output_shape) - - class RegularBlockModelDefinition(BaseModel): """Defines the block structure of a regular block model. If used on a sub-blocked model then everything here applies to the parent blocks only. """ - schema = "org.omf.v2.blockmodel.definition.regular" + schema = "org.omf.v2.elements.blockmodel.regular" block_count = properties.Array("Number of blocks in each of the u, v, and w directions.", dtype=int, shape=(3,)) block_size = properties.Vector3("Size of blocks in the u, v, and w directions.", default=lambda: (1.0, 1.0, 1.0)) @@ -62,7 +37,7 @@ def _validate_block_size(self, change): class TensorBlockModelDefinition(BaseModel): """Defines the block structure of a tensor grid block model.""" - schema = "org.omf.v2.blockmodel.definition.tensor" + schema = "org.omf.v2.elements.blockmodel.tensor" tensor_u = properties.Array("Tensor cell widths, u-direction", dtype=float, shape=("*",)) tensor_v = properties.Array("Tensor cell widths, v-direction", dtype=float, shape=("*",)) @@ -162,10 +137,10 @@ def ijk_to_index(self, ijk): """Map IJK triples to flat indices for a single triple or an array, preserving shape.""" if self.definition.block_count is None: raise ValueError("block count is not yet known") - return _ijk_to_index(self.definition.block_count, ijk) + return ijk_to_index(self.definition.block_count, ijk) def index_to_ijk(self, index): """Map flat indices to IJK triples for a single index or an array, preserving shape.""" if self.definition.block_count is None: raise ValueError("block count is not yet known") - return _index_to_ijk(self.definition.block_count, index) + return index_to_ijk(self.definition.block_count, index) diff --git a/omf/blockmodel/regular_subblocks.py b/omf/blockmodel/regular_subblocks.py index 248c494a..605fe654 100644 --- a/omf/blockmodel/regular_subblocks.py +++ b/omf/blockmodel/regular_subblocks.py @@ -4,33 +4,21 @@ from ..attribute import ArrayInstanceProperty from ..base import BaseModel -from ._subblock_check import check_subblocks, shrink_uint +from ._utils import shrink_uint, SubblockChecker -__all__ = ["OctreeSubblockDefinition", "RegularSubblockDefinition", "RegularSubblocks"] +__all__ = ["RegularSubblocks", "SubblockModeOctree", "SubblockModeFull"] -class RegularSubblockDefinition(BaseModel): - """The simplest gridded sub-block definition. +class SubblockModeFull(BaseModel): + """The parent is fully sub-blocked, with each sub-block having size (1, 1, 1). - Divide the parent block into a regular grid of `subblock_count` cells. Each block covers - a cuboid region within that grid. If a parent block is not sub-blocked then it will still - contain a single block that covers the entire grid. + The importing app may want to merge cells to make this more efficient. """ - schema = "org.omf.v2.blockmodel.subblocks.definition.regular" - - subblock_count = properties.Array( - "The maximum number of sub-blocks inside a parent in each direction.", dtype=int, shape=(3,) - ) - - @properties.validator("subblock_count") - def _validate_subblock_count(self, change): - for item in change["value"]: - if item < 1: - raise properties.ValidationError("sub-block counts must be >= 1", prop=change["name"], instance=self) + schema = "org.omf.v2.elements.blockmodel.subblocks.mode_full" -class OctreeSubblockDefinition(BaseModel): +class SubblockModeOctree(BaseModel): """Sub-blocks form an octree inside the parent block. Cut the parent block in half in all directions to create eight sub-blocks. Repeat that @@ -42,36 +30,25 @@ class OctreeSubblockDefinition(BaseModel): all axes to be equal. """ - schema = "org.omf.v2.blockmodel.subblocks.definition.octree" - - subblock_count = properties.Array( - "The maximum number of sub-blocks inside a parent in each direction.", dtype=int, shape=(3,) - ) - - @properties.validator("subblock_count") - def _validate_subblock_count(self, change): - for item in change["value"]: - if item < 1: - raise properties.ValidationError("sub-block counts must be >= 1", prop=change["name"], instance=self) - log = np.log2(item) - if np.trunc(log) != log: - raise properties.ValidationError( - "octree sub-block counts must be powers of two", prop=change["name"], instance=self - ) + schema = "org.omf.v2.elements.blockmodel.subblocks.mode_octree" class RegularSubblocks(BaseModel): """Defines regular or octree sub-blocks for a block model. - These sub-blocks must align with a lower-level grid inside the parent block. + Divide the parent block into a regular grid of `subblock_count` cells. Each block covers + a cuboid region within that grid. """ - schema = "org.omf.v2.blockmodel.subblocks.regular" + schema = "org.omf.v2.elements.blockmodel.subblocks.regular" - definition = properties.Union( + subblock_count = properties.Array( + "The maximum number of sub-blocks inside a parent in each direction.", dtype=int, shape=(3,) + ) + mode = properties.Union( "Defines the structure of sub-blocks within each parent block.", - props=[RegularSubblockDefinition, OctreeSubblockDefinition], - default=RegularSubblockDefinition, + props=[SubblockModeFull, SubblockModeOctree], + required=False, ) parent_indices = ArrayInstanceProperty( "The parent block IJK index of each sub-block", @@ -87,19 +64,39 @@ class RegularSubblocks(BaseModel): Sub-blocks must stay within the parent block and should not overlap. Gaps are allowed but it will be impossible for 'cell' attributes to assign values to - those areas. + those areas. A paret that is not sub-blocked but does have attributes should + be represented as a sub-block that covers the entire parent block. """, shape=("*", 6), dtype=int, ) + @properties.validator("subblock_count") + def _validate_subblock_count(self, change): + for item in change["value"]: + if item < 1: + raise properties.ValidationError("sub-block counts must be >= 1", prop=change["name"], instance=self) + + @properties.validator + def _validate(self): + if isinstance(self.mode, SubblockModeOctree): + for item in self.subblock_count: + log = np.log2(item) + if np.trunc(log) != log: + raise properties.ValidationError( + "in octree mode sub-block counts must be powers of two", prop="subblock_count", instance=self + ) + def validate_subblocks(self, model): """Checks the sub-block data against the given block model definition.""" shrink_uint(self.parent_indices) shrink_uint(self.corners) - check_subblocks( - model, self, instance=self, regular=True, octree=isinstance(self.definition, OctreeSubblockDefinition) - ) + checker = SubblockChecker.from_regular(model) + if isinstance(self.mode, SubblockModeOctree): + checker.octree = True + if isinstance(self.mode, SubblockModeFull): + checker.full = True + checker.check() @property def num_subblocks(self): diff --git a/tests/test_blockmodel.py b/tests/test_blockmodel.py index 267e9b18..a0d37be2 100644 --- a/tests/test_blockmodel.py +++ b/tests/test_blockmodel.py @@ -4,7 +4,7 @@ import pytest import omf -from omf.blockmodel.model import _ijk_to_index, _index_to_ijk +from omf.blockmodel._utils import ijk_to_index, index_to_ijk def _make_regular_definition(count): @@ -15,35 +15,35 @@ def test_ijk_index_errors(): """Test ijk indexing into parent blocks errors as expected""" with pytest.raises(TypeError): - _ijk_to_index([3, 4, 5], "a") + ijk_to_index([3, 4, 5], "a") with pytest.raises(TypeError): - _index_to_ijk([3, 4, 5], "a") + index_to_ijk([3, 4, 5], "a") with pytest.raises(ValueError): - _ijk_to_index([3, 4, 5], [0, 0]) + ijk_to_index([3, 4, 5], [0, 0]) with pytest.raises(TypeError): - _ijk_to_index([3, 4, 5], [0, 0, 0.5]) + ijk_to_index([3, 4, 5], [0, 0, 0.5]) with pytest.raises(TypeError): - _index_to_ijk([3, 4, 5], 0.5) + index_to_ijk([3, 4, 5], 0.5) with pytest.raises(IndexError): - _ijk_to_index([3, 4, 5], [0, 0, 5]) + ijk_to_index([3, 4, 5], [0, 0, 5]) with pytest.raises(IndexError): - _index_to_ijk([3, 4, 5], 60) + index_to_ijk([3, 4, 5], 60) with pytest.raises(IndexError): - _ijk_to_index([3, 4, 5], [[0, 0, 5], [0, 0, 3]]) + ijk_to_index([3, 4, 5], [[0, 0, 5], [0, 0, 3]]) with pytest.raises(IndexError): - _index_to_ijk([3, 4, 5], [0, 1, 60]) + index_to_ijk([3, 4, 5], [0, 1, 60]) def test_ijk_index_arrays(): """Test ijk array indexing into parent blocks works as expected""" ijk = [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1), (2, 3, 4)] index = [0, 1, 3, 12, 59] - assert np.array_equal(_ijk_to_index([3, 4, 5], ijk), index) - assert np.array_equal(_index_to_ijk([3, 4, 5], index), ijk) + assert np.array_equal(ijk_to_index([3, 4, 5], ijk), index) + assert np.array_equal(index_to_ijk([3, 4, 5], index), ijk) ijk = [[(0, 0, 0), (1, 0, 0)], [(0, 1, 0), (0, 0, 1)]] index = [(0, 1), (3, 12)] - assert np.array_equal(_ijk_to_index([3, 4, 5], ijk), index) - assert np.array_equal(_index_to_ijk([3, 4, 5], index), ijk) + assert np.array_equal(ijk_to_index([3, 4, 5], ijk), index) + assert np.array_equal(index_to_ijk([3, 4, 5], index), ijk) @pytest.mark.parametrize( @@ -52,8 +52,8 @@ def test_ijk_index_arrays(): ) def test_ijk_index(ijk, index): """Test ijk indexing into parent blocks works as expected""" - assert _ijk_to_index([3, 4, 5], ijk) == index - assert np.array_equal(_index_to_ijk([3, 4, 5], index), ijk) + assert ijk_to_index([3, 4, 5], ijk) == index + assert np.array_equal(index_to_ijk([3, 4, 5], index), ijk) def test_tensorblockmodel(): diff --git a/tests/test_subblockedmodel.py b/tests/test_subblockedmodel.py index f2215696..8d982f2b 100644 --- a/tests/test_subblockedmodel.py +++ b/tests/test_subblockedmodel.py @@ -4,12 +4,12 @@ import pytest import omf -from omf.blockmodel import _subblock_check +from omf.blockmodel import _utils def test_group_by(): """Test the array grouping function used by sub-block checks.""" - group_by = _subblock_check._group_by # pylint: disable=W0212 + group_by = _utils._group_by # pylint: disable=W0212 arr = np.array([0, 0, 1, 1, 1, 2]) assert list(group_by(arr)) == [(0, 2, 0), (2, 5, 1), (5, 6, 2)] arr = np.ones(1, dtype=int) @@ -26,8 +26,7 @@ def _bm_def(): def _test_regular(*corners): - block_model = omf.BlockModel(subblocks=omf.RegularSubblocks(), definition=_bm_def()) - block_model.subblocks.definition = omf.RegularSubblockDefinition(subblock_count=(5, 4, 3)) + block_model = omf.BlockModel(definition=_bm_def(), subblocks=omf.RegularSubblocks(subblock_count=(5, 4, 3))) block_model.subblocks.corners = np.array(corners) block_model.subblocks.parent_indices = np.zeros((len(corners), 3), dtype=int) block_model.validate() @@ -45,7 +44,7 @@ def test_outside_parent(): _test_regular((0, 0, -1, 4, 4, 1)) with pytest.raises(properties.ValidationError, match="min_corner < max_corner"): _test_regular((4, 0, 0, 0, 4, 2)) - with pytest.raises(properties.ValidationError, match=r"max_corner <= \(5, 4, 3\)"): + with pytest.raises(properties.ValidationError, match=r"max_corner <= \[5 4 3\]"): _test_regular((0, 0, 0, 4, 5, 2)) @@ -53,7 +52,7 @@ def test_invalid_parent_indices(): """Test invalid parent block indices are rejected.""" block_model = omf.BlockModel(subblocks=omf.RegularSubblocks()) block_model.definition = _bm_def() - block_model.subblocks.definition = omf.RegularSubblockDefinition(subblock_count=(5, 4, 3)) + block_model.subblocks.subblock_count = (5, 4, 3) block_model.subblocks.corners = np.array([(0, 0, 0, 5, 4, 3), (0, 0, 0, 5, 4, 3)]) block_model.subblocks.parent_indices = np.array([(0, 0, 0), (1, 0, 0)]) with pytest.raises(properties.ValidationError, match=r"subblock_parent_indices < \(1, 1, 1\)"): @@ -66,7 +65,7 @@ def test_invalid_parent_indices(): def _test_octree(*corners): block_model = omf.BlockModel( definition=_bm_def(), - subblocks=omf.RegularSubblocks(definition=omf.OctreeSubblockDefinition(subblock_count=(4, 4, 2))), + subblocks=omf.RegularSubblocks(subblock_count=(4, 4, 2), mode=omf.SubblockModeOctree()), ) block_model.subblocks.corners = np.array(corners) block_model.subblocks.parent_indices = np.zeros((len(corners), 3), dtype=int) @@ -107,10 +106,10 @@ def test_bad_position(): def test_pack_subblock_arrays(): """Test that packing of uint arrays during validation works.""" block_model = omf.BlockModel() - block_model.subblocks = omf.RegularSubblocks() - block_model.subblocks.definition.subblock_count = [2, 2, 2] block_model.definition.block_size = [1.0, 1.0, 1.0] block_model.definition.block_count = [10, 10, 10] + block_model.subblocks = omf.RegularSubblocks() + block_model.subblocks.subblock_count = [2, 2, 2] block_model.subblocks.parent_indices = np.array([(0, 0, 0)], dtype=int) block_model.subblocks.corners = np.array([(0, 0, 0, 2, 2, 2)], dtype=int) block_model.validate() @@ -122,12 +121,13 @@ def test_uninstantiated(): """Test that definitions are default and attributes are None on instantiation""" block_model = omf.BlockModel(subblocks=omf.RegularSubblocks()) assert isinstance(block_model.definition, omf.RegularBlockModelDefinition) - assert isinstance(block_model.subblocks.definition, omf.RegularSubblockDefinition) assert block_model.definition.block_count is None - assert block_model.subblocks.definition.subblock_count is None assert block_model.num_cells is None + assert isinstance(block_model.subblocks, omf.RegularSubblocks) + assert block_model.subblocks.subblock_count is None assert block_model.subblocks.parent_indices is None assert block_model.subblocks.corners is None + assert block_model.subblocks.mode is None np.testing.assert_array_equal(block_model.definition.block_size, (1.0, 1.0, 1.0)) @@ -136,9 +136,9 @@ def test_num_cells(): block_model = omf.BlockModel(subblocks=omf.RegularSubblocks()) block_model.definition.block_count = [2, 2, 2] block_model.definition.block_size = [1.0, 2.0, 3.0] - block_model.subblocks.definition.subblock_count = [5, 5, 5] + block_model.subblocks.subblock_count = [5, 5, 5] np.testing.assert_array_equal(block_model.definition.block_count, [2, 2, 2]) - np.testing.assert_array_equal(block_model.subblocks.definition.subblock_count, [5, 5, 5]) + np.testing.assert_array_equal(block_model.subblocks.subblock_count, [5, 5, 5]) block_model.subblocks.parent_indices = np.array([(0, 0, 0), (1, 0, 0)]) block_model.subblocks.corners = np.array([(0, 0, 0, 5, 5, 5), (1, 1, 1, 4, 4, 4)]) assert block_model.num_cells == 2 From 2a675665e31a5014648d059e0f2b17ba68545a1f Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Thu, 16 Mar 2023 14:30:13 +1300 Subject: [PATCH 35/42] Removed useless code, cleaned up, and improved documentation. --- assets/v2/test_file.omf | Bin 42193 -> 42176 bytes docs/content/blockmodel.rst | 63 +++++------- docs/content/examples.rst | 4 +- omf/__init__.py | 2 +- omf/base.py | 2 +- omf/blockmodel/__init__.py | 3 +- omf/blockmodel/freeform_subblocks.py | 47 --------- omf/blockmodel/index.py | 32 ++++++ omf/blockmodel/model.py | 75 +++++++------- omf/blockmodel/regular_subblocks.py | 104 ------------------- omf/blockmodel/subblock_check.py | 146 +++++++++++++++++++++++++++ omf/blockmodel/subblocks.py | 135 +++++++++++++++++++++++++ omf/compat/omf_v1.py | 13 ++- tests/test_blockmodel.py | 52 +++++----- tests/test_subblockedmodel.py | 64 ++++++------ 15 files changed, 441 insertions(+), 301 deletions(-) delete mode 100644 omf/blockmodel/freeform_subblocks.py create mode 100644 omf/blockmodel/index.py delete mode 100644 omf/blockmodel/regular_subblocks.py create mode 100644 omf/blockmodel/subblock_check.py create mode 100644 omf/blockmodel/subblocks.py diff --git a/assets/v2/test_file.omf b/assets/v2/test_file.omf index b7925e3429718cf48d18770a2f28b3aa11d8143a..df5e4025ea045705a9930bb1ac3c5d9a16aa6ea1 100644 GIT binary patch literal 42176 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 Yyrv2Ua^FKiAx6IMkb7!r?myrDA0sW082|tP literal 42193 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. For block models, -:code:`location='parent_blocks'`, :code:`location='vertices'`, and :code:`location='cells'` -are valid. +Attributes is a list of :ref:`attributes `. + +For block models :code:`location='parent_blocks'`, or the backward compatible +:code:`location='cells'`, places attribute values on the parent blocks. There must be a +value for each parent block and ordering is such that as you move down the attribute +array the U index increases fastest, then V, and finally W. + +Using :code:`location='vertices'` instead puts the attribute values on the parent block +corners. The ordering is the same. + +Sub-blocked models can still have attributes on their parent blocks using the above modes, +or on the sub-blocks using :code:`location='subblocks'`. For sub-blocks the ordering +matches the `corners` array. diff --git a/docs/content/examples.rst b/docs/content/examples.rst index 87875c6a..ba4c038b 100644 --- a/docs/content/examples.rst +++ b/docs/content/examples.rst @@ -141,10 +141,10 @@ bottom of page). ), ], ) - vol = omf.BlockModel( + vol = omf.blockmodel.BlockModel( name="vol", origin=[10.0, 10.0, -10], - definition=omf.TensorBlockModelDefinition( + grid=omf.blockmodel.TensorGrid( tensor_u=np.ones(10, dtype=float), tensor_v=np.ones(15, dtype=float), tensor_w=np.ones(20, dtype=float), diff --git a/omf/__init__.py b/omf/__init__.py index 41cae1a3..8efa2a44 100644 --- a/omf/__init__.py +++ b/omf/__init__.py @@ -1,4 +1,5 @@ """omf: API library for Open Mining Format file interchange format""" +from . import blockmodel from .attribute import ( Array, CategoryAttribute, @@ -10,7 +11,6 @@ VectorAttribute, ) from .base import Project -from .blockmodel import * from .composite import Composite from .fileio import __version__, load, save from .lineset import LineSet diff --git a/omf/base.py b/omf/base.py index 0bcae3ca..d98c39cc 100644 --- a/omf/base.py +++ b/omf/base.py @@ -204,7 +204,7 @@ class ProjectElementAttribute(ContentModel): "faces", "cells", "parent_blocks", - "sub_blocks", + "subblocks", "elements", ), ) diff --git a/omf/blockmodel/__init__.py b/omf/blockmodel/__init__.py index 27c0a5b1..c46ff95a 100644 --- a/omf/blockmodel/__init__.py +++ b/omf/blockmodel/__init__.py @@ -1,4 +1,3 @@ """blockmodel/__init__.py: sub-package for block models.""" -from .freeform_subblocks import * from .model import * -from .regular_subblocks import * +from .subblocks import * diff --git a/omf/blockmodel/freeform_subblocks.py b/omf/blockmodel/freeform_subblocks.py deleted file mode 100644 index 3594f45e..00000000 --- a/omf/blockmodel/freeform_subblocks.py +++ /dev/null @@ -1,47 +0,0 @@ -"""blockmodel/subblocks.py: sub-block definitions and containers.""" -import properties - -from ..attribute import ArrayInstanceProperty -from ..base import BaseModel -from ._utils import shrink_uint, SubblockChecker - -__all__ = ["FreeformSubblocks"] - - -class FreeformSubblocks(BaseModel): - """Defines free-form sub-blocks for a block model. - - These sub-blocks can exist anywhere without the parent block, subject to any extra - conditions the sub-block definition imposes. - """ - - schema = "org.omf.v2.elements.blockmodel.subblocks.freeform" - - parent_indices = ArrayInstanceProperty( - "The parent block IJK index of each sub-block", - shape=("*", 3), - dtype=int, - ) - corners = ArrayInstanceProperty( - """The positions of the sub-block corners on the grid within their parent block. - - The columns are (min_i, min_j, min_k, max_i, max_j, max_k). Values must be - between 0.0 and 1.0 inclusive. - - Sub-blocks must stay within the parent block and should not overlap. Gaps are - allowed but it will be impossible for 'cell' attributes to assign values to - those areas. - """, - shape=("*", 6), - dtype=float, - ) - - def validate_subblocks(self, model): - """Checks the sub-block data against the given block model definition.""" - shrink_uint(self.parent_indices) - SubblockChecker.from_freeform(model).check() - - @property - def num_subblocks(self): - """The total number of sub-blocks.""" - return None if self.corners is None else len(self.corners) diff --git a/omf/blockmodel/index.py b/omf/blockmodel/index.py new file mode 100644 index 00000000..d0b66213 --- /dev/null +++ b/omf/blockmodel/index.py @@ -0,0 +1,32 @@ +"""blockmodel/index.py: functions for handling block indexes.""" +import numpy as np + +__all__ = ["ijk_to_index", "index_to_ijk"] + + +def ijk_to_index(block_count, ijk): + """Maps IJK index to a flat index, scalar or array.""" + arr = np.asarray(ijk) + if arr.dtype.kind not in "ui": + raise TypeError(f"'ijk' must be integer typed, found {arr.dtype}") + if not arr.shape or arr.shape[-1] != 3: + raise ValueError("'ijk' must have 3 elements or be an array with shape (*_, 3)") + output_shape = arr.shape[:-1] + shaped = arr.reshape(-1, 3) + if (shaped < 0).any() or (shaped >= block_count).any(): + raise IndexError(f"0 <= ijk < ({block_count[0]}, {block_count[1]}, {block_count[2]}) failed") + indices = np.ravel_multi_index(multi_index=shaped.T, dims=block_count, order="F") + return indices[0] if output_shape == () else indices.reshape(output_shape) + + +def index_to_ijk(block_count, index): + """Maps flat index to a IJK index, scalar or array.""" + arr = np.asarray(index) + if arr.dtype.kind not in "ui": + raise TypeError(f"'index' must be integer typed, found {arr.dtype}") + output_shape = arr.shape + (3,) + shaped = arr.reshape(-1) + if (shaped < 0).any() or (shaped >= np.prod(block_count)).any(): + raise IndexError(f"0 <= index < {np.prod(block_count)} failed") + ijk = np.unravel_index(indices=shaped, shape=block_count, order="F") + return np.c_[ijk[0], ijk[1], ijk[2]].reshape(output_shape) diff --git a/omf/blockmodel/model.py b/omf/blockmodel/model.py index 6337472d..2d7b2d44 100644 --- a/omf/blockmodel/model.py +++ b/omf/blockmodel/model.py @@ -3,20 +3,17 @@ import properties from ..base import BaseModel, ProjectElement -from ._utils import ijk_to_index, index_to_ijk -from .freeform_subblocks import FreeformSubblocks -from .regular_subblocks import RegularSubblocks +from .index import ijk_to_index, index_to_ijk +from .subblock_check import subblock_check +from .subblocks import FreeformSubblocks, RegularSubblocks -__all__ = ["BlockModel", "RegularBlockModelDefinition", "TensorBlockModelDefinition"] +__all__ = ["BlockModel", "RegularGrid", "TensorGrid"] -class RegularBlockModelDefinition(BaseModel): - """Defines the block structure of a regular block model. +class RegularGrid(BaseModel): + """Describes a regular grid of blocks with equal sizes.""" - If used on a sub-blocked model then everything here applies to the parent blocks only. - """ - - schema = "org.omf.v2.elements.blockmodel.regular" + schema = "org.omf.v2.elements.blockmodel.grid.regular" block_count = properties.Array("Number of blocks in each of the u, v, and w directions.", dtype=int, shape=(3,)) block_size = properties.Vector3("Size of blocks in the u, v, and w directions.", default=lambda: (1.0, 1.0, 1.0)) @@ -34,10 +31,10 @@ def _validate_block_size(self, change): raise properties.ValidationError("block sizes must be > 0.0", prop=change["name"], instance=self) -class TensorBlockModelDefinition(BaseModel): - """Defines the block structure of a tensor grid block model.""" +class TensorGrid(BaseModel): + """Describes a grid with varied spacing in each direction.""" - schema = "org.omf.v2.elements.blockmodel.tensor" + schema = "org.omf.v2.elements.blockmodel.grid.tensor" tensor_u = properties.Array("Tensor cell widths, u-direction", dtype=float, shape=("*",)) tensor_v = properties.Array("Tensor cell widths, v-direction", dtype=float, shape=("*",)) @@ -63,10 +60,16 @@ def block_count(self): class BlockModel(ProjectElement): - """A block model, details are in the definition and sub-blocks attributes.""" + """A block model, details are in the definition and sub-blocks attributes. + + Sub-blocking is stored in the `subblocks` attribute. Use :code:`None` if there are no + sub-blocks, :class:`omf.blockmodel.RegularSubblocks` if the sub-blocks lie on a regular + grid within each parent block, or :class:`omf.blockmodel.FreeformSubblocks` if the + sub-blocks are not constrained. + """ schema = "org.omf.v2.elements.blockmodel" - _valid_locations = ("parent_blocks", "vertices", "cells") + _valid_locations = ("parent_blocks", "subblocks", "vertices", "cells") origin = properties.Vector3( "Minimum corner of the block model relative to Project coordinate reference system", @@ -75,16 +78,18 @@ class BlockModel(ProjectElement): 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) axis_w = properties.Vector3("Vector orientation of w-direction", default="Z", length=1) - definition = properties.Union( - """Block model definition, describing either a regular or tensor-based block layout.""", - props=[RegularBlockModelDefinition, TensorBlockModelDefinition], - default=RegularBlockModelDefinition, + grid = properties.Union( + """Describes the grid that the blocks occupy, either regular or tensor.""", + props=[RegularGrid, TensorGrid], + default=RegularGrid, ) subblocks = properties.Union( - """Optional sub-block details. + """Optional sub-blocks. - If this is `None` then there are no sub-blocks. Otherwise it can be a `FreeformSubblocks` - or `RegularSubblocks` object to define different types of sub-blocks. + Use :code:`None` if there are no sub-blocks, :class:`omf.blockmodel.RegularSubblocks` + if the sub-blocks lie on a regular grid within each parent block, or + :class:`omf.blockmodel.FreeformSubblocks` if the sub-blocks can be placed anywhere + within the parent block. """, props=[FreeformSubblocks, RegularSubblocks], required=False, @@ -98,8 +103,7 @@ def _validate(self): and np.abs(self.axis_w.dot(self.axis_u) < 1e-6) ): raise properties.ValidationError("axis_u, axis_v, and axis_w must be orthogonal", instance=self) - if self.subblocks is not None: - self.subblocks.validate_subblocks(self) + subblock_check(self) @property def block_count(self): @@ -107,40 +111,35 @@ def block_count(self): Equivalent to `block_model.definition.block_count`. """ - return self.definition.block_count + return self.grid.block_count @property def num_parent_blocks(self): """The number of cells.""" - return np.prod(self.definition.block_count) + return np.prod(self.grid.block_count) @property def num_parent_vertices(self): """Number of nodes or vertices.""" - count = self.definition.block_count + count = self.grid.block_count return None if count is None else np.prod(count + 1) - @property - def num_cells(self): - """The number of cells.""" - return self.num_parent_blocks if self.subblocks is None else self.subblocks.num_subblocks - def location_length(self, location): """Return correct attribute length for 'location'.""" if location == "vertices": return self.num_parent_vertices - if location == "cells" and self.subblocks is not None: - return self.subblocks.num_subblocks + if location == "subblocks": + return None if self.subblocks is None else self.subblocks.num_subblocks return self.num_parent_blocks def ijk_to_index(self, ijk): """Map IJK triples to flat indices for a single triple or an array, preserving shape.""" - if self.definition.block_count is None: + if self.grid.block_count is None: raise ValueError("block count is not yet known") - return ijk_to_index(self.definition.block_count, ijk) + return ijk_to_index(self.block_count, ijk) def index_to_ijk(self, index): """Map flat indices to IJK triples for a single index or an array, preserving shape.""" - if self.definition.block_count is None: + if self.block_count is None: raise ValueError("block count is not yet known") - return index_to_ijk(self.definition.block_count, index) + return index_to_ijk(self.block_count, index) diff --git a/omf/blockmodel/regular_subblocks.py b/omf/blockmodel/regular_subblocks.py deleted file mode 100644 index 605fe654..00000000 --- a/omf/blockmodel/regular_subblocks.py +++ /dev/null @@ -1,104 +0,0 @@ -"""blockmodel/subblocks.py: sub-block definitions and containers.""" -import numpy as np -import properties - -from ..attribute import ArrayInstanceProperty -from ..base import BaseModel -from ._utils import shrink_uint, SubblockChecker - -__all__ = ["RegularSubblocks", "SubblockModeOctree", "SubblockModeFull"] - - -class SubblockModeFull(BaseModel): - """The parent is fully sub-blocked, with each sub-block having size (1, 1, 1). - - The importing app may want to merge cells to make this more efficient. - """ - - schema = "org.omf.v2.elements.blockmodel.subblocks.mode_full" - - -class SubblockModeOctree(BaseModel): - """Sub-blocks form an octree inside the parent block. - - Cut the parent block in half in all directions to create eight sub-blocks. Repeat that - division for some or all of those new sub-blocks. Continue doing that until the limit - on sub-block count is reached or until the sub-blocks accurately model the inputs. - - This definition also allows the lower level cuts to be omitted in one or two axes, - giving a maximum sub-block count of (16, 16, 4) for example rather than requiring - all axes to be equal. - """ - - schema = "org.omf.v2.elements.blockmodel.subblocks.mode_octree" - - -class RegularSubblocks(BaseModel): - """Defines regular or octree sub-blocks for a block model. - - Divide the parent block into a regular grid of `subblock_count` cells. Each block covers - a cuboid region within that grid. - """ - - schema = "org.omf.v2.elements.blockmodel.subblocks.regular" - - subblock_count = properties.Array( - "The maximum number of sub-blocks inside a parent in each direction.", dtype=int, shape=(3,) - ) - mode = properties.Union( - "Defines the structure of sub-blocks within each parent block.", - props=[SubblockModeFull, SubblockModeOctree], - required=False, - ) - parent_indices = ArrayInstanceProperty( - "The parent block IJK index of each sub-block", - shape=("*", 3), - dtype=int, - ) - corners = ArrayInstanceProperty( - """The positions of the sub-block corners on the grid within their parent block. - - The columns are (min_i, min_j, min_k, max_i, max_j, max_k). Values must be - greater than or equal to zero and less than or equal to the maximum number of - sub-blocks in that axis. - - Sub-blocks must stay within the parent block and should not overlap. Gaps are - allowed but it will be impossible for 'cell' attributes to assign values to - those areas. A paret that is not sub-blocked but does have attributes should - be represented as a sub-block that covers the entire parent block. - """, - shape=("*", 6), - dtype=int, - ) - - @properties.validator("subblock_count") - def _validate_subblock_count(self, change): - for item in change["value"]: - if item < 1: - raise properties.ValidationError("sub-block counts must be >= 1", prop=change["name"], instance=self) - - @properties.validator - def _validate(self): - if isinstance(self.mode, SubblockModeOctree): - for item in self.subblock_count: - log = np.log2(item) - if np.trunc(log) != log: - raise properties.ValidationError( - "in octree mode sub-block counts must be powers of two", prop="subblock_count", instance=self - ) - - def validate_subblocks(self, model): - """Checks the sub-block data against the given block model definition.""" - shrink_uint(self.parent_indices) - shrink_uint(self.corners) - checker = SubblockChecker.from_regular(model) - if isinstance(self.mode, SubblockModeOctree): - checker.octree = True - if isinstance(self.mode, SubblockModeFull): - checker.full = True - checker.check() - - @property - def num_subblocks(self): - """The total number of sub-blocks.""" - return None if self.corners is None else len(self.corners) diff --git a/omf/blockmodel/subblock_check.py b/omf/blockmodel/subblock_check.py new file mode 100644 index 00000000..22e09716 --- /dev/null +++ b/omf/blockmodel/subblock_check.py @@ -0,0 +1,146 @@ +"""blockmodel/_subblock_check.py: functions for checking sub-block constraints.""" +from dataclasses import dataclass + +import numpy as np +import properties + +from .index import ijk_to_index +from .subblocks import FreeformSubblocks, RegularSubblocks + +__all__ = ["subblock_check"] + + +def _group_by(arr): + if len(arr) == 0: + return + diff = np.flatnonzero(arr[1:] != arr[:-1]) + diff += 1 + if len(diff) == 0: + yield 0, len(arr), arr[0] + else: + yield 0, diff[0], arr[0] + for start, end in zip(diff[:-1], diff[1:]): + yield start, end, arr[start] + yield diff[-1], len(arr), arr[-1] + + +def _sizes_to_ints(sizes): + sizes = np.array(sizes, dtype=np.uint64) + assert len(sizes.shape) == 2 and sizes.shape[1] == 3 + sizes[:, 0] *= 2**32 + sizes[:, 1] *= 2**16 + return sizes.sum(axis=1) + + +@dataclass +class _Checker: + parent_indices: np.ndarray + corners: np.ndarray + block_count: object = None + subblock_count: np.ndarray = np.ones(3, dtype=float) + regular: bool = False + octree: bool = False + full: bool = False + instance: object = None + + def check(self): + """Run all checks on the given defintions and sub-blocks.""" + if len(self.parent_indices) != len(self.corners): + self._error( + "'subblock_parent_indices' and 'subblock_corners' arrays must be the same length", + prop="subblock_corners", + ) + self._check_inside_parent() + self._check_parent_indices() + if self.regular: + if self.octree: + self._check_octree() + elif self.full: + self._check_full() + self._check_for_overlaps() + + def _error(self, message, prop=None): + raise properties.ValidationError(message, prop=prop, instance=self.instance) + + def _check_parent_indices(self): + if (self.parent_indices < 0).any() or (self.parent_indices >= self.block_count).any(): + nx, ny, nz = self.block_count + self._error(f"0 <= subblock_parent_indices < ({nx}, {ny}, {nz}) failed", prop="subblock_parent_indices") + + def _check_inside_parent(self): + min_corners = self.corners[:, :3] + max_corners = self.corners[:, 3:] + if min_corners.dtype.kind != "u" and not (0 <= min_corners).all(): + self._error("0 <= min_corner failed", prop="subblock_corners") + if not (min_corners < max_corners).all(): + self._error("min_corner < max_corner failed", prop="subblock_corners") + upper = 1.0 if self.subblock_count is None else self.subblock_count + if not (max_corners <= upper).all(): + self._error(f"max_corner <= {upper} failed", prop="subblock_corners") + + def _check_octree(self): + min_corners = self.corners[:, :3] + max_corners = self.corners[:, 3:] + sizes = max_corners - min_corners + # Sizes. + count = self.subblock_count.copy() + valid_sizes = [count.copy()] + while (count > 1).any(): + count[count > 1] //= 2 + valid_sizes.append(count.copy()) + valid_sizes = _sizes_to_ints(valid_sizes) + if not np.isin(_sizes_to_ints(sizes), valid_sizes).all(): + self._error("found non-octree sub-block sizes", prop="subblock_corners") + # Positions; octree blocks always start at a multiple of their size. + if (np.remainder(min_corners, sizes) != 0).any(): + self._error("found non-octree sub-block positions", prop="subblock_corners") + + def _check_full(self): + valid_sizes = _sizes_to_ints([self.subblock_count, (1, 1, 1)]) + sizes = self.corners[:, 3:] - self.corners[:, :3] + if not np.isin(_sizes_to_ints(sizes), valid_sizes).all(): + self._error("found sub-block size that does not match 'full' mode'", prop="subblock_corners") + + def _check_for_overlaps(self): + seen = np.zeros(np.prod(self.block_count), dtype=bool) + for start, end, value in _group_by(ijk_to_index(self.block_count, self.parent_indices)): + if seen[value]: + self._error( + "all sub-blocks inside one parent block must be adjacent in the arrays", + prop="subblock_parent_indices", + ) + seen[value] = True + if end - start > 1: + self._check_group_for_overlaps(self.corners[start:end]) + + def _check_group_for_overlaps(self, corners_in_one_parent): + # This won't be very fast but there doesn't seem to be a better option. + tracker = np.zeros(self.subblock_count[::-1], dtype=int) + for min_i, min_j, min_k, max_i, max_j, max_k in corners_in_one_parent: + tracker[min_k:max_k, min_j:max_j, min_i:max_i] += 1 + if (tracker > 1).any(): + self._error("found overlapping sub-blocks", prop="subblock_corners") + + +def subblock_check(model): + if isinstance(model.subblocks, RegularSubblocks): + checker = _Checker( + parent_indices=model.subblocks.parent_indices.array, + corners=model.subblocks.corners.array, + block_count=model.block_count, + subblock_count=model.subblocks.subblock_count, + regular=True, + octree=model.subblocks.mode == "octree", + full=model.subblocks.mode == "full", + instance=model.subblocks, + ) + elif isinstance(model.subblocks, FreeformSubblocks): + checker = _Checker( + parent_indices=model.subblocks.parent_indices.array, + corners=model.subblocks.corners.array, + block_count=model.block_count, + instance=model.subblocks, + ) + else: + return + checker.check() diff --git a/omf/blockmodel/subblocks.py b/omf/blockmodel/subblocks.py new file mode 100644 index 00000000..823ac793 --- /dev/null +++ b/omf/blockmodel/subblocks.py @@ -0,0 +1,135 @@ +"""blockmodel/subblocks.py: sub-block definitions and containers.""" +from enum import Enum + +import numpy as np +import properties + +from ..attribute import ArrayInstanceProperty +from ..base import BaseModel + +__all__ = ["FreeformSubblocks", "RegularSubblocks"] + + +def _shrink_uint(arr): + kind = arr.array.dtype.kind + if kind == "u" or (kind == "i" and arr.array.min() >= 0): + arr.array = arr.array.astype(np.min_scalar_type(arr.array.max())) + + +class RegularSubblocks(BaseModel): + """Defines regular sub-blocks for a block model. + + Divide the parent block into a regular grid of `subblock_count` cells. Each sub-block + covers a cuboid region within that grid and they must not overlap. Sub-blocks are + described by the `parent_indices` and `corners` arrays. + + Each row in `parent_indices` is an IJK index on the block model grid. Each row of + `corners` is (i_min, j_min, k_min, i_max, j_max, k_max) all integers that refer to + vertices of the sub-block grid within the parent block. For example: + + * A block with minimum size in the corner of the parent block would be (0, 0, 0, 1, 1, 1). + * If the `subblock_count` is (5, 5, 3) then a sub-block covering the whole parent would + be (0, 0, 0, 5, 5, 3). + + Sub-blocks must stay within their parent, must have a non-zero size in all directions, and + must not overlap. Further contraints can be added by setting `mode`: + + "octree" mode + Sub-blocks form a modified octree inside the parent block. To form this structure, + cut the parent block in half in all directions to create eight child blocks. + Repeat that cut for some or all of those children, and continue doing that until the + limit on sub-block count is reached or until the sub-blocks accurately model the inputs. + + This modified form of an octree also allows for stopping all cuts in one or two directions + before the others, so that the `subblock_count` can be (8, 4, 2) rather than (8, 8, 8). + All values in `subblock_count` must be a powers of two but they don't have to be equal. + + "full" mode + The parent blocks must be either fully sub-blocked or not sub-blocked at all. + In other words sub-blocks must either cover the whole parent or have size (1, 1, 1). + """ + + schema = "org.omf.v2.elements.blockmodel.subblocks.regular" + + subblock_count = properties.Array( + "The maximum number of sub-blocks inside a parent in each direction", dtype=int, shape=(3,) + ) + mode = properties.StringChoice( + "Extra constraints on the placement of sub-blocks", + choices=["octree", "full"], + required=False, + ) + parent_indices = ArrayInstanceProperty( + "The sub-block parent IJK indices", + shape=("*", 3), + dtype=int, + ) + corners = ArrayInstanceProperty( + """The integer positions of the sub-block corners on the grid within their parent block""", + shape=("*", 6), + dtype=int, + ) + + @properties.validator("subblock_count") + def _validate_subblock_count(self, change): + for item in change["value"]: + if item < 1: + raise properties.ValidationError("sub-block counts must be >= 1", prop=change["name"], instance=self) + + @properties.validator + def _validate(self): + _shrink_uint(self.parent_indices) + _shrink_uint(self.corners) + if self.mode == "octree": + for item in self.subblock_count: + log = np.log2(item) + if np.trunc(log) != log: + raise properties.ValidationError( + "in octree mode sub-block counts must be powers of two", prop="subblock_count", instance=self + ) + + @property + def num_subblocks(self): + """The total number of sub-blocks.""" + return None if self.corners is None else len(self.corners) + + +class FreeformSubblocks(BaseModel): + """Defines free-form sub-blocks for a block model. + + Sub-blocks are described by the `parent_indices` and `corners` arrays. + + Each row in `parent_indices` is an IJK index on the block model grid. Each row of + `corners` is (i_min, j_min, k_min, i_max, j_max, k_max) all floating point, with 0.0 + referring to the minimum side of the parent block and 1.0 the maximum side. + For example: + + * A block with size (0.3333, 0.1, 0.5) in the corner of the parent block would be + (0.0, 0.0, 0.0, 0.3333, 0.1, 0.5). + * A sub-block covering the whole parent would be (0.0, 0.0, 0.0, 1.0, 1.0, 1.0). + + Sub-blocks must stay within their parent and must have a non-zero size in all directions. + They shouldn't overlap but that isn't checked as it would take too long. + """ + + schema = "org.omf.v2.elements.blockmodel.subblocks.freeform" + + parent_indices = ArrayInstanceProperty( + "The parent block IJK index of each sub-block", + shape=("*", 3), + dtype=int, + ) + corners = ArrayInstanceProperty( + """The positions of the sub-block corners on the grid within their parent block""", + shape=("*", 6), + dtype=float, + ) + + @properties.validator + def _validate(self): + _shrink_uint(self.parent_indices) + + @property + def num_subblocks(self): + """The total number of sub-blocks.""" + return None if self.corners is None else len(self.corners) diff --git a/omf/compat/omf_v1.py b/omf/compat/omf_v1.py index 7b5ca12a..b0826a9a 100644 --- a/omf/compat/omf_v1.py +++ b/omf/compat/omf_v1.py @@ -18,12 +18,12 @@ VectorAttribute, ) from ..base import Project -from ..blockmodel import BlockModel, TensorBlockModelDefinition from ..lineset import LineSet from ..pointset import PointSet from ..surface import Surface, TensorGridSurface from ..texture import Image, ProjectedTexture -from .interface import IOMFReader, InvalidOMFFile, WrongVersionError +from ..blockmodel import BlockModel, TensorGrid +from .interface import InvalidOMFFile, IOMFReader, WrongVersionError COMPATIBILITY_VERSION = b"OMF-v0.9.0" _default = object() @@ -435,16 +435,15 @@ def _convert_volume_element(self, volume_v1): geometry_uuid = self.__get_attr(volume_v1, "geometry") geometry_v1 = self.__get_attr(self._project, geometry_uuid) self.__require_attr(geometry_v1, "__class__", "VolumeGridGeometry") - block_model = BlockModel() + block_model = BlockModel(grid=TensorGrid()) self.__copy_attr(volume_v1, "subtype", block_model.metadata) self.__copy_attr(geometry_v1, "origin", block_model) self.__copy_attr(geometry_v1, "axis_u", block_model) self.__copy_attr(geometry_v1, "axis_v", block_model) self.__copy_attr(geometry_v1, "axis_w", block_model) - block_model.definition = TensorBlockModelDefinition() - self.__copy_attr(geometry_v1, "tensor_u", block_model.definition) - self.__copy_attr(geometry_v1, "tensor_v", block_model.definition) - self.__copy_attr(geometry_v1, "tensor_w", block_model.definition) + self.__copy_attr(geometry_v1, "tensor_u", block_model.grid) + self.__copy_attr(geometry_v1, "tensor_v", block_model.grid) + self.__copy_attr(geometry_v1, "tensor_w", block_model.grid) valid_locations = ("vertices", "cells") return block_model, valid_locations diff --git a/tests/test_blockmodel.py b/tests/test_blockmodel.py index a0d37be2..3686f818 100644 --- a/tests/test_blockmodel.py +++ b/tests/test_blockmodel.py @@ -3,12 +3,8 @@ import properties import pytest -import omf -from omf.blockmodel._utils import ijk_to_index, index_to_ijk - - -def _make_regular_definition(count): - return omf.RegularBlockModelDefinition(block_count=count, block_size=(1.0, 1.0, 1.0)) +from omf.blockmodel import BlockModel, TensorGrid +from omf.blockmodel.index import ijk_to_index, index_to_ijk def test_ijk_index_errors(): @@ -58,14 +54,14 @@ def test_ijk_index(ijk, index): def test_tensorblockmodel(): """Test volume grid geometry validation""" - elem = omf.BlockModel(definition=omf.TensorBlockModelDefinition()) + elem = BlockModel(grid=TensorGrid()) assert elem.num_parent_vertices is None assert elem.num_parent_blocks is None - assert elem.definition.block_count is None - elem.definition.tensor_u = [1.0, 1.0] - elem.definition.tensor_v = [2.0, 2.0, 2.0] - elem.definition.tensor_w = [3.0] - np.testing.assert_array_equal(elem.definition.block_count, [2, 3, 1]) + assert elem.block_count is None + elem.grid.tensor_u = [1.0, 1.0] + elem.grid.tensor_v = [2.0, 2.0, 2.0] + elem.grid.tensor_w = [3.0] + np.testing.assert_array_equal(elem.block_count, [2, 3, 1]) assert elem.validate() assert elem.location_length("vertices") == 24 assert elem.location_length("cells") == 6 @@ -78,39 +74,39 @@ def test_tensorblockmodel(): @pytest.mark.parametrize("block_count", ([2, 2], [2, 2, 2, 2], [0, 2, 2], [2, 2, 0.5])) def test_bad_block_count(block_count): """Test mismatched block_count""" - block_model = omf.BlockModel() - block_model.definition.block_size = [1.0, 2.0, 3.0] + block_model = BlockModel() + block_model.grid.block_size = [1.0, 2.0, 3.0] with pytest.raises((ValueError, properties.ValidationError)): - block_model.definition.block_count = block_count + block_model.grid.block_count = block_count block_model.validate() @pytest.mark.parametrize("block_size", ([2.0, 2.0], [2.0, 2.0, 2.0, 2.0], [-1.0, 2, 2], [0.0, 2, 2])) def test_bad_block_size(block_size): """Test mismatched block_size""" - block_model = omf.BlockModel() - block_model.definition.block_count = [2, 2, 2] + block_model = BlockModel() + block_model.grid.block_count = [2, 2, 2] with pytest.raises((ValueError, properties.ValidationError)): - block_model.definition.block_size = block_size + block_model.grid.block_size = block_size block_model.validate() def test_uninstantiated(): """Test all attributes are None on instantiation""" - block_model = omf.BlockModel() - assert block_model.definition.block_count is None - assert block_model.num_cells is None - np.testing.assert_array_equal(block_model.definition.block_size, (1.0, 1.0, 1.0)) + block_model = BlockModel() + assert block_model.grid.block_count is None + assert block_model.num_parent_blocks is None + assert block_model.num_parent_vertices is None + assert block_model.subblocks is None + np.testing.assert_array_equal(block_model.grid.block_size, (1.0, 1.0, 1.0)) def test_num_cells(): """Test num_cells calculation is correct""" - block_model = omf.BlockModel() - block_model.definition.block_count = [2, 2, 2] - block_model.definition.block_size = [1.0, 2.0, 3.0] - np.testing.assert_array_equal(block_model.definition.block_count, [2, 2, 2]) - assert block_model.num_cells == 8 + block_model = BlockModel() + block_model.grid.block_count = [2, 2, 2] + block_model.grid.block_size = [1.0, 2.0, 3.0] + np.testing.assert_array_equal(block_model.grid.block_count, [2, 2, 2]) assert block_model.location_length("cells") == 8 assert block_model.location_length("vertices") == 27 assert block_model.location_length("parent_blocks") == 8 - assert block_model.location_length("") == 8 diff --git a/tests/test_subblockedmodel.py b/tests/test_subblockedmodel.py index 8d982f2b..2910a5c0 100644 --- a/tests/test_subblockedmodel.py +++ b/tests/test_subblockedmodel.py @@ -3,30 +3,29 @@ import properties import pytest -import omf -from omf.blockmodel import _utils +from omf.blockmodel import BlockModel, RegularGrid, RegularSubblocks +from omf.blockmodel.subblock_check import _group_by # pylint: disable=W0212 def test_group_by(): """Test the array grouping function used by sub-block checks.""" - group_by = _utils._group_by # pylint: disable=W0212 arr = np.array([0, 0, 1, 1, 1, 2]) - assert list(group_by(arr)) == [(0, 2, 0), (2, 5, 1), (5, 6, 2)] + assert list(_group_by(arr)) == [(0, 2, 0), (2, 5, 1), (5, 6, 2)] arr = np.ones(1, dtype=int) - assert list(group_by(arr)) == [(0, 1, 1)] + assert list(_group_by(arr)) == [(0, 1, 1)] arr = np.zeros(0, dtype=int) - assert not list(group_by(arr)) + assert not list(_group_by(arr)) -def _bm_def(): - return omf.RegularBlockModelDefinition( +def _bm_grid(): + return RegularGrid( block_size=(1.0, 1.0, 1.0), block_count=(1, 1, 1), ) def _test_regular(*corners): - block_model = omf.BlockModel(definition=_bm_def(), subblocks=omf.RegularSubblocks(subblock_count=(5, 4, 3))) + block_model = BlockModel(grid=_bm_grid(), subblocks=RegularSubblocks(subblock_count=(5, 4, 3))) block_model.subblocks.corners = np.array(corners) block_model.subblocks.parent_indices = np.zeros((len(corners), 3), dtype=int) block_model.validate() @@ -50,8 +49,8 @@ def test_outside_parent(): def test_invalid_parent_indices(): """Test invalid parent block indices are rejected.""" - block_model = omf.BlockModel(subblocks=omf.RegularSubblocks()) - block_model.definition = _bm_def() + block_model = BlockModel(subblocks=RegularSubblocks()) + block_model.grid = _bm_grid() block_model.subblocks.subblock_count = (5, 4, 3) block_model.subblocks.corners = np.array([(0, 0, 0, 5, 4, 3), (0, 0, 0, 5, 4, 3)]) block_model.subblocks.parent_indices = np.array([(0, 0, 0), (1, 0, 0)]) @@ -63,9 +62,9 @@ def test_invalid_parent_indices(): def _test_octree(*corners): - block_model = omf.BlockModel( - definition=_bm_def(), - subblocks=omf.RegularSubblocks(subblock_count=(4, 4, 2), mode=omf.SubblockModeOctree()), + block_model = BlockModel( + grid=_bm_grid(), + subblocks=RegularSubblocks(subblock_count=(4, 4, 2), mode="octree"), ) block_model.subblocks.corners = np.array(corners) block_model.subblocks.parent_indices = np.zeros((len(corners), 3), dtype=int) @@ -105,10 +104,10 @@ def test_bad_position(): def test_pack_subblock_arrays(): """Test that packing of uint arrays during validation works.""" - block_model = omf.BlockModel() - block_model.definition.block_size = [1.0, 1.0, 1.0] - block_model.definition.block_count = [10, 10, 10] - block_model.subblocks = omf.RegularSubblocks() + block_model = BlockModel() + block_model.grid.block_size = [1.0, 1.0, 1.0] + block_model.grid.block_count = [10, 10, 10] + block_model.subblocks = RegularSubblocks() block_model.subblocks.subblock_count = [2, 2, 2] block_model.subblocks.parent_indices = np.array([(0, 0, 0)], dtype=int) block_model.subblocks.corners = np.array([(0, 0, 0, 2, 2, 2)], dtype=int) @@ -118,30 +117,33 @@ def test_pack_subblock_arrays(): def test_uninstantiated(): - """Test that definitions are default and attributes are None on instantiation""" - block_model = omf.BlockModel(subblocks=omf.RegularSubblocks()) - assert isinstance(block_model.definition, omf.RegularBlockModelDefinition) - assert block_model.definition.block_count is None - assert block_model.num_cells is None - assert isinstance(block_model.subblocks, omf.RegularSubblocks) + """Test that grid is default and attributes are None on instantiation""" + block_model = BlockModel(subblocks=RegularSubblocks()) + assert isinstance(block_model.grid, RegularGrid) + assert block_model.grid.block_count is None + assert block_model.num_parent_blocks is None + assert block_model.num_parent_vertices is None + assert isinstance(block_model.subblocks, RegularSubblocks) assert block_model.subblocks.subblock_count is None + assert block_model.subblocks.num_subblocks is None assert block_model.subblocks.parent_indices is None assert block_model.subblocks.corners is None assert block_model.subblocks.mode is None - np.testing.assert_array_equal(block_model.definition.block_size, (1.0, 1.0, 1.0)) + np.testing.assert_array_equal(block_model.grid.block_size, (1.0, 1.0, 1.0)) def test_num_cells(): """Test num_cells calculation is correct""" - block_model = omf.BlockModel(subblocks=omf.RegularSubblocks()) - block_model.definition.block_count = [2, 2, 2] - block_model.definition.block_size = [1.0, 2.0, 3.0] + block_model = BlockModel(subblocks=RegularSubblocks()) + block_model.grid.block_count = [2, 2, 2] + block_model.grid.block_size = [1.0, 2.0, 3.0] block_model.subblocks.subblock_count = [5, 5, 5] - np.testing.assert_array_equal(block_model.definition.block_count, [2, 2, 2]) + np.testing.assert_array_equal(block_model.grid.block_count, [2, 2, 2]) np.testing.assert_array_equal(block_model.subblocks.subblock_count, [5, 5, 5]) block_model.subblocks.parent_indices = np.array([(0, 0, 0), (1, 0, 0)]) block_model.subblocks.corners = np.array([(0, 0, 0, 5, 5, 5), (1, 1, 1, 4, 4, 4)]) - assert block_model.num_cells == 2 assert block_model.num_parent_blocks == 8 - assert block_model.location_length("cells") == 2 + assert block_model.subblocks.num_subblocks == 2 + assert block_model.location_length("subblocks") == 2 + assert block_model.location_length("cells") == 8 assert block_model.location_length("parent_blocks") == 8 From 1a1b19e62bcb24d09377fa17644e28adabef9087 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Thu, 16 Mar 2023 14:33:45 +1300 Subject: [PATCH 36/42] Added missing part of sub-block docs. --- omf/blockmodel/subblocks.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/omf/blockmodel/subblocks.py b/omf/blockmodel/subblocks.py index 823ac793..6fdbbf4f 100644 --- a/omf/blockmodel/subblocks.py +++ b/omf/blockmodel/subblocks.py @@ -21,7 +21,8 @@ class RegularSubblocks(BaseModel): Divide the parent block into a regular grid of `subblock_count` cells. Each sub-block covers a cuboid region within that grid and they must not overlap. Sub-blocks are - described by the `parent_indices` and `corners` arrays. + described by the `parent_indices` and `corners` arrays. They must be the same length + and matching rows in each describe the same sub-block. Each row in `parent_indices` is an IJK index on the block model grid. Each row of `corners` is (i_min, j_min, k_min, i_max, j_max, k_max) all integers that refer to @@ -32,7 +33,8 @@ class RegularSubblocks(BaseModel): be (0, 0, 0, 5, 5, 3). Sub-blocks must stay within their parent, must have a non-zero size in all directions, and - must not overlap. Further contraints can be added by setting `mode`: + must not overlap. All sub-blocks with the same parent block must be contiguous in the arrays. + Further contraints can be added by setting `mode`: "octree" mode Sub-blocks form a modified octree inside the parent block. To form this structure, @@ -109,7 +111,8 @@ class FreeformSubblocks(BaseModel): * A sub-block covering the whole parent would be (0.0, 0.0, 0.0, 1.0, 1.0, 1.0). Sub-blocks must stay within their parent and must have a non-zero size in all directions. - They shouldn't overlap but that isn't checked as it would take too long. + They shouldn't overlap but that isn't checked as it would take too long. All sub-blocks + with the same parent block must be contiguous in the arrays. """ schema = "org.omf.v2.elements.blockmodel.subblocks.freeform" From 56adf6ad16ca73fcb7282d3c8230599e426408cb Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Thu, 16 Mar 2023 14:43:51 +1300 Subject: [PATCH 37/42] Fixed pylint errors. --- omf/blockmodel/model.py | 2 +- omf/blockmodel/subblock_check.py | 11 +++++++++-- omf/blockmodel/subblocks.py | 4 +--- tests/test_subblockedmodel.py | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/omf/blockmodel/model.py b/omf/blockmodel/model.py index 2d7b2d44..c8bb2d03 100644 --- a/omf/blockmodel/model.py +++ b/omf/blockmodel/model.py @@ -1,4 +1,4 @@ -"""blockmodel/models.py: block model elements.""" +"""blockmodel/models.py: block model element and grids.""" import numpy as np import properties diff --git a/omf/blockmodel/subblock_check.py b/omf/blockmodel/subblock_check.py index 22e09716..9ee779ab 100644 --- a/omf/blockmodel/subblock_check.py +++ b/omf/blockmodel/subblock_check.py @@ -34,6 +34,7 @@ def _sizes_to_ints(sizes): @dataclass class _Checker: + # pylint: disable=too-many-instance-attributes parent_indices: np.ndarray corners: np.ndarray block_count: object = None @@ -64,8 +65,10 @@ def _error(self, message, prop=None): def _check_parent_indices(self): if (self.parent_indices < 0).any() or (self.parent_indices >= self.block_count).any(): - nx, ny, nz = self.block_count - self._error(f"0 <= subblock_parent_indices < ({nx}, {ny}, {nz}) failed", prop="subblock_parent_indices") + self._error( + f"0 <= subblock_parent_indices < {self.block_count} failed", + prop="subblock_parent_indices", + ) def _check_inside_parent(self): min_corners = self.corners[:, :3] @@ -123,6 +126,10 @@ def _check_group_for_overlaps(self, corners_in_one_parent): def subblock_check(model): + """Checks the sub-blocks in the given block model, if any. + + Raises properties.ValidationError if there is a problem. + """ if isinstance(model.subblocks, RegularSubblocks): checker = _Checker( parent_indices=model.subblocks.parent_indices.array, diff --git a/omf/blockmodel/subblocks.py b/omf/blockmodel/subblocks.py index 6fdbbf4f..e99cbf72 100644 --- a/omf/blockmodel/subblocks.py +++ b/omf/blockmodel/subblocks.py @@ -1,6 +1,4 @@ -"""blockmodel/subblocks.py: sub-block definitions and containers.""" -from enum import Enum - +"""blockmodel/subblocks.py: sub-block containers.""" import numpy as np import properties diff --git a/tests/test_subblockedmodel.py b/tests/test_subblockedmodel.py index 2910a5c0..6957b435 100644 --- a/tests/test_subblockedmodel.py +++ b/tests/test_subblockedmodel.py @@ -54,7 +54,7 @@ def test_invalid_parent_indices(): block_model.subblocks.subblock_count = (5, 4, 3) block_model.subblocks.corners = np.array([(0, 0, 0, 5, 4, 3), (0, 0, 0, 5, 4, 3)]) block_model.subblocks.parent_indices = np.array([(0, 0, 0), (1, 0, 0)]) - with pytest.raises(properties.ValidationError, match=r"subblock_parent_indices < \(1, 1, 1\)"): + with pytest.raises(properties.ValidationError, match=r"subblock_parent_indices < \[1 1 1\]"): block_model.validate() block_model.subblocks.parent_indices = np.array([(0, 0, -1), (0, 0, 0)]) with pytest.raises(properties.ValidationError, match="0 <= subblock_parent_indices"): From 6da9dac4e36053ac27a5f90edee7b851128ec1a2 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Thu, 16 Mar 2023 14:49:08 +1300 Subject: [PATCH 38/42] Fixed error in Python 3.11. --- omf/blockmodel/subblock_check.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/omf/blockmodel/subblock_check.py b/omf/blockmodel/subblock_check.py index 9ee779ab..6f592cb1 100644 --- a/omf/blockmodel/subblock_check.py +++ b/omf/blockmodel/subblock_check.py @@ -37,8 +37,8 @@ class _Checker: # pylint: disable=too-many-instance-attributes parent_indices: np.ndarray corners: np.ndarray - block_count: object = None - subblock_count: np.ndarray = np.ones(3, dtype=float) + block_count: np.ndarray + subblock_count: np.ndarray regular: bool = False octree: bool = False full: bool = False @@ -146,6 +146,7 @@ def subblock_check(model): parent_indices=model.subblocks.parent_indices.array, corners=model.subblocks.corners.array, block_count=model.block_count, + subblock_count=np.array((1.0, 1.0, 1.0)), instance=model.subblocks, ) else: From 2476ea8bc534e1eeba9214f5f960ba392ce185bd Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Fri, 17 Mar 2023 15:06:50 +1300 Subject: [PATCH 39/42] Improved free-form docs. --- omf/blockmodel/subblocks.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/omf/blockmodel/subblocks.py b/omf/blockmodel/subblocks.py index e99cbf72..e8f1cc49 100644 --- a/omf/blockmodel/subblocks.py +++ b/omf/blockmodel/subblocks.py @@ -100,16 +100,18 @@ class FreeformSubblocks(BaseModel): Sub-blocks are described by the `parent_indices` and `corners` arrays. Each row in `parent_indices` is an IJK index on the block model grid. Each row of - `corners` is (i_min, j_min, k_min, i_max, j_max, k_max) all floating point, with 0.0 - referring to the minimum side of the parent block and 1.0 the maximum side. + `corners` is (i_min, j_min, k_min, i_max, j_max, k_max) in floating-point and + relative to the parent block, running from 0.0 to 1.0 across the parent block. For example: - * A block with size (0.3333, 0.1, 0.5) in the corner of the parent block would be - (0.0, 0.0, 0.0, 0.3333, 0.1, 0.5). - * A sub-block covering the whole parent would be (0.0, 0.0, 0.0, 1.0, 1.0, 1.0). + * A sub-block covering the whole parent will be (0.0, 0.0, 0.0, 1.0, 1.0, 1.0) + no matter the size of the parent. + * A sub-block covering the bottom third of the parent block would be + (0.0, 0.0, 0.0, 1.0, 1.0, 0.3333) and one covering the top two-thirds would be + (0.0, 0.0, 0.3333, 1.0, 1.0, 1.0), again no matter the size of the parent. Sub-blocks must stay within their parent and must have a non-zero size in all directions. - They shouldn't overlap but that isn't checked as it would take too long. All sub-blocks + They shouldn't overlap but that isn't checked because it would take too long. All sub-blocks with the same parent block must be contiguous in the arrays. """ From 99db672806a4b3c5b81fc87932d08f9fa38da599 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Fri, 17 Mar 2023 15:15:03 +1300 Subject: [PATCH 40/42] Fixed docs on BlockModel. --- omf/blockmodel/model.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/omf/blockmodel/model.py b/omf/blockmodel/model.py index c8bb2d03..8859cd1a 100644 --- a/omf/blockmodel/model.py +++ b/omf/blockmodel/model.py @@ -60,9 +60,12 @@ def block_count(self): class BlockModel(ProjectElement): - """A block model, details are in the definition and sub-blocks attributes. + """A block model with optional sub-blocks. - Sub-blocking is stored in the `subblocks` attribute. Use :code:`None` if there are no + The position and orientation are defined by the `origin`, `axis_u`, `axis_v`, `axis_w` + attributes, while the block layout and size are defined by the `grid` attribute. + + Sub-blocks are stored in the `subblocks` attribute. Use :code:`None` if there are no sub-blocks, :class:`omf.blockmodel.RegularSubblocks` if the sub-blocks lie on a regular grid within each parent block, or :class:`omf.blockmodel.FreeformSubblocks` if the sub-blocks are not constrained. @@ -79,18 +82,12 @@ class BlockModel(ProjectElement): axis_v = properties.Vector3("Vector orientation of v-direction", default="Y", length=1) axis_w = properties.Vector3("Vector orientation of w-direction", default="Z", length=1) grid = properties.Union( - """Describes the grid that the blocks occupy, either regular or tensor.""", + """Describes the grid that the blocks occupy, either regular or tensor""", props=[RegularGrid, TensorGrid], default=RegularGrid, ) subblocks = properties.Union( - """Optional sub-blocks. - - Use :code:`None` if there are no sub-blocks, :class:`omf.blockmodel.RegularSubblocks` - if the sub-blocks lie on a regular grid within each parent block, or - :class:`omf.blockmodel.FreeformSubblocks` if the sub-blocks can be placed anywhere - within the parent block. - """, + """Optional sub-blocks""", props=[FreeformSubblocks, RegularSubblocks], required=False, ) From 05b7d631a6103519507c6cd04e63373588a6cffc Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Thu, 23 Mar 2023 10:02:46 +1300 Subject: [PATCH 41/42] Added examples of writing and reading block models. --- .gitignore | 4 + examples/octree_subblocked_model.py | 133 +++++++++++++++++++++++++++ examples/regular_block_model.py | 95 +++++++++++++++++++ examples/regular_subblocked_model.py | 132 ++++++++++++++++++++++++++ 4 files changed, 364 insertions(+) create mode 100644 examples/octree_subblocked_model.py create mode 100644 examples/regular_block_model.py create mode 100644 examples/regular_subblocked_model.py diff --git a/.gitignore b/.gitignore index c836c6aa..1924e296 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,7 @@ testdata/ # VSCode project settings .vscode/ + +# Example outputs +examples/*.omf +examples/*.csv diff --git a/examples/octree_subblocked_model.py b/examples/octree_subblocked_model.py new file mode 100644 index 00000000..a146c402 --- /dev/null +++ b/examples/octree_subblocked_model.py @@ -0,0 +1,133 @@ +import numpy as np +import omf + + +def write(): + # This writes an OMF file containing a small octree sub-blocked model with a few example attributes. + # Only one of the parent blocks contains sub-blocks. + model = omf.blockmodel.BlockModel( + name="Block Model", + description="A regular block model with a couple of attributes.", + origin=(100.0, 200.0, 50.0), + grid=omf.blockmodel.RegularGrid(block_count=(2, 2, 1), block_size=(10.0, 10.0, 5.0)), + subblocks=omf.blockmodel.RegularSubblocks( + mode="octree", + subblock_count=(4, 4, 2), + parent_indices=np.array([(0, 0, 0)] * 11 + [(1, 0, 0), (0, 1, 0), (1, 1, 0)]), + corners=np.array( + [ + # size 1, 1, 1 + (0, 0, 0, 1, 1, 1), + (1, 0, 0, 2, 1, 1), + (1, 1, 0, 2, 2, 1), + (0, 1, 0, 1, 2, 1), + # size 2, 2, 1 + (2, 0, 0, 4, 2, 1), + (2, 2, 0, 4, 4, 1), + (0, 2, 0, 2, 4, 1), + (0, 0, 1, 2, 2, 2), + (2, 0, 1, 4, 2, 2), + (2, 2, 1, 4, 4, 2), + (0, 2, 1, 2, 4, 2), + # size 4, 4, 2 + (0, 0, 0, 4, 4, 2), + (0, 0, 0, 4, 4, 2), + (0, 0, 0, 4, 4, 2), + ] + ), + ), + ) + model.attributes.append( + omf.NumericAttribute( + name="Number", + description="From 0.0 to 1.0", + location="subblocks", + array=np.arange(14.0) / 13.0, + ) + ) + model.attributes.append( + omf.CategoryAttribute( + name="Category", + description="Checkerboard categories on parent blocks", + location="parent_blocks", + array=np.array([0, 1, 1, 0]), + categories=omf.CategoryColormap( + indices=[0, 1], + values=["White", "Red"], + colors=[(255, 255, 255), (255, 0, 0)], + ), + ) + ) + strings = [] + for i0, j0, k0, i1, j1, k1 in model.subblocks.corners.array: + strings.append(f"{i1 - i0} by {j1 - j0} by {k1 - k0}") + model.attributes.append( + omf.StringAttribute( + name="Strings", + description="Gives the block shape", + location="subblocks", + array=strings, + ) + ) + project = omf.Project() + project.metadata["comment"] = "An OMF file containing an octree sub-blocked model." + project.elements.append(model) + omf.fileio.save(project, "octree_subblocked_model.omf", mode="w") + + +def _subblock_centroid_and_size(model, corners, i, j, k): + min_corner = corners[:3] + max_corner = corners[3:] + # Calculate centre and size within the [0, 1] range of the parent block. + centre = (min_corner + max_corner) / model.subblocks.subblock_count / 2 + size = (max_corner - min_corner) / model.subblocks.subblock_count + # Transform to object space. + subblock_centroid = ( + model.origin + + model.axis_u * model.grid.block_size[0] * (i + centre[0]) + + model.axis_v * model.grid.block_size[1] * (j + centre[1]) + + model.axis_w * model.grid.block_size[2] * (k + centre[2]) + ) + subblock_size = size * model.grid.block_size + return subblock_centroid, subblock_size + + +def read(): + # Reads the OMF file written above and converts it into a CSV file. Category colour data + # is discarded because block model CSV files don't typically store it. + project = omf.fileio.load("octree_subblocked_model.omf") + model = project.elements[0] + assert isinstance(model, omf.blockmodel.BlockModel) + names = [] + data = [] + for attr in model.attributes: + if isinstance(attr, omf.CategoryAttribute): + map = {index: string for index, string in zip(attr.categories.indices, attr.categories.values)} + to_string = map.get + else: + to_string = str + names.append(attr.name) + data.append((attr.array, to_string, attr.location == "parent_blocks")) + with open("octree_subblocked_model.csv", "w") as f: + f.write(f"# {model.name}\n") + f.write(f"# {model.description}\n") + f.write(f"# origin = {model.origin}\n") + f.write(f"# block size = {model.grid.block_size}\n") + f.write(f"# block count = {model.grid.block_count}\n") + f.write(f"# sub-block count = {model.subblocks.subblock_count}\n") + f.write(f"x,y,z,dx,dy,dz,{','.join(names)}\n") + for subblock_index, ((i, j, k), corners) in enumerate( + zip(model.subblocks.parent_indices.array, model.subblocks.corners.array) + ): + parent_index = model.ijk_to_index((i, j, k)) + centroid, size = _subblock_centroid_and_size(model, corners, i, j, k) + f.write(f"{centroid[0]},{centroid[1]},{centroid[2]},{size[0]},{size[1]},{size[2]}") + for array, to_string, on_parent in data: + f.write(",") + f.write(to_string(array[parent_index if on_parent else subblock_index])) + f.write("\n") + + +if __name__ == "__main__": + write() + read() diff --git a/examples/regular_block_model.py b/examples/regular_block_model.py new file mode 100644 index 00000000..17c7ea62 --- /dev/null +++ b/examples/regular_block_model.py @@ -0,0 +1,95 @@ +import numpy as np +import omf + + +def write(): + # This writes an OMF file containing a small regular block model with a few example attributes. + model = omf.blockmodel.BlockModel( + name="Block Model", + description="A regular block model with a couple of attributes.", + origin=(100.0, 200.0, 50.0), + grid=omf.blockmodel.RegularGrid(block_count=(5, 5, 5), block_size=(10.0, 10.0, 5.0)), + ) + model.attributes.append( + omf.NumericAttribute( + name="Number", + description="From 0.0 to 1.0", + location="parent_blocks", + array=np.arange(125.0) / 124.0, + ) + ) + model.attributes.append( + omf.CategoryAttribute( + name="Category", + description="Checkerboard categories", + location="parent_blocks", + array=np.tile(np.array((0, 1)), 63)[:-1], + categories=omf.CategoryColormap( + indices=[0, 1], + values=["White", "Red"], + colors=[(255, 255, 255), (255, 0, 0)], + ), + ) + ) + strings = [] + for i in range(5): + strings += [f"Layer {i + 1}"] * 25 + model.attributes.append( + omf.StringAttribute( + name="Strings", + description="Gives the layer name", + location="parent_blocks", + array=strings, + ) + ) + project = omf.Project() + project.metadata["comment"] = "An OMF file containing a regular block model." + project.elements.append(model) + omf.fileio.save(project, "regular_block_model.omf", mode="w") + + +def read(): + # Reads the OMF file written above and converts it into a CSV file. Category colour data + # is discarded because block model CSV files don't typically store it. + project = omf.fileio.load("regular_block_model.omf") + model = project.elements[0] + assert isinstance(model, omf.blockmodel.BlockModel) + sizes = ",".join(str(s) for s in model.grid.block_size) + names = [] + data = [] + for attr in model.attributes: + if isinstance(attr, omf.CategoryAttribute): + map = {index: string for index, string in zip(attr.categories.indices, attr.categories.values)} + to_string = map.get + else: + to_string = str + names.append(attr.name) + data.append((attr.array, to_string)) + with open("regular_block_model.csv", "w") as f: + f.write(f"# {model.name}\n") + f.write(f"# {model.description}\n") + f.write(f"# origin = {model.origin}\n") + f.write(f"# block size = {model.grid.block_size}\n") + f.write(f"# block count = {model.grid.block_count}\n") + f.write(f"x,y,z,dx,dy,dz,{','.join(names)}\n") + index = 0 + for k in range(model.grid.block_count[2]): + for j in range(model.grid.block_count[1]): + for i in range(model.grid.block_count[0]): + x, y, z = ( + model.origin + + model.axis_u * model.grid.block_size[0] * (i + 0.5) + + model.axis_v * model.grid.block_size[1] * (j + 0.5) + + model.axis_w * model.grid.block_size[2] * (k + 0.5) + ) + f.write(f"{x},{y},{z},{sizes}") + for array, to_string in data: + f.write(",") + f.write(to_string(array[index])) + f.write("\n") + index += 1 + + +if __name__ == "__main__": + write() + read() diff --git a/examples/regular_subblocked_model.py b/examples/regular_subblocked_model.py new file mode 100644 index 00000000..64811ba8 --- /dev/null +++ b/examples/regular_subblocked_model.py @@ -0,0 +1,132 @@ +import numpy as np +import omf + + +def write(): + # This writes an OMF file containing a small regular sub-blocked model with a few example attributes. + # Only one of the parent blocks contains sub-blocks. + model = omf.blockmodel.BlockModel( + name="Block Model", + description="A regular block model with a couple of attributes.", + origin=(100.0, 200.0, 50.0), + grid=omf.blockmodel.RegularGrid(block_count=(2, 2, 1), block_size=(10.0, 10.0, 10.0)), + subblocks=omf.blockmodel.RegularSubblocks( + subblock_count=(3, 3, 3), + parent_indices=np.array( + [ + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + (1, 0, 0), + (0, 1, 0), + (1, 1, 0), + ] + ), + corners=np.array( + [ + (0, 0, 0, 1, 2, 3), + (1, 0, 0, 3, 3, 3), + (0, 2, 0, 1, 3, 1), + (0, 2, 1, 1, 3, 3), + (0, 0, 0, 3, 3, 3), + (0, 0, 0, 3, 3, 3), + (0, 0, 0, 3, 3, 3), + ] + ), + ), + ) + model.attributes.append( + omf.NumericAttribute( + name="Number", + description="From 0.0 to 1.0", + location="subblocks", + array=np.arange(7.0) / 6.0, + ) + ) + model.attributes.append( + omf.CategoryAttribute( + name="Category", + description="Checkerboard categories on parent blocks", + location="parent_blocks", + array=np.array([0, 1, 1, 0]), + categories=omf.CategoryColormap( + indices=[0, 1], + values=["White", "Red"], + colors=[(255, 255, 255), (255, 0, 0)], + ), + ) + ) + strings = [] + for i0, j0, k0, i1, j1, k1 in model.subblocks.corners.array: + strings.append(f"{i1 - i0} by {j1 - j0} by {k1 - k0}") + model.attributes.append( + omf.StringAttribute( + name="Strings", + description="Gives the block shape", + location="subblocks", + array=strings, + ) + ) + project = omf.Project() + project.metadata["comment"] = "An OMF file containing a regular sub-blocked model." + project.elements.append(model) + omf.fileio.save(project, "regular_subblocked_model.omf", mode="w") + + +def _subblock_centroid_and_size(model, corners, i, j, k): + min_corner = corners[:3] + max_corner = corners[3:] + # Calculate centre and size within the [0, 1] range of the parent block. + centre = (min_corner + max_corner) / model.subblocks.subblock_count / 2 + size = (max_corner - min_corner) / model.subblocks.subblock_count + # Transform to object space. + subblock_centroid = ( + model.origin + + model.axis_u * model.grid.block_size[0] * (i + centre[0]) + + model.axis_v * model.grid.block_size[1] * (j + centre[1]) + + model.axis_w * model.grid.block_size[2] * (k + centre[2]) + ) + subblock_size = size * model.grid.block_size + return subblock_centroid, subblock_size + + +def read(): + # Reads the OMF file written above and converts it into a CSV file. Category colour data + # is discarded because block model CSV files don't typically store it. + project = omf.fileio.load("regular_subblocked_model.omf") + model = project.elements[0] + assert isinstance(model, omf.blockmodel.BlockModel) + names = [] + data = [] + for attr in model.attributes: + if isinstance(attr, omf.CategoryAttribute): + map = {index: string for index, string in zip(attr.categories.indices, attr.categories.values)} + to_string = map.get + else: + to_string = str + names.append(attr.name) + data.append((attr.array, to_string, attr.location == "parent_blocks")) + with open("regular_subblocked_model.csv", "w") as f: + f.write(f"# {model.name}\n") + f.write(f"# {model.description}\n") + f.write(f"# origin = {model.origin}\n") + f.write(f"# block size = {model.grid.block_size}\n") + f.write(f"# block count = {model.grid.block_count}\n") + f.write(f"# sub-block count = {model.subblocks.subblock_count}\n") + f.write(f"x,y,z,dx,dy,dz,{','.join(names)}\n") + for subblock_index, ((i, j, k), corners) in enumerate( + zip(model.subblocks.parent_indices.array, model.subblocks.corners.array) + ): + parent_index = model.ijk_to_index((i, j, k)) + centroid, size = _subblock_centroid_and_size(model, corners, i, j, k) + f.write(f"{centroid[0]},{centroid[1]},{centroid[2]},{size[0]},{size[1]},{size[2]}") + for array, to_string, on_parent in data: + f.write(",") + f.write(to_string(array[parent_index if on_parent else subblock_index])) + f.write("\n") + + +if __name__ == "__main__": + write() + read() From 38284026ae35cd3899ccfecd0d3e723ae850cb9f Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Fri, 24 Mar 2023 10:19:40 +1300 Subject: [PATCH 42/42] Improved validation of tensor arrays and removed empty file. --- omf/blockmodel/model.py | 11 +++++++++++ tests/test_blockmodel.py | 13 ++++++++++++- tests/test_doc_example.py | 0 3 files changed, 23 insertions(+), 1 deletion(-) delete mode 100644 tests/test_doc_example.py diff --git a/omf/blockmodel/model.py b/omf/blockmodel/model.py index 8859cd1a..fb074057 100644 --- a/omf/blockmodel/model.py +++ b/omf/blockmodel/model.py @@ -41,9 +41,20 @@ class TensorGrid(BaseModel): tensor_w = properties.Array("Tensor cell widths, w-direction", dtype=float, shape=("*",)) @properties.validator("tensor_u") + def _validate_tensor_u(self, change): + self._validate_tensor(change) + @properties.validator("tensor_v") + def _validate_tensor_v(self, change): + self._validate_tensor(change) + @properties.validator("tensor_w") + def _validate_tensor_w(self, change): + self._validate_tensor(change) + def _validate_tensor(self, change): + if len(change["value"]) == 0: + raise properties.ValidationError("tensor array may not be empty", prop=change["name"], instance=self) for item in change["value"]: if item <= 0.0: raise properties.ValidationError("tensor sizes must be > 0.0", prop=change["name"], instance=self) diff --git a/tests/test_blockmodel.py b/tests/test_blockmodel.py index 3686f818..5c77fcf7 100644 --- a/tests/test_blockmodel.py +++ b/tests/test_blockmodel.py @@ -53,7 +53,7 @@ def test_ijk_index(ijk, index): def test_tensorblockmodel(): - """Test volume grid geometry validation""" + """Test tensor grid block models.""" elem = BlockModel(grid=TensorGrid()) assert elem.num_parent_vertices is None assert elem.num_parent_blocks is None @@ -71,6 +71,17 @@ def test_tensorblockmodel(): elem.axis_v = "Y" +def test_invalid_tensors(): + """Test invalid tensor arrays on tensor grid block models.""" + elem = BlockModel(grid=TensorGrid()) + with pytest.raises(properties.ValidationError): + elem.grid.tensor_u = [] + with pytest.raises(properties.ValidationError): + elem.grid.tensor_v = [1.0, 0.0, 3.0] + with pytest.raises(properties.ValidationError): + elem.grid.tensor_w = [-1.0, 2.0] + + @pytest.mark.parametrize("block_count", ([2, 2], [2, 2, 2, 2], [0, 2, 2], [2, 2, 0.5])) def test_bad_block_count(block_count): """Test mismatched block_count""" diff --git a/tests/test_doc_example.py b/tests/test_doc_example.py deleted file mode 100644 index e69de29b..00000000