def fault_throw_scaling(epc_file, source_grid=None, scaling_factor=None, connection_set=None, scaling_dict=None, ref_k0=0, ref_k_faces='top', cell_range=0, offset_decay=0.5, store_displacement=False, inherit_properties=False, inherit_realization=None, inherit_all_realizations=False, inherit_gcs=True, new_grid_title=None, new_epc_file=None): """Extends epc with a new grid with fault throws multiplied by scaling factors. 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 scaling_factor (float, optional): if present, the default scaling factor to apply to split pillars which do not appear in any of the faults in the scaling dictionary; if None, such pillars are left unchanged connection_set (fault.GridConnectionSet object): the connection set with associated fault feature list, used to identify which faces (and hence pillars) belong to which named fault scaling_dict (dictionary mapping string to float): the scaling factor to apply to each named fault; any faults not included in the dictionary will be left unadjusted (unless a default scaling factor is given as scaling_factor arg) ref_k0 (integer, default 0): the reference layer (zero based) to use when determining the pre-existing throws ref_k_faces (string, default 'top'): 'top' or 'base' identifying which bounding interface to use as the reference 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): DEPRECATED; ignored 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 fault throw scaling 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 inherit_gcs (boolean, default True): if True, any grid connection set objects related to the source grid will be inherited by the modified grid 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 derived grid (& crs) returns: new grid (grid.Grid object), with fault throws scaled according to values in the scaling dictionary notes: grid points are moved along pillar lines; stretch is towards or away from mid-point of throw; same shift is applied to all layers along pillar; pillar lines assumed to be straight; the offset decay argument might be changed in a future version to give improved smoothing; if a large fault is represented by a series of parallel minor faults 'stepping' down, each minor fault will have the scaling factor applied independently, leading to some unrealistic results """ 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 source_grid.has_split_coordinate_lines, 'cannot scale fault throws in unfaulted grid' assert scaling_factor is not None or (connection_set is not None and scaling_dict is not None) if ref_k_faces == 'base': ref_k0 += 1 assert ref_k0 >= 0 and ref_k0 <= source_grid.nk, 'reference layer out of range' # 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' primaries = (grid.nj + 1) * (grid.ni + 1) offsets = np.zeros(grid.points_cached.shape[1:]) if scaling_factor is not None: # apply global scaling to throws _set_offsets_based_on_scaling_factor(grid, scaling_factor, offsets, ref_k0, primaries) if connection_set is not None and scaling_dict is not None: # overwrite any global offsets with named fault throw adjustments _set_offsets_based_on_scaling_dict(grid, connection_set, scaling_dict, offsets, ref_k0) # initialise flag array for adjustments adjusted = np.zeros((primaries, ), dtype=bool) # insert adjusted throws to all layers of split pillars grid.points_cached[:, grid.split_pillar_indices_cached, :] += offsets[ grid.split_pillar_indices_cached, :].reshape(1, -1, 3) adjusted[grid.split_pillar_indices_cached] = True grid.points_cached[:, primaries:, :] += offsets[primaries:, :].reshape( 1, -1, 3) # iteratively look for pillars neighbouring adjusted pillars, adjusting by a decayed amount adjusted = adjusted.reshape((grid.nj + 1, grid.ni + 1)) while cell_range > 0: newly_adjusted = _neighbourly_adjustment(grid, offsets, adjusted, cell_range) adjusted = np.logical_or(adjusted, newly_adjusted) cell_range -= 1 # 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 with fault throws scaled by ' + str(scaling_factor) + ' from ' + \ str(rqet.citation_title_for_node(source_grid.root)) gcs_list = [] if inherit_gcs: gcs_uuids = model.uuids(obj_type='GridConnectionSetRepresentation', related_uuid=source_grid.uuid) for gcs_uuid in gcs_uuids: gcs = rqf.GridConnectionSet(model, uuid=gcs_uuid) gcs.cache_arrays() gcs_list.append((gcs, gcs.title)) log.debug(f'{len(gcs_list)} grid connection sets to be inherited') # 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') epc_file = new_epc_file 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') if len(gcs_list): log.debug( f'inheriting grid connection sets related to source grid: {source_grid.uuid}' ) _inherit_gcs_list(epc_file, gcs_list, source_grid, grid) return grid
def tilted_grid(epc_file, source_grid = None, pivot_xyz = None, azimuth = None, dip = None, store_displacement = False, inherit_properties = False, inherit_realization = None, inherit_all_realizations = False, new_grid_title = None, new_epc_file = None): """Extends epc file with a new grid which is a version of the source grid tilted. 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 pivot_xyz (triple float): a point in 3D space on the pivot axis, which is horizontal and orthogonal to azimuth azimuth: the direction of tilt (orthogonal to tilt axis), as a compass bearing in degrees dip: the angle to tilt the grid by, in degrees; a positive value tilts points in direction azimuth downwards (needs checking!) 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 tilting 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 tilted grid (& crs) returns: a new grid (grid.Grid object) which is a copy of the source grid tilted in 3D space """ 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) if grid.inactive is not None: log.debug('copied grid inactive shape: ' + str(grid.inactive.shape)) # tilt the grid grid.cache_all_geometry_arrays() # probably already cached anyway vec.tilt_points(pivot_xyz, azimuth, dip, grid.points_cached) # build cell displacement property array(s) if store_displacement: 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 = 'tilted version ({0:4.2f} degree dip) of '.format(abs(dip)) + 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 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 unsplit_grid(epc_file, source_grid=None, inherit_properties=False, inherit_realization=None, inherit_all_realizations=False, new_grid_title=None, new_epc_file=None): """Extends epc file with a new grid which is a version of the source grid with all faults healed. 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 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 unsplit grid (& crs) returns: a new grid (grid.Grid object) which is an unfaulted copy of the source grid notes: the faults are healed by shifting the thrown sides up and down to the midpoint, only along the line of the fault; to smooth the adjustments away from the line of the fault, use the global_fault_throw_scaling() function first """ 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 source_grid.has_split_coordinate_lines, 'source grid is unfaulted' # take a copy of the grid grid = copy_grid(source_grid, model) if grid.inactive is not None: log.debug('copied grid inactive shape: ' + str(grid.inactive.shape)) # heal faults in the grid grid.cache_all_geometry_arrays() # probably already cached anyway unsplit = source_grid.unsplit_points_ref() grid.points_cached = unsplit.copy() assert grid.points_cached.shape == ( grid.nk + 1, grid.nj + 1, grid.ni + 1, 3), 'unsplit points have incorrect shape' grid.has_split_coordinate_lines = False delattr(grid, 'split_pillar_indices_cached') delattr(grid, 'cols_for_split_pillars') delattr(grid, 'cols_for_split_pillars_cl') if hasattr(grid, 'pillars_for_column'): delattr(grid, 'pillars_for_column') collection = _prepare_simple_inheritance(grid, source_grid, inherit_properties, inherit_realization, inherit_all_realizations) # todo: recompute depth properties (and volumes, cell lengths etc. if being strict) if new_grid_title is None or len(new_grid_title) == 0: new_grid_title = 'unfaulted version of ' + str( rqet.citation_title_for_node(source_grid.root)) # write model 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 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 zonal_grid(epc_file, source_grid = None, zone_title = None, zone_uuid = None, zone_layer_range_list = None, k0_min = None, k0_max = None, use_dominant_zone = False, inactive_laissez_faire = True, new_grid_title = None, new_epc_file = None): """Extends an existing model with a new version of the source grid converted to a single, thick, layer per zone. 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 zone_title (string): if not None, a discrete property with this as the citation title is used as the zone property zone_uuid (string or uuid): if not None, a discrete property with this uuid is used as the zone property (see notes) zone_layer_range_list (list of (int, int, int)): each entry being (min_k0, max_k0, zone_index); alternative to working from a zone array k0_min (int, optional): the minimum layer number in the source grid (zero based) to include in the zonal version; default is zero (ie. top layer in source grid) k0_max (int, optional): the maximum layer number in the source grid (zero based) to include in the zonal version; default is nk - 1 (ie. bottom layer in source grid) use_dominant_zone (boolean, default False): if True, the most common zone value in each layer is used for the whole layer; if False, then variation of zone values in active cells in a layer will raise an assertion error inactive_laissez_faire (boolean, optional): if True, a cell in the zonal grid will be set active if any of the corresponding cells in the source grid are active; otherwise all corresponding cells in the source grid must be active for the zonal cell to be active; default is True 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 zonal grid (& crs) returns: new grid object (grid.Grid) with one layer per zone of the source grid notes: usually one of zone_title or zone_uuid or zone_layer_range_list should be passed, if none are passed then a single layer grid is generated; zone_layer_range_list will take precendence if present """ 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 in ['IjkGrid', 'IjkBlockGrid'] if source_grid.grid_representation == 'IjkBlockGrid': source_grid.make_regular_points_cached() assert model is not None single_layer_mode = (not zone_title and not zone_uuid and (zone_layer_range_list is None or len(zone_layer_range_list) == 1)) k0_min, k0_max = _set_or_check_k_min_max(k0_min, k0_max, source_grid) if not single_layer_mode: # process zone array if zone_layer_range_list is None: zone_array = _fetch_zone_array(source_grid, zone_title, zone_uuid) zone_layer_range_list = zone_layer_ranges_from_array(zone_array, k0_min, k0_max, use_dominant_zone = use_dominant_zone) zone_count = len(zone_layer_range_list) # above is list of (zone_min_k0, zone_max_k0, zone) sorted by zone_min_k0 log.info('following layer ranges are based on top layer being numbered 1 (simulator protocol)') for (zone_min_k0, zone_max_k0, zone) in zone_layer_range_list: log.info('zone id {0:1d} covers layers {1:1d} to {2:1d}'.format(zone, zone_min_k0 + 1, zone_max_k0 + 1)) else: zone_layer_range_list = [(k0_min, k0_max, 0)] zone_count = 1 assert zone_count > 0, 'unexpected lack of zones' # create a new, empty grid object is_regular = grr.is_regular_grid(source_grid.root) and single_layer_mode grid = _empty_grid(model, source_grid, is_regular, k0_min, k0_max, zone_count) # aggregate inactive cell mask depending on laissez faire argument _set_inactive_cell_mask(source_grid, grid, inactive_laissez_faire, single_layer_mode, k0_min, k0_max, zone_layer_range_list, zone_count) if not is_regular: _process_geometry(source_grid, grid, single_layer_mode, k0_min, k0_max, zone_layer_range_list, zone_count) # establish title for the new grid if new_grid_title is None or len(new_grid_title) == 0: if single_layer_mode: preamble = 'single layer' else: preamble = 'zonal' new_grid_title = preamble + ' version of ' + str(rqet.citation_title_for_node(source_grid.root)) # write the new grid model.h5_release() if new_epc_file: _write_grid(new_epc_file, grid, grid_title = new_grid_title, mode = 'w') else: _write_grid(epc_file, grid, ext_uuid = None, grid_title = new_grid_title, mode = 'a') return grid
def extract_box(epc_file=None, source_grid=None, box=None, box_inactive=None, inherit_properties=False, inherit_realization=None, inherit_all_realizations=False, set_parent_window=None, new_grid_title=None, new_epc_file=None): """Extends an existing model with a new grid extracted as a logical IJK box from the source grid. 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 box (numpy int array of shape (2, 3)): the minimum and maximum kji0 indices in the source grid (zero based) to include in the extracted grid; note that cells with index equal to maximum value are included (unlike with python ranges) box_inactive (numpy bool array, optional): if present, shape must match box and values will be or'ed in with the inactive mask inherited from the source grid; if None, inactive mask will be as inherited from source grid inherit_properties (boolean, default False): if True, the new grid will have a copy of any properties associated with the source grid, with values taken from the specified box 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 set_parent_window (boolean, optional): if True, the extracted grid has its parent window attribute set; if False, the parent window is not set; if None, the default will be True if new_epc_file is None or False otherwise 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 extracted grid (& crs) returns: new grid object with extent as implied by the box argument note: the epc file and associated hdf5 file are appended to (extended) with the new grid, unless a new_epc_file is specified, in which case the grid and inherited properties are written there instead """ log.debug('extracting grid for box') 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 if set_parent_window is None: set_parent_window = (new_epc_file is None) model, source_grid = _establish_model_and_source_grid( epc_file, source_grid) assert source_grid.grid_representation in ['IjkGrid', 'IjkBlockGrid'] assert model is not None assert box is not None and box.shape == (2, 3) assert np.all(box[1, :] >= box[0, :]) and np.all( box[0, :] >= 0) and np.all(box[1, :] < source_grid.extent_kji) if source_grid.grid_representation == 'IjkBlockGrid': source_grid.make_regular_points_cached() box_str = bx.string_iijjkk1_for_box_kji0(box) # create a new, empty grid object grid = grr.Grid(model) # inherit attributes from source grid grid.grid_representation = 'IjkGrid' grid.extent_kji = box[1, :] - box[0, :] + 1 if box_inactive is not None: assert box_inactive.shape == tuple(grid.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 # 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(grid.crs_uuid), add_as_part=True) grid.crs = rqc.Crs(model, uuid=grid.crs_uuid) # inherit k_gaps for selected layer range _inherit_k_gaps(source_grid, grid, box) # extract inactive cell mask _extract_inactive_cell_mask(source_grid, grid, box_inactive, box) # extract the grid geometry source_grid.cache_all_geometry_arrays() # determine cell geometry is defined if hasattr(source_grid, 'array_cell_geometry_is_defined'): grid.array_cell_geometry_is_defined = _array_box( source_grid.array_cell_geometry_is_defined, box) grid.geometry_defined_for_all_cells_cached = np.all( grid.array_cell_geometry_is_defined) else: grid.geometry_defined_for_all_cells_cached = source_grid.geometry_defined_for_all_cells_cached # copy info for pillar geometry is defined 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 = _array_box( source_grid.array_pillar_geometry_is_defined, box) grid.geometry_defined_for_all_pillars_cached = np.all( grid.array_pillar_geometry_is_defined) # get reference to points for source grid geometry source_points = source_grid.points_ref() pillar_box = box.copy() if source_grid.k_gaps: pillar_box[:, 0] = source_grid.k_raw_index_array[pillar_box[:, 0]] pillar_box[ 1, :] += 1 # pillar points have extent one greater than cells, in each axis if not source_grid.has_split_coordinate_lines: log.debug('no split pillars in source grid') grid.points_cached = _array_box( source_points, pillar_box) # should work, ie. preserve xyz axis else: _process_split_pillars(source_grid, grid, box, pillar_box) if set_parent_window: fine_coarse = fc.FineCoarse(grid.extent_kji, grid.extent_kji, within_coarse_box=box) fine_coarse.set_all_ratios_constant() grid.set_parent(source_grid.uuid, True, fine_coarse) collection = _inherit_collection(source_grid, grid, inherit_properties, box, inherit_realization, inherit_all_realizations) if new_grid_title is None or len(new_grid_title) == 0: new_grid_title = 'local grid ' + box_str + ' extracted from ' + str( rqet.citation_title_for_node(source_grid.root)) 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 add_faults(epc_file, source_grid, polylines=None, lines_file_list=None, lines_crs_uuid=None, full_pillar_list_dict=None, left_right_throw_dict=None, create_gcs=True, inherit_properties=False, inherit_realization=None, inherit_all_realizations=False, new_grid_title=None, new_epc_file=None): """Extends epc file with a new grid which is a version of the source grid with new curtain fault(s) added. 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 polylines (lines.PolylineSet or list of lines.Polyline, optional): list of poly lines for which curtain faults are to be added; either this or lines_file_list or full_pillar_list_dict must be present lines_file_list (list of str, optional): a list of file paths, each containing one or more poly lines in simple ascii format§; see notes; either this or polylines or full_pillar_list_dicr must be present lines_crs_uuid (uuid, optional): if present, the uuid of a coordinate reference system with which to interpret the contents of the lines files; if None, the crs used by the grid will be assumed full_pillar_list_dict (dict mapping str to list of pairs of ints, optional): dictionary mapping from a fault name to a list of pairs of ints being the ordered neigbouring primary pillar (j0, i0) defining the curtain fault; either this or polylines or lines_file_list must be present left_right_throw_dict (dict mapping str to pair of floats, optional): dictionary mapping from a fault name to a pair of floats being the semi-throw adjustment on the left and the right of the fault (see notes); semi-throw values default to (+0.5, -0.5) create_gcs (boolean, default True): if True, and faults are being defined by lines, a grid connection set is created with one feature per new fault and associated organisational objects are also created; ignored if lines_file_list is None 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 unsplit grid (& crs) returns: a new grid (grid.Grid object) which is a copy of the source grid with the structure modified to incorporate the new faults notes: full_pillar_list_dict is typically generated by Grid.make_face_sets_from_pillar_lists(); pillars will be split as needed to model the new faults, though existing splits will be used as appropriate, so this function may also be used to add a constant to the throw of existing faults; the left_right_throw_dict contains a pair of floats for each fault name (as found in keys of full_pillar_list_dict); these throw values are lengths in the uom of the crs used by the grid (which must have the same xy units as z units); this function does not add a GridConnectionSet to the model – calling code may wish to do that """ log.info('adding faults') assert epc_file or new_epc_file, 'epc file name not specified' assert epc_file or source_grid is not None, 'neither epc file name nor source grid supplied' 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 in [ 'IjkGrid', 'IjkBlockGrid' ] # unstructured grids not catered for assert model is not None assert len([ arg for arg in (polylines, lines_file_list, full_pillar_list_dict) if arg is not None ]) == 1 # take a copy of the resqpy grid object, without writing to hdf5 or creating xml # the copy will be a Grid, even if the source is a RegularGrid grid = copy_grid(source_grid, model) grid.crs_uuid = source_grid.crs_uuid if source_grid.model is not model: model.duplicate_node(source_grid.model.root_for_uuid(grid.crs_uuid), add_as_part=True) grid.crs = rqc.Crs(model, uuid=grid.crs_uuid) if isinstance(polylines, rql.PolylineSet): polylines = polylines.convert_to_polylines() composite_face_set_dict = {} # build pillar list dict for polylines if necessary if full_pillar_list_dict is None: full_pillar_list_dict = {} _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) else: # populate composite face set dictionary from full pillar list _populate_composite_face_sets_for_pillar_lists( source_grid, full_pillar_list_dict, composite_face_set_dict) # log.debug(f'full_pillar_list_dict:\n{full_pillar_list_dict}') _process_full_pillar_list_dict(grid, full_pillar_list_dict, left_right_throw_dict) collection = _prepare_simple_inheritance(grid, source_grid, inherit_properties, inherit_realization, inherit_all_realizations) # todo: recompute depth properties (and volumes, cell lengths etc. if being strict) if new_grid_title is None or len(new_grid_title) == 0: new_grid_title = 'copy of ' + str( rqet.citation_title_for_node( source_grid.root)) + ' with added faults' # write model 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() _write_grid(epc_file, grid, ext_uuid=ext_uuid, property_collection=collection, grid_title=new_grid_title, mode='a') # create grid connection set if requested _create_gcs_if_requested(create_gcs, composite_face_set_dict, new_epc_file, grid) return grid