def _add_single_surface(model, surf_file, surface_file_format, surface_role, quad_triangles, crs_uuid, rq_class, hdf5_file, h5_mode, make_horizon_interpretations_and_features, ext_uuid): _, short_name = os.path.split(surf_file) dot = short_name.rfind('.') if dot > 0: short_name = short_name[:dot] log.info('surface ' + short_name + ' processing file: ' + surf_file + ' using format: ' + surface_file_format) if rq_class == 'surface': if surface_file_format == 'GOCAD-Tsurf': surface = rqs.Surface(model, tsurf_file = surf_file, surface_role = surface_role, quad_triangles = quad_triangles) else: surface = rqs.Surface(model, mesh_file = surf_file, mesh_format = surface_file_format, surface_role = surface_role, quad_triangles = quad_triangles) elif rq_class == 'mesh': if surface_file_format == 'GOCAD-Tsurf': log.info(f"Cannot convert a GOCAD-Tsurf to mesh, only to TriangulatedSurface - skipping file {surf_file}") return model else: surface = rqs.Mesh(model, mesh_file = surf_file, mesh_format = surface_file_format, mesh_flavour = 'reg&z', surface_role = surface_role, crs_uuid = crs_uuid) else: log.critical('this is impossible') # NB. surface may be either a Surface object or a Mesh object log.debug('appending to hdf5 file for surface file: ' + surf_file) surface.write_hdf5(hdf5_file, mode = h5_mode) if make_horizon_interpretations_and_features: feature = rqo.GeneticBoundaryFeature(model, kind = 'horizon', feature_name = short_name) feature.create_xml() interp = rqo.HorizonInterpretation(model, genetic_boundary_feature = feature, domain = 'depth') interp_root = interp.create_xml() surface.set_represented_interpretation_root(interp_root) surface.create_xml(ext_uuid, add_as_part = True, add_relationships = True, title = short_name + ' sourced from ' + surf_file, originator = None) return model
def small_grid_and_surface( tmp_model: Model) -> Tuple[grr.RegularGrid, rqs.Surface]: """Creates a small RegularGrid and a random triangular surface.""" crs = Crs(tmp_model) crs.create_xml() extent = 10 extent_kji = (extent, extent, extent) dxyz = (1.0, 1.0, 1.0) crs_uuid = crs.uuid title = "small_grid" grid = grr.RegularGrid(tmp_model, extent_kji=extent_kji, dxyz=dxyz, crs_uuid=crs_uuid, title=title) grid.create_xml() n_points = 100 points = np.random.rand(n_points, 3) * extent triangles = tri.dt(points) surface = rqs.Surface(tmp_model, crs_uuid=crs_uuid, title="small_surface") surface.set_from_triangles_and_points(triangles, points) surface.triangles_and_points() surface.write_hdf5() surface.create_xml() tmp_model.store_epc() return grid, surface
def _set_support_uuid_notnone_supportnone(collection, support_uuid, model): import resqpy.fault as rqf import resqpy.grid as grr import resqpy.surface as rqs import resqpy.unstructured as rug import resqpy.well as rqw support_part = model.part_for_uuid(support_uuid) assert support_part is not None, 'supporting representation part missing in model' collection.support_root = model.root_for_part(support_part) support_type = model.type_of_part(support_part) assert support_type is not None if support_type == 'obj_IjkGridRepresentation': collection.support = grr.any_grid(model, uuid=collection.support_uuid, find_properties=False) elif support_type == 'obj_WellboreFrameRepresentation': collection.support = rqw.WellboreFrame(model, uuid=collection.support_uuid) elif support_type == 'obj_BlockedWellboreRepresentation': collection.support = rqw.BlockedWell(model, uuid=collection.support_uuid) elif support_type == 'obj_Grid2dRepresentation': collection.support = rqs.Mesh(model, uuid=collection.support_uuid) elif support_type == 'obj_GridConnectionSetRepresentation': collection.support = rqf.GridConnectionSet( model, uuid=collection.support_uuid) elif support_type == 'obj_TriangulatedSetRepresentation': collection.support = rqs.Surface(model, uuid=collection.support_uuid) elif support_type == 'obj_UnstructuredGridRepresentation': collection.support = rug.UnstructuredGrid(model, uuid=collection.support_uuid, geometry_required=False, find_properties=False) elif support_type == 'obj_WellboreMarkerFrameRepresentation': collection.support = rqw.WellboreMarkerFrame( model, uuid=collection.support_uuid) else: raise TypeError( 'unsupported property supporting representation class: ' + str(support_type))
def test_vertical_prism_grid_from_seed_points_and_surfaces(tmp_path): seed(23487656) # to ensure test reproducibility epc = os.path.join(tmp_path, 'voronoi_prism_grid.epc') model = rq.new_model(epc) crs = rqc.Crs(model) crs.create_xml() # define a boundary polyline: b_count = 7 boundary_points = np.empty((b_count, 3)) radius = 1000.0 for i in range(b_count): theta = -vec.radians_from_degrees(i * 360.0 / b_count) boundary_points[i] = (2.0 * radius * maths.cos(theta), radius * maths.sin(theta), 0.0) boundary = rql.Polyline(model, set_coord=boundary_points, set_bool=True, set_crs=crs.uuid, title='rough ellipse') boundary.write_hdf5() boundary.create_xml() # derive a larger area of interest aoi = rql.Polyline.from_scaled_polyline(boundary, 1.1, title='area of interest') aoi.write_hdf5() aoi.create_xml() min_xy = np.min(aoi.coordinates[:, :2], axis=0) - 50.0 max_xy = np.max(aoi.coordinates[:, :2], axis=0) + 50.0 print(f'***** min max xy aoi+ : {min_xy} {max_xy}') # debug # create some seed points within boundary seed_count = 5 seeds = rqs.PointSet(model, crs_uuid=crs.uuid, polyline=boundary, random_point_count=seed_count, title='seeds') seeds.write_hdf5() seeds.create_xml() seeds_xy = seeds.single_patch_array_ref(0) for seed_xy in seeds_xy: assert aoi.point_is_inside_xy( seed_xy), f'seed point {seed_xy} outwith aoi' print( f'***** min max xy seeds : {np.min(seeds_xy, axis = 0)} {np.max(seeds_xy, axis = 0)}' ) # debug # create some horizon surfaces ni, nj = 21, 11 lattice = rqs.Mesh(model, crs_uuid=crs.uuid, mesh_flavour='regular', ni=ni, nj=nj, origin=(min_xy[0], min_xy[1], 0.0), dxyz_dij=np.array([[ (max_xy[0] - min_xy[0]) / (ni - 1), 0.0, 0.0 ], [0.0, (max_xy[1] - min_xy[1]) / (nj - 1), 0.0]])) lattice.write_hdf5() lattice.create_xml() horizons = [] for i in range(4): horizon_depths = 1000.0 + 100.0 * i + 20.0 * (np.random.random( (nj, ni)) - 0.5) horizon_mesh = rqs.Mesh(model, crs_uuid=crs.uuid, mesh_flavour='ref&z', ni=ni, nj=nj, z_values=horizon_depths, z_supporting_mesh_uuid=lattice.uuid, title='h' + str(i)) horizon_mesh.write_hdf5() horizon_mesh.create_xml() horizon_surface = rqs.Surface(model, crs_uuid=crs.uuid, mesh=horizon_mesh, quad_triangles=True, title=horizon_mesh.title) horizon_surface.write_hdf5() horizon_surface.create_xml() horizons.append(horizon_surface) # create a re-triangulated Voronoi vertical prism grid grid = rug.VerticalPrismGrid.from_seed_points_and_surfaces( model, seeds_xy, horizons, aoi, title="giant's causeway") assert grid is not None grid.write_hdf5() grid.create_xml() # check cell thicknesses are in expected range thick = grid.thickness() assert np.all(thick >= 80.0) assert np.all(thick <= 120.0) model.store_epc()
def test_vertical_prism_grid_from_surfaces(tmp_path): epc = os.path.join(tmp_path, 'vertical_prism.epc') model = rq.new_model(epc) crs = rqc.Crs(model) crs.create_xml() # create a point set representing a pentagon with a centre node pentagon_points = np.array([[-100.0, -200.0, 1050.0], [-200.0, 0.0, 1050.0], [0.0, 200.0, 1025.0], [200.0, 0.0, 975.0], [100.0, -200.0, 999.0], [0.0, 0.0, 1000.0]]) pentagon = rqs.PointSet(model, points_array=pentagon_points, crs_uuid=crs.uuid, title='pentagon') pentagon.write_hdf5() pentagon.create_xml() # create a surface from the point set (will make a Delauney triangulation) top_surf = rqs.Surface(model, point_set=pentagon, title='top surface') top_surf.write_hdf5() top_surf.create_xml() surf_list = [top_surf] # check the pentagon surface pentagon_triangles, pentagon_points = top_surf.triangles_and_points() assert pentagon_points.shape == (6, 3) assert pentagon_triangles.shape == (5, 3) # create a couple of horizontal surfaces at greater depths boundary = np.array([[-300.0, -300.0, 0.0], [300.0, 300.0, 0.0]]) for depth in (1100.0, 1200.0): base = rqs.Surface(model) base.set_to_horizontal_plane(depth, boundary) base.write_hdf5() base.create_xml() surf_list.append(base) # now build a vertical prism grid from the surfaces grid = rug.VerticalPrismGrid.from_surfaces(model, surf_list, title='the pentagon') grid.write_hdf5() grid.create_xml() model.store_epc() # re-open model model = rq.Model(epc) assert model is not None # find grid by title grid_uuid = model.uuid(obj_type='UnstructuredGridRepresentation', title='the pentagon') assert grid_uuid is not None # re-instantiate the grid grid = rug.VerticalPrismGrid(model, uuid=grid_uuid) assert grid is not None assert grid.nk == 2 assert grid.cell_count == 10 assert grid.node_count == 18 assert grid.face_count == 35 # create a very similar grid using explicit triangulation arguments # make the same Delauney triangulation triangles = triangulation.dt(pentagon_points, algorithm="scipy") assert triangles.ndim == 2 and triangles.shape[1] == 3 # slightly shrink pentagon points to be within area of surfaces for i in range(len(pentagon_points)): if pentagon_points[i, 0] < 0.0: pentagon_points[i, 0] += 1.0 elif pentagon_points[i, 0] > 0.0: pentagon_points[i, 0] -= 1.0 if pentagon_points[i, 1] < 0.0: pentagon_points[i, 1] += 1.0 elif pentagon_points[i, 1] > 0.0: pentagon_points[i, 1] -= 1.0 # load the surfaces surf_uuids = model.uuids(obj_type='TriangulatedSetRepresentation', sort_by='oldest') surf_list = [] for surf_uuid in surf_uuids: surf_list.append(rqs.Surface(model, uuid=surf_uuid)) # create a new vertical prism grid using the explicit triangulation arguments similar = rug.VerticalPrismGrid.from_surfaces( model, surf_list, column_points=pentagon_points, column_triangles=triangles, title='similar pentagon') # check similarity for attr in ('cell_shape', 'nk', 'cell_count', 'node_count', 'face_count'): assert getattr(grid, attr) == getattr(similar, attr) # for index_attr in ('nodes_per_face', 'nodes_per_face_cl', 'faces_per_cell', 'faces_per_cell_cl'): for i, (index_attr, index_attr_cl) in enumerate([ ('nodes_per_face', 'nodes_per_face_cl'), ('faces_per_cell', 'faces_per_cell_cl') ]): ga_cl = getattr(grid, index_attr_cl) sa_cl = getattr(similar, index_attr_cl) assert np.all(ga_cl == sa_cl) ga = getattr(grid, index_attr) sa = getattr(similar, index_attr) ip = 0 if i == 0 else ga_cl[i - 1] assert set(ga[ip:ga_cl[i]]) == set(sa[ip:ga_cl[i]]) assert_allclose(grid.points_ref(), similar.points_ref(), atol=2.0) # check that isotropic horizontal permeability is preserved permeability = 250.0 primary_k = np.full((grid.cell_count, ), permeability) orthogonal_k = primary_k.copy() triple_k = grid.triple_horizontal_permeability(primary_k, orthogonal_k, 37.0) assert triple_k.shape == (grid.cell_count, 3) assert_array_almost_equal(triple_k, permeability) azimuth = np.linspace(0.0, 360.0, num=grid.cell_count) triple_k = grid.triple_horizontal_permeability(primary_k, orthogonal_k, azimuth) assert triple_k.shape == (grid.cell_count, 3) assert_array_almost_equal(triple_k, permeability) # check that anisotropic horizontal permeability is correctly bounded orthogonal_k *= 0.1 triple_k = grid.triple_horizontal_permeability(primary_k, orthogonal_k, azimuth) assert triple_k.shape == (grid.cell_count, 3) assert np.all(triple_k <= permeability) assert np.all(triple_k >= permeability * 0.1) assert np.min(triple_k) < permeability / 2.0 assert np.max(triple_k) > permeability / 2.0 # set up some properties pc = grid.property_collection assert pc is not None pc.add_cached_array_to_imported_list(cached_array=None, source_info='unit test', keyword='NETGRS', property_kind='net to gross ratio', discrete=False, uom='m3/m3', indexable_element='cells', const_value=0.75) pc.add_cached_array_to_imported_list(cached_array=None, source_info='unit test', keyword='PERMK', property_kind='permeability rock', facet_type='direction', facet='K', discrete=False, uom='mD', indexable_element='cells', const_value=10.0) pc.add_cached_array_to_imported_list(cached_array=None, source_info='unit test', keyword='PERM', property_kind='permeability rock', facet_type='direction', facet='primary', discrete=False, uom='mD', indexable_element='cells', const_value=100.0) pc.add_cached_array_to_imported_list(cached_array=None, source_info='unit test', keyword='PERM', property_kind='permeability rock', facet_type='direction', facet='orthogonal', discrete=False, uom='mD', indexable_element='cells', const_value=20.0) x_min, x_max = grid.xyz_box()[:, 0] relative_x = (grid.centre_point()[:, 0] - x_min) * (x_max - x_min) azi = relative_x * 90.0 + 45.0 pc.add_cached_array_to_imported_list( cached_array=azi, source_info='unit test', keyword='primary permeability azimuth', property_kind='plane angle', facet_type='direction', facet='primary', discrete=False, uom='dega', indexable_element='cells') pc.write_hdf5_for_imported_list() pc.create_xml_for_imported_list_and_add_parts_to_model() model.store_epc() # test that half cell transmissibilities can be computed half_t = grid.half_cell_transmissibility() assert np.all(half_t > 0.0) # add the half cell transmissibility array as a property pc.add_cached_array_to_imported_list(cached_array=half_t.flatten(), source_info='unit test', keyword='half transmissibility', property_kind='transmissibility', discrete=False, count=1, indexable_element='faces per cell') pc.write_hdf5_for_imported_list() pc.create_xml_for_imported_list_and_add_parts_to_model( extra_metadata={'uom': 'm3.cP/(d.kPa)'}) model.store_epc()
def from_surfaces(cls, parent_model, surfaces, column_points=None, column_triangles=None, title=None, originator=None, extra_metadata={}, set_handedness=False): """Create a layered vertical prism grid from an ordered list of untorn surfaces. arguments: parent_model (model.Model object): the model which this grid is part of surfaces (list of surface.Surface): list of two or more untorn surfaces ordered from shallowest to deepest; see notes column_points (2D numpy float array, optional): if present, the xy points to use for the grid's triangulation; see notes column_triangles (numpy int array of shape (M, 3), optional): if present, indices into the first dimension of column_points giving the xy triangulation to use for the grid; see notes title (str, optional): citation title for the new grid originator (str, optional): name of person creating the grid; defaults to login id extra_metadata (dict, optional): dictionary of extra metadata items to add to the grid returns: a newly created VerticalPrismGrid object notes: this method will not work for torn (faulted) surfaces, nor for surfaces with recumbent folds; the surfaces may not cross each other, ie. the depth ordering must be consistent over the area; the triangular pattern of the columns (in the xy plane) can be specified with the column_points and column_triangles arguments; if those arguments are None, the first, shallowest, surface is used as a master and determines the triangular pattern of the columns; where a gravity vector from a node above does not intersect a surface, the point is inherited as a copy of the node above and will be NaNs if no surface above has an intersection; the Surface class has methods for creating a Surface from a PointSet or a Mesh (RESQML Grid2dRepresentation), or for a horizontal plane; this class is represented in RESQML as an UnstructuredGridRepresentation – when a resqpy class is written for ColumnLayerGridRepresentation, a method will be added to that class to convert from a resqpy VerticalPrismGrid """ def find_pair(a, pair): # for sorted array a of shape (N, 2) returns index in first axis of a pair def frp(a, pair, b, c): m = b + ((c - b) // 2) assert m < len(a), 'pair not found in sorted array' if np.all(a[m] == pair): return m assert c > b, 'pair not found in sorted array' if a[m, 0] < pair[0]: return frp(a, pair, m + 1, c) elif a[m, 0] > pair[0]: return frp(a, pair, b, m) elif a[m, 1] < pair[1]: return frp(a, pair, m + 1, c) else: return frp(a, pair, b, m) return frp(a, pair, 0, len(a)) assert (column_points is None) == (column_triangles is None) assert len(surfaces) > 1 for s in surfaces: assert isinstance(s, rqs.Surface) vpg = cls(parent_model, title=title, originator=originator, extra_metadata=extra_metadata) assert vpg is not None top = surfaces[0] # set and check consistency of crs vpg.crs_uuid = top.crs_uuid for s in surfaces[1:]: if not bu.matching_uuids(vpg.crs_uuid, s.crs_uuid): # check for equivalence assert rqc.Crs(parent_model, uuid=vpg.crs_uuid) == rqc.Crs( parent_model, uuid=s.crs_uuid), 'mismatching surface crs' # fetch the data for the top surface, to be used as the master for the triangular pattern if column_triangles is None: top_triangles, top_points = top.triangles_and_points() column_edges = top.distinct_edges( ) # ordered pairs of node indices else: top_triangles = column_triangles if column_points.shape[1] == 3: top_points = column_points else: top_points = np.zeros((len(column_points), 3)) top_points[:, :column_points.shape[1]] = column_points column_surf = rqs.Surface(parent_model, crs_uuid=vpg.crs_uuid) column_surf.set_from_triangles_and_points(column_triangles, column_points) column_edges = column_surf.distinct_edges() assert top_triangles.ndim == 2 and top_triangles.shape[1] == 3 assert top_points.ndim == 2 and top_points.shape[1] in [2, 3] assert len(top_triangles) > 0 p_count = len(top_points) bad_points = np.zeros(p_count, dtype=bool) # setup size of arrays for the vertical prism grid column_count = top_triangles.shape[0] surface_count = len(surfaces) layer_count = surface_count - 1 column_edge_count = len(column_edges) vpg.cell_count = column_count * layer_count vpg.node_count = p_count * surface_count vpg.face_count = column_count * surface_count + column_edge_count * layer_count vpg.nk = layer_count if vpg.extra_metadata is None: vpg.extra_metadata = {} vpg.extra_metadata['layer count'] = vpg.nk # setup points with copies of points for top surface, z values to be updated later points = np.zeros((surface_count, p_count, 3)) points[:, :, :] = top_points # arrange faces with all triangles first, followed by the vertical quadrilaterals vpg.nodes_per_face_cl = np.zeros(vpg.face_count, dtype=int) vpg.nodes_per_face_cl[:column_count * surface_count] = \ np.arange(3, 3 * column_count * surface_count + 1, 3, dtype = int) quad_start = vpg.nodes_per_face_cl[column_count * surface_count - 1] + 4 vpg.nodes_per_face_cl[column_count * surface_count:] = \ np.arange(quad_start, quad_start + 4 * column_edge_count * layer_count, 4) assert vpg.nodes_per_face_cl[ -1] == 3 * column_count * surface_count + 4 * column_edge_count * layer_count # populate nodes per face for triangular faces vpg.nodes_per_face = np.zeros(vpg.nodes_per_face_cl[-1], dtype=int) for surface_index in range(surface_count): vpg.nodes_per_face[surface_index * 3 * column_count : (surface_index + 1) * 3 * column_count] = \ top_triangles.flatten() + surface_index * p_count # populate nodes per face for quadrilateral faces quad_nodes = np.empty((layer_count, column_edge_count, 2, 2), dtype=int) for layer in range(layer_count): quad_nodes[layer, :, 0, :] = column_edges + layer * p_count # reverse order of base pairs to maintain cyclic ordering of nodes per face quad_nodes[layer, :, 1, 0] = column_edges[:, 1] + (layer + 1) * p_count quad_nodes[layer, :, 1, 1] = column_edges[:, 0] + (layer + 1) * p_count vpg.nodes_per_face[3 * surface_count * column_count:] = quad_nodes.flatten() assert vpg.nodes_per_face[-1] > 0 # set up faces per cell vpg.faces_per_cell = np.zeros(5 * vpg.cell_count, dtype=int) vpg.faces_per_cell_cl = np.arange(5, 5 * vpg.cell_count + 1, 5, dtype=int) assert len(vpg.faces_per_cell_cl) == vpg.cell_count # set cell top triangle indices for layer in range(layer_count): # top triangular faces of cells vpg.faces_per_cell[5 * layer * column_count : (layer + 1) * 5 * column_count : 5] = \ layer * column_count + np.arange(column_count) # base triangular faces of cells vpg.faces_per_cell[layer * 5 * column_count + 1: (layer + 1) * 5 * column_count : 5] = \ (layer + 1) * column_count + np.arange(column_count) # todo: some clever numpy indexing to irradicate the following for loop for col in range(column_count): t_nodes = top_triangles[col] for t_edge in range(3): a, b = t_nodes[t_edge - 1], t_nodes[t_edge] if b < a: a, b = b, a edge = find_pair( column_edges, (a, b)) # returns index into first axis of column edges # set quadrilateral faces of cells in column, for this edge vpg.faces_per_cell[5 * col + t_edge + 2 : 5 * vpg.cell_count : 5 * column_count] = \ np.arange(column_count * surface_count + edge, vpg.face_count, column_edge_count) # check full population of faces_per_cell (face zero is a top triangle, only used once) assert np.count_nonzero( vpg.faces_per_cell) == vpg.faces_per_cell.size - 1 vpg.cell_face_is_right_handed = np.ones(len(vpg.faces_per_cell), dtype=bool) if set_handedness: # TODO: set handedness correctly and make default for set_handedness True raise NotImplementedError( 'code not written to set handedness for vertical prism grid from surfaces' ) # instersect gravity vectors from column points with other surfaces, and update z values in points gravity = np.zeros((p_count, 3)) gravity[:, 2] = 1.0 # up/down does not matter for the intersection function used below start = 1 if top_triangles is None else 0 for surf in range(start, surface_count): surf_triangles, surf_points = surfaces[surf].triangles_and_points() intersects = meet.line_set_triangles_intersects( top_points, gravity, surf_points[surf_triangles]) single_intersects = meet.last_intersects( intersects) # will be triple NaN where no intersection occurs # inherit point from surface above where no intersection has occurred nan_lines = np.isnan(single_intersects[:, 0]) if surf == 0: # allow NaN entries to handle unused distant circumcentres in Voronoi graph data # assert not np.any(nan_lines), 'top surface does not cover all column points' single_intersects[nan_lines] = np.NaN else: single_intersects[nan_lines] = points[surf - 1][nan_lines] # populate z values for layer of points points[surf, :, 2] = single_intersects[:, 2] vpg.points_cached = points.reshape((-1, 3)) assert np.all(vpg.nodes_per_face < len(vpg.points_cached)) return vpg