Ejemplo n.º 1
0
def _add_single_surface(model, surf_file, surface_file_format, surface_role, quad_triangles, crs_uuid, rq_class,
                        hdf5_file, h5_mode, make_horizon_interpretations_and_features, ext_uuid):
    _, short_name = os.path.split(surf_file)
    dot = short_name.rfind('.')
    if dot > 0:
        short_name = short_name[:dot]

    log.info('surface ' + short_name + ' processing file: ' + surf_file + ' using format: ' + surface_file_format)
    if rq_class == 'surface':
        if surface_file_format == 'GOCAD-Tsurf':
            surface = rqs.Surface(model,
                                  tsurf_file = surf_file,
                                  surface_role = surface_role,
                                  quad_triangles = quad_triangles)
        else:
            surface = rqs.Surface(model,
                                  mesh_file = surf_file,
                                  mesh_format = surface_file_format,
                                  surface_role = surface_role,
                                  quad_triangles = quad_triangles)
    elif rq_class == 'mesh':
        if surface_file_format == 'GOCAD-Tsurf':
            log.info(f"Cannot convert a GOCAD-Tsurf to mesh, only to TriangulatedSurface - skipping file {surf_file}")
            return model
        else:
            surface = rqs.Mesh(model,
                               mesh_file = surf_file,
                               mesh_format = surface_file_format,
                               mesh_flavour = 'reg&z',
                               surface_role = surface_role,
                               crs_uuid = crs_uuid)
    else:
        log.critical('this is impossible')
    # NB. surface may be either a Surface object or a Mesh object

    log.debug('appending to hdf5 file for surface file: ' + surf_file)
    surface.write_hdf5(hdf5_file, mode = h5_mode)

    if make_horizon_interpretations_and_features:
        feature = rqo.GeneticBoundaryFeature(model, kind = 'horizon', feature_name = short_name)
        feature.create_xml()
        interp = rqo.HorizonInterpretation(model, genetic_boundary_feature = feature, domain = 'depth')
        interp_root = interp.create_xml()
        surface.set_represented_interpretation_root(interp_root)

    surface.create_xml(ext_uuid,
                       add_as_part = True,
                       add_relationships = True,
                       title = short_name + ' sourced from ' + surf_file,
                       originator = None)

    return model
Ejemplo n.º 2
0
def small_grid_and_surface(
        tmp_model: Model) -> Tuple[grr.RegularGrid, rqs.Surface]:
    """Creates a small RegularGrid and a random triangular surface."""
    crs = Crs(tmp_model)
    crs.create_xml()

    extent = 10
    extent_kji = (extent, extent, extent)
    dxyz = (1.0, 1.0, 1.0)
    crs_uuid = crs.uuid
    title = "small_grid"
    grid = grr.RegularGrid(tmp_model,
                           extent_kji=extent_kji,
                           dxyz=dxyz,
                           crs_uuid=crs_uuid,
                           title=title)
    grid.create_xml()

    n_points = 100
    points = np.random.rand(n_points, 3) * extent
    triangles = tri.dt(points)
    surface = rqs.Surface(tmp_model, crs_uuid=crs_uuid, title="small_surface")
    surface.set_from_triangles_and_points(triangles, points)
    surface.triangles_and_points()
    surface.write_hdf5()
    surface.create_xml()

    tmp_model.store_epc()

    return grid, surface
Ejemplo n.º 3
0
def _set_support_uuid_notnone_supportnone(collection, support_uuid, model):
    import resqpy.fault as rqf
    import resqpy.grid as grr
    import resqpy.surface as rqs
    import resqpy.unstructured as rug
    import resqpy.well as rqw

    support_part = model.part_for_uuid(support_uuid)
    assert support_part is not None, 'supporting representation part missing in model'
    collection.support_root = model.root_for_part(support_part)
    support_type = model.type_of_part(support_part)
    assert support_type is not None
    if support_type == 'obj_IjkGridRepresentation':
        collection.support = grr.any_grid(model,
                                          uuid=collection.support_uuid,
                                          find_properties=False)
    elif support_type == 'obj_WellboreFrameRepresentation':
        collection.support = rqw.WellboreFrame(model,
                                               uuid=collection.support_uuid)
    elif support_type == 'obj_BlockedWellboreRepresentation':
        collection.support = rqw.BlockedWell(model,
                                             uuid=collection.support_uuid)
    elif support_type == 'obj_Grid2dRepresentation':
        collection.support = rqs.Mesh(model, uuid=collection.support_uuid)
    elif support_type == 'obj_GridConnectionSetRepresentation':
        collection.support = rqf.GridConnectionSet(
            model, uuid=collection.support_uuid)
    elif support_type == 'obj_TriangulatedSetRepresentation':
        collection.support = rqs.Surface(model, uuid=collection.support_uuid)
    elif support_type == 'obj_UnstructuredGridRepresentation':
        collection.support = rug.UnstructuredGrid(model,
                                                  uuid=collection.support_uuid,
                                                  geometry_required=False,
                                                  find_properties=False)
    elif support_type == 'obj_WellboreMarkerFrameRepresentation':
        collection.support = rqw.WellboreMarkerFrame(
            model, uuid=collection.support_uuid)
    else:
        raise TypeError(
            'unsupported property supporting representation class: ' +
            str(support_type))
Ejemplo n.º 4
0
def test_vertical_prism_grid_from_seed_points_and_surfaces(tmp_path):

    seed(23487656)  # to ensure test reproducibility

    epc = os.path.join(tmp_path, 'voronoi_prism_grid.epc')
    model = rq.new_model(epc)
    crs = rqc.Crs(model)
    crs.create_xml()

    # define a boundary polyline:
    b_count = 7
    boundary_points = np.empty((b_count, 3))
    radius = 1000.0
    for i in range(b_count):
        theta = -vec.radians_from_degrees(i * 360.0 / b_count)
        boundary_points[i] = (2.0 * radius * maths.cos(theta),
                              radius * maths.sin(theta), 0.0)
    boundary = rql.Polyline(model,
                            set_coord=boundary_points,
                            set_bool=True,
                            set_crs=crs.uuid,
                            title='rough ellipse')
    boundary.write_hdf5()
    boundary.create_xml()

    # derive a larger area of interest
    aoi = rql.Polyline.from_scaled_polyline(boundary,
                                            1.1,
                                            title='area of interest')
    aoi.write_hdf5()
    aoi.create_xml()
    min_xy = np.min(aoi.coordinates[:, :2], axis=0) - 50.0
    max_xy = np.max(aoi.coordinates[:, :2], axis=0) + 50.0

    print(f'***** min max xy aoi+ : {min_xy} {max_xy}')  # debug

    # create some seed points within boundary
    seed_count = 5
    seeds = rqs.PointSet(model,
                         crs_uuid=crs.uuid,
                         polyline=boundary,
                         random_point_count=seed_count,
                         title='seeds')
    seeds.write_hdf5()
    seeds.create_xml()
    seeds_xy = seeds.single_patch_array_ref(0)

    for seed_xy in seeds_xy:
        assert aoi.point_is_inside_xy(
            seed_xy), f'seed point {seed_xy} outwith aoi'

    print(
        f'***** min max xy seeds : {np.min(seeds_xy, axis = 0)} {np.max(seeds_xy, axis = 0)}'
    )  # debug

    # create some horizon surfaces
    ni, nj = 21, 11
    lattice = rqs.Mesh(model,
                       crs_uuid=crs.uuid,
                       mesh_flavour='regular',
                       ni=ni,
                       nj=nj,
                       origin=(min_xy[0], min_xy[1], 0.0),
                       dxyz_dij=np.array([[
                           (max_xy[0] - min_xy[0]) / (ni - 1), 0.0, 0.0
                       ], [0.0, (max_xy[1] - min_xy[1]) / (nj - 1), 0.0]]))
    lattice.write_hdf5()
    lattice.create_xml()
    horizons = []
    for i in range(4):
        horizon_depths = 1000.0 + 100.0 * i + 20.0 * (np.random.random(
            (nj, ni)) - 0.5)
        horizon_mesh = rqs.Mesh(model,
                                crs_uuid=crs.uuid,
                                mesh_flavour='ref&z',
                                ni=ni,
                                nj=nj,
                                z_values=horizon_depths,
                                z_supporting_mesh_uuid=lattice.uuid,
                                title='h' + str(i))
        horizon_mesh.write_hdf5()
        horizon_mesh.create_xml()
        horizon_surface = rqs.Surface(model,
                                      crs_uuid=crs.uuid,
                                      mesh=horizon_mesh,
                                      quad_triangles=True,
                                      title=horizon_mesh.title)
        horizon_surface.write_hdf5()
        horizon_surface.create_xml()
        horizons.append(horizon_surface)

    # create a re-triangulated Voronoi vertical prism grid
    grid = rug.VerticalPrismGrid.from_seed_points_and_surfaces(
        model, seeds_xy, horizons, aoi, title="giant's causeway")
    assert grid is not None
    grid.write_hdf5()
    grid.create_xml()

    # check cell thicknesses are in expected range
    thick = grid.thickness()
    assert np.all(thick >= 80.0)
    assert np.all(thick <= 120.0)

    model.store_epc()
Ejemplo n.º 5
0
def test_vertical_prism_grid_from_surfaces(tmp_path):

    epc = os.path.join(tmp_path, 'vertical_prism.epc')
    model = rq.new_model(epc)
    crs = rqc.Crs(model)
    crs.create_xml()

    # create a point set representing a pentagon with a centre node
    pentagon_points = np.array([[-100.0, -200.0, 1050.0],
                                [-200.0, 0.0, 1050.0], [0.0, 200.0, 1025.0],
                                [200.0, 0.0, 975.0], [100.0, -200.0, 999.0],
                                [0.0, 0.0, 1000.0]])
    pentagon = rqs.PointSet(model,
                            points_array=pentagon_points,
                            crs_uuid=crs.uuid,
                            title='pentagon')
    pentagon.write_hdf5()
    pentagon.create_xml()

    # create a surface from the point set (will make a Delauney triangulation)
    top_surf = rqs.Surface(model, point_set=pentagon, title='top surface')
    top_surf.write_hdf5()
    top_surf.create_xml()
    surf_list = [top_surf]

    # check the pentagon surface
    pentagon_triangles, pentagon_points = top_surf.triangles_and_points()
    assert pentagon_points.shape == (6, 3)
    assert pentagon_triangles.shape == (5, 3)

    # create a couple of horizontal surfaces at greater depths
    boundary = np.array([[-300.0, -300.0, 0.0], [300.0, 300.0, 0.0]])
    for depth in (1100.0, 1200.0):
        base = rqs.Surface(model)
        base.set_to_horizontal_plane(depth, boundary)
        base.write_hdf5()
        base.create_xml()
        surf_list.append(base)

    # now build a vertical prism grid from the surfaces
    grid = rug.VerticalPrismGrid.from_surfaces(model,
                                               surf_list,
                                               title='the pentagon')
    grid.write_hdf5()
    grid.create_xml()

    model.store_epc()

    # re-open model

    model = rq.Model(epc)
    assert model is not None

    # find grid by title
    grid_uuid = model.uuid(obj_type='UnstructuredGridRepresentation',
                           title='the pentagon')
    assert grid_uuid is not None

    # re-instantiate the grid
    grid = rug.VerticalPrismGrid(model, uuid=grid_uuid)
    assert grid is not None
    assert grid.nk == 2
    assert grid.cell_count == 10
    assert grid.node_count == 18
    assert grid.face_count == 35

    # create a very similar grid using explicit triangulation arguments

    # make the same Delauney triangulation
    triangles = triangulation.dt(pentagon_points, algorithm="scipy")
    assert triangles.ndim == 2 and triangles.shape[1] == 3

    # slightly shrink pentagon points to be within area of surfaces
    for i in range(len(pentagon_points)):
        if pentagon_points[i, 0] < 0.0:
            pentagon_points[i, 0] += 1.0
        elif pentagon_points[i, 0] > 0.0:
            pentagon_points[i, 0] -= 1.0
        if pentagon_points[i, 1] < 0.0:
            pentagon_points[i, 1] += 1.0
        elif pentagon_points[i, 1] > 0.0:
            pentagon_points[i, 1] -= 1.0

    # load the surfaces
    surf_uuids = model.uuids(obj_type='TriangulatedSetRepresentation',
                             sort_by='oldest')
    surf_list = []
    for surf_uuid in surf_uuids:
        surf_list.append(rqs.Surface(model, uuid=surf_uuid))

    # create a new vertical prism grid using the explicit triangulation arguments
    similar = rug.VerticalPrismGrid.from_surfaces(
        model,
        surf_list,
        column_points=pentagon_points,
        column_triangles=triangles,
        title='similar pentagon')

    # check similarity
    for attr in ('cell_shape', 'nk', 'cell_count', 'node_count', 'face_count'):
        assert getattr(grid, attr) == getattr(similar, attr)
    # for index_attr in ('nodes_per_face', 'nodes_per_face_cl', 'faces_per_cell', 'faces_per_cell_cl'):
    for i, (index_attr, index_attr_cl) in enumerate([
        ('nodes_per_face', 'nodes_per_face_cl'),
        ('faces_per_cell', 'faces_per_cell_cl')
    ]):
        ga_cl = getattr(grid, index_attr_cl)
        sa_cl = getattr(similar, index_attr_cl)
        assert np.all(ga_cl == sa_cl)
        ga = getattr(grid, index_attr)
        sa = getattr(similar, index_attr)
        ip = 0 if i == 0 else ga_cl[i - 1]
        assert set(ga[ip:ga_cl[i]]) == set(sa[ip:ga_cl[i]])
    assert_allclose(grid.points_ref(), similar.points_ref(), atol=2.0)

    # check that isotropic horizontal permeability is preserved
    permeability = 250.0
    primary_k = np.full((grid.cell_count, ), permeability)
    orthogonal_k = primary_k.copy()
    triple_k = grid.triple_horizontal_permeability(primary_k, orthogonal_k,
                                                   37.0)
    assert triple_k.shape == (grid.cell_count, 3)
    assert_array_almost_equal(triple_k, permeability)
    azimuth = np.linspace(0.0, 360.0, num=grid.cell_count)
    triple_k = grid.triple_horizontal_permeability(primary_k, orthogonal_k,
                                                   azimuth)
    assert triple_k.shape == (grid.cell_count, 3)
    assert_array_almost_equal(triple_k, permeability)

    # check that anisotropic horizontal permeability is correctly bounded
    orthogonal_k *= 0.1
    triple_k = grid.triple_horizontal_permeability(primary_k, orthogonal_k,
                                                   azimuth)
    assert triple_k.shape == (grid.cell_count, 3)
    assert np.all(triple_k <= permeability)
    assert np.all(triple_k >= permeability * 0.1)
    assert np.min(triple_k) < permeability / 2.0
    assert np.max(triple_k) > permeability / 2.0

    # set up some properties
    pc = grid.property_collection
    assert pc is not None
    pc.add_cached_array_to_imported_list(cached_array=None,
                                         source_info='unit test',
                                         keyword='NETGRS',
                                         property_kind='net to gross ratio',
                                         discrete=False,
                                         uom='m3/m3',
                                         indexable_element='cells',
                                         const_value=0.75)
    pc.add_cached_array_to_imported_list(cached_array=None,
                                         source_info='unit test',
                                         keyword='PERMK',
                                         property_kind='permeability rock',
                                         facet_type='direction',
                                         facet='K',
                                         discrete=False,
                                         uom='mD',
                                         indexable_element='cells',
                                         const_value=10.0)
    pc.add_cached_array_to_imported_list(cached_array=None,
                                         source_info='unit test',
                                         keyword='PERM',
                                         property_kind='permeability rock',
                                         facet_type='direction',
                                         facet='primary',
                                         discrete=False,
                                         uom='mD',
                                         indexable_element='cells',
                                         const_value=100.0)
    pc.add_cached_array_to_imported_list(cached_array=None,
                                         source_info='unit test',
                                         keyword='PERM',
                                         property_kind='permeability rock',
                                         facet_type='direction',
                                         facet='orthogonal',
                                         discrete=False,
                                         uom='mD',
                                         indexable_element='cells',
                                         const_value=20.0)
    x_min, x_max = grid.xyz_box()[:, 0]
    relative_x = (grid.centre_point()[:, 0] - x_min) * (x_max - x_min)
    azi = relative_x * 90.0 + 45.0
    pc.add_cached_array_to_imported_list(
        cached_array=azi,
        source_info='unit test',
        keyword='primary permeability azimuth',
        property_kind='plane angle',
        facet_type='direction',
        facet='primary',
        discrete=False,
        uom='dega',
        indexable_element='cells')
    pc.write_hdf5_for_imported_list()
    pc.create_xml_for_imported_list_and_add_parts_to_model()

    model.store_epc()

    # test that half cell transmissibilities can be computed
    half_t = grid.half_cell_transmissibility()
    assert np.all(half_t > 0.0)

    # add the half cell transmissibility array as a property
    pc.add_cached_array_to_imported_list(cached_array=half_t.flatten(),
                                         source_info='unit test',
                                         keyword='half transmissibility',
                                         property_kind='transmissibility',
                                         discrete=False,
                                         count=1,
                                         indexable_element='faces per cell')
    pc.write_hdf5_for_imported_list()
    pc.create_xml_for_imported_list_and_add_parts_to_model(
        extra_metadata={'uom': 'm3.cP/(d.kPa)'})

    model.store_epc()
Ejemplo n.º 6
0
    def from_surfaces(cls,
                      parent_model,
                      surfaces,
                      column_points=None,
                      column_triangles=None,
                      title=None,
                      originator=None,
                      extra_metadata={},
                      set_handedness=False):
        """Create a layered vertical prism grid from an ordered list of untorn surfaces.

        arguments:
           parent_model (model.Model object): the model which this grid is part of
           surfaces (list of surface.Surface): list of two or more untorn surfaces ordered from
              shallowest to deepest; see notes
           column_points (2D numpy float array, optional): if present, the xy points to use for
              the grid's triangulation; see notes
           column_triangles (numpy int array of shape (M, 3), optional): if present, indices into the
              first dimension of column_points giving the xy triangulation to use for the grid; see notes
           title (str, optional): citation title for the new grid
           originator (str, optional): name of person creating the grid; defaults to login id
           extra_metadata (dict, optional): dictionary of extra metadata items to add to the grid

        returns:
           a newly created VerticalPrismGrid object

        notes:
           this method will not work for torn (faulted) surfaces, nor for surfaces with recumbent folds;
           the surfaces may not cross each other, ie. the depth ordering must be consistent over the area;
           the triangular pattern of the columns (in the xy plane) can be specified with the column_points
           and column_triangles arguments;
           if those arguments are None, the first, shallowest, surface is used as a master and determines
           the triangular pattern of the columns;
           where a gravity vector from a node above does not intersect a surface, the point is inherited
           as a copy of the node above and will be NaNs if no surface above has an intersection;
           the Surface class has methods for creating a Surface from a PointSet or a Mesh (RESQML
           Grid2dRepresentation), or for a horizontal plane;
           this class is represented in RESQML as an UnstructuredGridRepresentation – when a resqpy
           class is written for ColumnLayerGridRepresentation, a method will be added to that class to
           convert from a resqpy VerticalPrismGrid
        """
        def find_pair(a, pair):
            # for sorted array a of shape (N, 2) returns index in first axis of a pair

            def frp(a, pair, b, c):
                m = b + ((c - b) // 2)
                assert m < len(a), 'pair not found in sorted array'
                if np.all(a[m] == pair):
                    return m
                assert c > b, 'pair not found in sorted array'
                if a[m, 0] < pair[0]:
                    return frp(a, pair, m + 1, c)
                elif a[m, 0] > pair[0]:
                    return frp(a, pair, b, m)
                elif a[m, 1] < pair[1]:
                    return frp(a, pair, m + 1, c)
                else:
                    return frp(a, pair, b, m)

            return frp(a, pair, 0, len(a))

        assert (column_points is None) == (column_triangles is None)
        assert len(surfaces) > 1
        for s in surfaces:
            assert isinstance(s, rqs.Surface)

        vpg = cls(parent_model,
                  title=title,
                  originator=originator,
                  extra_metadata=extra_metadata)
        assert vpg is not None

        top = surfaces[0]

        # set and check consistency of crs
        vpg.crs_uuid = top.crs_uuid
        for s in surfaces[1:]:
            if not bu.matching_uuids(vpg.crs_uuid, s.crs_uuid):
                # check for equivalence
                assert rqc.Crs(parent_model, uuid=vpg.crs_uuid) == rqc.Crs(
                    parent_model, uuid=s.crs_uuid), 'mismatching surface crs'

        # fetch the data for the top surface, to be used as the master for the triangular pattern
        if column_triangles is None:
            top_triangles, top_points = top.triangles_and_points()
            column_edges = top.distinct_edges(
            )  # ordered pairs of node indices
        else:
            top_triangles = column_triangles
            if column_points.shape[1] == 3:
                top_points = column_points
            else:
                top_points = np.zeros((len(column_points), 3))
                top_points[:, :column_points.shape[1]] = column_points
            column_surf = rqs.Surface(parent_model, crs_uuid=vpg.crs_uuid)
            column_surf.set_from_triangles_and_points(column_triangles,
                                                      column_points)
            column_edges = column_surf.distinct_edges()
        assert top_triangles.ndim == 2 and top_triangles.shape[1] == 3
        assert top_points.ndim == 2 and top_points.shape[1] in [2, 3]
        assert len(top_triangles) > 0
        p_count = len(top_points)
        bad_points = np.zeros(p_count, dtype=bool)

        # setup size of arrays for the vertical prism grid
        column_count = top_triangles.shape[0]
        surface_count = len(surfaces)
        layer_count = surface_count - 1
        column_edge_count = len(column_edges)
        vpg.cell_count = column_count * layer_count
        vpg.node_count = p_count * surface_count
        vpg.face_count = column_count * surface_count + column_edge_count * layer_count
        vpg.nk = layer_count
        if vpg.extra_metadata is None:
            vpg.extra_metadata = {}
        vpg.extra_metadata['layer count'] = vpg.nk

        # setup points with copies of points for top surface, z values to be updated later
        points = np.zeros((surface_count, p_count, 3))
        points[:, :, :] = top_points

        # arrange faces with all triangles first, followed by the vertical quadrilaterals
        vpg.nodes_per_face_cl = np.zeros(vpg.face_count, dtype=int)
        vpg.nodes_per_face_cl[:column_count * surface_count] =  \
           np.arange(3, 3 * column_count * surface_count + 1, 3, dtype = int)
        quad_start = vpg.nodes_per_face_cl[column_count * surface_count -
                                           1] + 4
        vpg.nodes_per_face_cl[column_count * surface_count:] =  \
           np.arange(quad_start, quad_start + 4 * column_edge_count * layer_count, 4)
        assert vpg.nodes_per_face_cl[
            -1] == 3 * column_count * surface_count + 4 * column_edge_count * layer_count
        # populate nodes per face for triangular faces
        vpg.nodes_per_face = np.zeros(vpg.nodes_per_face_cl[-1], dtype=int)
        for surface_index in range(surface_count):
            vpg.nodes_per_face[surface_index * 3 * column_count : (surface_index + 1) * 3 * column_count] =  \
               top_triangles.flatten() + surface_index * p_count
        # populate nodes per face for quadrilateral faces
        quad_nodes = np.empty((layer_count, column_edge_count, 2, 2),
                              dtype=int)
        for layer in range(layer_count):
            quad_nodes[layer, :, 0, :] = column_edges + layer * p_count
            # reverse order of base pairs to maintain cyclic ordering of nodes per face
            quad_nodes[layer, :, 1,
                       0] = column_edges[:, 1] + (layer + 1) * p_count
            quad_nodes[layer, :, 1,
                       1] = column_edges[:, 0] + (layer + 1) * p_count
        vpg.nodes_per_face[3 * surface_count *
                           column_count:] = quad_nodes.flatten()
        assert vpg.nodes_per_face[-1] > 0

        # set up faces per cell
        vpg.faces_per_cell = np.zeros(5 * vpg.cell_count, dtype=int)
        vpg.faces_per_cell_cl = np.arange(5,
                                          5 * vpg.cell_count + 1,
                                          5,
                                          dtype=int)
        assert len(vpg.faces_per_cell_cl) == vpg.cell_count
        # set cell top triangle indices
        for layer in range(layer_count):
            # top triangular faces of cells
            vpg.faces_per_cell[5 * layer * column_count : (layer + 1) * 5 * column_count : 5] =  \
               layer * column_count + np.arange(column_count)
            # base triangular faces of cells
            vpg.faces_per_cell[layer * 5 * column_count + 1: (layer + 1) * 5 * column_count : 5] =  \
               (layer + 1) * column_count + np.arange(column_count)
        # todo: some clever numpy indexing to irradicate the following for loop
        for col in range(column_count):
            t_nodes = top_triangles[col]
            for t_edge in range(3):
                a, b = t_nodes[t_edge - 1], t_nodes[t_edge]
                if b < a:
                    a, b = b, a
                edge = find_pair(
                    column_edges,
                    (a, b))  # returns index into first axis of column edges
                # set quadrilateral faces of cells in column, for this edge
                vpg.faces_per_cell[5 * col + t_edge + 2 : 5 * vpg.cell_count : 5 * column_count] =  \
                   np.arange(column_count * surface_count + edge, vpg.face_count, column_edge_count)
        # check full population of faces_per_cell (face zero is a top triangle, only used once)
        assert np.count_nonzero(
            vpg.faces_per_cell) == vpg.faces_per_cell.size - 1

        vpg.cell_face_is_right_handed = np.ones(len(vpg.faces_per_cell),
                                                dtype=bool)
        if set_handedness:
            # TODO: set handedness correctly and make default for set_handedness True
            raise NotImplementedError(
                'code not written to set handedness for vertical prism grid from surfaces'
            )

        # instersect gravity vectors from column points with other surfaces, and update z values in points
        gravity = np.zeros((p_count, 3))
        gravity[:,
                2] = 1.0  # up/down does not matter for the intersection function used below
        start = 1 if top_triangles is None else 0
        for surf in range(start, surface_count):
            surf_triangles, surf_points = surfaces[surf].triangles_and_points()
            intersects = meet.line_set_triangles_intersects(
                top_points, gravity, surf_points[surf_triangles])
            single_intersects = meet.last_intersects(
                intersects)  # will be triple NaN where no intersection occurs
            # inherit point from surface above where no intersection has occurred
            nan_lines = np.isnan(single_intersects[:, 0])
            if surf == 0:
                # allow NaN entries to handle unused distant circumcentres in Voronoi graph data
                # assert not np.any(nan_lines), 'top surface does not cover all column points'
                single_intersects[nan_lines] = np.NaN
            else:
                single_intersects[nan_lines] = points[surf - 1][nan_lines]
            # populate z values for layer of points
            points[surf, :, 2] = single_intersects[:, 2]

        vpg.points_cached = points.reshape((-1, 3))
        assert np.all(vpg.nodes_per_face < len(vpg.points_cached))

        return vpg