def combine_polylines(self, polylines): """Combines the isclosed boolean array, coordinates and count data for a list of polyline objects. args: polylines: list of polyline objects """ self.count_perpol = [] self.closed_array = [] for poly in polylines: if poly == polylines[0]: master_crs = rcrs.Crs(self.model, uuid = poly.crs_uuid) self.crs_uuid = poly.crs_uuid self.coordinates = poly.coordinates.copy() else: curr_crs = rcrs.Crs(self.model, uuid = poly.crs_uuid) if not curr_crs.is_equivalent(master_crs): shifted = curr_crs.convert_array_to(master_crs, poly.coordinates) self.coordinates = np.concatenate((self.coordinates, shifted)) else: self.coordinates = np.concatenate((self.coordinates, poly.coordinates)) self.closed_array.append(poly.isclosed) self.count_perpol.append(int(len(poly.coordinates))) self.count_perpol = np.array(self.count_perpol) assert len(self.closed_array) == len(self.count_perpol) assert np.sum(self.count_perpol) == len(self.coordinates) self.polys = polylines
def _populate_composite_face_sets_for_polylines(model, grid, polylines, lines_crs_uuid, grid_crs, lines_file_list, full_pillar_list_dict, composite_face_set_dict): lines_crs = None if lines_crs_uuid is None else rqc.Crs( model, uuid=lines_crs_uuid) if polylines: for i, polyline in enumerate(polylines): new_line = polyline.coordinates.copy() if polyline.crs_uuid is not None and polyline.crs_uuid != lines_crs_uuid: lines_crs_uuid = polyline.crs_uuid lines_crs = rqc.Crs(model, uuid=lines_crs_uuid) if lines_crs: lines_crs.convert_array_to(grid_crs, new_line) title = polyline.title if polyline.title else 'fault_' + str(i) _make_face_sets_for_new_lines([new_line], title, grid, full_pillar_list_dict, composite_face_set_dict) else: for filename in lines_file_list: new_lines = sl.read_lines(filename) if lines_crs is not None: for a in new_lines: lines_crs.convert_array_to(grid_crs, a) _, f_name = os.path.split(filename) if f_name.lower().endswith('.dat'): face_set_id = f_name[:-4] else: face_set_id = f_name _make_face_sets_for_new_lines(new_lines, face_set_id, grid, full_pillar_list_dict, composite_face_set_dict)
def test_model_context(tmp_path): # Create a new model epc_path = str(tmp_path / 'tmp_model.epc') model = rq.new_model(epc_path) crs = rqc.Crs(parent_model=model, title='kuzcotopia') crs_uuid = crs.uuid crs.create_xml() model.store_epc() del crs, model # Re-open model in read/write mode with rq.ModelContext(epc_path, mode="rw") as model2: crs2 = rqc.Crs(model2, uuid=crs_uuid) assert len(list(model2.iter_crs())) == 1 assert crs2.title == 'kuzcotopia' # Make a change crs2.title = 'wabajam' crs2.create_xml(reuse=False) # Re-open model in read mode with rq.ModelContext(epc_path, mode="r") as model3: # Check model has loaded correctly assert len(list(model3.iter_crs())) == 1 crs3 = rqc.Crs(model3, uuid=crs_uuid) assert crs3.title == 'wabajam' # Overwrite model with rq.ModelContext(epc_path, mode="create") as model4: # Should be empty crs_list = list(model4.iter_crs()) assert len(crs_list) == 0
def from_polyset(self, polyset): """Instantiates a pointset using points from an input polylineset (PolylineSetRepresentation) object arguments: polyset (resqpy.lines.PolylineSet object): a polylineset object to generate the pointset from """ master_crs = rcrs.Crs(self.model, uuid = polyset.polys[0].crs_uuid) if polyset.polys[0].isclosed and vec.isclose(polyset.polys[0].coordinates[0], polyset.polys[0].coordinates[-1]): poly_coords = polyset.polys[0].coordinates[:-1].copy() else: poly_coords = polyset.polys[0].coordinates.copy() for poly in polyset.polys[1:]: curr_crs = rcrs.Crs(self.model, uuid = poly.crs_uuid) assert master_crs is not None assert poly_coords is not None if not curr_crs.is_equivalent(master_crs): shifted = curr_crs.convert_array_to(master_crs, poly.coordinates) poly_coords = concat_polyset_points(poly.isclosed, shifted, poly_coords) else: poly_coords = concat_polyset_points(poly.isclosed, poly.coordinates, poly_coords) self.add_patch(poly_coords) if polyset.rep_int_root is not None: self.set_represented_interpretation_root(polyset.rep_int_root) self.check_crs_match(master_crs.uuid) if not self.title: self.title = polyset.title
def test_model_copy_all_parts(example_model_with_properties): epc = example_model_with_properties.epc_file dir = example_model_with_properties.epc_directory copied_epc = os.path.join(dir, 'copied.epc') # test copying without consolidation original = rq.Model(epc) assert original is not None copied = rq.new_model(copied_epc) copied.copy_all_parts_from_other_model(original, consolidate=False) assert set(original.uuids()) == set(copied.uuids()) assert set(original.parts()) == set(copied.parts()) # test without consolidation of two crs objects copied = rq.new_model(copied_epc) new_crs = rqc.Crs(copied) new_crs.create_xml() copied.copy_all_parts_from_other_model(original, consolidate=False) assert len(copied.parts()) == len(original.parts()) + 1 assert set(original.parts()).issubset(set(copied.parts())) assert len(copied.parts(obj_type='LocalDepth3dCrs')) == 2 # test with consolidation of two crs objects copied = rq.new_model(copied_epc) new_crs = rqc.Crs(copied) new_crs.create_xml() copied.copy_all_parts_from_other_model(original, consolidate=True) assert len(copied.parts()) == len(original.parts()) assert len(copied.parts(obj_type='LocalDepth3dCrs')) == 1 crs_uuid = copied.uuid(obj_type='LocalDepth3dCrs') assert (bu.matching_uuids(crs_uuid, new_crs.uuid) or bu.matching_uuids( crs_uuid, original.uuid(obj_type='LocalDepth3dCrs'))) # test write and re-load of copied model copied.store_epc() re_opened = rq.Model(copied_epc) assert re_opened is not None assert len(copied.parts()) == len(original.parts()) crs_uuid = re_opened.uuid(obj_type='LocalDepth3dCrs') assert (bu.matching_uuids(crs_uuid, new_crs.uuid) or bu.matching_uuids( crs_uuid, original.uuid(obj_type='LocalDepth3dCrs')))
def _inherit_basics(grid, grid_a, grid_b): grid.grid_representation = 'IjkGrid' grid.extent_kji = grid_a.extent_kji.copy() grid.nk, grid.nj, grid.ni = grid.extent_kji grid.k_direction_is_down = grid_a.k_direction_is_down grid.grid_is_right_handed = grid_a.grid_is_right_handed grid.pillar_shape = grid_a.pillar_shape grid.has_split_coordinate_lines = (grid_a.has_split_coordinate_lines or grid_b.has_split_coordinate_lines) # inherit the coordinate reference system used by the grid geometry grid.crs_uuid = grid_a.crs_uuid if grid_a.model is not grid.model: grid.model.duplicate_node(grid_a.model.root_for_uuid( grid_a, grid.crs_uuid), add_as_part=True) grid.crs = rqc.Crs(grid.model, uuid=grid.crs_uuid) if grid_a.inactive is None or grid_b.inactive is None: grid.inactive = None else: grid.inactive = np.logical_and(grid_a.inactive, grid_b.inactive) grid.geometry_defined_for_all_cells_cached = True grid.array_cell_geometry_is_defined = np.ones(tuple(grid.extent_kji), dtype=bool) grid.geometry_defined_for_all_pillars_cached = True
def test_model(tmp_path): epc = os.path.join(tmp_path, 'model.epc') model = rq.new_model(epc) assert model is not None crs = rqc.Crs(model) crs_root = crs.create_xml() model.store_epc() assert os.path.exists(epc) md_datum_1 = rqw.MdDatum(model, location=(0.0, 0.0, -50.0), crs_uuid=crs.uuid) md_datum_1.create_xml(title='Datum & 1') md_datum_2 = rqw.MdDatum(model, location=(3.0, 0.0, -50.0), crs_uuid=crs.uuid) md_datum_2.create_xml(title='Datum < 2') assert len(model.uuids(obj_type='MdDatum')) == 2 model.store_epc() model = rq.Model(epc) assert model is not None assert len(model.uuids(obj_type='MdDatum')) == 2 datum_part_1 = model.part(obj_type='MdDatum', title='1', title_mode='ends') datum_part_2 = model.part(obj_type='MdDatum', title='2', title_mode='ends') assert datum_part_1 is not None and datum_part_2 is not None and datum_part_1 != datum_part_2 datum_uuid_1 = rqet.uuid_in_part_name(datum_part_1) datum_uuid_2 = rqet.uuid_in_part_name(datum_part_2) assert not bu.matching_uuids(datum_uuid_1, datum_uuid_2) p1 = model.uuid_part_dict[bu.uuid_as_int(datum_uuid_1)] p2 = model.uuid_part_dict[bu.uuid_as_int(datum_uuid_2)] assert p1 == datum_part_1 and p2 == datum_part_2
def test_forestry(example_model_with_prop_ts_rels): model = example_model_with_prop_ts_rels full_parts_list = model.parts() dp_parts_list = model.parts(obj_type='DiscreteProperty') assert len(dp_parts_list) > 1 # remove an individual part model.remove_part(dp_parts_list[0]) # corrupt some forest dictionary entries and test tidy up for part in dp_parts_list[1:]: model.parts_forest[part] = (None, None, None) model.tidy_up_forests() assert len(model.parts()) + len(dp_parts_list) == len(full_parts_list) assert all(p not in model.parts() for p in dp_parts_list) # test patch_root_for_part() crs_uuid = model.uuid(obj_type='LocalDepth3dCrs') crs_part = model.part_for_uuid(crs_uuid) assert crs_uuid is not None and crs_part is not None crs = rqc.Crs(model, uuid=crs_uuid) assert crs is not None crs.title = 'relativity' crs.originator = 'einstein' new_crs_node = crs.create_xml(add_as_part=False, reuse=False) rqet.find_tag(new_crs_node, 'VerticalUom').text = 'ft[US]' model.patch_root_for_part(crs_part, new_crs_node) assert rqet.find_tag_text(model.root(uuid=crs_uuid), 'VerticalUom') == 'ft[US]' assert model.citation_title_for_part(crs_part) == 'relativity' assert model.title(uuid=crs_uuid) == 'relativity' assert rqet.find_nested_tags_text(model.root(uuid=crs_uuid), ['Citation', 'Originator']) == 'einstein' # rough test of low level fell_part() model.fell_part(crs_part) assert len(model.parts()) + len(dp_parts_list) + 1 == len(full_parts_list)
def _set_mesh_from_df(self): """Creates Mesh object; called before writing to hdf5 or creating xml.""" # note: actual data is stored in related Property if realization number is present, directly in Mesh otherwise assert self.n_rows == len(self.df) assert self.n_cols == len(self.df.columns) if self.mesh is None: origin = (0.0, 0.0, 0.0) dxyz_dij = np.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]) crs_uuids = self.model.uuids(obj_type='LocalDepth3dCrs') if len(crs_uuids) == 0: crs = rqc.Crs(self.model) crs.create_xml() crs_uuid = crs.uuid else: # use any available crs crs_uuid = crs_uuids[0] if self.realization is None: self.mesh = rqs.Mesh(self.model, mesh_flavour='reg&z', ni=self.n_cols, nj=self.n_rows, dxyz_dij=dxyz_dij, origin=origin, z_values=np.array(self.df), crs_uuid=crs_uuid) else: self.mesh = rqs.Mesh(self.model, mesh_flavour='regular', ni=self.n_cols, nj=self.n_rows, dxyz_dij=dxyz_dij, origin=origin, crs_uuid=crs_uuid) self.mesh.write_hdf5() mesh_root = self.mesh.create_xml(title=self.title) rqet.create_metadata_xml(mesh_root, {'dataframe': 'true'}) if self.realization is not None: self.pc = rqp.PropertyCollection() self.pc.set_support(support=self.mesh) dataframe_pk_uuid = self.model.uuid(obj_type='PropertyKind', title='dataframe') if dataframe_pk_uuid is None: dataframe_pk = rqp.PropertyKind(self.model, title='dataframe', example_uom='Euc') dataframe_pk.create_xml() dataframe_pk_uuid = dataframe_pk.uuid self.pc.add_cached_array_to_imported_list( np.array(self.df), 'dataframe', self.title, uom='Euc', property_kind='dataframe', local_property_kind_uuid=dataframe_pk_uuid, realization=self.realization, indexable_element='nodes') self.pc.write_hdf5_for_imported_list() self.pc.create_xml_for_imported_list_and_add_parts_to_model()
def _iter_crs(model): """Iterable of all CRS objects associated with the model.""" import resqpy.crs as rqc # imported here for speed, module is not always needed uuids = _uuids(model, obj_type='LocalDepth3dCrs') + _uuids( model, obj_type='LocalTime3dCrs') if uuids: for uuid in uuids: yield rqc.Crs(model, uuid=uuid)
def z_inc_down(grid): """Return True if z increases downwards in the coordinate reference system used by the grid geometry :meta common: """ if grid.crs is None: assert grid.crs_uuid is not None grid.crs = rqc.Crs(grid.model, uuid=grid.crs_uuid) return grid.crs.z_inc_down
def test_crs_reuse(): model = rq.Model(new_epc = True, create_basics = True) crs_a = rqc.Crs(model) crs_a.create_xml() crs_b = rqc.Crs(model) crs_b.create_xml() assert len(model.parts(obj_type = 'LocalDepth3dCrs')) == 1 assert crs_a == crs_b assert bu.matching_uuids(crs_a.uuid, crs_b.uuid) crs_c = rqc.Crs(model, z_inc_down = False) crs_c.create_xml() assert len(model.parts(obj_type = 'LocalDepth3dCrs')) == 2 assert crs_c != crs_a assert not bu.matching_uuids(crs_c.uuid, crs_a.uuid) crs_d = rqc.Crs(model, z_units = 'ft') crs_d.create_xml() assert len(model.parts(obj_type = 'LocalDepth3dCrs')) == 3 crs_e = rqc.Crs(model, z_inc_down = False) crs_e.create_xml() assert len(model.uuids(obj_type = 'LocalDepth3dCrs')) == 3 assert crs_e == crs_c assert bu.matching_uuids(crs_e.uuid, crs_c.uuid) crs_f = rqc.Crs(model) crs_f.create_xml(reuse = False) assert len(model.parts(obj_type = 'LocalDepth3dCrs')) == 4 assert crs_f == crs_a assert not bu.matching_uuids(crs_f.uuid, crs_a.uuid)
def test_resqml_units(tmp_path, resqml_xy_units, resqml_z_units): # Arrange current_filename = os.path.split(getsourcefile(lambda: 0))[0] base_folder = os.path.dirname(os.path.dirname(current_filename)) ensemble_dir = f'{base_folder}/test_data/wren' epc_file = f'{tmp_path}/test.epc' # Act import_vdb_ensemble(epc_file, ensemble_dir, resqml_xy_units=resqml_xy_units, resqml_z_units=resqml_z_units) model = rq.Model(epc_file) crs_uuid = model.uuid(obj_type='LocalDepth3dCrs') crs = rqc.Crs(model, uuid=crs_uuid) # todo: could check grid point values are being converted # Assert assert crs.xy_units == resqml_xy_units assert crs.z_units == resqml_z_units
def change_crs(self, required_crs): """Changes the crs of the point set, also sets a new uuid if crs changed. notes: this method is usually used to change the coordinate system for a temporary resqpy object; to add as a new part, call write_hdf5() and create_xml() methods """ old_crs = rcrs.Crs(self.model, uuid = self.crs_uuid) self.crs_uuid = required_crs.uuid if required_crs == old_crs or not self.patch_ref_list: log.debug(f'no crs change needed for {self.title}') return log.debug(f'crs change needed for {self.title} from {old_crs.title} to {required_crs.title}') self.load_all_patches() self.patch_ref_list = [] for patch_points in self.patch_array_list: required_crs.convert_array_from(old_crs, patch_points) self.patch_ref_list.append((None, None, len(patch_points))) self.full_array = None # clear cached full array for point set self.uuid = bu.new_uuid() # hope this doesn't cause problems
def _global_to_local_crs(grid, a, crs_uuid=None, global_xy_units=None, global_z_units=None, global_z_increasing_downward=None): """Converts array of points in situ from global coordinate system to established local one.""" if crs_uuid is None: crs_uuid = grid.crs_uuid if crs_uuid is None: return a flat_a = a.reshape(( -1, 3)) # flattened view of array a as vector of (x, y, z) points, in situ crs = rqc.Crs(grid.model, uuid=crs_uuid) if global_xy_units is not None: bwam.convert_lengths(flat_a[:, 0], global_xy_units, crs.xy_units) # x bwam.convert_lengths(flat_a[:, 1], global_xy_units, crs.xy_units) # y if global_z_units is not None: bwam.convert_lengths(flat_a[:, 2], global_z_units, crs.z_units) # z # This code assumes x, y, z offsets are in local crs units flat_a[:, 0] -= crs.x_offset flat_a[:, 1] -= crs.y_offset flat_a[:, 2] -= crs.z_offset # note: here negation is made in local crs; if z_offset is not zero, this might not be what is intended if global_z_increasing_downward is not None: if global_z_increasing_downward != crs.z_inc_down: flat_a[:, 2] = np.negative(flat_a[:, 2]) if crs.rotated: flat_a[:] = vec.rotate_array(crs.reverse_rotation_matrix, flat_a) return flat_a.reshape(a.shape)
def change_crs(self, required_crs): """Changes the crs of the surface, also sets a new uuid if crs changed. note: this method is usually used to change the coordinate system for a temporary resqpy object; to add as a new part, call write_hdf5() and create_xml() methods """ old_crs = rqc.Crs(self.model, uuid=self.crs_uuid) self.crs_uuid = required_crs.uuid if required_crs == old_crs or not self.patch_list: log.debug(f'no crs change needed for {self.title}') return log.debug( f'crs change needed for {self.title} from {old_crs.title} to {required_crs.title}' ) for patch in self.patch_list: patch.triangles_and_points() required_crs.convert_array_from(old_crs, patch.points) patch.crs_uuid = self.crs_uuid self.triangles = None # clear cached arrays for surface self.points = None self.uuid = bu.new_uuid() # hope this doesn't cause problems assert self.root is None
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 test_tetra_grid(tmp_path): epc = os.path.join(tmp_path, 'tetra_test.epc') model = rq.new_model(epc) crs = rqc.Crs(model) crs.create_xml() # create an empty TetraGrid tetra = rug.TetraGrid(model, title='star') assert tetra.cell_shape == 'tetrahedral' # hand craft all attribute data tetra.crs_uuid = model.uuid(obj_type='LocalDepth3dCrs') assert tetra.crs_uuid is not None assert bu.matching_uuids(tetra.crs_uuid, crs.uuid) tetra.set_cell_count(5) # faces tetra.face_count = 16 tetra.faces_per_cell_cl = np.arange(4, 4 * 5 + 1, 4, dtype=int) tetra.faces_per_cell = np.empty(20, dtype=int) tetra.faces_per_cell[:4] = (0, 1, 2, 3) # cell 0 tetra.faces_per_cell[4:8] = (0, 4, 5, 6) # cell 1 tetra.faces_per_cell[8:12] = (1, 7, 8, 9) # cell 2 tetra.faces_per_cell[12:16] = (2, 10, 11, 12) # cell 3 tetra.faces_per_cell[16:] = (3, 13, 14, 15) # cell 4 # nodes tetra.node_count = 8 tetra.nodes_per_face_cl = np.arange(3, 3 * 16 + 1, 3, dtype=int) tetra.nodes_per_face = np.empty(48, dtype=int) # internal faces (cell 0) tetra.nodes_per_face[:3] = (0, 1, 2) # face 0 tetra.nodes_per_face[3:6] = (0, 3, 1) # face 1 tetra.nodes_per_face[6:9] = (1, 3, 2) # face 2 tetra.nodes_per_face[9:12] = (2, 3, 0) # face 3 # external faces (cell 1) tetra.nodes_per_face[12:15] = (0, 1, 4) # face 4 tetra.nodes_per_face[15:18] = (1, 2, 4) # face 5 tetra.nodes_per_face[18:21] = (2, 0, 4) # face 6 # external faces (cell 2) tetra.nodes_per_face[21:24] = (0, 3, 5) # face 7 tetra.nodes_per_face[24:27] = (3, 1, 5) # face 8 tetra.nodes_per_face[27:30] = (1, 0, 5) # face 9 # external faces (cell 3) tetra.nodes_per_face[30:33] = (1, 3, 6) # face 10 tetra.nodes_per_face[33:36] = (3, 2, 6) # face 11 tetra.nodes_per_face[36:39] = (2, 1, 6) # face 12 # external faces (cell 4) tetra.nodes_per_face[39:42] = (2, 3, 7) # face 10 tetra.nodes_per_face[42:45] = (3, 0, 7) # face 11 tetra.nodes_per_face[45:] = (0, 2, 7) # face 12 # face handedness tetra.cell_face_is_right_handed = np.zeros( 20, dtype=bool) # False for all faces for external cells (1 to 4) tetra.cell_face_is_right_handed[: 4] = True # True for all faces of internal cell (0) # points tetra.points_cached = np.zeros((8, 3)) # internal cell (0) points half_edge = 36.152 one_over_root_two = 1.0 / maths.sqrt(2.0) tetra.points_cached[0] = (-half_edge, 0.0, -half_edge * one_over_root_two) tetra.points_cached[1] = (half_edge, 0.0, -half_edge * one_over_root_two) tetra.points_cached[2] = (0.0, half_edge, half_edge * one_over_root_two) tetra.points_cached[3] = (0.0, -half_edge, half_edge * one_over_root_two) # project remaining nodes outwards for fi, o_node in enumerate((3, 2, 0, 1)): fc = tetra.face_centre_point(fi) tetra.points_cached[4 + fi] = fc - (tetra.points_cached[o_node] - fc) # basic validity check tetra.check_tetra() # write arrays, create xml and store model tetra.write_hdf5() tetra.create_xml() model.store_epc() # re-open model and establish grid model = rq.Model(epc) assert model is not None tetra_uuid = model.uuid(obj_type='UnstructuredGridRepresentation', title='star') assert tetra_uuid is not None tetra = rug.TetraGrid(model, uuid=tetra_uuid) assert tetra is not None # perform basic checks assert tetra.cell_count == 5 assert tetra.cell_shape == 'tetrahedral' tetra.check_tetra() # test volume calculation expected_cell_volume = ((2.0 * half_edge)**3) / (6.0 * maths.sqrt(2.0)) for cell in range(tetra.cell_count): assert maths.isclose(tetra.volume(cell), expected_cell_volume, rel_tol=1.0e-3) assert maths.isclose(tetra.grid_volume(), 5.0 * expected_cell_volume) # test face area expected_area = maths.sqrt(3.0 * half_edge * (half_edge**3)) area = tetra.area_of_face(0) assert maths.isclose(area, expected_area, rel_tol=1.0e-3) # test internal / external face lists assert np.all(tetra.external_face_indices() == np.arange(4, 16, dtype=int)) inactive_mask = np.zeros(5, dtype=bool) assert np.all( tetra.external_face_indices_for_masked_cells(inactive_mask) == tetra.external_face_indices()) assert np.all( tetra.internal_face_indices_for_masked_cells(inactive_mask) == np.arange(4, dtype=int)) # mask out central cell inactive_mask[0] = True assert len(tetra.external_face_indices_for_masked_cells( inactive_mask)) == tetra.face_count assert len( tetra.internal_face_indices_for_masked_cells(inactive_mask)) == 0
def test_hexa_grid_from_grid(example_model_with_properties): model = example_model_with_properties ijk_grid_uuid = model.uuid(obj_type='IjkGridRepresentation') assert ijk_grid_uuid is not None # create an unstructured grid with hexahedral cells from an unsplit IJK grid (includes write_hdf5 and create_xml) hexa = rug.HexaGrid.from_unsplit_grid(model, ijk_grid_uuid, inherit_properties=True, title='HEXA') hexa_uuid = hexa.uuid epc = model.epc_file assert epc model.store_epc() # re-open model and check hexa grid model = rq.Model(epc) unstructured_uuids = model.uuids(obj_type='UnstructuredGridRepresentation') assert unstructured_uuids is not None and len(unstructured_uuids) == 1 hexa_grid = grr.any_grid(model, uuid=unstructured_uuids[0]) assert isinstance(hexa_grid, rug.HexaGrid) assert hexa_grid.cell_shape == 'hexahedral' hexa_grid.check_indices() hexa_grid.check_hexahedral() # instantiate ijk grid and compare hexa grid with it ijk_grid = grr.any_grid(model, uuid=ijk_grid_uuid) assert ijk_grid is not None assert not np.any(np.isnan(ijk_grid.points_ref())) assert hexa_grid.cell_count == ijk_grid.cell_count() assert hexa_grid.active_cell_count() == hexa_grid.cell_count assert hexa_grid.node_count == (ijk_grid.nk + 1) * (ijk_grid.nj + 1) * (ijk_grid.ni + 1) assert hexa_grid.face_count == ( (ijk_grid.nk + 1) * ijk_grid.nj * ijk_grid.ni + ijk_grid.nk * (ijk_grid.nj + 1) * ijk_grid.ni + ijk_grid.nk * ijk_grid.nj * (ijk_grid.ni + 1)) assert bu.matching_uuids(hexa_grid.extract_crs_uuid(), ijk_grid.crs_uuid) assert hexa_grid.crs_is_right_handed == rqc.Crs( model, uuid=ijk_grid.crs_uuid).is_right_handed_xyz() # points arrays should be identical for the two grids assert not np.any(np.isnan(hexa_grid.points_ref())) assert_array_almost_equal( hexa_grid.points_ref(), ijk_grid.points_ref(masked=False).reshape((-1, 3))) # compare centre points of cells (not sure if these would be coincident for irregular shaped cells) # note that the centre_point() method exercises several other methods hexa_centres = hexa_grid.centre_point() ijk_centres = ijk_grid.centre_point() assert_array_almost_equal(hexa_centres, ijk_centres.reshape((-1, 3)), decimal=3) # check that face and node indices are in range hexa_grid.cache_all_geometry_arrays() assert np.all(0 <= hexa_grid.faces_per_cell) assert np.all(hexa_grid.faces_per_cell < hexa_grid.face_count) assert np.all(0 <= hexa_grid.nodes_per_face) assert np.all(hexa_grid.nodes_per_face < hexa_grid.node_count) assert len(hexa_grid.faces_per_cell_cl) == hexa_grid.cell_count assert len(hexa_grid.nodes_per_face_cl) == hexa_grid.face_count # check distinct nodes for first cell cell_nodes = hexa_grid.distinct_node_indices_for_cell( 0) # is sorted by node index assert len(cell_nodes) == 8 ni1_nj1 = (ijk_grid.ni + 1) * (ijk_grid.nj + 1) expected_nodes = np.array( (0, 1, ijk_grid.ni + 1, ijk_grid.ni + 2, ni1_nj1, ni1_nj1 + 1, ni1_nj1 + ijk_grid.ni + 1, ni1_nj1 + ijk_grid.ni + 2), dtype=int) assert np.all(cell_nodes == expected_nodes) # check that some simple convenience methods work okay assert hexa_grid.face_count_for_cell(0) == 6 assert hexa_grid.max_face_count_for_any_cell() == 6 assert hexa_grid.max_node_count_for_any_face() == 4 # check that correct number of edges is found for a face edges = hexa_grid.edges_for_face(hexa_grid.face_count // 2) # arbitrary face in middle assert edges.shape == (4, 2) edges = hexa_grid.edges_for_face_with_node_indices_ordered_within_pairs( hexa_grid.face_count // 2) assert edges.shape == (4, 2) for a, b in edges: # check node within pair ordering assert a < b # compare corner points for first cell with those for ijk grid cell cp = hexa_grid.corner_points(0) assert cp.shape == (8, 3) assert_array_almost_equal( cp.reshape((2, 2, 2, 3)), ijk_grid.corner_points(cell_kji0=(0, 0, 0), cache_resqml_array=False)) # have a look at handedness of cell faces assert len(hexa_grid.cell_face_is_right_handed) == 6 * hexa_grid.cell_count # following assertion only applies to HexaGrid built from_unsplit_grid() assert np.count_nonzero( hexa_grid.cell_face_is_right_handed ) == 3 * hexa_grid.cell_count # half are right handed # compare cell volumes for first cell hexa_vol = hexa_grid.volume(0) ijk_vol = ijk_grid.volume(cell_kji0=0, cache_resqml_array=False, cache_volume_array=False) assert maths.isclose(hexa_vol, ijk_vol) assert maths.isclose(hexa_vol, 1.0, rel_tol=1.0e-3) # check face normal for first face (K- face of first cell) assert_array_almost_equal(hexa_grid.face_normal(0), (0.0, 0.0, -1.0)) # check that planar approximation of last face is about the same as the original fi = hexa_grid.face_count - 1 assert_array_almost_equal( hexa_grid.planar_face_points(fi), hexa_grid.points_cached[hexa_grid.node_indices_for_face(fi)], decimal=3) # construct a Delauney triangulation of the last face triangulated = hexa_grid.face_triangulation(fi, local_nodes=True) assert triangulated.shape == (2, 3) assert np.all(triangulated.flatten() < 4) assert np.all(np.unique(triangulated) == (0, 1, 2, 3)) # also test the triangulation using the global node indices, for all the faces of the last cell for face_index in hexa_grid.face_indices_for_cell(hexa_grid.cell_count - 1): triangulated = hexa_grid.face_triangulation(face_index, local_nodes=False) assert triangulated.shape == (2, 3) # check the area of a middle face (the example model has unit area faces) assert maths.isclose(hexa_grid.area_of_face(hexa_grid.face_count // 2), 1.0, rel_tol=1.0e-3) # compare properties ijk_pc = ijk_grid.extract_property_collection() hexa_pc = hexa_grid.extract_property_collection() assert ijk_pc is not None and hexa_pc is not None assert ijk_pc.number_of_parts() <= hexa_pc.number_of_parts( ) # hexa grid has extra active array in this test for ijk_part in ijk_pc.parts(): p_title = ijk_pc.citation_title_for_part(ijk_part) hexa_part = hexa_pc.singleton(citation_title=p_title) assert hexa_part is not None assert hexa_part != ijk_part # properties are different objects with distinct uuids assert hexa_pc.continuous_for_part( hexa_part) == ijk_pc.continuous_for_part(ijk_part) ijk_array = ijk_pc.cached_part_array_ref(ijk_part) hexa_array = hexa_pc.cached_part_array_ref(hexa_part) assert ijk_array is not None and hexa_array is not None if hexa_pc.continuous_for_part(hexa_part): assert_array_almost_equal(hexa_array.flatten(), ijk_array.flatten()) else: assert np.all(hexa_array.flatten() == ijk_array.flatten()) # test TetraGrid.from_unstructured_cell is as expected for a hexahedral cell tetra = rug.TetraGrid.from_unstructured_cell(hexa_grid, hexa_grid.cell_count // 2) tetra.check_indices() tetra.check_tetra() assert tetra.cell_count == 12 # 2 tetrahedra per hexa face assert tetra.node_count == 9 # 8 nodes of hexa cell plus centre point assert tetra.face_count == 6 * 2 + 12 + 6 # check total volume of tetra grid version of cell assert maths.isclose(tetra.grid_volume(), 1.0, rel_tol=1.0e-3)
def add_wells_from_ascii_file(epc_file, crs_uuid, trajectory_file, comment_character='#', space_separated_instead_of_csv=False, well_col='WELL', md_col='MD', x_col='X', y_col='Y', z_col='Z', length_uom='m', md_domain=None, drilled=False, z_inc_down=True, new_epc_file=None): """Adds new md datum, trajectory, interpretation and feature objects for each well in a tabular ascii file.. arguments: epc_file (string): file name to load model resqml model from (and rewrite to if new_epc_file is None) crs_uuid (uuid.UUID): the unique identifier of the coordinate reference system applicable to the x,y,z data; if None, a default crs will be created, making use of the length_uom and z_inc_down arguments trajectory_file (string): the path of the ascii file holding the well trajectory data to be loaded comment_character (string, default '#'): character deemed to introduce a comment in the trajectory file space_separated_instead_of_csv (boolean, default False): if True, the columns in the trajectory file are space separated; if False, comma separated well_col (string, default 'WELL'): the heading for the column containing well names md_col (string, default 'MD'): the heading for the column containing measured depths x_col (string, default 'X'): the heading for the column containing X (usually easting) data y_col (string, default 'Y'): the heading for the column containing Y (usually northing) data z_col (string, default 'Z'): the heading for the column containing Z (depth or elevation) data length_uom (string, default 'm'): the units of measure for the measured depths; should be 'm' or 'ft' md_domain (string, optional): the source of the original deviation data; may be 'logger' or 'driller' drilled (boolean, default False): True should be used for wells that have been drilled; False otherwise (planned, proposed, or a location being studied) z_inc_down (boolean, default True): indicates whether z values increase with depth; only used in the creation of a default coordinate reference system; ignored if crs_uuid is not None new_epc_file (string, optional): if None, the source epc_file is extended with the new property object; if present, a new epc file (& associated h5 file) is created to contain a copy of the grid and the new property returns: int: the number of wells added notes: ascii file must be table with first line being column headers, with columns for WELL, MD, X, Y & Z; actual column names can be set with optional arguments; all the objects are added to the model, with array data being written to the hdf5 file for the trajectories; the md_domain and drilled values are stored in the RESQML metadata but are only for human information and do not generally affect computations """ assert trajectory_file and os.path.exists(trajectory_file) if md_domain: assert md_domain in ['driller', 'logger'] if new_epc_file and epc_file and ( (new_epc_file == epc_file) or (os.path.exists(new_epc_file) and os.path.exists(epc_file) and os.path.samefile(new_epc_file, epc_file))): new_epc_file = None # open up model if new_epc_file: model = rq.Model(new_epc_file, copy_from=epc_file) else: model = rq.Model(epc_file) # sort out the coordinate reference system if crs_uuid is None: crs_uuid = model.crs_uuid if crs_uuid is None: if z_inc_down is None: z_inc_down = True crs = rqc.Crs(model, xy_units=length_uom, z_units=length_uom, z_inc_down=z_inc_down) crs.create_xml() crs_uuid = crs.uuid # add all the well related objects to the model, based on data in the ascii file (feature_list, interpretation_list, trajectory_list, md_datum_list) = \ rqw.add_wells_from_ascii_file(model, crs_uuid, trajectory_file, comment_character = comment_character, space_separated_instead_of_csv = space_separated_instead_of_csv, well_col = well_col, md_col = md_col, x_col = x_col, y_col = y_col, z_col = z_col, length_uom = length_uom, md_domain = md_domain, drilled = drilled) assert len(feature_list) == len(interpretation_list) == len( trajectory_list) == len(md_datum_list) count = len(feature_list) log.info('features, interpretations, trajectories and md data added for ' + str(count) + ' well' + _pl(count)) # write or re-write model model.h5_release() model.store_epc() return count
def test_crs(tmp_path): # create some coordinate reference systems model = rq.new_model(os.path.join(tmp_path, 'crs_test.epc')) crs_default = rqc.Crs(model) assert crs_default.null_transform crs_m = rqc.Crs(model, xy_units = 'm', z_units = 'm') crs_ft = rqc.Crs(model, xy_units = 'ft', z_units = 'ft') crs_mixed = rqc.Crs(model, xy_units = 'm', z_units = 'ft') crs_offset = rqc.Crs(model, xy_units = 'm', z_units = 'm', x_offset = 100.0, y_offset = -100.0, z_offset = -50.0) assert not crs_offset.null_transform crs_elevation = rqc.Crs(model, z_inc_down = False) crs_rotate = rqc.Crs(model, rotation = maths.pi / 2.0, rotation_units = 'rad') crs_south = rqc.Crs(model, axis_order = 'southing westing') crs_time_s = rqc.Crs(model, xy_units = 'm', time_units = 's') crs_time_ms = rqc.Crs(model, xy_units = 'm', time_units = 'ms') for crs_time in [crs_time_s, crs_time_ms]: assert crs_time.resqml_type == 'LocalTime3dCrs' # check that distincitveness is recognised assert crs_default.is_equivalent(crs_m) assert not crs_m.is_equivalent(crs_ft) assert not crs_mixed.is_equivalent(crs_m) assert not crs_m.is_equivalent(crs_offset) assert not crs_m.is_equivalent(crs_elevation) assert not crs_m.is_equivalent(crs_rotate) assert not crs_m.is_equivalent(crs_south) assert not crs_time_s.is_equivalent(crs_time_ms) for depth_crs in [crs_default, crs_m, crs_ft, crs_mixed, crs_offset, crs_elevation, crs_rotate, crs_south]: assert depth_crs.resqml_type == 'LocalDepth3dCrs' assert not crs_time_s == depth_crs assert not crs_time_ms == depth_crs # check handedness assert not crs_m.is_right_handed_xy() assert not crs_m.is_right_handed_xyz() assert not crs_elevation.is_right_handed_xy() assert crs_elevation.is_right_handed_xyz() assert crs_south.is_right_handed_xy() assert crs_south.is_right_handed_xyz() # create some xml for crs in [ crs_default, crs_m, crs_ft, crs_mixed, crs_offset, crs_elevation, crs_rotate, crs_south, crs_time_s, crs_time_ms ]: crs.create_xml() model.store_epc() # check re-use of equivalent crs'es assert bu.matching_uuids(crs_default.uuid, crs_m.uuid) # test conversion ft_to_m = 0.3048 a = np.empty((10, 3)) a[:, 0] = np.random.random(10) * 5.0e5 a[:, 1] = np.random.random(10) * 10.0e5 a[:, 2] = np.random.random(10) * 4.0e3 b = a.copy() crs_m.convert_array_from(crs_default, a) assert np.max(np.abs(b - a)) < 1.0e-6 a[:] = b crs_m.convert_array_to(crs_m, a) assert np.all(a == b) crs_ft.convert_array_from(crs_m, a) assert np.max(np.abs(b - a * ft_to_m)) < 1.0e-6 crs_ft.convert_array_to(crs_m, a) assert np.max(np.abs(b - a)) < 1.0e-6 a[:] = b crs_m.local_to_global_array(a) assert np.max(np.abs(b - a)) < 1.0e-6 a[:] = b crs_offset.global_to_local_array(a) a[:, 0] += 100.0 a[:, 1] -= 100.0 a[:, 2] -= 50.0 assert_array_almost_equal(a, b) # test single point conversion p = (456.78, 678.90, -1234.56) assert_array_almost_equal(p, crs_offset.global_to_local(crs_offset.local_to_global(p))) p_ft = crs_m.convert_to(crs_ft, np.array(p)) assert_array_almost_equal(p, crs_m.convert_from(crs_ft, p_ft)) # test time conversion pt = (123456.0, 234567.0, 1983.0) pt_s = np.array(crs_time_ms.convert_to(crs_time_s, pt)) pt_s[2] *= 1000.0 # convert from seconds back to milliseconds assert_array_almost_equal(pt, pt_s) # todo: test rotation p = (0.00, 234.00, 5678.90) pr = crs_rotate.local_to_global(p) assert_array_almost_equal(pr, (234.00, 0.00, 5678.90)) assert_array_almost_equal(crs_rotate.global_to_local(pr), p)
def s_bend_k_gap_grid(s_bend_model): nk = 5 nj = 12 ni_tail = 5 ni_bend = 18 ni_half_mid = 2 ni = 2 * (ni_tail + ni_bend + ni_half_mid) total_thickness = 12.0 layer_thickness = total_thickness / float(nk) flat_dx_di = 10.0 horst_dx_dk = 0.25 * flat_dx_di / float(nk) horst_dz = 1.73 * layer_thickness horst_half_dx = horst_dx_dk * horst_dz / layer_thickness dy_dj = 8.0 top_depth = 100.0 bend_theta_di = maths.pi / float(ni_bend) outer_radius = 2.0 * total_thickness bend_a_centre_xz = (flat_dx_di * float(ni_tail), top_depth + outer_radius) bend_b_centre_xz = (flat_dx_di * float(ni_tail - 2.0 * ni_half_mid), top_depth + 3.0 * outer_radius - total_thickness) points = np.empty((nk + 1, nj + 1, ni + 1, 3)) for k in range(nk + 1): if k == nk // 2 + 1: points[k] = points[k - 1] # pinched out layer else: for i in range(ni + 1): if i < ni_tail + 1: x = flat_dx_di * float(i) z = top_depth + float( k ) * layer_thickness # will introduce a thick layer after pinchout elif i < ni_tail + ni_bend: theta = (i - ni_tail) * bend_theta_di radius = outer_radius - float(k) * layer_thickness x = bend_a_centre_xz[0] + radius * maths.sin(theta) z = bend_a_centre_xz[1] - radius * maths.cos(theta) elif i < ni_tail + ni_bend + 2 * ni_half_mid + 1: x = flat_dx_di * float(ni_tail - (i - (ni_tail + ni_bend))) z = top_depth + 2.0 * outer_radius - float( k) * layer_thickness elif i < ni_tail + 2 * ni_bend + 2 * ni_half_mid: theta = ( i - (ni_tail + ni_bend + 2 * ni_half_mid)) * bend_theta_di radius = outer_radius - float(nk - k) * layer_thickness x = bend_b_centre_xz[0] - radius * maths.sin(theta) z = bend_b_centre_xz[1] - radius * maths.cos(theta) else: x = flat_dx_di * float((i - (ni - ni_tail)) + ni_tail - 2 * ni_half_mid) if i == ni - 1 or i == ni - 4: x += horst_dx_dk * float(k) elif i == ni - 2 or i == ni - 3: x -= horst_dx_dk * float(k) z = top_depth + 4.0 * outer_radius + float( k) * layer_thickness - 2.0 * total_thickness points[k, :, i] = (x, 0.0, z) for j in range(nj + 1): points[:, j, :, 1] = dy_dj * float(j) grid = grr.Grid(s_bend_model) crs = rqc.Crs(s_bend_model) crs_node = crs.create_xml() grid.grid_representation = 'IjkGrid' grid.extent_kji = np.array((nk, nj, ni), dtype='int') grid.nk, grid.nj, grid.ni = nk, nj, ni grid.k_direction_is_down = True # dominant layer direction, or paleo-direction grid.pillar_shape = 'straight' grid.has_split_coordinate_lines = False grid.k_gaps = None grid.crs_uuid = crs.uuid grid.crs_root = crs_node grid.points_cached = points grid.geometry_defined_for_all_pillars_cached = True grid.geometry_defined_for_all_cells_cached = True grid.grid_is_right_handed = crs.is_right_handed_xyz() cp = grid.corner_points(cache_cp_array=True).copy() bend_theta_di = maths.pi / float(ni_bend) # IK plane faults cp[:, 3:, :, :, :, :, :] += (flat_dx_di * 0.7, 0.0, layer_thickness * 1.3) cp[:, 5:, :, :, :, :, :] += (flat_dx_di * 0.4, 0.0, layer_thickness * 0.9) cp[:, 8:, :, :, :, :, :] += (flat_dx_di * 0.3, 0.0, layer_thickness * 0.6) # JK plane faults cp[:, :, ni_tail + ni_bend // 2:, :, :, :, 0] -= flat_dx_di * 0.57 # horizontal break mid top bend cp[:, :, ni_tail + ni_bend + ni_half_mid:, :, :, :, 2] += layer_thickness * 1.27 # vertical break in mid section # zig-zag fault j_step = nj // (ni_tail - 2) for i in range(ni_tail - 1): j_start = i * j_step if j_start >= nj: break cp[:, j_start:, i, :, :, :, 2] += 1.1 * total_thickness # JK horst blocks cp[:, :, ni - 4, :, :, :, :] -= (horst_half_dx, 0.0, horst_dz) cp[:, :, ni - 3:, :, :, :, 0] -= 2.0 * horst_half_dx cp[:, :, ni - 2, :, :, :, :] += (-horst_half_dx, 0.0, horst_dz) cp[:, :, ni - 1:, :, :, :, 0] -= 2.0 * horst_half_dx # JK horst block mid lower bend bend_horst_dz = horst_dz * maths.tan(bend_theta_di) cp[:, :, ni - (ni_tail + ni_bend // 2 + 1):ni - (ni_tail + ni_bend // 2 - 1), :, :, :, :] -= (horst_dz, 0.0, bend_horst_dz) cp[:, :, ni - (ni_tail + ni_bend // 2 - 1):, :, :, :, 2] -= 2.0 * bend_horst_dz k_gap_grid = rqi.grid_from_cp( s_bend_model, cp, crs.uuid, max_z_void=0.01, split_pillars=True, split_tolerance=0.01, ijk_handedness='right' if grid.grid_is_right_handed else 'left', known_to_be_straight=True) # convert second layer to a K gap k_gap_grid.nk -= 1 k_gap_grid.extent_kji[0] = k_gap_grid.nk k_gap_grid.k_gaps = 1 k_gap_grid.k_gap_after_array = np.zeros(k_gap_grid.nk - 1, dtype=bool) k_gap_grid.k_gap_after_array[0] = True k_gap_grid.k_raw_index_array = np.zeros(k_gap_grid.nk, dtype=int) for k in range(1, k_gap_grid.nk): k_gap_grid.k_raw_index_array[k] = k + 1 # clear some attributes which may no longer be valid k_gap_grid.pinchout = None k_gap_grid.inactive = None k_gap_grid.grid_skin = None if hasattr(k_gap_grid, 'array_thickness'): delattr(k_gap_grid, 'array_thickness') # k_gap_grid.write_hdf5_from_caches() # k_gap_grid.create_xml() return k_gap_grid
def equivalent_uuid_for_part(self, part, immigrant_model=None, ignore_identical_part=False): """Returns uuid of an equivalent part in resident model, or None if no equivalent found.""" # log.debug('Looking for equivalent uuid for: ' + str(part)) if not part: return None if immigrant_model is None: immigrant_model = self.model immigrant_uuid = rqet.uuid_in_part_name(part) # log.debug(' immigrant uuid: ' + str(immigrant_uuid)) if immigrant_uuid in self.map: # log.debug(' known to be equivalent to: ' + str(self.map[immigrant_uuid])) return self.map[immigrant_uuid] obj_type = immigrant_model.type_of_part(part, strip_obj=True) if obj_type is None or obj_type not in consolidatable_list: return None # log.debug(' object type is consolidatable') resident_uuids = self.model.uuids(obj_type=obj_type) if resident_uuids is None or len(resident_uuids) == 0: # log.debug(' no resident parts found of type: ' + str(obj_type)) return None # log.debug(f' {len(resident_uuids)} resident parts of same class') if not ignore_identical_part: for resident_uuid in resident_uuids: if bu.matching_uuids(resident_uuid, immigrant_uuid): # log.debug(' uuid already resident: ' + str(resident_uuid)) return resident_uuid # log.debug(' preparing immigrant object') if obj_type.endswith('Interpretation') or obj_type.endswith('Feature'): immigrant_obj = rqo.__dict__[obj_type](immigrant_model, uuid=immigrant_uuid) elif obj_type.endswith('Crs'): immigrant_obj = rqc.Crs(immigrant_model, uuid=immigrant_uuid) elif obj_type == 'TimeSeries': immigrant_obj = rqt.TimeSeries(immigrant_model, uuid=immigrant_uuid) elif obj_type == 'StringTableLookup': immigrant_obj = rqp.StringLookup(immigrant_model, uuid=immigrant_uuid) elif obj_type == 'PropertyKind': immigrant_obj = rqp.PropertyKind(immigrant_model, uuid=immigrant_uuid) else: raise Exception('code failure') assert immigrant_obj is not None for resident_uuid in resident_uuids: # log.debug(' considering resident: ' + str(resident_uuid)) if ignore_identical_part and bu.matching_uuids( resident_uuid, immigrant_uuid): continue if obj_type.endswith('Interpretation') or obj_type.endswith( 'Feature'): resident_obj = rqo.__dict__[obj_type](self.model, uuid=resident_uuid) elif obj_type.endswith('Crs'): resident_obj = rqc.Crs(self.model, uuid=resident_uuid) elif obj_type == 'TimeSeries': resident_obj = rqt.TimeSeries(self.model, uuid=resident_uuid) elif obj_type == 'StringTableLookup': resident_obj = rqp.StringLookup(self.model, uuid=resident_uuid) elif obj_type == 'PropertyKind': resident_obj = rqp.PropertyKind(self.model, uuid=resident_uuid) else: raise Exception('code failure') assert resident_obj is not None # log.debug(' comparing with: ' + str(resident_obj.uuid)) if immigrant_obj == resident_obj: # note: == operator overloaded with equivalence method for these classes while resident_uuid in self.map: # log.debug(' following equivalence for: ' + str(resident_uuid)) resident_uuid = self.map[resident_uuid] self.map[immigrant_uuid] = resident_uuid # log.debug(' new equivalence found with: ' + str(resident_uuid)) return resident_uuid return None
def local_depth_adjustment(epc_file, source_grid, centre_x, centre_y, radius, centre_shift, use_local_coords, decay_shape = 'quadratic', ref_k0 = 0, store_displacement = False, inherit_properties = False, inherit_realization = None, inherit_all_realizations = False, new_grid_title = None, new_epc_file = None): """Applies a local depth adjustment to the grid, adding as a new grid part in the model. arguments: epc_file (string): file name to rewrite the model's xml to; if source grid is None, model is loaded from this file source_grid (grid.Grid object, optional): a multi-layer RESQML grid object; if None, the epc_file is loaded and it should contain one ijk grid object (or one 'ROOT' grid) which is used as the source grid centre_x, centre_y (floats): the centre of the depth adjustment, corresponding to the location of maximum change in depth; crs is implicitly that of the grid but see also use_local_coords argument radius (float): the radius of adjustment of depths; units are implicitly xy (projected) units of grid crs centre_shift (float): the maximum vertical depth adjustment; units are implicily z (vertical) units of grid crs; use positive value to increase depth, negative to make shallower use_local_coords (boolean): if True, centre_x & centre_y are taken to be in the local coordinates of the grid's crs; otherwise the global coordinates decay_shape (string): 'linear' yields a cone shaped change in depth values; 'quadratic' (the default) yields a bell shaped change ref_k0 (integer, default 0): the layer in the grid to use as reference for determining the distance of a pillar from the centre of the depth adjustment; the corners of the top face of the reference layer are used store_displacement (boolean, default False): if True, 3 grid property parts are created, one each for x, y, & z displacement of cells' centres brought about by the local depth shift inherit_properties (boolean, default False): if True, the new grid will have a copy of any properties associated with the source grid inherit_realization (int, optional): realization number for which properties will be inherited; ignored if inherit_properties is False inherit_all_realizations (boolean, default False): if True (and inherit_realization is None), properties for all realizations will be inherited; if False, only properties with a realization of None are inherited; ignored if inherit_properties is False or inherit_realization is not None new_grid_title (string): used as the citation title text for the new grid object new_epc_file (string, optional): if None, the source epc_file is extended with the new grid object; if present, a new epc file (& associated h5 file) is created to contain the adjusted grid (& crs) returns: new grid object which is a copy of the source grid with the local depth adjustment applied """ log.info('adjusting depth') log.debug('centre x: {0:3.1f}; y: {1:3.1f}'.format(centre_x, centre_y)) if use_local_coords: log.debug('centre x & y interpreted in local crs') log.debug('radius of influence: {0:3.1f}'.format(radius)) log.debug('depth shift at centre: {0:5.3f}'.format(centre_shift)) log.debug('decay shape: ' + decay_shape) log.debug('reference layer (k0 protocol): ' + str(ref_k0)) assert epc_file or new_epc_file, 'epc file name not specified' if new_epc_file and epc_file and ( (new_epc_file == epc_file) or (os.path.exists(new_epc_file) and os.path.exists(epc_file) and os.path.samefile(new_epc_file, epc_file))): new_epc_file = None model, source_grid = _establish_model_and_source_grid(epc_file, source_grid) assert source_grid.grid_representation == 'IjkGrid' assert model is not None # take a copy of the grid grid = copy_grid(source_grid, model, copy_crs = True) # if not use_local_coords, convert centre_x & y into local_coords if grid.crs is None: grid.crs = rqc.Crs(model, uuid = grid.crs_uuid) if not use_local_coords: rotation = grid.crs.rotation if rotation > 0.001: log.error('unable to account for rotation in crs: use local coordinates') return centre_x -= grid.crs.x_offset centre_y -= grid.crs.y_offset z_inc_down = grid.crs.z_inc_down if not z_inc_down: centre_shift = -centre_shift # cache geometry in memory; needed prior to writing new coherent set of arrays to hdf5 grid.cache_all_geometry_arrays() if grid.has_split_coordinate_lines: reshaped_points = grid.points_cached.copy() else: nkp1, njp1, nip1, xyz = grid.points_cached.shape reshaped_points = grid.points_cached.copy().reshape((nkp1, njp1 * nip1, xyz)) assert reshaped_points.ndim == 3 and reshaped_points.shape[2] == 3 assert ref_k0 >= 0 and ref_k0 < reshaped_points.shape[0] log.debug('reshaped_points.shape: ' + str(reshaped_points.shape)) log.debug('min z before depth adjustment: ' + str(np.nanmin(reshaped_points[:, :, 2]))) # for each pillar, find x, y for k = reference_layer_k0 pillars_adjusted = 0 # todo: replace with numpy array operations radius_sqr = radius * radius for pillar in range(reshaped_points.shape[1]): x, y, z = tuple(reshaped_points[ref_k0, pillar, :]) # find distance of this pillar from the centre dx = centre_x - x dy = centre_y - y distance_sqr = (dx * dx) + (dy * dy) # if this pillar is beyond radius of influence, no action needed if distance_sqr > radius_sqr: continue distance = maths.sqrt(distance_sqr) # compute decayed shift as function of distance shift = _decayed_shift(centre_shift, distance, radius, decay_shape) # adjust depth values for pillar in cached array log.debug('adjusting pillar number {0} at x: {1:3.1f}, y: {2:3.1f}, distance: {3:3.1f} by {4:5.3f}'.format( pillar, x, y, distance, shift)) reshaped_points[:, pillar, 2] += shift pillars_adjusted += 1 # if no pillars adjusted: warn and return if pillars_adjusted == 0: log.warning('no pillars adjusted') return log.debug('min z after depth adjustment: ' + str(np.nanmin(reshaped_points[:, :, 2]))) if grid.has_split_coordinate_lines: grid.points_cached[:] = reshaped_points else: grid.points_cached[:] = reshaped_points.reshape((nkp1, njp1, nip1, xyz)) # model.copy_part(old_uuid, grid.uuid, change_hdf5_refs = True) # copies the xml, substituting the new uuid in the root node (and in hdf5 refs) log.info(str(pillars_adjusted) + ' pillars adjusted') # build cell displacement property array(s) if store_displacement: log.debug('generating cell displacement property arrays') displacement_collection = _displacement_properties(grid, source_grid) else: displacement_collection = None collection = _prepare_simple_inheritance(grid, source_grid, inherit_properties, inherit_realization, inherit_all_realizations) if collection is None: collection = displacement_collection elif displacement_collection is not None: collection.inherit_imported_list_from_other_collection(displacement_collection, copy_cached_arrays = False) if new_grid_title is None or len(new_grid_title) == 0: new_grid_title = 'grid derived from {0} with local depth shift of {1:3.1f} applied'.format( str(rqet.citation_title_for_node(source_grid.root)), centre_shift) # write model model.h5_release() if new_epc_file: _write_grid(new_epc_file, grid, property_collection = collection, grid_title = new_grid_title, mode = 'w') else: ext_uuid, _ = model.h5_uuid_and_path_for_node(rqet.find_nested_tags(source_grid.root, ['Geometry', 'Points']), 'Coordinates') _write_grid(epc_file, grid, ext_uuid = ext_uuid, property_collection = collection, grid_title = new_grid_title, mode = 'a') return grid
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={}): """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 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 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) else: assert bu.is_uuid(uuid) assert 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) self.grid_representation = 'IjkBlockGrid' if dxyz is None and dxyz_dkji is None: # find cell length properties and populate dxyz from those values assert self.property_collection is not None dxi_part = self.property_collection.singleton( property_kind='cell length', facet_type='direction', facet='I') dyj_part = self.property_collection.singleton( property_kind='cell length', facet_type='direction', facet='J') dzk_part = self.property_collection.singleton( property_kind='cell length', facet_type='direction', facet='K') if 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) if dxi is not None and dyj is not None and dzk is not None: dxyz = (float(dxi), float(dyj), float(dzk)) 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' if set_points_cached: self.make_regular_points_cached() 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 if self.uuid is None: self.uuid = bu.new_uuid()
def triangulated_polygons(p, v, centres=None): """Returns triangulation of polygons using centres as extra points. arguments: p (2D numpy float array): points used as vertices of polygons v (list of list of ints): ordered indices into p for each polygon centres (2D numpy float array, optional): the points to use as the centre for each polygon returns: points, triangles where: points is a copy of p extended with the centre points of polygons; and triangles is a numpy int array of shape (N, 3) being the triangulation of points, where N is equal to the overall length of v notes: if no centres are provided, balanced centre points are computed for the polygons; the polygons must be convex (at least from the perspective of the centre points); the clockwise/anti-clockwise order of the triangle edges will match that of the polygon; the centre point is the first point in each triangle; the order of triangles will match the order of vertices in a flattened view of list v; p and centres may have a shape of 2 or 3 in the second dimension (xy or xyz data); p & v could be the values (c, v) returned by the voronoi() function, in which case the original seed points p passed into voronoi() can be passed as centres here """ assert p.ndim == 2 and p.shape[1] in [2, 3] assert len(v) > 0 if centres is not None: assert centres.ndim == 2 assert len(centres) == len(v) assert centres.shape[1] == p.shape[1] model = rq.Model( create_basics=True) # temporary object for handling Polylines crs = rqc.Crs(model) points = np.zeros( (len(p) + len(v), p.shape[1])) # polygon nodes, to be extended with centre points points[:len(p)] = p if centres is not None: points[len(p):] = centres t_count = sum([len(x) for x in v]) triangles = np.empty((t_count, 3), dtype=int) t_index = 0 for cell, poly_vertices in enumerate(v): # add polygon centre points to points array, if not already provided centre_i = len(p) + cell if centres is None: polygon = rql.Polyline(model, set_coord=p[np.array(poly_vertices, dtype=int)], set_bool=True, set_crs=crs.uuid, title='v cell') poly_centre = polygon.balanced_centre() points[centre_i] = poly_centre[:p.shape[1]] # and populate triangles for this polygon for ti in range(len(poly_vertices)): triangles[t_index] = (centre_i, poly_vertices[ti], poly_vertices[(ti + 1) % len(poly_vertices)]) t_index += 1 assert t_index == t_count return points, triangles
def drape_to_surface(epc_file, source_grid=None, surface=None, scaling_factor=None, ref_k0=0, ref_k_faces='top', quad_triangles=True, border=None, store_displacement=False, inherit_properties=False, inherit_realization=None, inherit_all_realizations=False, new_grid_title=None, new_epc_file=None): """Return a new grid with geometry draped to a surface. Extend a resqml model with a new grid where the reference layer boundary of the source grid has been re-draped to a surface. arguments: epc_file (string): file name to rewrite the model's xml to; if source grid is None, model is loaded from this file source_grid (grid.Grid object, optional): if None, the epc_file is loaded and it should contain one ijk grid object (or one 'ROOT' grid) which is used as the source grid surface (surface.Surface object, optional): the surface to drape the grid to; if None, a surface is generated from the reference layer boundary (which can then be scaled with the scaling_factor) scaling_factor (float, optional): if not None, prior to draping, the surface is stretched vertically by this factor, away from a horizontal plane located at the surface's shallowest depth ref_k0 (integer, default 0): the reference layer (zero based) to drape to the surface ref_k_faces (string, default 'top'): 'top' or 'base' identifying which bounding interface to use as the reference quad_triangles (boolean, default True): if True and surface is None, each cell face in the reference boundary layer is represented by 4 triangles (with a common vertex at the face centre) in the generated surface; if False, only 2 trianges are used for each cell face (which gives a non-unique solution) cell_range (integer, default 0): the number of cells away from faults which will have depths adjusted to spatially smooth the effect of the throw scaling (ie. reduce sudden changes in gradient due to the scaling) offset_decay (float, default 0.5): the factor to reduce depth shifts by with each cell step away from faults (used in conjunction with cell_range) store_displacement (boolean, default False): if True, 3 grid property parts are created, one each for x, y, & z displacement of cells' centres brought about by the local depth shift inherit_properties (boolean, default False): if True, the new grid will have a copy of any properties associated with the source grid inherit_realization (int, optional): realization number for which properties will be inherited; ignored if inherit_properties is False inherit_all_realizations (boolean, default False): if True (and inherit_realization is None), properties for all realizations will be inherited; if False, only properties with a realization of None are inherited; ignored if inherit_properties is False or inherit_realization is not None new_grid_title (string): used as the citation title text for the new grid object new_epc_file (string, optional): if None, the source epc_file is extended with the new grid object; if present, a new epc file (& associated h5 file) is created to contain the draped grid (& crs) returns: new grid (grid.Grid object), with geometry draped to surface notes: at least one of a surface or a scaling factor must be given; if no surface is given, one is created from the fault-healed grid points for the reference layer interface; if a scaling factor other than 1.0 is given, the surface is flexed vertically, relative to its shallowest point; layer thicknesses measured along pillars are maintained; cell volumes may change; the coordinate reference systems for the surface and the grid are assumed to be the same; this function currently uses an exhaustive, computationally and memory intensive algorithm; setting quad_triangles argument to False should give a factor of 2 speed up and reduction in memory requirement; the epc file and associated hdf5 file are appended to (extended) with the new grid, as a side effect of this function """ log.info('draping grid to surface') assert epc_file or new_epc_file, 'epc file name not specified' if new_epc_file and epc_file and ( (new_epc_file == epc_file) or (os.path.exists(new_epc_file) and os.path.exists(epc_file) and os.path.samefile(new_epc_file, epc_file))): new_epc_file = None model, source_grid = _establish_model_and_source_grid( epc_file, source_grid) assert source_grid.grid_representation == 'IjkGrid' assert model is not None assert ref_k0 >= 0 and ref_k0 < source_grid.nk assert ref_k_faces in ['top', 'base'] assert surface is not None or (scaling_factor is not None and scaling_factor != 1.0) if surface is None: surface = rgs.generate_untorn_surface_for_layer_interface( source_grid, k0=ref_k0, ref_k_faces=ref_k_faces, quad_triangles=quad_triangles, border=border) if scaling_factor is not None and scaling_factor != 1.0: scaled_surf = copy.deepcopy(surface) scaled_surf.vertical_rescale_points(scaling_factor=scaling_factor) surface = scaled_surf # check that surface and grid use same crs; if not, convert to same surface_crs = rqc.Crs(surface.model, surface.crs_uuid) if source_grid.crs is None: source_grid.crs = rqc.Crs(source_grid.model, uuid=source_grid.crs_uuid) if surface_crs != source_grid.crs: surface.triangles_and_points() surface_crs.convert_array_to(source_grid.crs, surface.points) # take a copy of the grid log.debug('copying grid') grid = copy_grid(source_grid, model) grid.cache_all_geometry_arrays() # probably already cached anyway # todo: handle pillars with no geometry defined, and cells without geometry defined assert grid.geometry_defined_for_all_pillars( ), 'not all pillars have defined geometry' assert grid.geometry_defined_for_all_cells( ), 'not all cells have defined geometry' # fetch unsplit equivalent of grid points log.debug('fetching unsplit equivalent grid points') unsplit_points = grid.unsplit_points_ref(cache_array=True) # assume pillars to be straight lines based on top and base points log.debug('setting up pillar sample points and directional vectors') line_p = unsplit_points[0, :, :, :].reshape((-1, 3)) line_v = unsplit_points[-1, :, :, :].reshape((-1, 3)) - line_p if ref_k_faces == 'base': ref_k0 += 1 # access triangulated surface as triangle node indices into array of points log.debug('fetching surface points and triangle corner indices') t, p = surface.triangles_and_points( ) # will pick up cached crs converted copy if appropriate # compute intersections of all pillars with all triangles (sparse array returned with NaN for no intersection) log.debug('computing intersections of all pillars with all triangles') intersects = meet.line_set_triangles_intersects(line_p, line_v, p[t]) # reduce to a single intersection point per pillar; todo: flag multiple intersections with a warning log.debug( 'selecting last intersection for each pillar (there should be only one intersection anyway)' ) picks = meet.last_intersects(intersects) # count the number of pillars with no intersection at surface (indicated by triple nan) log.debug( 'counting number of pillars which fail to intersect with surface') failures = np.count_nonzero(np.isnan(picks)) // 3 log.info('number of pillars which do not intersect with surface: ' + str(failures)) assert failures == 0, 'cannot proceed as some pillars do not intersect with surface' # compute a translation vector per pillar log.debug('computing translation vectors for pillars') translate = picks - unsplit_points[ref_k0, :, :, :].reshape((-1, 3)) # shift all points by translation vectors _shift_by_translation_vectors(grid, translate) # check cell edge relative directions (in x,y) to ensure geometry is still coherent log.debug('checking grid geometry coherence') grid.check_top_and_base_cell_edge_directions() # build cell displacement property array(s) if store_displacement: log.debug('generating cell displacement property arrays') displacement_collection = _displacement_properties(grid, source_grid) else: displacement_collection = None collection = _prepare_simple_inheritance(grid, source_grid, inherit_properties, inherit_realization, inherit_all_realizations) if collection is None: collection = displacement_collection elif displacement_collection is not None: collection.inherit_imported_list_from_other_collection( displacement_collection, copy_cached_arrays=False) if new_grid_title is None or len(new_grid_title) == 0: new_grid_title = 'grid flexed from ' + str( rqet.citation_title_for_node(source_grid.root)) # write model model.h5_release() if new_epc_file: _write_grid(new_epc_file, grid, property_collection=collection, grid_title=new_grid_title, mode='w') else: ext_uuid, _ = model.h5_uuid_and_path_for_node( rqet.find_nested_tags(source_grid.root, ['Geometry', 'Points']), 'Coordinates') _write_grid(epc_file, grid, ext_uuid=ext_uuid, property_collection=collection, grid_title=new_grid_title, mode='a') return grid
def _refined_unfaulted_grid(model, source_grid, fine_coarse): source_points = source_grid.points_ref() assert source_points is not None, 'geometry not available for refinement of unfaulted grid' # create a new, empty grid object grid = grr.Grid(model) # inherit attributes from source grid grid.grid_representation = 'IjkGrid' grid.extent_kji = fine_coarse.fine_extent_kji grid.nk, grid.nj, grid.ni = grid.extent_kji[0], grid.extent_kji[ 1], grid.extent_kji[2] grid.k_direction_is_down = source_grid.k_direction_is_down grid.grid_is_right_handed = source_grid.grid_is_right_handed grid.pillar_shape = source_grid.pillar_shape grid.has_split_coordinate_lines = source_grid.has_split_coordinate_lines grid.split_pillars_count = source_grid.split_pillars_count grid.k_gaps = source_grid.k_gaps if grid.k_gaps: grid.k_gap_after_array = np.zeros((grid.nk - 1, ), dtype=bool) grid.k_raw_index_array = np.zeros((grid.nk, ), dtype=int) # k gap arrays populated below # inherit the coordinate reference system used by the grid geometry grid.crs_uuid = source_grid.crs_uuid if source_grid.model is not model: model.duplicate_node(source_grid.model.root_for_uuid( source_grid, grid.crs_uuid), add_as_part=True) grid.crs = rqc.Crs(model, uuid=grid.crs_uuid) refined_points = np.empty( (grid.nk_plus_k_gaps + 1, grid.nj + 1, grid.ni + 1, 3)) # log.debug(f'source grid: {source_grid.extent_kji}; k gaps: {source_grid.k_gaps}') # log.debug(f'refined grid: {grid.extent_kji}; k gaps: {grid.k_gaps}') fk0 = 0 gaps_so_far = 0 for ck0 in range(fine_coarse.coarse_extent_kji[0] + 1): end_k = (ck0 == fine_coarse.coarse_extent_kji[0]) if end_k: k_ratio = 1 k_interpolation = [0.0] else: k_ratio = fine_coarse.ratio(0, ck0) k_interpolation = fine_coarse.interpolation(0, ck0) one_if_gap = 1 if source_grid.k_gaps and ck0 < fine_coarse.coarse_extent_kji[ 0] - 1 and source_grid.k_gap_after_array[ck0] else 0 for flk0 in range(k_ratio + one_if_gap): # log.debug(f'ck0: {ck0}; fk0: {fk0}; flk0: {flk0}; k_ratio: {k_ratio}; one_if_gap: {one_if_gap}; gaps so far: {gaps_so_far}') if flk0 < k_ratio: k_fraction = k_interpolation[flk0] else: k_fraction = 1.0 if grid.k_gaps: if end_k: k_plane = source_points[source_grid.k_raw_index_array[ck0 - 1] + 1, :, :, :] else: k_plane = ( k_fraction * source_points[source_grid.k_raw_index_array[ck0] + 1, :, :, :] + (1.0 - k_fraction) * source_points[ source_grid.k_raw_index_array[ck0], :, :, :]) if flk0 == k_ratio: grid.k_gap_after_array[fk0 - 1] = True elif fk0 < grid.nk: grid.k_raw_index_array[fk0] = fk0 + gaps_so_far else: if end_k: k_plane = source_points[ck0, :, :, :] else: k_plane = k_fraction * source_points[ck0 + 1, :, :, :] + ( 1.0 - k_fraction) * source_points[ck0, :, :, :] fj0 = 0 for cj0 in range(fine_coarse.coarse_extent_kji[1] + 1): end_j = (cj0 == fine_coarse.coarse_extent_kji[1]) if end_j: j_ratio = 1 j_interpolation = [0.0] else: j_ratio = fine_coarse.ratio(1, cj0) j_interpolation = fine_coarse.interpolation(1, cj0) for flj0 in range(j_ratio): j_fraction = j_interpolation[flj0] # note: shape of j_line will be different if there are split pillars in play if end_j: j_line = k_plane[cj0, :, :] else: j_line = j_fraction * k_plane[cj0 + 1, :, :] + ( 1.0 - j_fraction) * k_plane[cj0, :, :] fi0 = 0 for ci0 in range(fine_coarse.coarse_extent_kji[2] + 1): end_i = (ci0 == fine_coarse.coarse_extent_kji[2]) if end_i: i_ratio = 1 i_interpolation = [0.0] else: i_ratio = fine_coarse.ratio(2, ci0) i_interpolation = fine_coarse.interpolation(2, ci0) for fli0 in range(i_ratio): i_fraction = i_interpolation[fli0] if end_i: p = j_line[ci0, :] else: p = i_fraction * j_line[ci0 + 1, :] + ( 1.0 - i_fraction) * j_line[ci0, :] refined_points[fk0 + gaps_so_far, fj0, fi0] = p fi0 += 1 assert fi0 == fine_coarse.fine_extent_kji[2] + 1 fj0 += 1 assert fj0 == fine_coarse.fine_extent_kji[1] + 1 if flk0 == k_ratio: gaps_so_far += 1 else: fk0 += 1 assert fk0 == fine_coarse.fine_extent_kji[0] + 1 assert grid.nk + gaps_so_far == grid.nk_plus_k_gaps grid.points_cached = refined_points grid.geometry_defined_for_all_pillars_cached = True grid.geometry_defined_for_all_cells_cached = True grid.array_cell_geometry_is_defined = np.full(tuple(grid.extent_kji), True, dtype=bool) return grid
def copy_grid(source_grid, target_model = None, copy_crs = True): """Creates a copy of the IJK grid object in the target model (usually prior to modifying points in situ). note: this function is not usually called directly by application code; it does not write to the hdf5 file nor create xml for the copied grid; the copy will be a resqpy Grid even if the source grid is a RegularGrid """ assert source_grid.grid_representation in ['IjkGrid', 'IjkBlockGrid'] model = source_grid.model if target_model is None: target_model = model if target_model is model: copy_crs = False # if the source grid is a RegularGrid, ensure that it has explicit points if source_grid.grid_representation == 'IjkBlockGrid': source_grid.make_regular_points_cached() # create empty grid object (with new uuid) grid = grr.Grid(target_model) # inherit attributes from source grid grid.grid_representation = 'IjkGrid' grid.extent_kji = np.array(source_grid.extent_kji, dtype = 'int') grid.nk, grid.nj, grid.ni = source_grid.nk, source_grid.nj, source_grid.ni grid.k_direction_is_down = source_grid.k_direction_is_down grid.grid_is_right_handed = source_grid.grid_is_right_handed grid.pillar_shape = source_grid.pillar_shape grid.has_split_coordinate_lines = source_grid.has_split_coordinate_lines grid.k_gaps = source_grid.k_gaps if grid.k_gaps: grid.k_gap_after_array = source_grid.k_gap_after_array.copy() grid.k_raw_index_array = source_grid.k_raw_index_array.copy() # inherit a copy of the coordinate reference system used by the grid geometry grid.crs_uuid = source_grid.crs_uuid if target_model is source_grid.model: grid.crs = rqc.Crs(model, uuid = grid.crs_uuid) elif copy_crs and source_grid.crs_uuid is not None: model.duplicate_node(source_grid.model.root_for_uuid(source_grid.crs_uuid), add_as_part = True) else: grid.crs = None # inherit a copy of the inactive cell mask if source_grid.inactive is None: grid.inactive = None else: grid.inactive = source_grid.inactive.copy() grid.active_property_uuid = source_grid.active_property_uuid # take a copy of the grid geometry source_grid.cache_all_geometry_arrays() grid.geometry_defined_for_all_pillars_cached = source_grid.geometry_defined_for_all_pillars_cached if hasattr(source_grid, 'array_pillar_geometry_is_defined'): grid.array_pillar_geometry_is_defined = source_grid.array_pillar_geometry_is_defined.copy() if hasattr(source_grid, 'array_cell_geometry_is_defined'): grid.array_cell_geometry_is_defined = source_grid.array_cell_geometry_is_defined.copy() grid.geometry_defined_for_all_cells_cached = source_grid.geometry_defined_for_all_cells_cached grid.points_cached = source_grid.points_cached.copy() if grid.has_split_coordinate_lines: source_grid.create_column_pillar_mapping() grid.split_pillar_indices_cached = source_grid.split_pillar_indices_cached.copy() grid.cols_for_split_pillars = source_grid.cols_for_split_pillars.copy() grid.cols_for_split_pillars_cl = source_grid.cols_for_split_pillars_cl.copy() grid.split_pillars_count = source_grid.split_pillars_count grid.pillars_for_column = source_grid.pillars_for_column.copy() return grid