diff --git a/pyproject.toml b/pyproject.toml index 308b0554c..a7d3fcdc3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,6 +117,8 @@ sw_model_utilities = "subscript.sw_model_utilities.sw_model_utilities:main" sunsch = "subscript.sunsch.sunsch:main" vfp2csv = "subscript.vfp2csv.vfp2csv:main" welltest_dpds = "subscript.welltest_dpds.welltest_dpds:main" +field_statistics = "subscript.field_statistics.field_statistics:main" + [project.entry-points.ert] subscript_jobs = "subscript.hook_implementations.jobs" diff --git a/src/subscript/field_statistics/WF_FIELD_PARAM_STATISTICS b/src/subscript/field_statistics/WF_FIELD_PARAM_STATISTICS new file mode 100644 index 000000000..cc70664c7 --- /dev/null +++ b/src/subscript/field_statistics/WF_FIELD_PARAM_STATISTICS @@ -0,0 +1,21 @@ +-- Workflow job for ERT to calculate +-- - mean and stdev of ensemble of continuous 3D parameters with name saved for geogrid +-- under /realization-*/iter-*/share/results/grids/geogrid--.roff +-- - estimate facies probabilities of discrete 3D parameters with name saved for geogrid +-- under /realization-*/iter-*/share/results/grids/geogrid--.roff +INTERNAL False +EXECUTABLE ../scripts/field_statistics.py + +MIN_ARG 6 +ARG_TYPE 0 STRING +ARG_TYPE 1 STRING +ARG_TYPE 2 STRING +ARG_TYPE 3 STRING +ARG_TYPE 4 STRING +ARG_TYPE 5 STRING +ARG_TYPE 6 STRING +ARG_TYPE 7 STRING + + + + diff --git a/src/subscript/field_statistics/__init__.py b/src/subscript/field_statistics/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/subscript/field_statistics/field_param_stat.yml b/src/subscript/field_statistics/field_param_stat.yml new file mode 100644 index 000000000..7bf22bf83 --- /dev/null +++ b/src/subscript/field_statistics/field_param_stat.yml @@ -0,0 +1,59 @@ +# Configuration file for script wf_field_param_statistics.py +field_stat: + # Number of realizations for specified ensemble + # Required. + nreal: 100 + + # Iteration numbers from ES-MDA in ERT (iteration = 0 is initial ensemble, + # usually iteration=3 is final updated ensemble) + # Required. + iterations: [0, 3] + + # Selected set of zone names to use in calculations of statistics. + # Must be one or more of the defined zones. + # Require at least one zone to be selected. + use_zones: ["Valysar", "Therys", "Volon"] + + # Zone numbers with zone name dictionary + zone_code_names: + 1: "Valysar" + 2: "Therys" + 3: "Volon" + + # For each zone specify either Proportional, Top_conform or Base_conform + # as grid conformity. + # Conformity can be checked by opening the RMS job that has created + # the geogrid and check the grid settings for grid layers. + # Proportional means that number of layers is specified. + # Top or base conform means that grid cell thickness is specified. + # Required (but only for zones you want to use) + zone_conformity: + "Valysar": "Proportional" + "Therys": "Top_conform" + "Volon": "Proportional" + + # For each zone specify which discrete parameter to use to calculate + # facies probability estimates. + # Possible names are those found in the + # share/results/grids/geogrid--.roff + # files that are of discrete type. + # This key can be omitted or some of the lines specifying parameters + # for a zone if you don't want to use it. + discrete_property_param_per_zone: + "Valysar": ["facies"] + "Therys": ["facies"] + "Volon": ["facies"] + + # For each zone specify which continuous parameter to use to + # calculate estimate of mean and stdev over ensemble. + # Possible names are those found in the + # share/results/grids/geogrid--.roff + # files that are of continuous type + # This key can be omitted or some of the lines specifying + # parameters for a zone if you don't want to use it. + continuous_property_param_per_zone: + "Valysar": ["phit"] + "Therys": ["phit"] + + # Size of ertbox grid for (nx, ny, nz) + ertbox_size: [92, 146, 66] \ No newline at end of file diff --git a/src/subscript/field_statistics/field_statistics.py b/src/subscript/field_statistics/field_statistics.py new file mode 100644 index 000000000..9cb3b50a6 --- /dev/null +++ b/src/subscript/field_statistics/field_statistics.py @@ -0,0 +1,856 @@ +#!/usr/bin/env python3 +"""Purpose: + Calculate mean and stdev of continuous field parameters + Estimate volume fraction of facies (facies probabilities) + Works for non-shared grids where nx, ny is fixed, + but nz per zone may vary from realization to realization. +Result: + Output mean and stdev for continuous field parameter + Output estimate of facies probabilities for facies parameters +""" + +# pylint: disable=missing-function-docstring, too-many-arguments +# pylint: disable=too-many-branches, too-many-statements +# pylint: disable= too-many-locals, too-many-nested-blocks +import argparse +import copy +import logging +import sys +from pathlib import Path + +import fmu.config.utilities as utils +import numpy as np +import xtgeo +import yaml + +import subscript + +logger = subscript.getLogger(__name__) +DESCRIPTION = """Calculate mean, stdev and estimated facies probabilities +from field parameters using ERTBOX grid. + +The script reads ensembles of realizations from scratch disk from +share/results/grids/geogrid--.roff. + +Since the realizations may have a grid geometry that is realization dependent +and may have multiple zones, the values are first copied over to a static grid +called ERTBOX grid. Depending on the grid conformity of the geogrid zones, +the values are filled up from top or bottom of the ERTBOX grid. This is to +ensure that zones with varying number of layers for each realization can +be handled when calculating mean, standard deviation or estimated facies +probabilities. + +The grid cell indices in the ERTBOX grid is a label of the parameter value, +and mean, standard deviation and facies fractions are calculated for +each specified property for each individual grid cell (I,J,K) in the ERTBOX grid. +Number of realizations of property values may vary from grid cell to grid cell +due to varying number of layers per realization and due to stair case faults. +Therefore also a parameter counting the number of realizations present for each +individual grid cell is also calculated and used in the estimates of mean, +standard deviation and facies probabilities. + +The assumption behind this method (using ERTBOX grid as a fixed common grid for +all realizations) is: +1. The lateral extension of the geogrid is close to a regular grid with same +orientation and grid resolution as the ERTBOX grid. +2. The ERTBOX grid should be the same as used in ERT when field parameters +are updated using the ERT keyword FIELD in the ERT configuration file. +3. Any lateral variability from realization to realization or curved shaped +lateral grid is ignored. Only the cell indices are used to identify +grid cell field parameters from each realization. This means that +mean, standard deviation and estimated facies probabilities are estimated +for each cell labeled with index (I,J,K) and not physical position (x,y,z). + +The output statistical properties (mean, stdev, prob) is saved in a user +specified folder, but default if not specified is share/grid_statistics +folder under the top level of the scratch directory for the ERT case. +The default estimate of standard deviation is the sample standard deviation +(variance = sum( (x(i)-x_mean)^2 )/(N-1) ) +and number of realizations must be at least 2. Optionally, the population +standard deviation +(variance = sum( (x(i)-x_mean)^2 )/N ) +can be specified. + +For grid cells where number of realizations are less than 2, +the standard deviation parameter calculated will be set to 0. +""" + +EPILOGUE = """ +.. code-block:: yaml + + # Example config file for wf_field_param_statistics + +field_stat: + # Number of realizations for specified ensemble + # Required. + nreal: 100 + + # Iteration numbers from ES-MDA in ERT (iteration = 0 is initial ensemble, + # usually iteration=3 is final updated ensemble) + # Required. + iterations: [0, 3] + + # Selected set of zone names to use in calculations of statistics. + # Must be one or more of the defined zones. + # Require at least one zone to be selected. + use_zones: ["Valysar", "Therys", "Volon"] + + # Zone numbers with zone name dictionary ordered in increasing order of zone code + # Required for multi-zone grids. + zone_code_names: + 1: "Valysar" + 2: "Therys" + 3: "Volon" + + # For each zone specify either Proportional, Top_conform or Base_conform + # as grid conformity. + # Conformity can be checked by opening the RMS job that has created + # the geogrid and check the grid settings for grid layers. + # Proportional means that number of layers is specified. + # Top or base conform means that grid cell thickness is specified. + # Required (but only for zones you want to use) + zone_conformity: + "Valysar": "Proportional" + "Therys": "Top_conform" + "Volon": "Proportional" + + # For each zone specify which discrete parameter to use to calculate + # facies probability estimates. + # Possible names are those found in the + # share/results/grids/geogrid--.roff + # files that are of discrete type. + # This key can be omitted or some of the lines specifying parameters + # for a zone if you don't want to use it. + discrete_property_param_per_zone: + "Valysar": ["facies"] + "Therys": ["facies"] + "Volon": ["facies"] + + # For each zone specify which continuous parameter to use to + # calculate estimate of mean and stdev over ensemble. + # Possible names are those found in the + # share/results/grids/geogrid--.roff + # files that are of continuous type + # This key can be omitted or some of the lines specifying + # parameters for a zone if you don't want to use it. + continuous_property_param_per_zone: + "Valysar": ["phit"] + "Therys": ["phit"] + "Volon": ["phit"] + + # Size of ertbox grid for (nx, ny, nz) + # Required. + ertbox_size: [92, 146, 66] + + # Standard deviation estimator. + # Optional. Default is False which means that + # sample standard deviation ( normalize by (N-1)) is used + # where N is number of realizations. + # The alternative is True which means that + # population standard deviation ( normalize by N) is used. + use_population_stdev: False + +""" + +CATEGORY = "modelling.reservoir" + +EXAMPLES = """ +.. code-block:: console + +-- Installation of the ERT workflow: +DEFINE ../input/config/field_param_stat.yml +LOAD_WORKFLOW_JOB ../../bin/jobs/WF_FIELD_PARAM_STATISTICS +LOAD_WORKFLOW ../../bin/workflows/wf_field_param_statistics + +-- The workflow file to be located under ert/bin/workflows: +WF_FIELD_PARAM_STATISTICS // + +-- The workflow job file to be located under ert/bin/jobs: +-- Workflow job for ERT to calculate +-- - mean and stdev of ensemble of continuous 3D parameters with name saved for geogrid +-- under /realization-*/iter-*/share/results/grids/geogrid--.roff +-- - estimate facies probabilities of discrete 3D parameters with name saved for geogrid +-- under /realization-*/iter-*/share/results/grids/geogrid--.roff +INTERNAL False +EXECUTABLE ../scripts/wf_field_param_statistics.py + +MIN_ARG 6 +ARG_TYPE 0 STRING +ARG_TYPE 1 STRING +ARG_TYPE 2 STRING +ARG_TYPE 3 STRING +ARG_TYPE 4 STRING +ARG_TYPE 5 STRING +ARG_TYPE 6 STRING +ARG_TYPE 7 STRING +ARG_TYPE 8 STRING + + +""" # noqa + + +def main(): + """Invocated from the command line, parsing command line arguments""" + parser = get_parser() + args = parser.parse_args() + + logger.setLevel(logging.INFO) + + # parse the config file for this script + if not Path(args.configfile).exists(): + sys.exit("No such file:" + args.configfile) + + config_file = args.configfile + config_dict = read_field_stat_config(config_file) + field_stat = config_dict["field_stat"] + + # Path to FMU project models ert/model directory (ordinary CONFIG PATH in ERT) + if not Path(args.ertconfigpath).exists(): + sys.exit("No such file:" + args.ertconfigpath) + ert_config_path = args.ertconfigpath + + # Path to ensemble on SCRATCH disk + if not Path(args.ensemblepath).exists(): + sys.exit("No such file:" + args.ensemblepath) + ens_path = args.ensemblepath + + # Relative path for result of ensemble statistics calculations + # relative to ensemble path on scratch disk + # Default path is defined. + result_path = "share/grid_statistics" + if Path(args.resultpath).exists(): + result_path = args.resultpath + + glob_var_config_path = ( + ert_config_path + "/../../fmuconfig/output/global_variables.yml" + ) + cfg_global = utils.yaml_load(glob_var_config_path)["global"] + keyword = "FACIES_ZONE" + if keyword in cfg_global: + facies_per_zone = cfg_global[keyword] + else: + raise KeyError(f"Missing keyword: {keyword} in {glob_var_config_path}") + + logger.info(f"Config path to FMU project: {ert_config_path}") + logger.info(f"Ensemble path on scratch disk: {ens_path}") + logger.info(f"Result relative path on scratch disk: {result_path}") + calc_stats(field_stat, ens_path, facies_per_zone, result_path) + logger.info( + "Finished running workflow to calculate statistics " + "for ensemble of field parameters" + ) + + +def get_parser() -> argparse.ArgumentParser: + """ + Define the argparse parser + """ + parser = argparse.ArgumentParser( + description=DESCRIPTION, + epilog=EPILOGUE, + formatter_class=argparse.RawTextHelpFormatter, + ) + + parser.add_argument( + "-c", + "-C", + "--configfile", + type=str, + help="Name of YAML config file", + required=True, + ) + parser.add_argument( + "-p", + "--ertconfigpath", + type=str, + default="./", + help=( + "Root path assumed for relative paths" + " in config file, except for the output file." + ), + ) + parser.add_argument( + "-e", + "--ensemblepath", + type=str, + help="File path to ensemble directory on scratch disk", + required=True, + ) + parser.add_argument( + "-r", + "--resultpath", + type=str, + default="share/grid_statistics", + help=( + "Relative file path to result files relative to " + "ensemble directory on scratch disk", + ), + ) + parser.add_argument( + "--version", + action="version", + version="%(prog)s (subscript version " + subscript.__version__ + ")", + ) + return parser + + +def read_field_stat_config(config_file_name): + txt = f"Settings for field statistics using config file: {config_file_name}" + logging.info(txt) + + with open(config_file_name, encoding="utf-8") as yml_file: + return yaml.safe_load(yml_file) + + +def read_ensemble_realization( + ensemble_path, + realization_number, + iter_number, + property_param_name, + zone_code_names, +): + realization_path = Path(f"realization-{realization_number}/iter-{iter_number}") + grid_path = Path("share/results/grids/geogrid.roff") + file_path_grid = Path(ensemble_path) / realization_path / grid_path + if file_path_grid.exists(): + grid = xtgeo.grid_from_file(file_path_grid, fformat="roff") + subgrids = grid.subgrids if grid.subgrids else None + else: + return None, None, None + + property_path = Path(f"share/results/grids/geogrid--{property_param_name}.roff") + file_path_property = Path(ensemble_path) / realization_path / property_path + property_param = xtgeo.gridproperty_from_file(file_path_property, fformat="roff") + + # Update subgrid names if default names are used and multi-zone grid + if zone_code_names and len(zone_code_names) > 1: + set_subgrid_names(grid, zone_code_names) + subgrids = grid.subgrids + + return grid.dimensions, subgrids, property_param + + +def get_values_in_ertbox( + geogrid_dimensions, + geogrid_subgrids, + geogrid_property_param, + zone_name, + ertbox_size, + conformity, + is_continuous=True, +): + if geogrid_subgrids: + # Multi-zone grid + assert zone_name in geogrid_subgrids + # layers count from 1, change to count from 0 + layers = geogrid_subgrids[zone_name] + start_layer = layers[0] - 1 + end_layer = layers[-1] + nz_zone = end_layer - start_layer + else: + # Single zone grid + nz_zone = geogrid_dimensions[2] + start_layer = 0 + end_layer = nz_zone + + assert geogrid_dimensions[0] == ertbox_size[0] + assert geogrid_dimensions[1] == ertbox_size[1] + assert ertbox_size[2] >= nz_zone + prop_values = geogrid_property_param.values + if is_continuous: + ertbox_prop_values = np.ma.masked_all( + (ertbox_size[0], ertbox_size[1], ertbox_size[2]), dtype=np.float32 + ) + else: + ertbox_prop_values = np.ma.masked_all( + (ertbox_size[0], ertbox_size[1], ertbox_size[2]), dtype=np.int32 + ) + if conformity.upper() in ["PROPORTIONAL", "TOP_CONFORM"]: + ertbox_prop_values[:, :, :nz_zone] = prop_values[:, :, start_layer:end_layer] + # print(f"Top conform or proportional zone: {zone_name}") + elif conformity.upper() == "BASE_CONFORM": + start_layer_ertbox = ertbox_size[2] - nz_zone + ertbox_prop_values[:, :, start_layer_ertbox:] = prop_values[ + :, :, start_layer:end_layer + ] + # print(f"Base conform zone: {zone_name}") + + return ertbox_prop_values + + +def set_subgrid_names(grid, zone_code_names=None, new_subgrids=None): + if new_subgrids: + # Use specified new subgrids when it is defined + grid.set_subgrids(new_subgrids) + return + + # Modify zone names of existing subgrids if not consistent with zone_code_names + assert zone_code_names + subgrids = grid.get_subgrids() + new_subgrids = {} + for zone_number, zone_name in zone_code_names.items(): + # Replace subgrid_0 with first zone name and subgrid_1 + # with second zone name and so on + for name, val in subgrids.items(): + if name == f"subgrid_{zone_number - 1}": + new_subgrids[zone_name] = copy.copy(val) + grid.set_subgrids(new_subgrids) + + +def write_mean_stdev_nactive( + ensemble_path, + iter_number, + zone_name, + param_name, + mean_values_masked, + stdev_values_masked, + ncount_active_values, + result_path, +): + output_path = ensemble_path / Path(result_path) + if not output_path.exists(): + # Create the directory + output_path.mkdir() + dims = mean_values_masked.shape + name_mean = "mean_" + zone_name + "_" + param_name + "_" + str(iter_number) + name_stdev = "stdev_" + zone_name + "_" + param_name + "_" + str(iter_number) + name_nactive = "nactive_" + zone_name + "_" + str(iter_number) + result_mean_file_path = output_path / Path(name_mean + ".roff") + result_stdev_file_path = output_path / Path(name_stdev + ".roff") + result_nactive_file_path = output_path / Path(name_nactive + ".roff") + + # Fill masked values with 0 + mean_values = mean_values_masked.filled(fill_value=0.0) + stdev_values = stdev_values_masked.filled(fill_value=0.0) + + xtgeo_mean = xtgeo.GridProperty( + ncol=dims[0], nrow=dims[1], nlay=dims[2], name=name_mean, values=mean_values + ) + xtgeo_stdev = xtgeo.GridProperty( + ncol=dims[0], nrow=dims[1], nlay=dims[2], name=name_stdev, values=stdev_values + ) + + xtgeo_ncount_active = xtgeo.GridProperty( + ncol=dims[0], + nrow=dims[1], + nlay=dims[2], + name=name_nactive, + values=ncount_active_values, + ) + + logger.info(f"Write parameter: {name_mean}") + xtgeo_mean.to_file(result_mean_file_path, fformat="roff") + + logger.info(f"Write parameter: {name_stdev}") + xtgeo_stdev.to_file(result_stdev_file_path, fformat="roff") + + logger.info(f"Write parameter: {name_nactive}") + xtgeo_ncount_active.to_file(result_nactive_file_path, fformat="roff") + + +def write_fraction_nactive( + ensemble_path, + iter_number, + zone_name, + facies_name, + fraction_masked, + result_path, + ncount_active_values=None, +): + output_path = ensemble_path / Path(result_path) + if not output_path.exists(): + # Create the directory + output_path.mkdir() + dims = fraction_masked.shape + name_fraction = "prob_" + zone_name + "_" + facies_name + "_" + str(iter_number) + name_nactive = "nactive_" + zone_name + "_" + str(iter_number) + + result_fraction_file_path = output_path / Path(name_fraction + ".roff") + result_nactive_file_path = output_path / Path(name_nactive + ".roff") + + # Fill masked values with 0 + fraction = fraction_masked.filled(fill_value=0.0) + + xtgeo_fraction = xtgeo.GridProperty( + ncol=dims[0], nrow=dims[1], nlay=dims[2], name=name_fraction, values=fraction + ) + + logger.info(f"Write parameter: {name_fraction}") + xtgeo_fraction.to_file(result_fraction_file_path, fformat="roff") + + if ncount_active_values is not None: + xtgeo_ncount_active = xtgeo.GridProperty( + ncol=dims[0], + nrow=dims[1], + nlay=dims[2], + name=name_nactive, + values=ncount_active_values, + ) + + logger.info(f"Write parameter: {name_nactive}") + xtgeo_ncount_active.to_file(result_nactive_file_path, fformat="roff") + + +def get_specifications(input_dict): + key = "zone_code_names" + if key in input_dict: + zone_code_names = input_dict[key] + else: + raise KeyError( + f"Missing keyword: {key} specifying " "zone name for each zone number." + ) + + key = "ertbox_size" + if key in input_dict: + ertbox_size = input_dict[key] + else: + raise KeyError(f"Missing keyword: {key} specifying a tuple (nx, ny, nz)") + + key = "nreal" + if key in input_dict: + nreal = input_dict[key] + else: + raise KeyError(f"Missing keyword: {key} specifying number of realizations") + + key = "iterations" + if key in input_dict: + iter_list = input_dict[key] + else: + raise KeyError( + f"Missing keyword: {key} specifying a list of iteration numbers " + " for ensembles from ERT ES-MDA" + ) + key = "use_zones" + zone_names_used = copy.copy(list(zone_code_names.values())) + if key in input_dict: + zone_names_input = input_dict[key] + if zone_names_input is not None and len(zone_names_input) > 0: + zone_names_used = zone_names_input + check_use_zones(zone_code_names, zone_names_used) + + key = "zone_conformity" + if key in input_dict: + zone_conformity = input_dict[key] + else: + raise KeyError(f"Missing keyword: {key} specifying conformity per zone.") + check_zone_conformity(zone_code_names, zone_names_used, zone_conformity) + + key = "use_population_stdev" + use_population_stdev = False + if key in input_dict: + use_population_stdev = input_dict[key] + + param_name_dict = None + key = "continuous_property_param_per_zone" + if key in input_dict: + param_name_dict = input_dict[key] + check_param_name_dict(zone_code_names, param_name_dict) + + disc_param_name_dict = None + key = "discrete_property_param_per_zone" + if key in input_dict: + disc_param_name_dict = input_dict[key] + check_disc_param_name_dict(zone_code_names, disc_param_name_dict) + + check_used_params(zone_names_used, param_name_dict, disc_param_name_dict) + + return ( + ertbox_size, + nreal, + iter_list, + zone_names_used, + zone_conformity, + zone_code_names, + use_population_stdev, + param_name_dict, + disc_param_name_dict, + ) + + +def check_zone_conformity(zone_code_names, zone_names_used, zone_conformity): + for zone_name, conformity in zone_conformity.items(): + if zone_name not in list(zone_code_names.values()): + raise ValueError("Unknown zone names in keyword 'zone_conformity'.") + if conformity.upper() not in ["TOP_CONFORM", "BASE_CONFORM", "PROPORTIONAL"]: + raise ValueError( + "Undefined zone conformity specified " + "(Must be Top_conform, Base_conform or Proportional)." + ) + for zone_name in zone_names_used: + if zone_name not in zone_conformity: + raise ValueError( + f"Zone with name {zone_name} is missing in keyword 'zone_conformity'." + ) + + +def check_param_name_dict(zone_code_names, param_name_dict): + if not param_name_dict: + return + for zone_name, prop_list in param_name_dict.items(): + if zone_name not in list(zone_code_names.values()): + raise ValueError( + "Unknown zone name in specification of keyword " + "'continuous_property_param_per_zone'." + ) + if prop_list is None or len(prop_list) == 0: + raise ValueError( + "Missing list of property names for a specified zone in " + "keyword 'continuous_property_param_per_zone'." + ) + + +def check_disc_param_name_dict(zone_code_names, disc_param_name_dict): + if not disc_param_name_dict: + return + for zone_name, prop_list in disc_param_name_dict.items(): + if zone_name not in list(zone_code_names.values()): + raise ValueError( + "Unknown zone name in specification of keyword " + "'discrete_property_param_per_zone'." + ) + if prop_list is None or len(prop_list) == 0: + raise ValueError( + "Missing list of property names for a specified zone in keyword " + "'discrete_property_param_per_zone'." + ) + + +def check_use_zones(zone_code_names, zone_names): + if not zone_names or len(zone_names) == 0: + return + for zone_name in zone_names: + if zone_name not in list(zone_code_names.values()): + raise ValueError( + "Unknown zone name in specification of keyword 'use_zones'." + ) + + +def check_used_params(zone_names_used, param_name_dict, disc_param_name_dict): + for zone_name in zone_names_used: + found = False + if param_name_dict and zone_name in param_name_dict: + found = True + if disc_param_name_dict and zone_name in disc_param_name_dict: + found = True + if not found: + raise ValueError( + f"Zone with name {zone_name} is specified to be used, " + "but not specified in keywords 'continuous_property_param_per_zone' " + "or 'discrete_property_param_per_zone'." + "If keyword 'use_zones' is not specified, " + "it is assumed that all defined zones in keyword 'zone_code_names' " + "are used." + ) + + +def calc_stats(input_dict, ens_path, facies_per_zone, result_path): + ( + ertbox_size, + nreal, + iter_list, + zone_names, + zone_conformity, + zone_code_names, + use_population_stdev, + param_name_dict, + disc_param_name_dict, + ) = get_specifications(input_dict) + + ensemble_path = ens_path + + logger.info(f"Number of realizations: {nreal}") + for iter_number in iter_list: + logger.info(f"Ensemble iteration: {iter_number}") + for zone_name in zone_names: + logger.info(f"Zone name: {zone_name}") + has_written_nactive = False + if param_name_dict: + if zone_name not in param_name_dict: + continue + for param_name in param_name_dict[zone_name]: + logger.info(f"Property: {param_name}") + all_values = np.ma.masked_all( + (ertbox_size[0], ertbox_size[1], ertbox_size[2], nreal), + dtype=np.float32, + ) + number_of_skipped = 0 + for real_number in range(nreal): + grid_dimensions, subgrids, property_param = ( + read_ensemble_realization( + ensemble_path, + real_number, + iter_number, + param_name, + zone_code_names, + ) + ) + if grid_dimensions is None: + txt = f" Skip non-existing realization: {real_number}" + logger.info(txt) + number_of_skipped += 1 + continue + ertbox_prop_values = get_values_in_ertbox( + grid_dimensions, + subgrids, + property_param, + zone_name, + ertbox_size, + zone_conformity[zone_name], + is_continuous=True, + ) + + all_values[:, :, :, real_number] = ertbox_prop_values + + calc_mean = False + calc_stdev = False + mean_values = None + stdev_values = None + if number_of_skipped < nreal: + # Mean value + mean_values = all_values.mean(axis=3) + calc_mean = True + if number_of_skipped < (nreal - 1): + # Std deviation + if use_population_stdev: + stdev_values = all_values.std(axis=3, ddof=0) + else: + stdev_values = all_values.std(axis=3, ddof=1) + calc_stdev = True + # Number of realization for each grid cell + ncount_active_values = nreal - np.ma.count_masked( + all_values, axis=3 + ) + + # Write mean, stdev + if calc_mean and calc_stdev: + write_mean_stdev_nactive( + ensemble_path, + iter_number, + zone_name, + param_name, + mean_values, + stdev_values, + ncount_active_values, + result_path, + ) + has_written_nactive = True + else: + info_txt = f"No mean and stdev calculated for {param_name} " + f"for zone {zone_name} for ensemble iteration " + f"{iter_number}" + logger.info(info_txt) + + if disc_param_name_dict: + if zone_name not in disc_param_name_dict: + continue + for param_name in disc_param_name_dict[zone_name]: + logger.info(f"Property: {param_name}") + all_values = np.ma.masked_all( + (ertbox_size[0], ertbox_size[1], ertbox_size[2], nreal), + dtype=np.int32, + ) + all_active = np.ma.masked_all( + (ertbox_size[0], ertbox_size[1], ertbox_size[2], nreal), + dtype=np.int32, + ) + ones = np.ma.ones( + (ertbox_size[0], ertbox_size[1], ertbox_size[2], nreal), + dtype=np.int32, + ) + number_of_skipped = 0 + for real_number in range(nreal): + grid_dimensions, subgrids, property_param = ( + read_ensemble_realization( + ensemble_path, + real_number, + iter_number, + param_name, + zone_code_names, + ) + ) + + if grid_dimensions is None: + logger.info(f" Skip realization: {real_number}") + number_of_skipped += 1 + continue + ertbox_prop_values = get_values_in_ertbox( + grid_dimensions, + subgrids, + property_param, + zone_name, + ertbox_size, + zone_conformity[zone_name], + is_continuous=False, + ) + + all_values[:, :, :, real_number] = ertbox_prop_values + all_active[:, :, :, real_number] = ~ertbox_prop_values.mask + + # Count number of realizations per discrete code per grid cell + # Count number of realizations per grid cell + if number_of_skipped < nreal: + sum_fraction = 0 + for code, facies_name in facies_per_zone[zone_name].items(): + selected_cells = np.ma.masked_where( + all_values != code, ones + ) + sum_active = np.ma.sum(all_active, axis=3) + number_of_cells = np.ma.sum(selected_cells, axis=3) + prob_with_code = np.ma.divide(number_of_cells, sum_active) + sum_total_active = np.ma.sum(sum_active) / nreal + sum_total_code = np.ma.sum(number_of_cells) / nreal + fraction = sum_total_code / sum_total_active + txt1 = f"Average number of active cells: {sum_total_active}" + logger.info(txt1) + + txt2 = ( + f"Average number of cells with facies " + f"{facies_name} is {sum_total_code}" + ) + logger.info(txt2) + + txt3 = ( + "Average estimated facies probability for facies " + f"{facies_name}: {fraction}" + ) + logger.info(txt3) + + sum_fraction += fraction + + # Write fraction (estimated facies probability from ensemble + + if not has_written_nactive: + # The parameter for number of realization for each grid + # cell value is not already written + write_fraction_nactive( + ensemble_path, + iter_number, + zone_name, + facies_name, + prob_with_code, + result_path, + ncount_active_values=sum_active, + ) + has_written_nactive = True + else: + write_fraction_nactive( + ensemble_path, + iter_number, + zone_name, + facies_name, + prob_with_code, + result_path, + ) + txt4 = f"Sum facies volume fraction: {sum_fraction}" + logger.info(txt4) + else: + txt = ( + "No probability estimate calculated for " + f"{param_name} for zone {zone_name}" + f" for ensemble iteration {iter_number}" + ) + logger.info(txt) + + +if __name__ == "__main__": + main() diff --git a/src/subscript/field_statistics/wf_field_param_statistics b/src/subscript/field_statistics/wf_field_param_statistics new file mode 100644 index 000000000..96948ff57 --- /dev/null +++ b/src/subscript/field_statistics/wf_field_param_statistics @@ -0,0 +1 @@ +WF_FIELD_PARAM_STATISTICS -c -p -e // diff --git a/tests/test_field_statistics.py b/tests/test_field_statistics.py new file mode 100644 index 000000000..566059b3e --- /dev/null +++ b/tests/test_field_statistics.py @@ -0,0 +1,813 @@ +# import logging +from pathlib import Path + +import fmu.config.utilities as utils +import numpy as np +import pytest + +# import yaml +import xtgeo + +from subscript.field_statistics.field_statistics import ( + calc_stats, + check_disc_param_name_dict, + check_param_name_dict, + check_use_zones, + check_zone_conformity, + get_specifications, + set_subgrid_names, +) + +# logger = subscript.getLogger(__name__) +# logger.setLevel(logging.INFO) + +TESTDATA = Path(__file__).absolute().parent / "testdata_field_statistics" +ENS_PATH = Path(__file__).absolute().parent / "testdata_field_statistics" / "ensemble" +ERT_CONFIG_PATH = ( + Path(__file__).absolute().parent / "testdata_field_statistics" / "ert" / "model" +) +RESULT_PATH = Path("share/grid_statistics") + +CONFIG_DICT = { + "nreal": 10, + "iterations": [0, 3], + "use_zones": ["A", "B", "C"], + "zone_code_names": { + 1: "A", + 2: "B", + 3: "C", + }, + "zone_conformity": { + "A": "Top_conform", + "B": "Proportional", + "C": "Base_conform", + }, + "discrete_property_param_per_zone": { + "A": ["facies"], + "B": ["facies"], + "C": ["facies"], + }, + "continuous_property_param_per_zone": { + "A": ["P1", "P2"], + "B": ["P1"], + "C": ["P2"], + }, + "ertbox_size": [5, 6, 5], +} +GLOB_VAR_CFG_PATH = ERT_CONFIG_PATH / Path( + "../../fmuconfig/output/global_variables.yml" +) +CFG_GLOBAL = utils.yaml_load(GLOB_VAR_CFG_PATH)["global"] +KEYWORD = "FACIES_ZONE" +if KEYWORD in CFG_GLOBAL: + FACIES_PER_ZONE = CFG_GLOBAL[KEYWORD] +else: + raise KeyError(f"Missing keyword: {KEYWORD} in {GLOB_VAR_CFG_PATH}") + + +def make_box_grid(dimensions, grid_name, ens_path): + filename = ens_path / Path("share/grid_statistics") / Path(grid_name + ".roff") + grid = xtgeo.create_box_grid(dimensions) + grid.name = grid_name + print(f"Grid name: {grid.name}") + print(f"Grid dimensions: {grid.dimensions}") + print(f"Write grid to file: {filename}") + grid.to_file(filename, fformat="roff") + + +def make_file_names(ensemble_path, iter_number, real_number, param_name): + filedir = ensemble_path / Path("realization-" + str(real_number)) + if not filedir.exists(): + filedir.mkdir() + filedir = filedir / Path("iter-" + str(iter_number)) + if not filedir.exists(): + filedir.mkdir() + filedir = filedir / Path("share") + if not filedir.exists(): + filedir.mkdir() + filedir = filedir / Path("results") + if not filedir.exists(): + filedir.mkdir() + filedir = filedir / Path("grids") + if not filedir.exists(): + filedir.mkdir() + filename = filedir / Path("geogrid--" + param_name + ".roff") + filename_active = filedir / Path("geogrid--active.roff") + filename_grid = filedir / Path("geogrid.roff") + return filename, filename_active, filename_grid + + +def make_ensemble_test_data( + config_dict, facies_per_zone, nx, ny, nz_ertbox, ensemble_path +): + print("Start make test data") + + iteration_list = [0, 3] + zone_code_names = config_dict["zone_code_names"] + facies_per_zone = facies_per_zone + discrete_param_name_per_zone = config_dict["discrete_property_param_per_zone"] + param_name_per_zone = config_dict["continuous_property_param_per_zone"] + nreal = 10 + vparam = 1.0 + for iter_number in iteration_list: + for zone_number, zone_name in zone_code_names.items(): + param_name_list = [] + if zone_name in param_name_per_zone: + param_name_list = param_name_per_zone[zone_name] + disc_param_name_list = [] + if zone_name in discrete_param_name_per_zone: + disc_param_name_list = discrete_param_name_per_zone[zone_name] + if len(param_name_list): + for n, param_name in enumerate(param_name_list): + print(f"Param name: {param_name}") + for real_number in range(nreal): + values = np.ma.masked_all( + (nx, ny, 3 * nz_ertbox), dtype=np.float32 + ) + filename, filename_active, filename_grid = make_file_names( + ensemble_path, iter_number, real_number, param_name + ) + values = assign_values_continuous_param( + nz_ertbox, + nreal, + vparam * (n + 1), + iter_number, + len(iteration_list), + real_number, + values, + ) + xtgeo_param = xtgeo.GridProperty( + ncol=nx, + nrow=ny, + nlay=3 * nz_ertbox, + discrete=False, + values=values, + name=param_name, + ) + xtgeo_param.to_file(filename, fformat="roff") + + active = ~values.mask + xtgeo_active = xtgeo.GridProperty( + ncol=nx, + nrow=ny, + nlay=3 * nz_ertbox, + discrete=False, + values=active, + name=param_name, + ) + xtgeo_active.to_file(filename_active, fformat="roff") + + if len(disc_param_name_list): + for param_name in disc_param_name_list: + print(f"Param name: {param_name}") + for real_number in range(nreal): + values = np.ma.masked_all( + (nx, ny, 3 * nz_ertbox), dtype=np.uint8 + ) + filename, filename_active, filename_grid = make_file_names( + ensemble_path, iter_number, real_number, param_name + ) + + values, code_names = assign_values_discrete_param( + nz_ertbox, + facies_per_zone, + zone_code_names, + real_number, + values, + ) + xtgeo_param = xtgeo.GridProperty( + ncol=nx, + nrow=ny, + nlay=3 * nz_ertbox, + discrete=True, + values=values, + codes=code_names, + name=param_name, + ) + xtgeo_param.to_file(filename, fformat="roff") + + active = ~values.mask + xtgeo_active = xtgeo.GridProperty( + ncol=nx, + nrow=ny, + nlay=3 * nz_ertbox, + discrete=False, + values=active, + name=param_name, + ) + xtgeo_active.to_file(filename_active, fformat="roff") + + # The geogrid is here not realization dependent + # but need to be saved for each realization anyway + xtgeo_geogrid = xtgeo.create_box_grid((nx, ny, 3 * nz_ertbox)) + subgrid_dict = { + "A": nz_ertbox, + "B": nz_ertbox, + "C": nz_ertbox, + } + + xtgeo_geogrid.set_actnum(xtgeo_active) + set_subgrid_names(xtgeo_geogrid, new_subgrids=subgrid_dict) + xtgeo_geogrid.to_file(filename_grid, fformat="roff") + print( + "Finished make test data for ensemble for zone " + f"{zone_name} for iteration {iter_number}" + ) + + +def assign_values_continuous_param( + nz, nreal, vparam, iter_number, niter, real_number, values +): + layer_values = np.ma.masked_all(3 * nz, dtype=np.float32) + for k in range(3 * nz): + if real_number < (nreal - 1): + if 0 <= k <= (nz - 2): + # Zone 1 Top conform (layer 0,..,nz-1) + # Bottom layer of zone is inactive for most realizations + layer_values[k] = ( + 1.0 + * vparam + * k + * (real_number + 1) + / nreal + * (iter_number + 1) + / niter + ) + elif (2 * nz + 1) <= k <= (3 * nz - 1): + # Zone 3 Base conform (layer nz,..,2*nz-1) + # Top layer of zone is inactive for most realizations + layer_values[k] = ( + 3.0 + * vparam + * k + * (real_number + 1) + / nreal + * (iter_number + 1) + / niter + ) + elif nz <= k <= (2 * nz - 1): + # Zone 2 Proportional (layer nz,.. 2*nz-1) + # All layer of zone is active + layer_values[k] = ( + 2.0 + * vparam + * k + * (real_number + 1) + / nreal + * (iter_number + 1) + / niter + ) + else: + # For 1 realizations, fill all layers + if 0 <= k <= (nz - 1): + # Zone 1 Top conform (layer 0,..,nz-1) + # Bottom layer of zone is active for some realizations + layer_values[k] = ( + 1.0 + * vparam + * k + * (real_number + 1) + / nreal + * (iter_number + 1) + / niter + ) + elif (2 * nz) <= k <= (3 * nz - 1): + # Zone 3 Base conform (layer nz,..,2*nz-1) + # Top layer of zone is active for some realizations + layer_values[k] = ( + 3.0 + * vparam + * k + * (real_number + 1) + / nreal + * (iter_number + 1) + / niter + ) + elif nz <= k <= (2 * nz - 1): + # Zone 2 Proportional (layer nz,.. 2*nz-1) + # All layer of zone is active + layer_values[k] = ( + 2.0 + * vparam + * k + * (real_number + 1) + / nreal + * (iter_number + 1) + / niter + ) + for k in range(3 * nz): + values[:, :, k] = layer_values[k] + return values + + +def assign_values_discrete_param( + nz, facies_per_zone, zone_code_names, real_number, values +): + # Test data made for nz = 5 and nreal = 10 + nreal = 10 + assert real_number < 10 + assert nz * 3 == 15 + facies_code_per_layer_per_realization = [ + [1, 1, 3, 3, 2, 2, 1, 3, 1, 2], + [2, 3, 1, 1, 2, 1, 2, 3, 1, 1], + [2, 1, 2, 2, 2, 2, 1, 3, 3, 3], + [1, 1, 1, 3, 2, 1, 1, 3, 3, 3], + [3, 3, 1, 3, 2, 2, 1, 3, 1, 3], + [3, 3, 1, 3, 1, 2, 2, 3, 2, 1], + [3, 3, 3, 1, 2, 3, 1, 3, 1, 1], + [3, 3, 1, 3, 2, 2, 1, 3, 1, 1], + [2, 3, 1, 3, 1, 3, 3, 3, 1, 2], + [1, 3, 1, 1, 3, 2, 1, 3, 2, 3], + [2, 3, 1, 3, 3, 1, 3, 3, 1, 3], + [1, 1, 2, 3, 3, 1, 1, 3, 1, 2], + [3, 3, 2, 3, 2, 1, 1, 3, 1, 2], + [3, 3, 1, 3, 2, 2, 2, 3, 2, 1], + [3, 3, 1, 3, 2, 2, 1, 3, 1, 2], + ] + + all_code_names = {} + for zone_name in zone_code_names.values(): + code_names = facies_per_zone[zone_name] + for code, name in code_names.items(): + if code not in all_code_names: + all_code_names[code] = name + + for code, name in all_code_names.items(): + assert code in [1, 2, 3] + + for k in range(nz * 3): + if real_number < (nreal - 1): + if 0 <= k <= (nz - 2): + # Zone 1 Top conform (layer 0,..,nz-1) + # Bottom layer of zone is inactive for most realizations + values[:, :, k] = facies_code_per_layer_per_realization[k][real_number] + elif (2 * nz + 1) <= k <= (3 * nz - 1): + # Zone 3 Base conform (layer nz,..,2*nz-1) + # Top layer of zone is inactive for most realizations + values[:, :, k] = facies_code_per_layer_per_realization[k][real_number] + elif nz <= k <= (2 * nz - 1): + # Zone 2 Proportional (layer nz,.. 2*nz-1) + # All layer of zone is active' + values[:, :, k] = facies_code_per_layer_per_realization[k][real_number] + else: + # For 1 realizations, fill all layers + values[:, :, k] = facies_code_per_layer_per_realization[k][real_number] + + return values, all_code_names + + +def compare_with_referencedata(ens_path, result_path): + lines = [] + file_list = Path(ens_path) / Path(result_path) / Path("referencedata/files.txt") + with open(file_list, "r") as file: + lines = file.readlines() + is_ok = [] + print_check = False + ncount = 0 + for nameinput in lines: + name = nameinput.strip() + words = name.split("_") + if words[0] in ["mean", "stdev", "prob"]: + fullfilename = Path(ens_path) / Path(result_path) / Path(name) + reference_filename = ( + Path(ens_path) / Path(result_path) / Path("referencedata") / Path(name) + ) + + grid_property = xtgeo.gridproperty_from_file(fullfilename, fformat="roff") + grid_property_reference = xtgeo.gridproperty_from_file( + reference_filename, fformat="roff" + ) + values = grid_property.values + ref_values = grid_property_reference.values + ncount += 1 + if np.ma.allequal(values, ref_values): + if print_check: + print(f" {name} OK") + is_ok.append(True) + else: + if print_check: + print(f" {name} Failed") + print(f"Not equal to reference for {fullfilename}") + is_ok.append(False) + + is_success = True + for i in range(ncount): + if not is_ok[i]: + is_success = False + return is_success + + +@pytest.mark.parametrize( + "config_dict, ens_path, facies_per_zone, result_path", + [(CONFIG_DICT, ENS_PATH, FACIES_PER_ZONE, RESULT_PATH)], +) +def test_calc_statistics(config_dict, ens_path, facies_per_zone, result_path): + """Main test script""" + + (nx, ny, nz) = config_dict["ertbox_size"] + + # Write file with ERTBOX grid for the purpose to import to visualize + # the test data in e.g. RMS. Saved in share directory at + # top of ensemble directory + make_box_grid((nx, ny, nz), "ERTBOX", ens_path) + + # Write file with geogrid for the purpose to import to visualize + # the test data in e.g. RMS". Geogrid for the test data has 3 zones, + # each with 5 layers. Saved in share directory at top of ensemble directory + make_box_grid((nx, ny, nz * 3), "Geogrid", ens_path) + + # Make ensemble of test data + make_ensemble_test_data(config_dict, facies_per_zone, nx, ny, nz, ens_path) + + # Run the calculations of mean, stdev, prob + calc_stats(config_dict, ens_path, facies_per_zone, result_path) + + # Check that the result is equal to reference data set + assert compare_with_referencedata(ens_path, result_path) + + +@pytest.mark.parametrize( + "zone_code_names, zone_names_used, zone_conformity, expected_error", + [ + ( + { + 1: "A", + 2: "B", + 3: "C", + }, + ["A", "B"], + { + "A": "Top_conform", + "B": "Proportional", + "D": "Base_conform", + }, + "Unknown zone names in keyword 'zone_conformity'.", + ), + ( + { + 1: "A", + 2: "B", + 3: "C", + }, + ["A", "B", "C"], + { + "A": "Top_conform", + "B": "Proportional", + "C": "BaseConform", + }, + "Undefined zone conformity specified " + "(Must be Top_conform, Base_conform or Proportional).", + ), + ( + { + 1: "A", + 2: "B", + 3: "C", + }, + ["A", "B"], + { + "A": "Top_conform", + "C": "Base_conform", + }, + "is missing in keyword 'zone_conformity'.", + ), + ], +) +def test_zone_conformity( + zone_code_names, zone_names_used, zone_conformity, expected_error +): + if expected_error is not None: + with pytest.raises(ValueError) as validation_error: + check_zone_conformity(zone_code_names, zone_names_used, zone_conformity) + assert expected_error in str(validation_error) + + +@pytest.mark.parametrize( + "zone_code_names, param_name_dict, expected_error", + [ + ( + { + 1: "A", + 2: "B", + 3: "C", + }, + { + "A": ["P1", "P2"], + "B": ["P1"], + "D": ["P2"], + }, + "Unknown zone name in specification of keyword " + "'continuous_property_param_per_zone'.", + ), + ( + { + 1: "A", + 2: "B", + 3: "C", + }, + { + "A": ["P1", "P2"], + "B": None, + "C": ["P2"], + }, + "Missing list of property names for a specified zone " + "in keyword 'continuous_property_param_per_zone'.", + ), + ( + { + 1: "A", + 2: "B", + 3: "C", + }, + { + "A": ["P1", "P2"], + "B": [], + "C": ["P2"], + }, + "Missing list of property names for a specified zone in " + "keyword 'continuous_property_param_per_zone'.", + ), + ], +) +def test_param_name_dict(zone_code_names, param_name_dict, expected_error): + if expected_error is not None: + with pytest.raises(ValueError) as validation_error: + check_param_name_dict(zone_code_names, param_name_dict) + assert expected_error in str(validation_error) + + +@pytest.mark.parametrize( + "zone_code_names, disc_param_name_dict, expected_error", + [ + ( + { + 1: "A", + 2: "B", + 3: "C", + }, + { + "A": ["facies"], + "B": ["facies"], + "D": ["facies"], + }, + "Unknown zone name in specification of keyword " + "'discrete_property_param_per_zone'.", + ), + ( + { + 1: "A", + 2: "B", + 3: "C", + }, + { + "A": ["facies"], + "B": None, + "C": ["facies"], + }, + "Missing list of property names for a specified zone " + "in keyword 'discrete_property_param_per_zone'.", + ), + ( + { + 1: "A", + 2: "B", + 3: "C", + }, + { + "A": ["facies"], + "B": [], + "C": ["facies"], + }, + "Missing list of property names for a specified zone " + "in keyword 'discrete_property_param_per_zone'.", + ), + ], +) +def test_disc_param_name_dict(zone_code_names, disc_param_name_dict, expected_error): + if expected_error is not None: + with pytest.raises(ValueError) as validation_error: + check_disc_param_name_dict(zone_code_names, disc_param_name_dict) + assert expected_error in str(validation_error) + + +@pytest.mark.parametrize( + "zone_code_names, zone_names, expected_error", + [ + ( + { + 1: "A", + 2: "B", + 3: "C", + }, + ["A", "D"], + "Unknown zone name in specification of keyword 'use_zones'.", + ), + ], +) +def test_check_use_zones_errors(zone_code_names, zone_names, expected_error): + if expected_error is not None: + with pytest.raises(ValueError) as validation_error: + check_use_zones(zone_code_names, zone_names) + assert expected_error in str(validation_error) + + +CONFIG_DICT_REF = { + "nreal": 10, + "iterations": [0, 3], + "use_zones": ["A", "B", "C"], + "zone_code_names": { + 1: "A", + 2: "B", + 3: "C", + }, + "zone_conformity": { + "A": "Top_conform", + "B": "Proportional", + "C": "Base_conform", + }, + "discrete_property_param_per_zone": { + "A": ["facies"], + "B": ["facies"], + "C": ["facies"], + }, + "continuous_property_param_per_zone": { + "A": ["P1", "P2"], + "B": ["P1"], + "C": ["P2"], + }, + "ertbox_size": [5, 6, 5], + "use_population_stdev": False, +} + +CONFIG_A = { + "nreal": 10, + "iterations": [0], + "zone_code_names": { + 1: "A", + 2: "B", + }, + "zone_conformity": { + "A": "Base_conform", + "B": "Top_conform", + }, + "discrete_property_param_per_zone": { + "A": ["facies"], + }, + "continuous_property_param_per_zone": { + "A": ["P1", "P2"], + "B": ["P1"], + }, + "ertbox_size": [50, 60, 50], +} + +CONFIG_A_REF = { + "nreal": 10, + "iterations": [0], + "zone_code_names": { + 1: "A", + 2: "B", + }, + "zone_conformity": { + "A": "Base_conform", + "B": "Top_conform", + }, + "discrete_property_param_per_zone": { + "A": ["facies"], + }, + "continuous_property_param_per_zone": { + "A": ["P1", "P2"], + "B": ["P1"], + }, + "ertbox_size": [50, 60, 50], + "zone_names": ["A", "B"], + "use_population_stdev": False, + "use_zones": ["A", "B"], +} + +CONFIG_B = { + "nreal": 10, + "iterations": [0], + "zone_code_names": { + 1: "A", + 2: "B", + 3: "C", + 4: "D", + }, + "use_zones": ["B", "D"], + "zone_conformity": { + "B": "Top_conform", + "D": "Proportional", + }, + "continuous_property_param_per_zone": { + "D": ["P1", "P2"], + "B": ["P1"], + }, + "ertbox_size": [10, 6, 15], + "use_population_stdev": True, +} + +CONFIG_B_REF = { + "nreal": 10, + "iterations": [0], + "zone_code_names": { + 1: "A", + 2: "B", + 3: "C", + 4: "D", + }, + "zone_conformity": { + "B": "Top_conform", + "D": "Proportional", + }, + "continuous_property_param_per_zone": { + "D": ["P1", "P2"], + "B": ["P1"], + }, + "discrete_property_param_per_zone": None, + "ertbox_size": [10, 6, 15], + "use_zones": ["B", "D"], + "use_population_stdev": True, +} + +CONFIG_C = { + "nreal": 10, + "iterations": [0], + "zone_code_names": { + 1: "A", + 2: "B", + 3: "C", + 4: "D", + }, + "zone_conformity": { + "A": "Base_conform", + "B": "Top_conform", + "C": "Top_conform", + "D": "Proportional", + }, + "discrete_property_param_per_zone": { + "A": ["facies1", "facies2"], + "D": ["facies1", "facies2"], + "B": ["facies3"], + "C": ["facies1", "facies2"], + }, + "ertbox_size": [10, 6, 15], + "use_population_stdev": True, +} + +CONFIG_C_REF = { + "nreal": 10, + "iterations": [0], + "zone_code_names": { + 1: "A", + 2: "B", + 3: "C", + 4: "D", + }, + "zone_conformity": { + "A": "Base_conform", + "B": "Top_conform", + "C": "Top_conform", + "D": "Proportional", + }, + "discrete_property_param_per_zone": { + "A": ["facies1", "facies2"], + "D": ["facies1", "facies2"], + "C": ["facies1", "facies2"], + "B": ["facies3"], + }, + "property_param_per_zone": None, + "ertbox_size": [10, 6, 15], + "use_zones": ["A", "B", "C", "D"], + "use_population_stdev": True, +} + + +@pytest.mark.parametrize( + "input_dict, reference_dict", + [ + (CONFIG_DICT, CONFIG_DICT_REF), + (CONFIG_A, CONFIG_A_REF), + (CONFIG_B, CONFIG_B_REF), + ], +) +def test_get_specification(input_dict, reference_dict): + ( + ertbox_size, + nreal, + iter_list, + zone_names, + zone_conformity, + zone_code_names, + use_population_stdev, + param_name_dict, + disc_param_name_dict, + ) = get_specifications(input_dict) + assert ertbox_size == reference_dict["ertbox_size"] + assert zone_names == reference_dict["use_zones"] + assert nreal == reference_dict["nreal"] + assert iter_list == reference_dict["iterations"] + assert zone_conformity == reference_dict["zone_conformity"] + assert zone_code_names == reference_dict["zone_code_names"] + assert use_population_stdev == reference_dict["use_population_stdev"] + assert param_name_dict == reference_dict["continuous_property_param_per_zone"] + assert disc_param_name_dict == reference_dict["discrete_property_param_per_zone"] diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/ERTBOX.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/ERTBOX.roff new file mode 100644 index 000000000..8a29f2654 Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/ERTBOX.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/Geogrid.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/Geogrid.roff new file mode 100644 index 000000000..354e6271f Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/Geogrid.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/files.txt b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/files.txt new file mode 100644 index 000000000..ca85224f4 --- /dev/null +++ b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/files.txt @@ -0,0 +1,60 @@ +nactive_A_0.roff +prob_A_F3_0.roff +prob_A_F2_0.roff +prob_A_F1_0.roff + +nactive_A_3.roff +prob_A_F3_3.roff +prob_A_F2_3.roff +prob_A_F1_3.roff + +nactive_B_0.roff +prob_B_F3_0.roff +prob_B_F2_0.roff +prob_B_F1_0.roff + +nactive_B_3.roff +prob_B_F3_3.roff +prob_B_F2_3.roff +prob_B_F1_3.roff + +nactive_C_0.roff +prob_C_F3_0.roff +prob_C_F2_0.roff +prob_C_F1_0.roff + +nactive_C_3.roff +prob_C_F3_3.roff +prob_C_F2_3.roff +prob_C_F1_3.roff + +stdev_A_P1_0.roff +mean_A_P1_0.roff + +stdev_A_P2_0.roff +mean_A_P2_0.roff + +stdev_A_P1_3.roff +mean_A_P1_3.roff + +stdev_A_P2_3.roff +mean_A_P2_3.roff + +stdev_B_P1_0.roff +mean_B_P1_0.roff + +stdev_B_P1_3.roff +mean_B_P1_3.roff + +stdev_C_P2_0.roff +mean_C_P2_0.roff + +stdev_C_P2_3.roff +mean_C_P2_3.roff + + + + +Geogrid.roff + + diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_A_P1_0.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_A_P1_0.roff new file mode 100644 index 000000000..975f8f497 Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_A_P1_0.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_A_P1_3.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_A_P1_3.roff new file mode 100644 index 000000000..01da7ca18 Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_A_P1_3.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_A_P2_0.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_A_P2_0.roff new file mode 100644 index 000000000..eed38b9c1 Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_A_P2_0.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_A_P2_3.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_A_P2_3.roff new file mode 100644 index 000000000..17a197d24 Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_A_P2_3.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_B_P1_0.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_B_P1_0.roff new file mode 100644 index 000000000..8dc17714a Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_B_P1_0.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_B_P1_3.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_B_P1_3.roff new file mode 100644 index 000000000..a9079036a Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_B_P1_3.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_C_P2_0.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_C_P2_0.roff new file mode 100644 index 000000000..f9e7adf47 Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_C_P2_0.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_C_P2_3.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_C_P2_3.roff new file mode 100644 index 000000000..747e19ce1 Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_C_P2_3.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_A_0.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_A_0.roff new file mode 100644 index 000000000..c690c4d69 Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_A_0.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_A_3.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_A_3.roff new file mode 100644 index 000000000..cf4dbfed5 Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_A_3.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_B_0.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_B_0.roff new file mode 100644 index 000000000..fcd43abeb Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_B_0.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_B_3.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_B_3.roff new file mode 100644 index 000000000..6b9a4b4e9 Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_B_3.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_C_0.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_C_0.roff new file mode 100644 index 000000000..71253c00d Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_C_0.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_C_3.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_C_3.roff new file mode 100644 index 000000000..fe12da78e Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_C_3.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F1_0.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F1_0.roff new file mode 100644 index 000000000..3444f5034 Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F1_0.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F1_3.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F1_3.roff new file mode 100644 index 000000000..7de49009b Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F1_3.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F2_0.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F2_0.roff new file mode 100644 index 000000000..eaa3cc52a Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F2_0.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F2_3.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F2_3.roff new file mode 100644 index 000000000..ddee2fa9e Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F2_3.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F3_0.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F3_0.roff new file mode 100644 index 000000000..a2405ac1b Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F3_0.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F3_3.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F3_3.roff new file mode 100644 index 000000000..8a9d76b7c Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F3_3.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F1_0.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F1_0.roff new file mode 100644 index 000000000..c6ae7ffd7 Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F1_0.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F1_3.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F1_3.roff new file mode 100644 index 000000000..abe473228 Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F1_3.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F2_0.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F2_0.roff new file mode 100644 index 000000000..99ea631ae Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F2_0.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F2_3.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F2_3.roff new file mode 100644 index 000000000..600b3dfae Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F2_3.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F3_0.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F3_0.roff new file mode 100644 index 000000000..5b2a2546e Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F3_0.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F3_3.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F3_3.roff new file mode 100644 index 000000000..0bfcba2cd Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F3_3.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F1_0.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F1_0.roff new file mode 100644 index 000000000..1b43de54e Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F1_0.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F1_3.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F1_3.roff new file mode 100644 index 000000000..2a9a66d44 Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F1_3.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F2_0.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F2_0.roff new file mode 100644 index 000000000..11a574f69 Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F2_0.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F2_3.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F2_3.roff new file mode 100644 index 000000000..7af1c2b4e Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F2_3.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F3_0.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F3_0.roff new file mode 100644 index 000000000..444b9c015 Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F3_0.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F3_3.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F3_3.roff new file mode 100644 index 000000000..09ec45601 Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F3_3.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_A_P1_0.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_A_P1_0.roff new file mode 100644 index 000000000..dabe78467 Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_A_P1_0.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_A_P1_3.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_A_P1_3.roff new file mode 100644 index 000000000..68a872e55 Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_A_P1_3.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_A_P2_0.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_A_P2_0.roff new file mode 100644 index 000000000..47646404c Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_A_P2_0.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_A_P2_3.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_A_P2_3.roff new file mode 100644 index 000000000..be6a09e9d Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_A_P2_3.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_B_P1_0.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_B_P1_0.roff new file mode 100644 index 000000000..973d70e08 Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_B_P1_0.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_B_P1_3.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_B_P1_3.roff new file mode 100644 index 000000000..093a6ad9a Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_B_P1_3.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_C_P2_0.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_C_P2_0.roff new file mode 100644 index 000000000..c5ae102f0 Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_C_P2_0.roff differ diff --git a/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_C_P2_3.roff b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_C_P2_3.roff new file mode 100644 index 000000000..cc810a1ef Binary files /dev/null and b/tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_C_P2_3.roff differ diff --git a/tests/testdata_field_statistics/ert/model/0readme b/tests/testdata_field_statistics/ert/model/0readme new file mode 100644 index 000000000..9760401f9 --- /dev/null +++ b/tests/testdata_field_statistics/ert/model/0readme @@ -0,0 +1 @@ +This directory is empty. Only used as part of file paths. \ No newline at end of file diff --git a/tests/testdata_field_statistics/fmuconfig/output/global_variables.yml b/tests/testdata_field_statistics/fmuconfig/output/global_variables.yml new file mode 100644 index 000000000..df2f0d991 --- /dev/null +++ b/tests/testdata_field_statistics/fmuconfig/output/global_variables.yml @@ -0,0 +1,18 @@ +# Autogenerated from global configuration. +# DO NOT EDIT THIS FILE MANUALLY! +# Machine st-lintgx0053.st.statoil.no by user olia, at 2024-08-26 20:07:32.033241, using fmu.config ver. 1.1.0 +global: + FACIES_ZONE: + A: + 1: F1 + 2: F2 + 3: F3 + B: + 1: F1 + 2: F2 + 3: F3 + C: + 1: F1 + 2: F2 + 3: F3 +