Example #1
0
def test_grid_from_cp_simple_inactive_nogeom(example_model_and_crs):
    # Arrange
    model, crs = example_model_and_crs
    corns = simple_grid_corns(undefined=True)
    expected_bool = np.ones((2, 2, 2))
    expected_bool[0, 0, 0] = 0
    active_mask = np.ones((2, 2, 2))
    active_mask[1, 1, 1] = 0
    expected_active = active_mask.copy()
    expected_active[0, 0, 0] = 0
    expected_inactive = ~expected_active.astype(bool)

    # Act
    grid = rqi.grid_from_cp(model,
                            cp_array=corns,
                            crs_uuid=crs.uuid,
                            ijk_handedness=None,
                            geometry_defined_everywhere=False,
                            active_mask=active_mask)

    # Assert
    assert grid is not None
    assert_array_almost_equal(grid.extent_kji, (2, 2, 2))
    assert grid.grid_is_right_handed
    assert grid.k_gaps is None
    assert grid.k_direction_is_down
    assert grid.pillar_shape == 'curved'
    assert grid.crs_uuid == crs.uuid
    assert not grid.geometry_defined_for_all_cells_cached
    assert_array_almost_equal(grid.array_cell_geometry_is_defined,
                              expected_active)
    assert_array_almost_equal(grid.inactive, expected_inactive)
Example #2
0
def test_grid_from_cp_simple(example_model_and_crs):
    # Arrange
    model, crs = example_model_and_crs
    corns = simple_grid_corns()
    expected_points = np.array([[[[0, 2, 0], [1, 2, 0], [2, 2, 0]],
                                 [[0, 1, 0], [1, 1, 0], [2, 1, 0]],
                                 [[0, 0, 0], [1, 0, 0], [2, 0, 0]]],
                                [[[0, 2, 1], [1, 2, 1], [2, 2, 1]],
                                 [[0, 1, 1], [1, 1, 1], [2, 1, 1]],
                                 [[0, 0, 1], [1, 0, 1], [2, 0, 1]]],
                                [[[0, 2, 2], [1, 2, 2], [2, 2, 2]],
                                 [[0, 1, 2], [1, 1, 2], [2, 1, 2]],
                                 [[0, 0, 2], [1, 0, 2], [2, 0, 2]]]])

    # Act
    grid = rqi.grid_from_cp(model,
                            cp_array=corns,
                            crs_uuid=crs.uuid,
                            ijk_handedness=None)

    # Assert
    assert grid is not None
    assert_array_almost_equal(grid.extent_kji, (2, 2, 2))
    assert grid.grid_is_right_handed
    assert grid.k_gaps is None
    assert grid.k_direction_is_down
    assert grid.pillar_shape == 'curved'
    assert grid.crs_uuid == crs.uuid
    assert grid.geometry_defined_for_all_cells_cached
    assert_array_almost_equal(grid.points_cached, expected_points)
Example #3
0
def test_grid_from_cp_kgap(example_model_and_crs):
    # Arrange
    model, crs = example_model_and_crs
    corns = simple_grid_corns(k_gap=True)

    # Act
    grid = rqi.grid_from_cp(model, cp_array=corns, crs_uuid=crs.uuid)

    # Assert
    assert grid is not None
    assert_array_almost_equal(grid.extent_kji, (2, 2, 2))
    assert grid.grid_is_right_handed
    assert grid.k_gaps == 1
    assert grid.k_direction_is_down
Example #4
0
def _refined_faulted_grid(model, source_grid, fine_coarse):

    source_grid.corner_points(cache_cp_array=True)
    fnk, fnj, fni = fine_coarse.fine_extent_kji
    fine_cp = np.empty((fnk, fnj, fni, 2, 2, 2, 3))
    for ck0 in range(source_grid.nk):
        fine_k_base = fine_coarse.fine_base_for_coarse_axial(0, ck0)
        k_ratio = fine_coarse.ratio(0, ck0)
        k_interp = np.ones((k_ratio + 1, ))
        k_interp[:-1] = fine_coarse.interpolation(0, ck0)
        for cj0 in range(source_grid.nj):
            fine_j_base = fine_coarse.fine_base_for_coarse_axial(1, cj0)
            j_ratio = fine_coarse.ratio(1, cj0)
            j_interp = np.ones((j_ratio + 1, ))
            j_interp[:-1] = fine_coarse.interpolation(1, cj0)
            for ci0 in range(source_grid.ni):
                fine_i_base = fine_coarse.fine_base_for_coarse_axial(2, ci0)
                i_ratio = fine_coarse.ratio(2, ci0)
                i_interpolation = fine_coarse.interpolation(2, ci0)
                i_interp = np.ones((i_ratio + 1, ))
                i_interp[:-1] = fine_coarse.interpolation(2, ci0)

                shared_fine_points = source_grid.interpolated_points(
                    (ck0, cj0, ci0), (k_interp, j_interp, i_interp))

                fine_cp[fine_k_base : fine_k_base + k_ratio, fine_j_base : fine_j_base + j_ratio, fine_i_base : fine_i_base + i_ratio, 0, 0, 0] =  \
                   shared_fine_points[:-1, :-1, :-1]
                fine_cp[fine_k_base : fine_k_base + k_ratio, fine_j_base : fine_j_base + j_ratio, fine_i_base : fine_i_base + i_ratio, 0, 0, 1] =  \
                   shared_fine_points[:-1, :-1, 1:]
                fine_cp[fine_k_base : fine_k_base + k_ratio, fine_j_base : fine_j_base + j_ratio, fine_i_base : fine_i_base + i_ratio, 0, 1, 0] =  \
                   shared_fine_points[:-1, 1:, :-1]
                fine_cp[fine_k_base : fine_k_base + k_ratio, fine_j_base : fine_j_base + j_ratio, fine_i_base : fine_i_base + i_ratio, 0, 1, 1] =  \
                   shared_fine_points[:-1, 1:, 1:]
                fine_cp[fine_k_base : fine_k_base + k_ratio, fine_j_base : fine_j_base + j_ratio, fine_i_base : fine_i_base + i_ratio, 1, 0, 0] =  \
                   shared_fine_points[1:, :-1, :-1]
                fine_cp[fine_k_base : fine_k_base + k_ratio, fine_j_base : fine_j_base + j_ratio, fine_i_base : fine_i_base + i_ratio, 1, 0, 1] =  \
                   shared_fine_points[1:, :-1, 1:]
                fine_cp[fine_k_base : fine_k_base + k_ratio, fine_j_base : fine_j_base + j_ratio, fine_i_base : fine_i_base + i_ratio, 1, 1, 0] =  \
                   shared_fine_points[1:, 1:, :-1]
                fine_cp[fine_k_base : fine_k_base + k_ratio, fine_j_base : fine_j_base + j_ratio, fine_i_base : fine_i_base + i_ratio, 1, 1, 1] =  \
                   shared_fine_points[1:, 1:, 1:]

    return rqi.grid_from_cp(
        model,
        fine_cp,
        source_grid.crs_uuid,
        ijk_handedness='right' if source_grid.grid_is_right_handed else 'left')
Example #5
0
def test_grid_from_cp_simple_straight(example_model_and_crs):
    # Arrange
    model, crs = example_model_and_crs
    corns = simple_grid_corns()

    # Act
    grid = rqi.grid_from_cp(model,
                            cp_array=corns,
                            crs_uuid=crs.uuid,
                            known_to_be_straight=True)

    # Assert
    assert grid is not None
    assert_array_almost_equal(grid.extent_kji, (2, 2, 2))
    assert grid.grid_is_right_handed
    assert grid.k_gaps is None
    assert grid.k_direction_is_down
    assert grid.pillar_shape == 'straight'
Example #6
0
def test_grid_from_cp_simple_left(example_model_and_crs):
    # Arrange
    model, crs = example_model_and_crs
    corns = simple_grid_corns(righthanded=False)

    # Act
    grid = rqi.grid_from_cp(model,
                            cp_array=corns,
                            crs_uuid=crs.uuid,
                            ijk_handedness=None)

    # Assert
    assert grid is not None
    assert_array_almost_equal(grid.extent_kji, (2, 2, 2))
    assert not grid.grid_is_right_handed
    assert grid.k_gaps is None
    assert grid.k_direction_is_down
    assert grid.pillar_shape == 'curved'
    assert grid.crs_uuid == crs.uuid
Example #7
0
def test_grid_from_cp_kgap_zvoid(example_model_and_crs):
    # Arrange
    model, crs = example_model_and_crs
    corns = simple_grid_corns()

    # Add a k-gap
    corns[1, :, :, :, :, :, 2] += 0.5

    # Act
    grid = rqi.grid_from_cp(model,
                            cp_array=corns,
                            crs_uuid=crs.uuid,
                            max_z_void=1)

    # Assert
    assert grid is not None
    assert_array_almost_equal(grid.extent_kji, (2, 2, 2))
    assert grid.grid_is_right_handed
    assert grid.k_gaps is None
    assert grid.k_direction_is_down
Example #8
0
def s_bend_k_gap_grid(s_bend_model):
    nk = 5
    nj = 12
    ni_tail = 5
    ni_bend = 18
    ni_half_mid = 2
    ni = 2 * (ni_tail + ni_bend + ni_half_mid)

    total_thickness = 12.0
    layer_thickness = total_thickness / float(nk)
    flat_dx_di = 10.0
    horst_dx_dk = 0.25 * flat_dx_di / float(nk)
    horst_dz = 1.73 * layer_thickness
    horst_half_dx = horst_dx_dk * horst_dz / layer_thickness
    dy_dj = 8.0
    top_depth = 100.0

    bend_theta_di = maths.pi / float(ni_bend)
    outer_radius = 2.0 * total_thickness

    bend_a_centre_xz = (flat_dx_di * float(ni_tail), top_depth + outer_radius)
    bend_b_centre_xz = (flat_dx_di * float(ni_tail - 2.0 * ni_half_mid),
                        top_depth + 3.0 * outer_radius - total_thickness)

    points = np.empty((nk + 1, nj + 1, ni + 1, 3))

    for k in range(nk + 1):
        if k == nk // 2 + 1:
            points[k] = points[k - 1]  # pinched out layer
        else:
            for i in range(ni + 1):
                if i < ni_tail + 1:
                    x = flat_dx_di * float(i)
                    z = top_depth + float(
                        k
                    ) * layer_thickness  # will introduce a thick layer after pinchout
                elif i < ni_tail + ni_bend:
                    theta = (i - ni_tail) * bend_theta_di
                    radius = outer_radius - float(k) * layer_thickness
                    x = bend_a_centre_xz[0] + radius * maths.sin(theta)
                    z = bend_a_centre_xz[1] - radius * maths.cos(theta)
                elif i < ni_tail + ni_bend + 2 * ni_half_mid + 1:
                    x = flat_dx_di * float(ni_tail - (i - (ni_tail + ni_bend)))
                    z = top_depth + 2.0 * outer_radius - float(
                        k) * layer_thickness
                elif i < ni_tail + 2 * ni_bend + 2 * ni_half_mid:
                    theta = (
                        i -
                        (ni_tail + ni_bend + 2 * ni_half_mid)) * bend_theta_di
                    radius = outer_radius - float(nk - k) * layer_thickness
                    x = bend_b_centre_xz[0] - radius * maths.sin(theta)
                    z = bend_b_centre_xz[1] - radius * maths.cos(theta)
                else:
                    x = flat_dx_di * float((i - (ni - ni_tail)) + ni_tail -
                                           2 * ni_half_mid)
                    if i == ni - 1 or i == ni - 4:
                        x += horst_dx_dk * float(k)
                    elif i == ni - 2 or i == ni - 3:
                        x -= horst_dx_dk * float(k)
                    z = top_depth + 4.0 * outer_radius + float(
                        k) * layer_thickness - 2.0 * total_thickness
                points[k, :, i] = (x, 0.0, z)

    for j in range(nj + 1):
        points[:, j, :, 1] = dy_dj * float(j)

    grid = grr.Grid(s_bend_model)

    crs = rqc.Crs(s_bend_model)
    crs_node = crs.create_xml()

    grid.grid_representation = 'IjkGrid'
    grid.extent_kji = np.array((nk, nj, ni), dtype='int')
    grid.nk, grid.nj, grid.ni = nk, nj, ni
    grid.k_direction_is_down = True  # dominant layer direction, or paleo-direction
    grid.pillar_shape = 'straight'
    grid.has_split_coordinate_lines = False
    grid.k_gaps = None
    grid.crs_uuid = crs.uuid
    grid.crs_root = crs_node

    grid.points_cached = points

    grid.geometry_defined_for_all_pillars_cached = True
    grid.geometry_defined_for_all_cells_cached = True
    grid.grid_is_right_handed = crs.is_right_handed_xyz()
    cp = grid.corner_points(cache_cp_array=True).copy()

    bend_theta_di = maths.pi / float(ni_bend)

    # IK plane faults
    cp[:, 3:, :, :, :, :, :] += (flat_dx_di * 0.7, 0.0, layer_thickness * 1.3)
    cp[:, 5:, :, :, :, :, :] += (flat_dx_di * 0.4, 0.0, layer_thickness * 0.9)
    cp[:, 8:, :, :, :, :, :] += (flat_dx_di * 0.3, 0.0, layer_thickness * 0.6)

    # JK plane faults
    cp[:, :, ni_tail + ni_bend // 2:, :, :, :,
       0] -= flat_dx_di * 0.57  # horizontal break mid top bend
    cp[:, :, ni_tail + ni_bend + ni_half_mid:, :, :, :,
       2] += layer_thickness * 1.27  # vertical break in mid section

    # zig-zag fault
    j_step = nj // (ni_tail - 2)
    for i in range(ni_tail - 1):
        j_start = i * j_step
        if j_start >= nj:
            break
        cp[:, j_start:, i, :, :, :, 2] += 1.1 * total_thickness

    # JK horst blocks
    cp[:, :, ni - 4, :, :, :, :] -= (horst_half_dx, 0.0, horst_dz)
    cp[:, :, ni - 3:, :, :, :, 0] -= 2.0 * horst_half_dx
    cp[:, :, ni - 2, :, :, :, :] += (-horst_half_dx, 0.0, horst_dz)
    cp[:, :, ni - 1:, :, :, :, 0] -= 2.0 * horst_half_dx

    # JK horst block mid lower bend
    bend_horst_dz = horst_dz * maths.tan(bend_theta_di)
    cp[:, :, ni - (ni_tail + ni_bend // 2 + 1):ni -
       (ni_tail + ni_bend // 2 - 1), :, :, :, :] -= (horst_dz, 0.0,
                                                     bend_horst_dz)
    cp[:, :, ni - (ni_tail + ni_bend // 2 - 1):, :, :, :,
       2] -= 2.0 * bend_horst_dz

    k_gap_grid = rqi.grid_from_cp(
        s_bend_model,
        cp,
        crs.uuid,
        max_z_void=0.01,
        split_pillars=True,
        split_tolerance=0.01,
        ijk_handedness='right' if grid.grid_is_right_handed else 'left',
        known_to_be_straight=True)

    # convert second layer to a K gap
    k_gap_grid.nk -= 1
    k_gap_grid.extent_kji[0] = k_gap_grid.nk
    k_gap_grid.k_gaps = 1
    k_gap_grid.k_gap_after_array = np.zeros(k_gap_grid.nk - 1, dtype=bool)
    k_gap_grid.k_gap_after_array[0] = True
    k_gap_grid.k_raw_index_array = np.zeros(k_gap_grid.nk, dtype=int)
    for k in range(1, k_gap_grid.nk):
        k_gap_grid.k_raw_index_array[k] = k + 1

    # clear some attributes which may no longer be valid
    k_gap_grid.pinchout = None
    k_gap_grid.inactive = None
    k_gap_grid.grid_skin = None
    if hasattr(k_gap_grid, 'array_thickness'):
        delattr(k_gap_grid, 'array_thickness')

    # k_gap_grid.write_hdf5_from_caches()
    # k_gap_grid.create_xml()
    return k_gap_grid
Example #9
0
def test_s_bend_fn(tmp_path, epc=None):
    if epc is None:
        # use pytest temporary directory fixture
        # https://docs.pytest.org/en/stable/tmpdir.html
        epc = str(os.path.join(tmp_path, f"{bu.new_uuid()}.epc"))

    # create s-bend grid

    nk = 5
    nj = 12
    ni_tail = 5
    ni_bend = 18
    ni_half_mid = 2
    ni = 2 * (ni_tail + ni_bend + ni_half_mid)

    total_thickness = 12.0
    layer_thickness = total_thickness / float(nk)
    flat_dx_di = 10.0
    horst_dx_dk = 0.25 * flat_dx_di / float(nk)
    horst_dz = 1.73 * layer_thickness
    horst_half_dx = horst_dx_dk * horst_dz / layer_thickness
    dy_dj = 8.0
    top_depth = 100.0

    assert ni_bend % 2 == 0, 'ni_bend must be even for horizontal faulting'
    assert ni_tail >= 5, 'ni_tail must be at least 5 for horst blocks'

    bend_theta_di = maths.pi / float(ni_bend)
    outer_radius = 2.0 * total_thickness

    bend_a_centre_xz = (flat_dx_di * float(ni_tail), top_depth + outer_radius)
    bend_b_centre_xz = (flat_dx_di * float(ni_tail - 2.0 * ni_half_mid),
                        top_depth + 3.0 * outer_radius - total_thickness)

    points = np.empty((nk + 1, nj + 1, ni + 1, 3))

    for k in range(nk + 1):
        if k == nk // 2 + 1:
            points[k] = points[k - 1]  # pinched out layer
        else:
            for i in range(ni + 1):
                if i < ni_tail + 1:
                    x = flat_dx_di * float(i)
                    z = top_depth + float(
                        k
                    ) * layer_thickness  # will introduce a thick layer after pinchout
                elif i < ni_tail + ni_bend:
                    theta = (i - ni_tail) * bend_theta_di
                    radius = outer_radius - float(k) * layer_thickness
                    x = bend_a_centre_xz[0] + radius * maths.sin(theta)
                    z = bend_a_centre_xz[1] - radius * maths.cos(theta)
                elif i < ni_tail + ni_bend + 2 * ni_half_mid + 1:
                    x = flat_dx_di * float(ni_tail - (i - (ni_tail + ni_bend)))
                    z = top_depth + 2.0 * outer_radius - float(
                        k) * layer_thickness
                elif i < ni_tail + 2 * ni_bend + 2 * ni_half_mid:
                    theta = (
                        i -
                        (ni_tail + ni_bend + 2 * ni_half_mid)) * bend_theta_di
                    radius = outer_radius - float(nk - k) * layer_thickness
                    x = bend_b_centre_xz[0] - radius * maths.sin(theta)
                    z = bend_b_centre_xz[1] - radius * maths.cos(theta)
                else:
                    x = flat_dx_di * float((i - (ni - ni_tail)) + ni_tail -
                                           2 * ni_half_mid)
                    if i == ni - 1 or i == ni - 4:
                        x += horst_dx_dk * float(k)
                    elif i == ni - 2 or i == ni - 3:
                        x -= horst_dx_dk * float(k)
                    z = top_depth + 4.0 * outer_radius + float(
                        k) * layer_thickness - 2.0 * total_thickness
                points[k, :, i] = (x, 0.0, z)

    for j in range(nj + 1):
        points[:, j, :, 1] = dy_dj * float(j)

    model = rq.Model(epc_file=epc,
                     new_epc=True,
                     create_basics=True,
                     create_hdf5_ext=True)

    grid = grr.Grid(model)

    crs = rqc.Crs(model)
    crs_node = crs.create_xml()
    if model.crs_uuid is None:
        model.crs_uuid = crs.crs_uuid

    grid.grid_representation = 'IjkGrid'
    grid.extent_kji = np.array((nk, nj, ni), dtype='int')
    grid.nk, grid.nj, grid.ni = nk, nj, ni
    grid.k_direction_is_down = True  # dominant layer direction, or paleo-direction
    grid.pillar_shape = 'straight'
    grid.has_split_coordinate_lines = False
    grid.k_gaps = None
    grid.crs_uuid = crs.uuid
    grid.crs = crs

    grid.points_cached = points

    grid.geometry_defined_for_all_pillars_cached = True
    grid.geometry_defined_for_all_cells_cached = True
    grid.grid_is_right_handed = crs.is_right_handed_xyz()

    grid.write_hdf5_from_caches()
    grid.create_xml()

    # create a well trajectory and md datum

    def df_trajectory(x, y, z):
        N = len(x)
        assert len(y) == N and len(z) == N
        df = pd.DataFrame(columns=['MD', 'X', 'Y', 'Z'])
        md = np.zeros(N)
        for n in range(N - 1):
            md[n + 1] = md[n] + vec.naive_length(
                (x[n + 1] - x[n], y[n + 1] - y[n], z[n + 1] - z[n]))
        df.MD = md
        df.X = x
        df.Y = y
        df.Z = z
        return df

    x = np.array([
        0.0, flat_dx_di * float(ni_tail) + outer_radius,
        flat_dx_di * (float(ni_tail) - 0.5), 0.0, -outer_radius
    ])
    y = np.array([
        0.0, dy_dj * 0.5, dy_dj * float(nj) / 2.0, dy_dj * (float(nj) - 0.5),
        dy_dj * float(nj)
    ])
    z = np.array([
        0.0, top_depth - total_thickness,
        top_depth + 2.0 * outer_radius - total_thickness / 2.0,
        top_depth + 3.0 * outer_radius - total_thickness,
        top_depth + 4.0 * outer_radius
    ])

    df = df_trajectory(x, y, z)

    datum = rqw.MdDatum(model, crs_uuid=crs.uuid, location=(x[0], y[0], z[0]))
    datum.create_xml()

    trajectory = rqw.Trajectory(model,
                                md_datum=datum,
                                data_frame=df,
                                length_uom='m',
                                well_name='ANGLED_WELL')

    assert bu.matching_uuids(trajectory.crs_uuid, crs.uuid)

    trajectory.write_hdf5()
    trajectory.create_xml()

    # add more wells

    x = np.array([
        0.0, flat_dx_di * float(ni_tail),
        flat_dx_di * 2.0 * float(ni_tail - ni_half_mid) + outer_radius,
        -outer_radius
    ])
    y = np.array([0.0, dy_dj * float(nj) * 0.59, dy_dj * 0.67, dy_dj * 0.5])
    z = np.array([
        0.0, top_depth - total_thickness,
        top_depth + 4.0 * outer_radius - 1.7 * total_thickness,
        top_depth + 4.0 * outer_radius - 1.7 * total_thickness
    ])

    df = df_trajectory(x, y, z)

    traj_2 = rqw.Trajectory(model,
                            md_datum=datum,
                            data_frame=df,
                            length_uom='m',
                            well_name='HORST_WELL')
    traj_2.write_hdf5()
    traj_2.create_xml()
    traj_2.control_points

    x = np.array([0.0, 0.0, 0.0])
    y = np.array([0.0, dy_dj * float(nj) * 0.53, dy_dj * float(nj) * 0.53])
    z = np.array(
        [0.0, top_depth - total_thickness, top_depth + 4.0 * outer_radius])

    df = df_trajectory(x, y, z)

    traj_3 = rqw.Trajectory(model,
                            md_datum=datum,
                            data_frame=df,
                            length_uom='m',
                            well_name='VERTICAL_WELL')
    traj_3.write_hdf5()
    traj_3.create_xml()
    traj_3.control_points

    n_x = flat_dx_di * float(ni_tail) * 0.48
    n_y = dy_dj * float(nj) / 9.1
    o_y = -dy_dj * 0.45
    nd_x = n_y / 3.0
    x = np.array([
        0.0, n_x, n_x, n_x + nd_x, n_x + 2.0 * nd_x, n_x + 3.0 * nd_x,
        n_x + 4.0 * nd_x, n_x + 5.0 * nd_x, n_x + 6.0 * nd_x, n_x + 7.0 * nd_x,
        n_x + 8.0 * nd_x, n_x + 8.0 * nd_x
    ])
    y = np.array([
        0.0, o_y, o_y + n_y, o_y + 2.0 * n_y, o_y + 3.0 * n_y, o_y + 4.0 * n_y,
        o_y + 5.0 * n_y, o_y + 6.0 * n_y, o_y + 7.0 * n_y, o_y + 8.0 * n_y,
        o_y + 9.0 * n_y, o_y + 10.0 * n_y
    ])
    n_z1 = top_depth + total_thickness * 0.82
    n_z2 = top_depth - total_thickness * 0.17
    z = np.array([
        0.0, n_z1, n_z1, n_z2, n_z2, n_z1, n_z1, n_z2, n_z2, n_z1, n_z1, n_z2
    ])

    df = df_trajectory(x, y, z)

    traj_4 = rqw.Trajectory(model,
                            md_datum=datum,
                            data_frame=df,
                            length_uom='m',
                            well_name='NESSIE_WELL')
    traj_4.write_hdf5()
    traj_4.create_xml()
    traj_4.control_points

    # block wells against grid geometry

    log.info('unfaulted grid blocking of well ' +
             str(rqw.well_name(trajectory)))
    bw = rqw.BlockedWell(model, grid=grid, trajectory=trajectory)
    bw.write_hdf5()
    bw.create_xml()
    assert bw.cell_count == 19
    assert len(bw.cell_indices) == bw.cell_count
    np.testing.assert_array_equal(
        bw.cell_indices,
        np.array([
            108, 708, 709, 1909, 1959, 2559, 2673, 2073, 2123, 923, 924, 974,
            374, 587, 588, 1188, 2388, 2389, 2989
        ]))

    log.info('unfaulted grid blocking of well ' + str(rqw.well_name(traj_2)))
    bw_2 = rqw.BlockedWell(model, grid=grid, trajectory=traj_2)
    bw_2.write_hdf5()
    bw_2.create_xml()
    assert bw_2.cell_count == 33
    assert len(bw_2.cell_indices) == bw_2.cell_count
    np.testing.assert_array_equal(
        bw_2.cell_indices,
        np.array([
            306, 256, 856, 857, 2057, 2058, 2059, 2659, 2660, 2610, 2611, 2612,
            2613, 2614, 2014, 2015, 2016, 1966, 766, 767, 167, 649, 648, 647,
            646, 645, 644, 643, 642, 1842, 1841, 1840, 2440
        ]))

    log.info('unfaulted grid blocking of well ' + str(rqw.well_name(traj_3)))
    bw_3 = rqw.BlockedWell(model, grid=grid, trajectory=traj_3)
    bw_3.write_hdf5()
    bw_3.create_xml()
    assert bw_3.cell_count == 18
    assert len(bw_3.cell_indices) == bw_3.cell_count
    np.testing.assert_array_equal(
        bw_3.cell_indices,
        np.array([
            300, 900, 2100, 2700, 2729, 2129, 2130, 930, 931, 331, 332, 339,
            340, 940, 941, 2141, 2142, 2742
        ]))

    log.info('unfaulted grid blocking of well ' + str(rqw.well_name(traj_4)))
    bw_4 = rqw.BlockedWell(model, grid=grid, trajectory=traj_4)
    bw_4.write_hdf5()
    bw_4.create_xml()
    assert bw_4.cell_count == 26
    assert len(bw_4.cell_indices) == bw_4.cell_count
    np.testing.assert_array_equal(
        bw_4.cell_indices,
        np.array([
            2402, 1802, 1852, 652, 52, 153, 753, 803, 2003, 2603, 2653, 2703,
            2103, 903, 904, 304, 354, 454, 1054, 2254, 2304, 2904, 2905, 2955,
            2355, 1155
        ]))

    # derive a faulted version of the grid

    cp = grid.corner_points(cache_cp_array=True).copy()

    # IK plane faults
    cp[:, 3:, :, :, :, :, :] += (flat_dx_di * 0.7, 0.0, layer_thickness * 1.3)
    cp[:, 5:, :, :, :, :, :] += (flat_dx_di * 0.4, 0.0, layer_thickness * 0.9)
    cp[:, 8:, :, :, :, :, :] += (flat_dx_di * 0.3, 0.0, layer_thickness * 0.6)

    # JK plane faults
    cp[:, :, ni_tail + ni_bend // 2:, :, :, :,
       0] -= flat_dx_di * 0.57  # horizontal break mid top bend
    cp[:, :, ni_tail + ni_bend + ni_half_mid:, :, :, :,
       2] += layer_thickness * 1.27  # vertical break in mid section

    # zig-zag fault
    j_step = nj // (ni_tail - 2)
    for i in range(ni_tail - 1):
        j_start = i * j_step
        if j_start >= nj:
            break
        cp[:, j_start:, i, :, :, :, 2] += 1.1 * total_thickness

    # JK horst blocks
    cp[:, :, ni - 4, :, :, :, :] -= (horst_half_dx, 0.0, horst_dz)
    cp[:, :, ni - 3:, :, :, :, 0] -= 2.0 * horst_half_dx
    cp[:, :, ni - 2, :, :, :, :] += (-horst_half_dx, 0.0, horst_dz)
    cp[:, :, ni - 1:, :, :, :, 0] -= 2.0 * horst_half_dx

    # JK horst block mid lower bend
    bend_horst_dz = horst_dz * maths.tan(bend_theta_di)
    cp[:, :, ni - (ni_tail + ni_bend // 2 + 1):ni -
       (ni_tail + ni_bend // 2 - 1), :, :, :, :] -= (horst_dz, 0.0,
                                                     bend_horst_dz)
    cp[:, :, ni - (ni_tail + ni_bend // 2 - 1):, :, :, :,
       2] -= 2.0 * bend_horst_dz

    faulted_grid = rqi.grid_from_cp(
        model,
        cp,
        crs.uuid,
        max_z_void=0.01,
        split_pillars=True,
        split_tolerance=0.01,
        ijk_handedness='right' if grid.grid_is_right_handed else 'left',
        known_to_be_straight=True)

    faulted_grid.write_hdf5_from_caches()
    faulted_grid.create_xml()

    # create some grid connection sets
    f_gcs, _ = faulted_grid.fault_connection_set(add_to_model=True)
    assert f_gcs is not None and f_gcs.count > 0
    p_gcs, _ = faulted_grid.pinchout_connection_set(add_to_model=True)
    assert p_gcs is not None and p_gcs.count > 0

    # block wells against faulted grid

    log.info('faulted grid blocking of well ' + str(rqw.well_name(trajectory)))
    fbw = rqw.BlockedWell(model, grid=faulted_grid, trajectory=trajectory)
    fbw.write_hdf5()
    fbw.create_xml()
    assert fbw.cell_count == 32
    assert len(fbw.cell_indices) == fbw.cell_count
    np.testing.assert_array_equal(
        fbw.cell_indices,
        np.array([
            108, 708, 709, 1909, 1956, 2556, 2673, 2674, 2724, 2124, 2174,
            2775, 2175, 2225, 2226, 2276, 2277, 1077, 1078, 1079, 1129, 1130,
            1131, 2331, 2332, 2382, 2383, 2384, 2984, 2385, 2386, 2986
        ]))

    log.info('faulted grid blocking of well ' + str(rqw.well_name(traj_2)))
    fbw_2 = rqw.BlockedWell(model, grid=faulted_grid, trajectory=traj_2)
    fbw_2.write_hdf5()
    fbw_2.create_xml()
    assert fbw_2.cell_count == 26
    assert len(fbw_2.cell_indices) == fbw_2.cell_count
    np.testing.assert_array_equal(
        fbw_2.cell_indices,
        np.array([
            254, 854, 2054, 2654, 2655, 2614, 2014, 2015, 1965, 1966, 766, 767,
            167, 49, 47, 1846, 45, 44, 43, 643, 642, 641, 1841, 1840, 2440,
            2439
        ]))

    log.info('faulted grid blocking of well ' + str(rqw.well_name(traj_3)))
    fbw_3 = rqw.BlockedWell(model, grid=faulted_grid, trajectory=traj_3)
    fbw_3.write_hdf5()
    fbw_3.create_xml()
    assert fbw_3.cell_count == 14
    assert len(fbw_3.cell_indices) == fbw_3.cell_count
    np.testing.assert_array_equal(
        fbw_3.cell_indices,
        np.array([
            2730, 2731, 2131, 2132, 2133, 933, 934, 937, 938, 2138, 2139, 2140,
            2740, 2741
        ]))

    log.info('faulted grid blocking of well ' + str(rqw.well_name(traj_4)))
    fbw_4 = rqw.BlockedWell(model, grid=faulted_grid, trajectory=traj_4)
    fbw_4.write_hdf5()
    fbw_4.create_xml()
    assert fbw_4.cell_count == 16
    assert len(fbw_4.cell_indices) == fbw_4.cell_count
    np.testing.assert_array_equal(
        fbw_4.cell_indices,
        np.array([
            2402, 1802, 1852, 652, 52, 202, 802, 2002, 852, 902, 302, 453, 503,
            1103, 1153, 553
        ]))

    # create a version of the faulted grid with a k gap

    k_gap_grid = rqi.grid_from_cp(
        model,
        cp,
        crs.uuid,
        max_z_void=0.01,
        split_pillars=True,
        split_tolerance=0.01,
        ijk_handedness='right' if grid.grid_is_right_handed else 'left',
        known_to_be_straight=True)

    # convert second layer to a K gap
    k_gap_grid.nk -= 1
    k_gap_grid.extent_kji[0] = k_gap_grid.nk
    k_gap_grid.k_gaps = 1
    k_gap_grid.k_gap_after_array = np.zeros(k_gap_grid.nk - 1, dtype=bool)
    k_gap_grid.k_gap_after_array[0] = True
    k_gap_grid.k_raw_index_array = np.zeros(k_gap_grid.nk, dtype=int)
    for k in range(1, k_gap_grid.nk):
        k_gap_grid.k_raw_index_array[k] = k + 1

    # clear some attributes which may no longer be valid
    k_gap_grid.pinchout = None
    k_gap_grid.inactive = None
    k_gap_grid.grid_skin = None
    if hasattr(k_gap_grid, 'array_thickness'):
        delattr(k_gap_grid, 'array_thickness')

    k_gap_grid.write_hdf5_from_caches()
    k_gap_grid.create_xml()

    k_gap_grid_uuid = k_gap_grid.uuid

    # reload k gap grid object to ensure it is properly initialised

    k_gap_grid = None
    k_gap_grid = grr.Grid(model, uuid=k_gap_grid_uuid)

    # block wells against faulted grid with k gap

    log.info('k gap grid blocking of well ' + str(rqw.well_name(trajectory)))
    try:
        gbw = rqw.BlockedWell(model, grid=k_gap_grid, trajectory=trajectory)
        gbw.write_hdf5()
        gbw.create_xml()
        assert gbw.cell_count == 24
    except Exception:
        log.exception('failed to block well against k gap grid')

    log.info('k gap grid blocking of well ' + str(rqw.well_name(traj_2)))
    try:
        gbw_2 = rqw.BlockedWell(model, grid=k_gap_grid, trajectory=traj_2)
        gbw_2.write_hdf5()
        gbw_2.create_xml()
        assert gbw_2.cell_count == 20
    except Exception:
        log.exception('failed to block well against k gap grid')

    log.info('k gap grid blocking of well ' + str(rqw.well_name(traj_3)))
    try:
        gbw_3 = rqw.BlockedWell(model, grid=k_gap_grid, trajectory=traj_3)
        gbw_3.write_hdf5()
        gbw_3.create_xml()
        assert gbw_3.cell_count == 10
    except Exception:
        log.exception('failed to block well against k gap grid')

    log.info('k gap grid blocking of well ' + str(rqw.well_name(traj_4)))
    try:
        gbw_4 = rqw.BlockedWell(model, grid=k_gap_grid, trajectory=traj_4)
        gbw_4.write_hdf5()
        gbw_4.create_xml()
        assert gbw_4.cell_count == 10
    except Exception:
        log.exception('failed to block well against k gap grid')

    # store model

    model.store_epc()

    assert k_gap_grid.k_gaps
    assert len(k_gap_grid.k_raw_index_array) == k_gap_grid.nk
    assert len(k_gap_grid.k_gap_after_array) == k_gap_grid.nk - 1
    assert k_gap_grid.pinched_out((1, 0, 2))

    # check gcs iterator
    gcs_list = list(model.iter_grid_connection_sets())
    assert len(gcs_list) == 2
    assert not bu.matching_uuids(gcs_list[0].uuid, gcs_list[1].uuid)

    # clean up
    model.h5_release()
    os.remove(model.h5_file_name())
Example #10
0
def import_nexus(
        resqml_file_root,  # output path and file name without .epc or .h5 extension
        extent_ijk = None,  # 3 element numpy vector
        vdb_file = None,  # vdb input file: either this or corp_file should be not None
        vdb_case = None,  # if None, first case in vdb is used (usually a vdb only holds one case)
        corp_file = None,  # corp ascii input file: nexus corp data without keyword
        corp_bin_file = None,  # corp binary file: nexus corp data in bespoke binary format
        corp_xy_units = 'm',
        corp_z_units = 'm',
        corp_z_inc_down = True,
        ijk_handedness = 'right',
        corp_eight_mode = False,
        geometry_defined_everywhere = True,
        treat_as_nan = None,
        active_mask_file = None,
        use_binary = False,  # this refers to pure binary arrays, not corp bin format
        resqml_xy_units = 'm',
        resqml_z_units = 'm',
        resqml_z_inc_down = True,
        shift_to_local = False,
        local_origin_place = 'centre',  # 'centre' or 'minimum'
        max_z_void = 0.1,  # vertical gaps greater than this will introduce k gaps intp resqml grid
        split_pillars = True,
        split_tolerance = 0.01,  # applies to each of x, y, z differences
        property_array_files = None,  # actually, list of (filename, keyword, uom, time_index, null_value, discrete)
        summary_file = None,  # used to extract timestep dates when loading recurrent data from vdb
        vdb_static_properties = True,
        # if True, static vdb properties are imported (only relevant if vdb_file is not None)
        vdb_recurrent_properties = False,
        timestep_selection = 'all',
        # 'first', 'last', 'first and last', 'all', or list of ints being reporting timestep numbers
        use_compressed_time_series = True,
        decoarsen = True,  # where ICOARSE is present, redistribute data to uncoarse cells
        ab_property_list = None,
        # list of (file_name, keyword, property_kind, facet_type, facet, uom, time_index, null_value, discrete)
        create_property_set = False,
        ensemble_case_dirs_root = None,  # path upto but excluding realisation number
        ensemble_property_dictionary = None,
        # dictionary mapping title (or keyword) to (filename, property_kind, facet_type, facet,
        #                                           uom, time_index, null_value, discrete)
        ensemble_size_limit = None,
        grid_title = 'ROOT',
        mode = 'w',
        progress_fn = None):
    """Read a simulation grid geometry and optionally grid properties.

    Input may be from nexus ascii input files, or nexus vdb output.

    Arguments:
        resqml_file_root (str): output path and file name without .epc or .h5 extension
        extent_ijk (triple float, optional): ijk extents (fortran ordering)
        vdb_file (str, optional): vdb input file, either this or corp_file should be not None. Required if importing from a vdb
        vdb_case (str, optional): required if the vdb contains more than one case. If None, first case in vdb is used
        corp_file (str, optional): required if importing from corp ascii file. corp ascii input file: nexus corp data without keyword
        corp_bin_file (str, optional): required if importing from corp binary file
        corp_xy_units (str, default 'm'): xy length units
        corp_z_units (str, default 'm'): z length units
        corp_z_inc_down (bool, default True): if True z values increase with depth
        ijk_handedness (str, default 'right'): 'right' or 'left'
        corp_eight_mode (bool, default False): if True the ordering of corner point data is in nexus EIGHT mode
        geometry_defined_everywhere (bool, default True): if False then inactive cells are marked as not having geometry
        treat_as_nan (float, default None): if a value is provided corner points with this value will be assigned nan
        active_mask_file (str, default None): ascii property file holding values 0 or 1, with 1 indicating active cells
        use_binary (bool, default False): if True a cached binary version of ascii files will be used (pure binary, not corp bin format)
        resqml_xy_units (str, default 'm'): output xy units for resqml file
        resqml_z_units (str, default 'm'): output z units for resqml file
        resqml_z_inc_down (bool, default True): if True z values increase with depth for output resqml file
        shift_to_local (bool, default False): if True then a local origin will be used in the CRS
        local_origin_place (str, default 'centre'): 'centre' or 'minimum'. If 'centre' the local origin is placed at the centre of the grid; ignored if shift_to_local is False
        max_z_void (float, default 0.1): maximum z gap between vertically neighbouring corner points. Vertical gaps greater than this will introduce k gaps into resqml grid. Units are corp z units
        split_pillars (bool, default True): if False an unfaulted grid will be generated
        split_tolerance (float, default 0.01): maximum distance between neighbouring corner points before a pillar is considered 'split'. Applies to each of x, y, z differences
        property_array_files (list, default None): list of (filename, keyword, uom, time_index, null_value, discrete)
        summary_file (str, default None): nexus output summary file, used to extract timestep dates when loading recurrent data from vdb
        vdb_static_properties (bool, default True): if True, static vdb properties are imported (only relevant if vdb_file is not None)
        vdb_recurrent_properties (bool, default False): # if True, recurrent vdb properties are imported (only relevant if vdb_file is not None)
        timestep_selection (str, default 'all): 'first', 'last', 'first and last', 'all', or list of ints being reporting timestep numbers. Ignored if vdb_recurrent_properties is False
        use_compressed_time_series (bool, default True): generates reduced time series containing timesteps with recurrent properties from vdb, rather than full nexus summary time series
        decoarsen (bool, default True): where ICOARSE is present, redistribute data to uncoarse cells
        ab_property_list (list, default None):  list of (file_name, keyword, property_kind, facet_type, facet, uom, time_index, null_value, discrete)
        create_property_set (bool, default False): if True a resqml PropertySet is created
        ensemble_case_dirs_root (str, default None): path up to but excluding realisation number
        ensemble_property_dictionary (str, default None): dictionary mapping title (or keyword) to (filename, property_kind, facet_type, facet, uom, time_index, null_value, discrete)
        ensemble_size_limit (int, default None): if present processing of ensemble will terminate after this number of cases is reached
        grid_title (str, default 'ROOT'): grid citation title
        mode (str, default 'w'): 'w' or 'a', mode to write or append to hdf5
        progress_fn (function, default None): if present function must have one floating argument with value increasing from 0 to 1, and is called at intervals to indicate progress

    Returns:
        resqml model in memory & written to disc
    """

    if resqml_file_root.endswith('.epc'):
        resqml_file_root = resqml_file_root[:-4]
    assert mode in ['w', 'a']

    if vdb_file:
        using_vdb = True
        corp_file = corp_bin_file = None
        grid_title = grid_title.upper()
        log.info('starting import of Nexus ' + str(grid_title) + ' corp from vdb ' + str(vdb_file))
        tm.log_nexus_tm('info')
        vdbase = vdb.VDB(vdb_file)
        case_list = vdbase.cases()
        assert len(case_list) > 0, 'no cases found in vdb'
        if vdb_case is None:
            vdb_case = case_list[0]
        else:
            assert vdb_case in case_list, 'case ' + vdb_case + ' not found in vdb: ' + vdb_file
            vdbase.set_use_case(vdb_case)
        assert grid_title in vdbase.list_of_grids(), 'grid ' + str(grid_title) + ' not found in vdb'
        if extent_ijk is not None:
            vdbase.set_extent_kji(tuple(reversed(extent_ijk)))
        log.debug('using case ' + vdb_case + ' and grid ' + grid_title + ' from vdb')
        if vdb_recurrent_properties and not summary_file:
            if vdb_file.endswith('.vdb.zip'):
                summary_file = vdb_file[:-8] + '.sum'
            elif vdb_file.endswith('.vdb') or vdb_file.endswith('.zip'):
                summary_file = vdb_file[:-4] + '.sum'
            else:
                sep = vdb_file.rfind(os.sep)
                dot = vdb_file[sep + 1:].find('.')
                if dot > 0:
                    summary_file = vdb_file[:sep + 1 + dot] + ',sum'
                else:
                    summary_file = vdb_file + '.sum'
        cp_array = vdbase.grid_corp(grid_title)
        cp_extent_kji = cp_array.shape[:3]
        if cp_extent_kji[:2] == (1, 1):  # auto determination of extent failed
            assert extent_ijk is not None, 'failed to determine extent of grid from corp data'
            (ni, nj, nk) = extent_ijk
            assert cp_extent_kji[2] == ni * nj * nk, 'number of cells in grid corp does not match extent'
            cp_extent = (nk, nj, ni, 2, 2, 2, 3)  # (nk, nj, ni, kp, jp, ip, xyz)
            cp_array = cp_array.reshape(cp_extent)
        elif extent_ijk is not None:
            for axis in range(3):
                assert cp_extent_kji[axis] == extent_ijk[
                    2 - axis], 'extent of grid corp data from vdb does not match that supplied'

    elif corp_file or corp_bin_file:
        if corp_bin_file:
            corp_file = None
        using_vdb = False
        #     geometry_defined_everywhere = (active_mask_file is None)
        log.info('starting import of Nexus corp file ' + str(corp_file if corp_file else corp_bin_file))
        tm.log_nexus_tm('info')
        if extent_ijk is None:  # auto detect extent
            extent_kji = None
            cp_extent = None
        else:
            (ni, nj, nk) = extent_ijk
            extent_kji = np.array((nk, nj, ni), dtype = 'int')
            cp_extent = (nk, nj, ni, 2, 2, 2, 3)  # (nk, nj, ni, kp, jp, ip, xyz)
        log.debug('reading and resequencing corp data')
        if corp_bin_file:  # bespoke nexus corp bin format, not to be confused with pure binary files used below
            cp_array = ld.load_corp_array_from_file(
                corp_bin_file,
                extent_kji,
                corp_bin = True,
                comment_char = None,  # comment char will be detected automatically
                data_free_of_comments = False,
                use_binary = use_binary)
        else:
            cp_binary_file = abt.cp_binary_filename(
                corp_file, nexus_ordering = False)  # pure binary, not bespoke corp bin used above
            recent_binary_exists = ld.file_exists(cp_binary_file, must_be_more_recent_than_file = corp_file)
            cp_array = None
            if use_binary and (extent_ijk is not None) and recent_binary_exists:
                try:
                    cp_array = ld.load_array_from_file(cp_binary_file, cp_extent, use_binary = True)
                except Exception:
                    cp_array = None
            if cp_array is None:
                cp_array = ld.load_corp_array_from_file(
                    corp_file,
                    extent_kji,
                    corp_bin = False,
                    comment_char = None,  # comment char will be detected automatically
                    data_free_of_comments = False,
                    use_binary = use_binary)
                if use_binary:
                    wd.write_pure_binary_data(cp_binary_file,
                                              cp_array)  # NB: this binary file is resequenced, not in nexus ordering!

    else:
        raise ValueError('vdb_file and corp_file are both None in import_nexus() call')

    if cp_array is None:
        log.error('failed to create corner point array')
        return None

    if extent_ijk is None:
        cp_extent = cp_array.shape
        extent_kji = cp_extent[:3]
        (nk, nj, ni) = extent_kji
        extent_ijk = (ni, nj, nk)
    else:
        ni, nj, nk = extent_ijk

    # convert units
    log.debug('Converting units')
    if corp_xy_units == corp_z_units and resqml_xy_units == resqml_z_units:
        bwam.convert_lengths(cp_array, corp_xy_units, resqml_xy_units)
    else:
        bwam.convert_lengths(cp_array[:, :, :, :, :, :, 0:1], corp_xy_units, resqml_xy_units)
        bwam.convert_lengths(cp_array[:, :, :, :, :, :, 2], corp_z_units, resqml_z_units)

    # invert z if required
    if resqml_z_inc_down != corp_z_inc_down:
        log.debug('Inverting z values')
        inversion = np.negative(cp_array[:, :, :, :, :, :, 2])
        cp_array[:, :, :, :, :, :, 2] = inversion

    # read active cell mask
    log.debug('Setting up active cell mask')
    active_mask = inactive_mask = None
    if vdb_file:
        assert vdbase is not None, 'problem with vdb object'
        inactive_mask = vdbase.grid_kid_inactive_mask(grid_title)  # TODO: check conversion of KID to boolean for LGRs
        if inactive_mask is not None:
            log.debug('using kid array as inactive cell mask')
            active_mask = np.logical_not(inactive_mask)
        else:
            log.warning('kid array not found, using unpack array as active cell indicator')
            unp = vdbase.grid_unpack(grid_title)
            assert unp is not None, 'failed to load active cell indicator mask from vdb kid or unpack arrays'
            active_mask = np.empty((nk, nj, ni), dtype = 'bool')
            active_mask[:] = (unp > 0)
            inactive_mask = np.logical_not(active_mask)
    elif active_mask_file:
        active_mask = ld.load_array_from_file(active_mask_file, extent_kji, data_type = 'bool', use_binary = use_binary)
        if active_mask is None:
            log.error('failed to load active cell indicator array from file: ' + active_mask_file)
        else:
            inactive_mask = np.logical_not(active_mask)  # will crash if active mask load failed

    # shift grid geometry to local crs
    local_origin = np.zeros(3)
    if shift_to_local:
        log.debug('shifting to local origin at ' + local_origin_place)
        if local_origin_place == 'centre':
            local_origin = np.nanmean(cp_array, axis = (0, 1, 2, 3, 4, 5))
        elif local_origin_place == 'minimum':
            local_origin = np.nanmin(cp_array, axis = (0, 1, 2, 3, 4, 5)) - 1.0  # The -1 ensures all coords are >0
        else:
            assert (False)
        cp_array -= local_origin

    # create empty resqml model
    log.debug('creating an empty resqml model')
    if mode == 'w':
        model = rq.Model(resqml_file_root, new_epc = True, create_basics = True, create_hdf5_ext = True)
    else:
        model = rq.Model(resqml_file_root)
    assert model is not None
    ext_uuid = model.h5_uuid()
    assert ext_uuid is not None

    # create coodinate reference system (crs) in model and set references in grid object
    log.debug('creating coordinate reference system')
    crs_uuids = model.uuids(obj_type = 'LocalDepth3dCrs')
    new_crs = rqc.Crs(model,
                      x_offset = local_origin[0],
                      y_offset = local_origin[1],
                      z_offset = local_origin[2],
                      xy_units = resqml_xy_units,
                      z_units = resqml_z_units,
                      z_inc_down = resqml_z_inc_down)
    new_crs.create_xml(reuse = True)
    crs_uuid = new_crs.uuid

    grid = grid_from_cp(model,
                        cp_array,
                        crs_uuid,
                        active_mask = active_mask,
                        geometry_defined_everywhere = geometry_defined_everywhere,
                        treat_as_nan = treat_as_nan,
                        max_z_void = max_z_void,
                        split_pillars = split_pillars,
                        split_tolerance = split_tolerance,
                        ijk_handedness = ijk_handedness,
                        known_to_be_straight = False)

    # create hdf5 file using arrays cached in grid above
    log.info('writing grid geometry to hdf5 file ' + resqml_file_root + '.h5')
    grid.write_hdf5_from_caches(resqml_file_root + '.h5', mode = mode, write_active = False)

    # build xml for grid geometry
    log.debug('building xml for grid')
    ijk_node = grid.create_xml(ext_uuid = None, title = grid_title, add_as_part = True, add_relationships = True)
    assert ijk_node is not None, 'failed to create IjkGrid node in xml tree'

    # impprt property arrays into a collection
    prop_import_collection = None
    decoarsen_array = None
    ts_node = None
    ts_uuid = None

    if active_mask is None and grid.inactive is not None:
        active_mask = np.logical_not(grid.inactive)

    if using_vdb:
        prop_import_collection = rp.GridPropertyCollection()
        if vdb_static_properties:
            props = vdbase.grid_list_of_static_properties(grid_title)
            if len(props) > 0:
                prop_import_collection = rp.GridPropertyCollection()
                prop_import_collection.set_grid(grid)
                for keyword in props:
                    prop_import_collection.import_vdb_static_property_to_cache(vdbase, keyword, grid_name = grid_title)
    #      if active_mask is not None:
    #         prop_import_collection.add_cached_array_to_imported_list(active_mask, active_mask_file, 'ACTIVE', property_kind = 'active',
    #                                                                  discrete = True, uom = None, time_index = None, null_value = None)

    elif property_array_files is not None and len(property_array_files) > 0:
        prop_import_collection = rp.GridPropertyCollection()
        prop_import_collection.set_grid(grid)
        for (p_filename, p_keyword, p_uom, p_time_index, p_null_value, p_discrete) in property_array_files:
            prop_import_collection.import_nexus_property_to_cache(p_filename,
                                                                  p_keyword,
                                                                  grid.extent_kji,
                                                                  discrete = p_discrete,
                                                                  uom = p_uom,
                                                                  time_index = p_time_index,
                                                                  null_value = p_null_value,
                                                                  use_binary = use_binary)
    #      if active_mask is not None:
    #         prop_import_collection.add_cached_array_to_imported_list(active_mask, active_mask_file, 'ACTIVE', property_kind = 'active',
    #                                                                  discrete = True, uom = None, time_index = None, null_value = None)

    #  ab_property_list: list of (filename, keyword, property_kind, facet_type, facet, uom, time_index, null_value, discrete)
    elif ab_property_list is not None and len(ab_property_list) > 0:
        prop_import_collection = rp.GridPropertyCollection()
        prop_import_collection.set_grid(grid)
        for (p_filename, p_keyword, p_property_kind, p_facet_type, p_facet, p_uom, p_time_index, p_null_value,
             p_discrete) in ab_property_list:
            prop_import_collection.import_ab_property_to_cache(p_filename,
                                                               p_keyword,
                                                               grid.extent_kji,
                                                               discrete = p_discrete,
                                                               property_kind = p_property_kind,
                                                               facet_type = p_facet_type,
                                                               facet = p_facet,
                                                               uom = p_uom,
                                                               time_index = p_time_index,
                                                               null_value = p_null_value)
    #      if active_mask is not None:
    #         prop_import_collection.add_cached_array_to_imported_list(active_mask, active_mask_file, 'ACTIVE', property_kind = 'active',
    #                                                                  discrete = True, uom = None, time_index = None, null_value = None)

    # ensemble_property_dictionary: mapping title (or keyword) to
    #    (filename, property_kind, facet_type, facet, uom, time_index, null_value, discrete)
    elif ensemble_case_dirs_root and ensemble_property_dictionary:
        case_path_list = glob.glob(ensemble_case_dirs_root + '*')
        assert len(case_path_list) > 0, 'no case directories found with path starting: ' + str(ensemble_case_dirs_root)
        case_number_place = len(ensemble_case_dirs_root)
        case_zero_used = False
        case_count = 0
        for case_path in case_path_list:
            if ensemble_size_limit is not None and case_count >= ensemble_size_limit:
                log.warning('stopping after reaching ensemble size limit')
                break
            # NB. import each case individually rather than holding property arrays for whole ensemble in memory at once
            prop_import_collection = rp.GridPropertyCollection()
            prop_import_collection.set_grid(grid)
            tail = case_path[case_number_place:]
            try:
                case_number = int(tail)
                assert case_number >= 0, 'negative case number encountered'
                if case_number == 0:
                    assert not case_zero_used, 'more than one case number evaluated to zero'
                    case_zero_used = True
            except Exception:
                log.error('failed to determine case number for tail: ' + str(tail))
                continue
            for keyword in ensemble_property_dictionary.keys():
                (filename, p_property_kind, p_facet_type, p_facet, p_uom, p_time_index, p_null_value,
                 p_discrete) = ensemble_property_dictionary[keyword]
                p_filename = os.path.join(case_path, filename)
                if not os.path.exists(p_filename):
                    log.error('missing property file: ' + p_filename)
                    continue
                prop_import_collection.import_nexus_property_to_cache(p_filename,
                                                                      keyword,
                                                                      grid.extent_kji,
                                                                      discrete = p_discrete,
                                                                      uom = p_uom,
                                                                      time_index = p_time_index,
                                                                      null_value = p_null_value,
                                                                      property_kind = p_property_kind,
                                                                      facet_type = p_facet_type,
                                                                      facet = p_facet,
                                                                      realization = case_number,
                                                                      use_binary = False)
            if len(prop_import_collection.imported_list) > 0:
                # create hdf5 file using arrays cached in grid above
                log.info('writing properties to hdf5 file ' + str(resqml_file_root) + '.h5 for case: ' +
                         str(case_number))
                grid.write_hdf5_from_caches(resqml_file_root + '.h5',
                                            geometry = False,
                                            imported_properties = prop_import_collection,
                                            write_active = False)
                # add imported properties parts to model, building property parts list
                prop_import_collection.create_xml_for_imported_list_and_add_parts_to_model(ext_uuid,
                                                                                           time_series_uuid = ts_uuid)
                if create_property_set:
                    prop_import_collection.create_property_set_xml('realisation ' + str(case_number))
                case_count += 1
            # remove cached static property arrays from memory

            #         prop_import_collection.remove_all_cached_arrays()
            del prop_import_collection
            prop_import_collection = None
        log.info(f'Nexus ascii ensemble input processed {case_count} cases')
        tm.log_nexus_tm('info')

    # create hdf5 file using arrays cached in grid above
    if prop_import_collection is not None and len(prop_import_collection.imported_list) > 0:
        if decoarsen:
            decoarsen_array = prop_import_collection.decoarsen_imported_list()
            if decoarsen_array is not None:
                log.info('static properties decoarsened')
                prop_import_collection.add_cached_array_to_imported_list(decoarsen_array,
                                                                         'decoarsen',
                                                                         'DECOARSEN',
                                                                         discrete = True,
                                                                         uom = None,
                                                                         time_index = None,
                                                                         null_value = -1,
                                                                         property_kind = 'discrete')
        log.info('writing ' + str(len(prop_import_collection.imported_list)) + ' properties to hdf5 file ' +
                 resqml_file_root + '.h5')
    elif not ensemble_case_dirs_root:
        log.info('no static grid properties to import')
        prop_import_collection = None
    grid.write_hdf5_from_caches(resqml_file_root + '.h5',
                                geometry = False,
                                imported_properties = prop_import_collection,
                                write_active = True)
    # remove cached static property arrays from memory
    if prop_import_collection is not None:
        prop_import_collection.remove_all_cached_arrays()

    ts_selection = None
    if using_vdb and vdb_recurrent_properties and timestep_selection is not None and str(timestep_selection) != 'none':
        if prop_import_collection is None:
            prop_import_collection = rp.GridPropertyCollection()
            prop_import_collection.set_grid(grid)
        # extract timestep dates from summary file (this info might be hidden in the recurrent binary files but I couldn't find it
        # todo: create cut down time series from recurrent files and differentiate between reporting time index and mapped time step number
        full_time_series = rts.time_series_from_nexus_summary(summary_file)
        if full_time_series is None:
            log.error('failed to fetch time series from Nexus summary file; recurrent data excluded')
            tm.log_nexus_tm('error')
        else:
            full_time_series.set_model(model)
            timestep_list = vdbase.grid_list_of_timesteps(
                grid_title)  # get list of timesteps for which recurrent files exist
            recur_time_series = None
            for timestep_number in timestep_list:
                if isinstance(timestep_selection, list):
                    if timestep_number not in timestep_selection:
                        continue
                else:
                    if timestep_selection == 'first':
                        if timestep_number != timestep_list[0]:
                            break
                    elif timestep_selection == 'last':
                        if timestep_number != timestep_list[-1]:
                            continue
                    elif timestep_selection == 'first and last':
                        if timestep_number != timestep_list[0] and timestep_number != timestep_list[-1]:
                            continue
                    # default to importing all timesteps
                stamp = full_time_series.timestamp(timestep_number)
                if stamp is None:
                    log.error('timestamp number for which recurrent data exists was not found in summary file: ' +
                              str(timestep_number))
                    continue
                recur_prop_list = vdbase.grid_list_of_recurrent_properties(grid_title, timestep_number)
                common_recur_prop_set = set()
                if recur_time_series is None:
                    recur_time_series = rts.TimeSeries(model, first_timestamp = stamp)
                    if recur_prop_list is not None:
                        common_recur_prop_set = set(recur_prop_list)
                else:
                    recur_time_series.add_timestamp(stamp)
                    if recur_prop_list is not None:
                        common_recur_prop_set = common_recur_prop_set.intersection(set(recur_prop_list))
                step_import_collection = rp.GridPropertyCollection()
                step_import_collection.set_grid(grid)
                # for each property for this timestep, cache array and add to recur prop import collection for this time step
                if recur_prop_list:
                    for keyword in recur_prop_list:
                        if not keyword or not keyword.isalnum():
                            continue
                        prop_kind, facet_type, facet = rp.property_kind_and_facet_from_keyword(keyword)
                        step_import_collection.import_vdb_recurrent_property_to_cache(
                            vdbase,
                            timestep_number,  # also used as time_index?
                            keyword,
                            grid_name = grid_title,
                            property_kind = prop_kind,
                            facet_type = facet_type,
                            facet = facet)
                # extend hdf5 with cached arrays for this timestep
                log.info('number of recurrent grid property arrays for timestep: ' + str(timestep_number) + ' is: ' +
                         str(step_import_collection.number_of_imports()))
                if decoarsen_array is not None:
                    log.info('decoarsening recurrent properties for timestep: ' + str(timestep_number))
                    step_import_collection.decoarsen_imported_list(decoarsen_array = decoarsen_array)
                log.info('extending hdf5 file with recurrent properties for timestep: ' + str(timestep_number))
                grid.write_hdf5_from_caches(resqml_file_root + '.h5',
                                            mode = 'a',
                                            geometry = False,
                                            imported_properties = step_import_collection,
                                            write_active = False)
                # add imported list for this timestep to full imported list
                prop_import_collection.inherit_imported_list_from_other_collection(step_import_collection)
                log.debug('total number of property arrays after timestep: ' + str(timestep_number) + ' is: ' +
                          str(prop_import_collection.number_of_imports()))
                # remove cached copies of arrays
                step_import_collection.remove_all_cached_arrays()

            ts_node = full_time_series.create_xml(title = 'simulator full timestep series')
            model.time_series = ts_node  # save as the primary time series for the model
            ts_uuid = rqet.uuid_for_part_root(ts_node)
            # create xml for recur_time_series (as well as for full_time_series) and add as part; not needed?
            if recur_time_series is not None:
                rts_node = recur_time_series.create_xml(title = 'simulator recurrent array timestep series')
                if use_compressed_time_series:
                    ts_uuid = rqet.uuid_for_part_root(rts_node)
                    ts_selection = timestep_list

    # add imported properties parts to model, building property parts list
    if prop_import_collection is not None and prop_import_collection.imported_list is not None:
        prop_import_collection.set_grid(grid)  # update to pick up on recently created xml root node for grid
        prop_import_collection.create_xml_for_imported_list_and_add_parts_to_model(
            ext_uuid, time_series_uuid = ts_uuid, selected_time_indices_list = ts_selection)
        if create_property_set:
            prop_import_collection.create_property_set_xml('property set for import for grid ' + str(grid_title))

    # mark model as modified (will already have happened anyway)
    model.set_modified()

    # create epc file
    log.info('storing model in epc file ' + resqml_file_root + '.epc')
    model.store_epc(resqml_file_root + '.epc')

    # return resqml model
    return model