Esempio n. 1
0
    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()
Esempio n. 2
0
    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()
Esempio n. 3
0
    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()
Esempio n. 4
0
    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()
Esempio n. 5
0
    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()
Esempio n. 6
0
    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
Esempio n. 7
0
    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)
Esempio n. 8
0
    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)
Esempio n. 9
0
File: _common.py Progetto: bp/resqpy
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
Esempio n. 10
0
    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()
Esempio n. 11
0
    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()
Esempio n. 12
0
File: _xml.py Progetto: bp/resqpy
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
Esempio n. 13
0
    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)
Esempio n. 14
0
    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()
Esempio n. 15
0
File: _common.py Progetto: bp/resqpy
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
Esempio n. 16
0
    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()
Esempio n. 17
0
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()
Esempio n. 18
0
    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
Esempio n. 19
0
    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
Esempio n. 20
0
    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()
Esempio n. 21
0
    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)
Esempio n. 22
0
 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()
Esempio n. 23
0
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
Esempio n. 24
0
    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
Esempio n. 25
0
    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)
Esempio n. 26
0
    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)
Esempio n. 27
0
    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()
Esempio n. 28
0
    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
Esempio n. 29
0
File: _mesh.py Progetto: bp/resqpy
    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)
Esempio n. 30
0
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