def __init__(self, model, uuid=None, title=None, originator=None, extra_metadata=None): """Load an existing resqml object, or create new. Args: model (resqpy.model.Model): Parent model uuid (str, optional): Load from existing uuid (if given), else create new. title (str, optional): Citation title originator (str, optional): Creator of object. By default, uses user id. """ self.model = model self.title = title #: Citation title self.originator = originator #: Creator of object. By default, user id. self.extra_metadata = {} if extra_metadata: self.extra_metadata = extra_metadata self._standardise_extra_metadata( ) # has side effect of making a copy if uuid is None: self.uuid = bu.new_uuid() #: Unique identifier else: self.uuid = uuid root_node = self.root citation_node = rqet.find_tag(root_node, 'Citation') if citation_node is not None: self.title = rqet.find_tag_text(citation_node, 'Title') self.originator = rqet.find_tag_text(citation_node, 'Originator') self.extra_metadata = rqet.load_metadata_from_xml(root_node) self._load_from_xml()
def set_to_horizontal_plane(self, depth, box_xyz, border=0.0, quad_triangles=False): """Populate this (empty) surface with a patch of two triangles. Triangles define a flat, horizontal plane at a given depth. arguments: depth (float): z value to use in all points in the triangulated patch box_xyz (float[2, 3]): the min, max values of x, y (&z) giving the area to be covered (z ignored) border (float): an optional border width added around the x,y area defined by box_xyz quad_triangles (bool, default False): if True, 4 triangles are used instead of 2 :meta common: """ tri_patch = TriangulatedPatch(self.model, patch_index=0, crs_uuid=self.crs_uuid) tri_patch.set_to_horizontal_plane(depth, box_xyz, border=border, quad_triangles=quad_triangles) self.patch_list = [tri_patch] self.uuid = bu.new_uuid()
def set_to_triangle(self, corners): """Populate this (empty) surface with a patch of one triangle.""" tri_patch = TriangulatedPatch(self.model, patch_index=0, crs_uuid=self.crs_uuid) tri_patch.set_to_triangle(corners) self.patch_list = [tri_patch] self.uuid = bu.new_uuid()
def set_from_triangles_and_points(self, triangles, points): """Populate this (empty) Surface object from an array of triangle corner indices and an array of points.""" tri_patch = TriangulatedPatch(self.model, patch_index=0, crs_uuid=self.crs_uuid) tri_patch.set_from_triangles_and_points(triangles, points) self.patch_list = [tri_patch] self.uuid = bu.new_uuid()
def set_to_sail(self, n, centre, radius, azimuth, delta_theta): """Populate this (empty) surface with a patch representing a triangle wrapped on a sphere.""" tri_patch = TriangulatedPatch(self.model, patch_index=0, crs_uuid=self.crs_uuid) tri_patch.set_to_sail(n, centre, radius, azimuth, delta_theta) self.patch_list = [tri_patch] self.uuid = bu.new_uuid()
def __init__(self, parent_model, parent_frame, marker_index, marker_node=None, marker_type=None, interpretation_uuid=None, title=None, originator=None, extra_metadata=None): """Creates a new wellbore marker object and optionally loads it from xml. arguments: parent_model (model.Model object): the model which the new wellbore marker belongs to parent_frame (wellbore_marker_frame.WellboreMarkerFramer object): the wellbore marker frame to which the wellbore marker belongs marker_index (int): index of the wellbore marker in the parent WellboreMarkerFrame object marker_node (xml node, optional): if given, loads from xml. Else, creates new marker_type (str, optional): the type of geologic, fluid or contact feature e.g. "fault", "geobody", "horizon ", "gas/oil/water down to", "gas/oil/water up to", "free water contact", "gas oil contact", "gas water contact", "water oil contact", "seal" interpretation_uuid (uuid.UUID or string, optional): uuid of the boundary feature Interpretation organizational object that the marker refers to. note: it is highly recommended that a related boundary feature interpretation is provided title (str, optional): the citation title to use for a new wellbore marker ignored if uuid is not None; originator (str, optional): the name of the person creating the wellbore marker, defaults to login id; ignored if uuid is not None extra_metadata (dict, optional): string key, value pairs to add as extra metadata for the wellbore marker; ignored if uuid is not None returns: the newly created wellbore marker object """ # verify that marker type is valid if marker_type is not None: assert marker_type in ([ "fault", "geobody", "horizon", "gas down to", "oil down to", "water down to", "gas up to", "oil up to", "water up to", "free water contact", "gas oil contact", "gas water contact", "water oil contact", "seal" ]), "invalid marker type specified" self.model = parent_model self.wellbore_frame = parent_frame self.marker_index = marker_index self.uuid = None self.marker_type = marker_type self.interpretation_uuid = interpretation_uuid self.title = title self.originator = originator self.extra_metadata = extra_metadata if marker_node is not None: self._load_from_xml(marker_node=marker_node) if self.uuid is None: self.uuid = bu.new_uuid() assert self.uuid is not None
def write_hdf5(self, file_name=None, mode='a'): """Create or append to an hdf5 file, writing datasets for the measured depths.""" # NB: array data must have been set up prior to calling this function if self.uuid is None: self.uuid = bu.new_uuid() h5_reg = rwh5.H5Register(self.model) h5_reg.register_dataset(self.uuid, 'NodeMd', self.node_mds) h5_reg.write(file=file_name, mode=mode)
def write_hdf5(self, file_name=None, mode='a'): """Create or append the coordinates hdf5 array to hdf5 file. :meta common: """ if self.uuid is None: self.uuid = bu.new_uuid() h5_reg = rwh5.H5Register(self.model) h5_reg.register_dataset(self.uuid, 'points_patch0', self.coordinates) h5_reg.write(file_name, mode=mode)
def shift_polyline(parent_model, poly_root, xyz_shift=(0, 0, 0), title=''): """Returns a new polyline object, shifted by given coordinates.""" from resqpy.lines._polyline import Polyline poly = Polyline(parent_model=parent_model, poly_root=poly_root) if title != '': poly.title = title else: poly.title = poly.title + f" shifted by xyz({xyz_shift})" poly.uuid = bu.new_uuid() poly.coordinates = np.array(xyz_shift) + poly.coordinates return poly
def set_to_triangle_pair(self, corners): """Populate this (empty) surface with a patch of two triangles. arguments: corners (numpy float array of shape [2, 2, 3] or [4, 3]): 4 corners in logical ordering """ tri_patch = TriangulatedPatch(self.model, patch_index=0, crs_uuid=self.crs_uuid) tri_patch.set_to_triangle_pair(corners.reshape((4, 3))) self.patch_list = [tri_patch] self.uuid = bu.new_uuid()
def set_to_single_cell_faces_from_corner_points(self, cp, quad_triangles=True): """Populates this (empty) surface to represent faces of a cell, from corner points of shape (2, 2, 2, 3).""" assert cp.size == 24 tri_patch = TriangulatedPatch(self.model, patch_index=0, crs_uuid=self.crs_uuid) tri_patch.set_to_cell_faces_from_corner_points( cp, quad_triangles=quad_triangles) self.patch_list = [tri_patch] self.uuid = bu.new_uuid()
def _new_obj_node(flavour, name_space='resqml2', is_top_lvl_obj=True): """Creates a new main object element and sets attributes (does not add children).""" if flavour.startswith('obj_'): flavour = flavour[4:] node = rqet.Element(ns[name_space] + flavour) node.set('schemaVersion', '2.0') node.set('uuid', str(bu.new_uuid())) if is_top_lvl_obj: node.set(ns['xsi'] + 'type', ns[name_space] + 'obj_' + flavour) node.text = rqet.null_xml_text return node
def write_hdf5(self, file_name = None, mode = 'a'): """Create or append to an hdf5 file, writing datasets for the point set patches after caching arrays. :meta common: """ if not file_name: file_name = self.model.h5_file_name() if self.uuid is None: self.uuid = bu.new_uuid() # NB: patch arrays must all have been set up prior to calling this function h5_reg = rwh5.H5Register(self.model) for patch_index in range(self.patch_count): h5_reg.register_dataset(self.uuid, 'points_{}'.format(patch_index), self.patch_array_list[patch_index]) h5_reg.write(file_name, mode = mode)
def set_from_sparse_mesh(self, mesh_xyz): """Populate this (empty) Surface object from a mesh array of shape (N, M, 3) with NaNs. arguments: mesh_xyz (numpy float array of shape (N, M, 3)): a 2D lattice of points in 3D space, with NaNs in z """ mesh_shape = mesh_xyz.shape assert len(mesh_shape) == 3 and mesh_shape[2] == 3 tri_patch = TriangulatedPatch(self.model, patch_index=0, crs_uuid=self.crs_uuid) tri_patch.set_from_sparse_mesh(mesh_xyz) self.patch_list = [tri_patch] self.uuid = bu.new_uuid()
def flatten_polyline(parent_model, poly_root, axis="z", value=0.0, title=''): """Returns a new polyline object, flattened (projected) on a chosen axis to a given value.""" from resqpy.lines._polyline import Polyline axis = axis.lower() value = float(value) assert axis in ["x", "y", "z"], 'Axis must be x, y or z' poly = Polyline(parent_model=parent_model, poly_root=poly_root) if title != '': poly.title = title else: poly.title = poly.title + f" flattened in {axis} to value {value:.3f}" poly.uuid = bu.new_uuid() index = "xyz".index(axis) poly.coordinates[..., index] = value return poly
def set_to_trimmed_surface(self, large_surface, xyz_box=None, xy_polygon=None): """Populate this (empty) surface with triangles and points which overlap with a trimming volume. arguments: large_surface (Surface): the larger surface, a copy of which is to be trimmed xyz_box (numpy float array of shape (2, 3), optional): if present, a cuboid in xyz space against which to trim the surface xy_polygon (closed convex resqpy.lines.Polyline, optional): if present, an xy boundary against which to trim notes: at least one of xyz_box or xy_polygon must be present; if both are present, a triangle must have at least one point within both boundaries to survive the trimming; xyz_box and xy_polygon must be in the same crs as the large surface """ assert xyz_box is not None or xy_polygon is not None if xyz_box is not None: assert xyz_box.shape == (2, 3) log.debug( f'trimming surface {large_surface.title} from {large_surface.triangle_count()} triangles' ) if not self.title: self.title = str(large_surface.title) + ' trimmed' self.crs_uuid = large_surface.crs_uuid self.patch_list = [] for triangulated_patch in large_surface.patch_list: trimmed_patch = TriangulatedPatch(self.model, patch_index=len(self.patch_list), crs_uuid=self.crs_uuid) trimmed_patch.set_to_trimmed_patch(triangulated_patch, xyz_box=xyz_box, xy_polygon=xy_polygon) if trimmed_patch is not None and trimmed_patch.triangle_count > 0: self.patch_list.append(trimmed_patch) if len(self.patch_list): log.debug( f'trimmed surface {self.title} has {self.triangle_count()} triangles' ) else: log.warning('surface does not intersect trimming volume') self.uuid = bu.new_uuid()
def _inherit_gcs_list(epc_file, gcs_list, source_grid, grid): gcs_inheritance_model = rq.Model(epc_file) for gcs, gcs_title in gcs_list: # log.debug(f'inheriting gcs: {gcs_title}; old gcs uuid: {gcs.uuid}') gcs.uuid = bu.new_uuid() grid_list_modifications = [] for gi, g in enumerate(gcs.grid_list): # log.debug(f'gcs uses grid: {g.title}; grid uuid: {g.uuid}') if bu.matching_uuids(g.uuid, source_grid.uuid): grid_list_modifications.append(gi) assert len(grid_list_modifications) for gi in grid_list_modifications: gcs.grid_list[gi] = grid gcs.model = gcs_inheritance_model gcs.write_hdf5() gcs.create_xml(title=gcs_title) gcs_inheritance_model.store_epc() gcs_inheritance_model.h5_release()
def trim_to_xy_polygon(self, xy_polygon): """Converts point set to a single patch, holding only those points within the polygon when projected in xy. arguments: xy_polygon (closed convex resqpy.lines.Polyline): the polygon outlining the area in xy within which points are kept notes: usually used to reduce the point set volume for a temprary object; a new uuid is assigned; to add as a new part, call write_hdf5() and create_xml() methods """ points = self.full_array_ref() keep_mask = xy_polygon.points_are_inside_xy(points) self.patch_count = 0 self.patch_ref_list = [] self.patch_array_list = [] self.full_array = None self.add_patch(points[keep_mask, :].copy()) self.uuid = bu.new_uuid() # hope this doesn't cause problems
def trim_to_xyz_box(self, xyz_box): """Converts point set to a single patch, holding only those points within the xyz box. arguments: xyz_box (numpy float array of shape (2, 3)): the minimum and maximum range to keep in x,y,z notes: usually used to reduce the point set volume for a temprary object; a new uuid is assigned; to add as a new part, call write_hdf5() and create_xml() methods """ points = self.full_array_ref() keep_mask = np.where( np.logical_and(np.all(points >= np.expand_dims(xyz_box[0], axis = 0), axis = -1), np.all(points <= np.expand_dims(xyz_box[1], axis = 0), axis = -1))) self.patch_count = 0 self.patch_ref_list = [] self.patch_array_list = [] self.full_array = None self.add_patch(points[keep_mask, :].copy()) self.uuid = bu.new_uuid() # hope this doesn't cause problems
def set_from_irregular_mesh(self, mesh_xyz, quad_triangles=False): """Populate this (empty) Surface object from an untorn mesh array of shape (N, M, 3). arguments: mesh_xyz (numpy float array of shape (N, M, 3)): a 2D lattice of points in 3D space quad_triangles: (boolean, optional, default False): if True, each quadrangle is represented by 4 triangles in the surface, with the mean of the 4 corner points used as a common centre node; if False (the default), only 2 triangles are used for each quadrangle; note that the 2 triangle mode gives a non-unique triangulated result """ mesh_shape = mesh_xyz.shape assert len(mesh_shape) == 3 and mesh_shape[2] == 3 tri_patch = TriangulatedPatch(self.model, patch_index=0, crs_uuid=self.crs_uuid) tri_patch.set_from_irregular_mesh(mesh_xyz, quad_triangles=quad_triangles) self.patch_list = [tri_patch] self.uuid = bu.new_uuid()
def write_hdf5(self, file_name=None, mode='a'): """Create or append to an hdf5 file, writing datasets for the triangulated patches after caching arrays. :meta common: """ if self.uuid is None: self.uuid = bu.new_uuid() # NB: patch arrays must all have been set up prior to calling this function h5_reg = rwh5.H5Register(self.model) # todo: sort patches by patch index and check sequence for triangulated_patch in self.patch_list: (t, p) = triangulated_patch.triangles_and_points() h5_reg.register_dataset( self.uuid, 'points_patch{}'.format(triangulated_patch.patch_index), p) h5_reg.register_dataset( self.uuid, 'triangles_patch{}'.format(triangulated_patch.patch_index), t) h5_reg.write(file_name, mode=mode)
def set_to_multi_cell_faces_from_corner_points(self, cp, quad_triangles=True): """Populates this (empty) surface to represent faces of a set of cells. From corner points of shape (N, 2, 2, 2, 3). """ assert cp.size % 24 == 0 cp = cp.reshape((-1, 2, 2, 2, 3)) self.patch_list = [] p_index = 0 for cell_cp in cp: tri_patch = TriangulatedPatch(self.model, patch_index=p_index, crs_uuid=self.crs_uuid) tri_patch.set_to_cell_faces_from_corner_points( cell_cp, quad_triangles=quad_triangles) self.patch_list.append(tri_patch) p_index += 1 self.uuid = bu.new_uuid()
def test_is_equivalent_is_sameUUIDs(tmp_model): # Arrange object_uuid = uuid.new_uuid() fault_interp = rqo.FaultInterpretation( tmp_model, title='test_fault_interpretation', ) fault_interp.uuid = object_uuid # Act other = rqo.FaultInterpretation( tmp_model, title='test_fault_interpretation_other', ) other.uuid = object_uuid result = fault_interp.is_equivalent(other=other) # Assert assert result is True
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 write_hdf5(self, file_name=None, mode='a'): """Create or append to an hdf5 file, writing datasets for the measured depths, control points and tangent vectors. :meta common: """ # NB: array data must all have been set up prior to calling this function if self.uuid is None: self.uuid = bu.new_uuid() h5_reg = rwh5.H5Register(self.model) h5_reg.register_dataset(self.uuid, 'controlPointParameters', self.measured_depths) h5_reg.register_dataset(self.uuid, 'controlPoints', self.control_points) if self.tangent_vectors is not None: h5_reg.register_dataset(self.uuid, 'tangentVectors', self.tangent_vectors) h5_reg.write(file=file_name, mode=mode)
def write_hdf5(self, file_name = None, mode = 'a', save_polylines = False): """Create or append the coordinates, counts and indices hdf5 arrays to hdf5 file. :meta common: """ if self.uuid is None: self.uuid = bu.new_uuid() self.combine_polylines(self.polys) self.bool_array_format(self.closed_array) self.save_polys = save_polylines if self.save_polys: for poly in self.polys: poly.write_hdf5(file_name) h5_reg = rwh5.H5Register(self.model) h5_reg.register_dataset(self.uuid, 'points_patch0', self.coordinates) h5_reg.register_dataset(self.uuid, 'NodeCountPerPolyline_patch0', self.count_perpol.astype(np.int32)) if self.boolnotconstant: h5_reg.register_dataset(self.uuid, 'indices_patch0', self.indices) h5_reg.write(file_name, mode = mode)
def set_from_torn_mesh(self, mesh_xyz, quad_triangles=False): """Populate this (empty) Surface object from a torn mesh array of shape (nj, ni, 2, 2, 3). arguments: mesh_xyz (numpy float array of shape (nj, ni, 2, 2, 3)): corner points of 2D faces in 3D space quad_triangles: (boolean, optional, default False): if True, each quadrangle (face) is represented by 4 triangles in the surface, with the mean of the 4 corner points used as a common centre node; if False (the default), only 2 triangles are used for each quadrangle; note that the 2 triangle mode gives a non-unique triangulated result note: this method uses a single patch to represent the torn surface, whereas strictly the RESQML standard requires speparate patches where parts of a surface are completely disconnected """ mesh_shape = mesh_xyz.shape assert len(mesh_shape) == 5 and mesh_shape[2:] == (2, 2, 3) tri_patch = TriangulatedPatch(self.model, patch_index=0, crs_uuid=self.crs_uuid) tri_patch.set_from_torn_mesh(mesh_xyz, quad_triangles=quad_triangles) self.patch_list = [tri_patch] self.uuid = bu.new_uuid()
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 write_hdf5(self, file_name=None, mode='a', use_xy_only=False): """Create or append to an hdf5 file, writing datasets for the mesh depending on flavour.""" if not file_name: file_name = self.model.h5_file_name() if self.uuid is None: self.uuid = bu.new_uuid() if self.flavour == 'regular': return # NB: arrays must have been set up prior to calling this function h5_reg = rwh5.H5Register(self.model) a = self.full_array_ref() if self.flavour == 'explicit': if use_xy_only: h5_reg.register_dataset( self.uuid, 'points', a[..., :2]) # todo: check what others use here else: h5_reg.register_dataset(self.uuid, 'points', a) elif self.flavour == 'ref&z' or self.flavour == 'reg&z': h5_reg.register_dataset(self.uuid, 'zvalues', a[..., 2]) else: log.error('bad mesh flavour when writing hdf5 array') h5_reg.write(file_name, mode=mode)
def test_find_marker_index_from_interp_uuid(example_model_and_crs): # --------- Arrange ---------- # Create a WellboreMarkerFrame object in memory # Load example model from a fixture model, crs = example_model_and_crs # Create a trajectory well_name = 'Banoffee' elevation = 100 datum = resqpy.well.MdDatum(parent_model=model, crs_uuid=crs.uuid, location=(0, 0, -elevation), md_reference='kelly bushing') mds = np.array([300.0, 310.0, 330.0]) zs = mds - elevation source_dataframe = pd.DataFrame({ 'MD': mds, 'X': [150.0, 165.0, 180.0], 'Y': [240.0, 260.0, 290.0], 'Z': zs, }) trajectory = resqpy.well.Trajectory(parent_model=model, data_frame=source_dataframe, well_name=well_name, md_datum=datum, length_uom='m') trajectory.write_hdf5() trajectory.create_xml() trajectory_uuid = trajectory.uuid # Create features and interpretations horizon_feature_1 = rqo.GeneticBoundaryFeature( parent_model=model, kind='horizon', feature_name='horizon_feature_1') horizon_feature_1.create_xml() horizon_interp_1 = rqo.HorizonInterpretation( parent_model=model, title='horizon_interp_1', genetic_boundary_feature=horizon_feature_1, sequence_stratigraphy_surface='flooding', boundary_relation_list=['conformable']) horizon_interp_1.create_xml() horizon_interp_1_uuid = horizon_interp_1.uuid woc_feature_1 = rqo.FluidBoundaryFeature(parent_model=model, kind='water oil contact', feature_name='woc_1') # fluid boundary feature does not have an associated interpretation woc_feature_1.create_xml() fault_feature_1 = rqo.TectonicBoundaryFeature( parent_model=model, kind='fault', feature_name='fault_feature_1') fault_feature_1.create_xml() fault_interp_1 = rqo.FaultInterpretation( parent_model=model, title='fault_interp_1', tectonic_boundary_feature=fault_feature_1, is_normal=True, maximum_throw=15) fault_interp_1.create_xml() df = pd.DataFrame({ 'MD': [400.0, 410.0, 430.0], 'Boundary_Feature_Type': ['horizon', 'water oil contact', 'fault'], 'Marker_Citation_Title': ['marker_horizon_1', 'marker_woc_1', 'marker_fault_1'], 'Interp_Citation_Title': ['horizon_interp_1', None, 'fault_interp_1'], }) # Create a wellbore marker frame from a dataframe wellbore_marker_frame = resqpy.well.WellboreMarkerFrame.from_dataframe( parent_model=model, dataframe=df, trajectory_uuid=trajectory_uuid, title='WBF1', originator='Human', extra_metadata={'target_reservoir': 'treacle'}) # Create a random uuid that's not related to any interpretation random_uuid = bu.new_uuid() # --------- Act ---------- # Find marker indices based on interpretation uuids horizon_index = wellbore_marker_frame.find_marker_index_from_interp( interpretation_uuid=horizon_interp_1_uuid) random_index = wellbore_marker_frame.find_marker_index_from_interp( interpretation_uuid=random_uuid) # --------- Act ---------- assert horizon_index == 0 assert random_index is None