"""Submodule containing the RegularGrid class."""
import logging
log = logging.getLogger(__name__)
import math as maths
import numpy as np
import resqpy.crs as rqc
import resqpy.grid
import resqpy.olio.transmission as rqtr
import resqpy.olio.uuid as bu
import resqpy.olio.vector_utilities as vec
import resqpy.olio.xml_et as rqet
import resqpy.property as rprop
from resqpy.olio.xml_namespaces import curly_namespace as ns
import resqpy.grid._grid as grr_g
import resqpy.grid._grid_types as grr_gt
import resqpy.grid._create_grid_xml as grr_xml
# for a regular grid, the ...is_defined xml is created as a constant bool array, if the following set True
always_write_pillar_geometry_is_defined = True # required True for Fesapi interoperability (xsd compliance)
always_write_cell_geometry_is_defined = False
class RegularGrid(grr_g.Grid):
"""Class for completely regular block grids, usually aligned with xyz axes."""
# WIP: use RESQML lattice like geometry specification
[docs] def __init__(self,
parent_model,
uuid = None,
extent_kji = None,
dxyz = None,
dxyz_dkji = None,
origin = (0.0, 0.0, 0.0),
crs_uuid = None,
use_vertical = False,
mesh = None,
mesh_dz_dk = 1.0,
set_points_cached = False,
as_irregular_grid = False,
find_properties = True,
title = None,
originator = None,
extra_metadata = {},
load_inactive = True):
"""Creates a regular grid object based on dxyz, or derived from a Mesh object.
arguments:
parent_model (model.Model object): the model to which the new grid will be assigned
uuid (optional): the uuid for an existing grid part; if present, the RegularGrid object is
based on existing data or a mix of that data and other arguments where present
extent_kji (triple positive integers, optional): the number of cells in the grid (nk, nj, ni);
required unless uuid is present
dxyz (triple float, optional): use when the I,J,K axes align with the x,y,z axes (possible with inverted
directions); the size of each cell (dx, dy, dz); values may be negative
dxyz_dkji (numpy float array of shape (3, 3), optional): how x,y,z values increase with each step in each
direction K,J,I; first index is KJI, second index is xyz; only one of dxyz, dxyz_dkji and mesh should be
present; NB axis ordering is different to that used in Mesh class for dxyz_dij
origin (triple float, default (0.0, 0.0, 0.0)): the location in the local coordinate space of the crs of
the 'first' corner point of the grid
crs_uuid (uuid.UUID, optional): the uuid of the coordinate reference system for the grid
use_vertical (boolean, default False): if True and the pillars of the regular grid are vertical then a
pillar shape of 'vertical' is used; if False (or the pillars are not vertical), then a pillar shape of
'straight' is used
mesh (surface.Mesh object, optional): if present, the I,J layout of the grid is based on the mesh, which
must be regular, and the K cell size is given by the mesh_dz_dk argument; if present, then dxyz and
dxyz_dkji must be None
mesh_dz_dk (float, default 1.0): the size of cells in the K axis, which is aligned with the z axis, when
starting from a mesh; ignored if mesh is None
set_points_cached (boolean, default False): if True, an explicit geometry is created for the regular grid
in the form of the cached points array; will be treated as True if as_irregular_grid is True
as_irregular_grid (boolean, default False): if True, the grid is setup such that it will appear as a Grid
object when next loaded from disc
find_properties (boolean, default True): if True and uuid is not None, a grid property collection is
instantiated as an attribute, holding properties for which this grid is the supporting representation
title (str, optional): citation title for new grid; ignored if loading from xml
originator (str, optional): name of person creating the grid; defaults to login id;
ignored if loading from xml
extra_metadata (dict, optional): dictionary of extra metadata items to add to the grid;
ignored if loading from xml
load_inactive (bool, default True): if True and uuid is provided, the inactive attribubte is
populated if a property of kind 'active' is found for the grid
returns:
a newly created RegularGrid object with inheritance from the Grid class
notes:
the RESQML standard allows for regular grid geometry pillars to be stored as parametric lines
but that is not yet supported by this code base; however, constant dx, dy, dz arrays are supported;
alternatively, regular meshes (Grid2d) may be stored in parameterized form and used to generate a
regular grid here;
if uuid, dxyz, dxyz_dkji and mesh arguments are all None then unit cube cells aligned with
the x,y,z axes will be generated;
to store the geometry explicitly set as_irregular_grid True and use the following methods:
make_regular_points_cached(), write_hdf5(), create_xml(..., write_geometry = True);
otherwise, avoid write_hdf5() and call create_xml(..., write_geometry = False);
if geometry is not stored explicitly, the uuid of the crs is stored as extra metadata
if origin is not triple zero, a new crs will be created with the origin moved appropriately
:meta common:
"""
if as_irregular_grid:
set_points_cached = True
self.is_aligned = False
else:
self.is_aligned = None #: boolean indicating alignment of IJK axes with +/- xyz respectively
if uuid is None:
assert extent_kji is not None and len(extent_kji) == 3
super().__init__(parent_model, title = title, originator = originator, extra_metadata = extra_metadata)
self.grid_representation = 'IjkGrid' if as_irregular_grid else 'IjkBlockGrid'
self.extent_kji = np.array(extent_kji).copy()
self.nk, self.nj, self.ni = self.extent_kji
self.k_direction_is_down = True # assumed direction
self.grid_is_right_handed = False # todo: work it out from dxyz_dkji and crs xyz handedness
self.has_split_coordinate_lines = False
self.k_gaps = None
self.k_gap_after_array = None
self.k_raw_index_array = None
self.inactive = None
self.all_inactive = None
if mesh is not None:
assert mesh.flavour == 'regular'
assert dxyz is None and dxyz_dkji is None
origin = mesh.regular_origin
dxyz_dkji = np.empty((3, 3))
dxyz_dkji[0, :] = mesh_dz_dk
dxyz_dkji[1, :] = mesh.regular_dxyz_dij[1] # J axis
dxyz_dkji[2, :] = mesh.regular_dxyz_dij[0] # I axis
if crs_uuid is None:
crs_uuid = mesh.crs_uuid
else:
assert bu.matching_uuids(crs_uuid, mesh.crs_uuid)
assert dxyz is None or dxyz_dkji is None
if dxyz is None and dxyz_dkji is None:
dxyz = (1.0, 1.0, 1.0)
if dxyz_dkji is None:
dxyz_dkji = np.array([[0.0, 0.0, dxyz[2]], [0.0, dxyz[1], 0.0], [dxyz[0], 0.0, 0.0]])
self.block_origin = np.array(origin).copy() if uuid is None else np.zeros(3)
self.block_dxyz_dkji = np.array(dxyz_dkji).copy()
if self.is_aligned is None:
self._set_is_aligned()
if use_vertical and dxyz_dkji[0][0] == 0.0 and dxyz_dkji[0][1] == 0.0: # ie. no x,y change with k
self.pillar_shape = 'vertical'
else:
self.pillar_shape = 'straight'
new_crs = None
shift_origin = np.any(origin != 0.0) and uuid is None and not as_irregular_grid
if crs_uuid is None and self.extra_metadata is not None:
crs_uuid = bu.uuid_from_string(self.extra_metadata.get('crs uuid'))
shift_origin = shift_origin and crs_uuid is None
if crs_uuid is None:
crs_uuid = parent_model.crs_uuid
if crs_uuid is None:
new_crs = rqc.Crs(parent_model, x_offset = origin[0], y_offset = origin[1], z_offset = origin[2])
shift_origin = False
new_crs.create_xml(reuse = True)
crs_uuid = new_crs.uuid
if shift_origin:
new_crs = rqc.Crs(parent_model, uuid = crs_uuid)
new_crs.uuid = bu.new_uuid()
new_crs.x_offset += origin[0]
new_crs.y_offset += origin[1]
new_crs.z_offset += origin[2]
new_crs.create_xml(reuse = True)
crs_uuid = new_crs.uuid
self.crs_uuid = crs_uuid
self.crs = rqc.Crs(parent_model, uuid = crs_uuid) if new_crs is None else new_crs
else:
assert bu.is_uuid(uuid)
assert grr_gt.is_regular_grid(parent_model.root_for_uuid(uuid))
super().__init__(parent_model,
uuid = uuid,
find_properties = find_properties,
geometry_required = False,
title = title,
originator = originator,
extra_metadata = extra_metadata,
load_inactive = load_inactive)
self.geometry_defined_for_all_cells_cached = True
self.geometry_defined_for_all_pillars_cached = True
# self.array_cell_geometry_is_defined = np.full(tuple(self.extent_kji), True, dtype = bool)
self.array_cell_geometry_is_defined = None
if set_points_cached:
self.make_regular_points_cached()
if self.uuid is None:
self.uuid = bu.new_uuid()
def _load_regular_grid_from_xml(self):
"""Load from xml, called from super() init."""
def get_point3d(node):
assert node is not None and rqet.node_type(node) == 'Point3d'
xyz = []
for c in '123':
xyz.append(rqet.find_tag_float(node, f'Coordinate{c}'))
assert xyz[-1] is not None
return tuple(xyz)
self.grid_representation = 'IjkBlockGrid'
self.is_aligned = True # currently only aligned regular grids supported for lattice mode
self.geometry_defined_for_all_pillars_cached = True
self.geometry_defined_for_all_cells_cached = True
self.pillar_shape = 'vertical'
self.has_split_coordinate_lines = False
self.block_origin = np.zeros(3, dtype = float)
self.block_dxyz_dkji = np.zeros((3, 3), dtype = float)
if self.geometry_root is None:
log.debug('loading from xml without geometry node')
# find cell length properties and populate dxyz from those values
pc = self.extract_property_collection()
assert pc is not None
dxi_part = pc.singleton(property_kind = 'cell length',
facet_type = 'direction',
facet = 'I',
indexable = 'cells',
const_value = '*')
dyj_part = pc.singleton(property_kind = 'cell length',
facet_type = 'direction',
facet = 'J',
indexable = 'cells',
const_value = '*')
dzk_part = pc.singleton(property_kind = 'cell length',
facet_type = 'direction',
facet = 'K',
indexable = 'cells',
const_value = '*')
assert dxi_part is not None and dyj_part is not None and dzk_part is not None
dxi = self.property_collection.constant_value_for_part(dxi_part)
dyj = self.property_collection.constant_value_for_part(dyj_part)
dzk = self.property_collection.constant_value_for_part(dzk_part)
assert dxi is not None and dyj is not None and dzk is not None
self.block_dxyz_dkji[2, 0] = dxi
self.block_dxyz_dkji[1, 1] = dyj
self.block_dxyz_dkji[0, 2] = dzk
else:
log.debug('loading from xml with lattice geometry node')
p_node = rqet.find_tag(self.geometry_root, 'Points')
assert p_node is not None and rqet.node_type(p_node) == 'Point3dLatticeArray'
assert rqet.find_tag_bool(p_node, 'AllDimensionsAreOrthogonal',
must_exist = True), 'only aligned regular grids catered for'
origin_node = rqet.find_tag(p_node, 'Origin')
assert origin_node is not None, 'origin missing in xml for regular mesh (lattice)'
self.block_origin[:] = get_point3d(origin_node)
# following constraint is for aligned regular grids
assert np.all(self.block_origin == 0.0), \
'aligned regular grid origin must be coincident with local crs origin'
offset_nodes = rqet.list_of_tag(p_node, 'Offset')
assert len(offset_nodes) == 3, 'missing (or too many) offset nodes in xml for regular grid (lattice)'
for axis, main_o_node in enumerate(offset_nodes): # order K, J, I; ie. z, y, x
d_xyz = get_point3d(rqet.find_tag(main_o_node, 'Offset'))
self.block_dxyz_dkji[axis] = d_xyz
# following constraint is for aligned regular grids
for ax in range(3):
# note: these constraints allow for a negative cell length, ie. xyz axis reverse of IJK
is_non_zero = not maths.isclose(d_xyz[2 - ax], 0.0)
assert (ax == axis) == is_non_zero, 'unaligned lattice for regular grid not supported'
spacing_node = rqet.find_tag(main_o_node, 'Spacing')
assert spacing_node is not None
spacing_value = rqet.find_tag_float(spacing_node, 'Value')
assert spacing_value is not None and maths.isclose(spacing_value, 1.0)
spacing_count = rqet.find_tag_int(spacing_node, 'Count')
assert spacing_count is not None and spacing_count == self.extent_kji[axis]
# log.debug(f'block dxyz_dkji from lattice: {self.block_dxyz_dkji}')
[docs] def cell_geometry_is_defined(self, cell_kji0 = None, cell_geometry_is_defined_root = None, cache_array = True):
"""Override of Grid method indicating whether a cell has defined geometry, here always True."""
return True
[docs] def pillar_geometry_is_defined(self, pillar_ji0 = None, pillar_geometry_is_defined_root = None, cache_array = True):
"""Override of Grid method indicating whether a pillar has defined geometry, here always True."""
return True
[docs] def make_regular_points_cached(self, apply_origin_offset = True):
"""Set up the cached points array as an explicit representation of the regular grid geometry.
arguments:
apply_origin_offset (boolean, default True): if True, this method includes the regular grid
origin in the calculated points data
note:
if apply_origin_offset is True, the related crs must not store the origin as offset data
"""
if hasattr(self, 'points_cached') and self.points_cached is not None:
return
self.points_cached = np.zeros((self.nk + 1, self.nj + 1, self.ni + 1, 3))
# todo: replace for loops with linspace
for k in range(self.nk):
self.points_cached[k + 1, 0, 0] = self.points_cached[k, 0, 0] + self.block_dxyz_dkji[0]
for j in range(self.nj):
self.points_cached[:, j + 1, 0] = self.points_cached[:, j, 0] + self.block_dxyz_dkji[1]
for i in range(self.ni):
self.points_cached[:, :, i + 1] = self.points_cached[:, :, i] + self.block_dxyz_dkji[2]
if apply_origin_offset:
self.points_cached[:, :, :] += self.block_origin
[docs] def axial_lengths_kji(self):
"""Returns a triple float being lengths of primary axes (K, J, I) for each cell."""
return vec.naive_lengths(self.block_dxyz_dkji)
# override of Grid methods
[docs] def point_raw(self, index = None, points_root = None, cache_array = True):
"""Returns element from points data, indexed as corner point (k0, j0, i0).
Can optionally be used to cache points data.
arguments:
index (3 integers, optional): if not None, the index into the raw points data for the point of interest
points_root (ignored)
cache_array (boolean, default True): if True, the raw points data is cached in memory as a side effect
returns:
(x, y, z) of selected point as a 3 element numpy vector, or None if index is None
notes:
this function is typically called either to cache the points data in memory, or to fetch the coordinates of
a single corner point;
the index should be a triple kji0 with axes ranging over the shared corners nk+1, nj+1, ni+1
"""
assert cache_array or index is not None
if cache_array:
self.make_regular_points_cached()
if index is None:
return None
return self.points_cached[tuple(index)]
return self.block_origin + np.sum(np.repeat(np.array(index).reshape(
(3, 1)), 3, axis = -1) * self.block_dxyz_dkji,
axis = 0)
[docs] def half_cell_transmissibility(self, use_property = None, realization = None, tolerance = None):
"""Returns (and caches if realization is None) half cell transmissibilities for this regular grid.
arguments:
use_property (ignored)
realization (int, optional) if present, only a property with this realization number will be used
tolerance (ignored)
returns:
numpy float array of shape (nk, nj, ni, 3, 2) where the 3 covers K,J,I and the 2 covers the
face polarity: - (0) and + (1); units will depend on the length units of the coordinate reference
system for the grid; the units will be m3.cP/(kPa.d) or bbl.cP/(psi.d) for grid length units of m
and ft respectively
notes:
the values for - and + polarity will always be equal, the data is duplicated for compatibility with
parent Grid class;
the returned array is in the logical resqpy arrangement; it must be discombobulated before being
added as a property; this method does not write to hdf5, nor create a new property or xml;
if realization is None, a grid attribute cached array will be used
"""
# todo: allow passing of property uuids for ntg, k_k, j, i
if realization is None and hasattr(self, 'array_half_cell_t'):
return self.array_half_cell_t
half_t = rqtr.half_cell_t(
self, realization = realization) # note: properties must be identifiable in property_collection
# introduce facial polarity axis for compatibility with parent Grid class
assert half_t.ndim == 4
half_t = np.expand_dims(half_t, -1)
half_t = np.repeat(half_t, 2, axis = -1)
if realization is None:
self.array_half_cell_t = half_t
return half_t
[docs] def corner_points(self, cell_kji0 = None, points_root = None, cache_resqml_array = None, cache_cp_array = False):
"""Returns a numpy array of corner points for a single cell or the whole grid.
arguments:
cell_kji0 (triple float, optional): if present, index of cell for which corner points are required; if None,
an array holding corner points for all cells is returned
points_root (ignored): not used; exists for signature compatibility with parent class method
cache_resqml_array (ignored): not used; exists for signature compatibility with parent class method
cache_cp_array (bool, default False): if True (or cell_kji0 is None), the full corner point array is
cached as an attribute of the grid
notes:
if cell_kji0 is not None, a 4D array of shape (2, 2, 2, 3) holding single cell corner points in logical order
[kp, jp, ip, xyz] is returned; if cell_kji0 is None, a pagoda style 7D array [k, j, i, kp, jp, ip, xyz] is
cached and returned;
the ordering of the corner points is in the logical order, which is not the same as that used by Nexus CORP data;
olio.grid_functions.resequence_nexus_corp() can be used to switch back and forth between this pagoda ordering
and Nexus corp ordering;
this is the usual way to access full corner points for cells where working with native resqml data is undesirable;
this method returns coordinates in the local crs space
:meta common:
"""
if cell_kji0 is None:
cache_cp_array = True
if hasattr(self, 'array_corner_points'):
if cell_kji0 is None:
return self.array_corner_points
return self.array_corner_points[tuple(cell_kji0)]
if not self.is_aligned:
return super().corner_points(cell_kji0 = cell_kji0,
points_root = points_root,
cache_resqml_array = cache_resqml_array,
cache_cp_array = cache_cp_array)
if cache_cp_array:
acp = np.zeros((self.nk, self.nj, self.ni, 2, 2, 2, 3), dtype = float)
# set x coordinates
acp[:, :, :, 0, 0, 0, 0] = np.linspace(0.0,
self.block_dxyz_dkji[2, 0] * self.ni,
num = self.ni,
endpoint = False).reshape((1, 1, self.ni))
acp[:, :, :, 0, 0, 1, 0] = acp[:, :, :, 0, 0, 0, 0] + self.block_dxyz_dkji[2, 0]
acp[:, :, :, 0, 1, :, 0] = acp[:, :, :, 0, 0, :, 0]
acp[:, :, :, 1, :, :, 0] = acp[:, :, :, 0, :, :, 0]
# set y coordinates
acp[:, :, :, 0, 0, 0, 1] = np.linspace(0.0,
self.block_dxyz_dkji[1, 1] * self.nj,
num = self.nj,
endpoint = False).reshape((1, self.nj, 1))
acp[:, :, :, 0, 1, 0, 1] = acp[:, :, :, 0, 0, 0, 1] + self.block_dxyz_dkji[1, 1]
acp[:, :, :, 0, :, 1, 1] = acp[:, :, :, 0, :, 0, 1]
acp[:, :, :, 1, :, :, 1] = acp[:, :, :, 0, :, :, 1]
# set z coordinates
acp[:, :, :, 0, 0, 0, 2] = np.linspace(0.0,
self.block_dxyz_dkji[0, 2] * self.nk,
num = self.nk,
endpoint = False).reshape((self.nk, 1, 1))
acp[:, :, :, 1, 0, 0, 2] = acp[:, :, :, 0, 0, 0, 2] + self.block_dxyz_dkji[0, 2]
acp[:, :, :, :, 0, 1, 2] = acp[:, :, :, :, 0, 0, 2]
acp[:, :, :, :, 1, :, 2] = acp[:, :, :, :, 0, :, 2]
# translate by regular grid origin
acp[:] += np.array(self.block_origin, dtype = float).reshape((1, 1, 1, 1, 1, 1, 3))
self.array_corner_points = acp
if cell_kji0 is None:
return acp
return acp[tuple(cell_kji0)]
# setup corner point array for a single cell only
cp = np.zeros((2, 2, 2, 3), dtype = float)
# set x coordinates
cp[0, 0, 0, 0] = self.block_dxyz_dkji[2, 0] * cell_kji0[2]
cp[0, 0, 1, 0] = cp[0, 0, 0, 0] + self.block_dxyz_dkji[2, 0]
cp[0, 1, :, 0] = cp[0, 0, :, 0]
cp[1, :, :, 0] = cp[0, :, :, 0]
# set y coordinates
cp[0, 0, 0, 1] = self.block_dxyz_dkji[1, 1] * cell_kji0[1]
cp[0, 1, 0, 1] = cp[0, 0, 0, 1] + self.block_dxyz_dkji[1, 1]
cp[0, :, 1, 1] = cp[0, :, 0, 1]
cp[1, :, :, 1] = cp[0, :, :, 1]
# set z coordinates
cp[0, 0, 0, 2] = self.block_dxyz_dkji[0, 2] * cell_kji0[0]
cp[1, 0, 0, 2] = cp[0, 0, 0, 2] + self.block_dxyz_dkji[0, 2]
cp[:, 0, 1, 2] = cp[:, 0, 0, 2]
cp[:, 1, :, 2] = cp[:, 0, :, 2]
# translate by regular grid origin
cp[:] += np.array(self.block_origin, dtype = float).reshape((1, 1, 1, 3))
return cp
[docs] def centre_point(self, cell_kji0 = None, cache_centre_array = False, use_origin = True):
"""Returns centre point of a cell or array of centre points of all cells.
arguments:
cell_kji0 (optional): if present, the (k, j, i) indices of the individual cell for which the
centre point is required; zero based indexing
cache_centre_array (bool, default False): If True, or cell_kji0 is None, an array of centre points
is generated and added as an attribute of the grid, with attribute name array_centre_point
use_origin (bool, default True): if True, the x, y & z offsets (local origin) are added to the
computed cell centre points
returns:
(x, y, z) 3 element numpy array of floats holding centre point of cell;
or numpy 3+1D array if cell_kji0 is None
note:
resulting coordinates are in the same (local) crs as the grid points if use_origin is True
"""
if cell_kji0 is None:
cache_centre_array = True
if cache_centre_array and (not hasattr(self, 'array_centre_point') or self.array_centre_point is None or
not use_origin):
centres = np.zeros((self.nk, self.nj, self.ni, 3))
if self.is_aligned:
centres[:, :, :, 0] = np.linspace(0.0,
self.block_dxyz_dkji[2, 0] * self.ni,
num = self.ni,
endpoint = False).reshape((1, 1, self.ni))
centres[:, :, :, 1] = np.linspace(0.0,
self.block_dxyz_dkji[1, 1] * self.nj,
num = self.nj,
endpoint = False).reshape((1, self.nj, 1))
centres[:, :, :, 2] = np.linspace(0.0,
self.block_dxyz_dkji[0, 2] * self.nk,
num = self.nk,
endpoint = False).reshape((self.nk, 1, 1))
else:
# todo: replace for loops with linspace
for k in range(self.nk - 1):
centres[k + 1, 0, 0] = centres[k, 0, 0] + self.block_dxyz_dkji[0]
for j in range(self.nj - 1):
centres[:, j + 1, 0] = centres[:, j, 0] + self.block_dxyz_dkji[1]
for i in range(self.ni - 1):
centres[:, :, i + 1] = centres[:, :, i] + self.block_dxyz_dkji[2]
centres += 0.5 * np.sum(self.block_dxyz_dkji, axis = 0)
if use_origin:
centres += self.block_origin
self.array_centre_point = centres
else:
centres = None
if cell_kji0 is not None:
if centres is not None:
return centres[tuple(cell_kji0)]
if hasattr(self, 'array_centre_point') and self.array_centre_point is not None and use_origin:
return self.array_centre_point[tuple(cell_kji0)]
float_kji0 = np.array(cell_kji0, dtype = float) + 0.5
centre = np.sum(self.block_dxyz_dkji * np.expand_dims(float_kji0, axis = -1).repeat(3, axis = -1), axis = 0)
if use_origin:
centre += self.block_origin
return centre
if centres is not None:
return centres
return self.array_centre_point
[docs] def aligned_column_centres(self):
"""For an aligned grid, returns an array of column centres in xy, of shape (nj, ni, 2)."""
assert self.is_aligned
centres = np.zeros((self.nj, self.ni, 2))
centres[:, :, 0] = np.linspace(0.0, self.block_dxyz_dkji[2, 0] * self.ni, num = self.ni,
endpoint = False).reshape((1, self.ni))
centres[:, :, 1] = np.linspace(0.0, self.block_dxyz_dkji[1, 1] * self.nj, num = self.nj,
endpoint = False).reshape((self.nj, 1))
centres += self.block_origin[:2] + 0.5 * np.sum(self.block_dxyz_dkji[1:, :2], axis = 0)
return centres
[docs] def volume(self, cell_kji0 = None, required_uom = None):
"""Returns bulk rock volume of cell or numpy array of bulk rock volumes for all cells.
arguments:
cell_kji0 (optional): if present, the (k, j, i) indices of the individual cell for which the
volume is required; zero based indexing
required_uom (str, optional): if present, the RESQML unit of measure (for quantity volume) that
the volumes will be returned in; if None, the grid's CRS z units cubed
will be used
returns:
float, being the volume of cell identified by cell_kji0;
or numpy float array of shape (nk, nj, ni) if cell_kji0 is None
notes:
the function can be used to find the volume of a single cell, or all cells;
the method currently assumes that the primary i, j, k axes are mutually orthogonal
"""
required_uom = self.get_volume_uom(required_uom)
conversion_factor = self.get_volume_conversion_factor(required_uom)
vol = np.prod(vec.naive_lengths(self.block_dxyz_dkji))
if conversion_factor is not None:
vol *= conversion_factor
if cell_kji0 is not None:
return vol
return np.full((self.nk, self.nj, self.ni), vol)
[docs] def thickness(self, cell_kji0 = None, **kwargs):
"""Returns cell thickness (K axial length) for a single cell or full array.
arguments:
cell_kji0 (triple int, optional): if present, the thickness for a single cell is returned;
if None, an array is returned
all other arguments ignored; present for compatibility with same method in Grid()
returns:
float, or numpy float array filled with a constant
"""
thick = self.axial_lengths_kji()[0]
if cell_kji0 is not None:
return thick
return np.full((self.nk, self.nj, self.ni), thick, dtype = float)
[docs] def pinched_out(self, cell_kji0 = None, **kwargs):
"""Returns pinched out boolean (always False) for a single cell or full array.
arguments:
cell_kji0 (triple int, optional): if present, the pinched out flag for a single cell is returned;
if None, an array is returned
all other arguments ignored; present for compatibility with same method in Grid()
returns:
False, or numpy array filled with False
"""
if cell_kji0 is not None:
return False
return np.full((self.nk, self.nj, self.ni), False, dtype = bool)
[docs] def actual_pillar_shape(self, patch_metadata = False, tolerance = 0.001):
"""Returns actual shape of pillars.
arguments:
patch_metadata (boolean, default False): if True, the actual shape replaces whatever was in the metadata
tolerance (float, ignored)
returns:
string: 'vertical', 'straight' or 'curved'
note:
setting patch_metadata True will affect the attribute in this Grid object; however, it will not be
preserved unless the create_xml() method is called, followed at some point with model.store_epc()
"""
if np.all(self.block_dxyz_dkji[0, :2] == 0.0):
return 'vertical'
return 'straight'
[docs] def xyz_box(grid, points_root = None, lazy = True, local = False):
"""Returns the minimum and maximum xyz for the grid geometry.
arguments:
points_root (ignored): for compatibility with Grid method signature
lazy (ignored): for compatibility with Grid method signature
local (boolean, default False): if True, the xyz ranges that are returned are in the local
coordinate space, otherwise the global (crs parent) coordinate space
returns:
numpy array of float of shape (2, 3); the first axis is minimum, maximum; the second axis is x, y, z
:meta common:
"""
if grid.xyz_box_cached is None:
grid.xyz_box_cached = np.empty((2, 3))
if grid.is_aligned:
dxyz = np.array([grid.block_dxyz_dkji[2 - axis, axis] for axis in range(3)])
temp_box = np.zeros((2, 3))
temp_box[1] = np.array((grid.ni, grid.nj, grid.nk), dtype = float) * dxyz
grid.xyz_box_cached[0] = np.amin(temp_box, axis = 0)
grid.xyz_box_cached[1] = np.amax(temp_box, axis = 0)
else:
# generate points for outer grid corners, find min & max by axis
grid_cp = np.zeros((2, 2, 2, 3))
for k in [0, 1]:
lcp_k = float(k * grid.nk) * grid.block_dxyz_dkji[0]
for j in [0, 1]:
lcp_j = lcp_k + float(j * grid.nj) * grid.block_dxyz_dkji[1]
for i in [0, 1]:
grid_cp[k, j, i] = lcp_j + float(i * grid.ni) * grid.block_dxyz_dkji[2]
grid_cp = grid_cp.reshape((8, 3))
grid.xyz_box_cached[0] = np.amin(grid_cp, axis = 0)
grid.xyz_box_cached[1] = np.amax(grid_cp, axis = 0)
grid.xyz_box_cached_thoroughly = True
if local:
return grid.xyz_box_cached
global_xyz_box = grid.xyz_box_cached.copy()
grid.local_to_global_crs(global_xyz_box, crs_uuid = grid.crs_uuid)
g_xyz_box = np.empty((2, 3), dtype = float)
g_xyz_box[0] = np.amin(global_xyz_box, axis = 0)
g_xyz_box[1] = np.amax(global_xyz_box, axis = 0)
return g_xyz_box
[docs] def find_cell_for_point_xy(self, x, y, k0 = 0, vertical_ref = 'top', local_coords = True):
"""Searches in 2D for a cell containing point x,y in layer k0; return (j0, i0) or (None, None)."""
if x is None or y is None:
return (None, None)
if not local_coords and self.crs_uuid is not None:
a = np.array([[x, y, 0.0]]) # extra axis needed to keep global_to_local_crs happy
self.global_to_local_crs(a, crs_uuid = self.crs_uuid)
x = a[0, 0]
y = a[0, 1]
log.debug(f'find for local x: {x}; y: {y}')
if self.is_aligned:
i0 = maths.floor(x / self.block_dxyz_dkji[2, 0])
j0 = maths.floor(y / self.block_dxyz_dkji[1, 1])
else:
# TODO
raise NotImplementedError('finding cell for x,y in unaligned regular grid')
i0 = j0 = -1
log.debug(f'found j0: {j0}; i0: {i0}')
if 0 <= i0 < self.ni and 0 <= j0 < self.nj:
return (j0, i0)
return (None, None)
[docs] def create_xml(self,
ext_uuid = None,
add_as_part = True,
add_relationships = True,
set_as_grid_root = True,
title = None,
originator = None,
write_active = True,
write_geometry = None,
extra_metadata = {},
expand_const_arrays = False,
add_cell_length_properties = True,
use_lattice = True):
"""Creates xml for this RegularGrid object; by default the explicit geometry is not included.
see docstring for Grid.create_xml()
additional argument:
add_cell_length_properties (boolean, default True): if True, 3 constant property arrays with cells as
indexable element are created to hold the lengths of the primary axes of the cells; the xml is
created for the properties and they are added to the model (no hdf5 write needed)
use_lattice (boolean, default True): if True, a geometry node is created with a Point3dLatticeArray;
note:
if the regular grid has been instatiated using the as_irregular_grid mode, a lattice will not be used,
regardless of the use_lattice argument;
if using a lattice representation, the write_geomety argument is ignored and a lattice geometry will
be created
:meta common:
"""
if extra_metadata is None:
extra_metadata = {}
if write_geometry is None:
write_geometry = (self.grid_representation == 'IjkGrid')
if self.grid_representation == 'IjkGrid':
use_lattice = False
if write_geometry is None:
write_geometry = (self.grid_representation == 'IjkGrid')
else:
extra_metadata = extra_metadata.copy()
if write_geometry and not use_lattice:
extra_metadata['grid_flavour'] = 'IjkGrid'
if write_geometry or use_lattice:
if 'crs uuid' in extra_metadata:
extra_metadata.pop('crs uuid')
elif self.crs_uuid is not None:
extra_metadata['crs uuid'] = str(self.crs_uuid)
node = super().create_xml(ext_uuid = ext_uuid,
add_as_part = add_as_part,
add_relationships = add_relationships,
set_as_grid_root = set_as_grid_root,
title = title,
originator = originator,
write_active = write_active,
write_geometry = write_geometry or use_lattice,
extra_metadata = extra_metadata,
use_lattice = use_lattice)
assert node is not None
# patch in constant ...is_defined arrays if required
if write_geometry or use_lattice:
geom = rqet.find_tag(node, 'Geometry')
assert geom is not None
if always_write_pillar_geometry_is_defined and rqet.find_tag(geom, 'PillarGeometryIsDefined') is None:
grr_xml._add_constant_pillar_geometry_is_defined(geom, self.extent_kji)
if always_write_cell_geometry_is_defined and rqet.find_tag(geom, 'CellGeometryIsDefined') is None:
grr_xml._add_constant_cell_geometry_is_defined(geom, self.extent_kji)
if add_cell_length_properties:
axes_lengths_kji = self.axial_lengths_kji()
dpc = rprop.GridPropertyCollection()
dpc.set_grid(self)
for axis in range(3):
dpc.add_cached_array_to_imported_list(None,
'regular grid',
'D' + 'ZYX'[axis],
discrete = False,
uom = self.xy_units(),
property_kind = 'cell length',
facet_type = 'direction',
facet = 'KJI'[axis],
indexable_element = 'cells',
count = 1,
const_value = axes_lengths_kji[axis])
if expand_const_arrays:
dpc.write_hdf5_for_imported_list(expand_const_arrays = True)
dpc.create_xml_for_imported_list_and_add_parts_to_model(expand_const_arrays = expand_const_arrays)
if self.property_collection is None:
self.property_collection = dpc
else:
if self.property_collection.support is None:
self.property_collection.set_support(support = self)
self.property_collection.inherit_parts_from_other_collection(dpc)
if add_relationships and self.crs_uuid is not None:
self.model.create_reciprocal_relationship_uuids(self.uuid, 'destinationObject', self.crs_uuid,
'sourceObject')
return node
def _set_is_aligned(self):
"""Sets is_aligned attribute True if IJK axes align with +/- xyz respectively."""
if self.block_dxyz_dkji is None:
self.is_aligned = None
self.is_aligned = (np.count_nonzero(self.block_dxyz_dkji * np.array([[1.0, 1, 0], [1, 0, 1], [0, 1, 1]])) == 0)
[docs] def aligned_dxyz(self):
"""Returns triple float dx, dy, dz cell dimensions for aligned grids only."""
assert self.is_aligned
return (self.block_dxyz_dkji[2, 0], self.block_dxyz_dkji[1, 1], self.block_dxyz_dkji[0, 2])
[docs] def slice_points(self, axis = 0, ref_slice = 0, local = False):
"""Returns a slice of points data for a layer or a cross section.
arguments:
axis (int, default 0): the axis of slicing 0 for K, 1 for J, 2 for I
ref_slice (int, default 0): the slice index in the direction of axis
local (bool, default False): if True, returned points are in the local crs; if False in global
returns:
numpy float array of points data of shape (Na, Nb, 3); (Na, Nb) are (NJ+1, NI+1), (NK+1, NI+1) or
(NK+1, NJ+1) for axis values of 0, 1, or 2 respectively
"""
assert 0 <= axis < 3
assert 0 <= ref_slice <= self.extent_kji[axis]
other_axes = np.array([[1, 2], [0, 2], [0, 1]], dtype = int)[axis]
shape = (self.extent_kji[other_axes[0]] + 1, self.extent_kji[other_axes[1]] + 1, 3)
dxyz_da = self.block_dxyz_dkji[other_axes[0]]
dxyz_db = self.block_dxyz_dkji[other_axes[1]]
p = np.zeros(shape, dtype = float)
if ref_slice:
p[0, 0] = ref_slice * self.block_dxyz_dkji[axis]
for a in range(1, shape[0]):
p[a, 0] = p[0, 0] + a * dxyz_da
for b in range(1, shape[1]):
p[:, b] = p[:, 0] + b * dxyz_db
if not local:
assert self.crs is not None
self.crs.local_to_global_array(p)
return p
def _add_geom_points_xml(self, geom_node, ext_uuid):
# RegularGrid override method using lattice representation, called from Grid.create_xml()
def make_axial_offset(model, p_node, axis, d, n):
dxyz = np.zeros(3, dtype = float)
dxyz[2 - axis] = d
o_node = rqet.SubElement(p_node, ns['resqml2'] + 'Offset')
o_node.set(ns['xsi'] + 'type', ns['resqml2'] + 'Point3dOffset')
o_node.text = '\n'
model.create_solitary_point3d('Offset', o_node, dxyz)
space_node = rqet.SubElement(o_node, ns['resqml2'] + 'Spacing')
space_node.set(ns['xsi'] + 'type',
ns['resqml2'] + 'DoubleConstantArray') # nothing else catered for just now
space_node.text = '\n'
ov_node = rqet.SubElement(space_node, ns['resqml2'] + 'Value')
ov_node.set(ns['xsi'] + 'type', ns['xsd'] + 'double')
ov_node.text = '1'
oc_node = rqet.SubElement(space_node, ns['resqml2'] + 'Count')
oc_node.set(ns['xsi'] + 'type', ns['xsd'] + 'positiveInteger')
oc_node.text = str(n)
assert self.crs_uuid is not None
if self.grid_representation == 'IjkBlockGrid':
# todo: handle non aligned regular grids
assert self.is_aligned
points_node = rqet.SubElement(geom_node, ns['resqml2'] + 'Points')
points_node.set(ns['xsi'] + 'type', ns['resqml2'] + 'Point3dLatticeArray')
points_node.text = '\n'
adao = rqet.SubElement(points_node, ns['resqml2'] + 'AllDimensionsAreOrthogonal')
adao.set(ns['xsi'] + 'type', ns['xsd'] + 'boolean')
adao.text = 'true'
self.model.create_solitary_point3d('Origin', points_node, (0.0, 0.0, 0.0))
for axis in range(3): # note: 1st Offset is for K axis, 2nd for J, then I
make_axial_offset(self.model, points_node, axis, self.aligned_dxyz()[2 - axis], self.extent_kji[axis])
else: # generate geometry as irregular grid
super()._add_geom_points_xml(geom_node, ext_uuid)