From 6a6cd89c91b673bfdecf608e0fed65389861f860 Mon Sep 17 00:00:00 2001 From: "Oddvar Lia (ST MSU GEO)" Date: Fri, 13 Sep 2024 13:25:28 +0200 Subject: [PATCH] Add script to calculate mean, stdev for continuous 3D field parameters and estimated facies probabilities for discrete 3D parameter --- pyproject.toml | 2 + .../WF_FIELD_PARAM_STATISTICS | 21 + src/subscript/field_statistics/__init__.py | 0 .../field_statistics/field_param_stat.yml | 59 ++ .../field_statistics/field_statistics.py | 856 ++++++++++++++++++ .../wf_field_param_statistics | 1 + tests/test_field_statistics.py | 813 +++++++++++++++++ .../grid_statistics/referencedata/ERTBOX.roff | Bin 0 -> 6000 bytes .../referencedata/Geogrid.roff | Bin 0 -> 13440 bytes .../grid_statistics/referencedata/files.txt | 60 ++ .../referencedata/mean_A_P1_0.roff | Bin 0 -> 938 bytes .../referencedata/mean_A_P1_3.roff | Bin 0 -> 938 bytes .../referencedata/mean_A_P2_0.roff | Bin 0 -> 938 bytes .../referencedata/mean_A_P2_3.roff | Bin 0 -> 938 bytes .../referencedata/mean_B_P1_0.roff | Bin 0 -> 938 bytes .../referencedata/mean_B_P1_3.roff | Bin 0 -> 938 bytes .../referencedata/mean_C_P2_0.roff | Bin 0 -> 938 bytes .../referencedata/mean_C_P2_3.roff | Bin 0 -> 938 bytes .../referencedata/nactive_A_0.roff | Bin 0 -> 938 bytes .../referencedata/nactive_A_3.roff | Bin 0 -> 938 bytes .../referencedata/nactive_B_0.roff | Bin 0 -> 938 bytes .../referencedata/nactive_B_3.roff | Bin 0 -> 938 bytes .../referencedata/nactive_C_0.roff | Bin 0 -> 938 bytes .../referencedata/nactive_C_3.roff | Bin 0 -> 938 bytes .../referencedata/prob_A_F1_0.roff | Bin 0 -> 938 bytes .../referencedata/prob_A_F1_3.roff | Bin 0 -> 938 bytes .../referencedata/prob_A_F2_0.roff | Bin 0 -> 938 bytes .../referencedata/prob_A_F2_3.roff | Bin 0 -> 938 bytes .../referencedata/prob_A_F3_0.roff | Bin 0 -> 938 bytes .../referencedata/prob_A_F3_3.roff | Bin 0 -> 938 bytes .../referencedata/prob_B_F1_0.roff | Bin 0 -> 938 bytes .../referencedata/prob_B_F1_3.roff | Bin 0 -> 938 bytes .../referencedata/prob_B_F2_0.roff | Bin 0 -> 938 bytes .../referencedata/prob_B_F2_3.roff | Bin 0 -> 938 bytes .../referencedata/prob_B_F3_0.roff | Bin 0 -> 938 bytes .../referencedata/prob_B_F3_3.roff | Bin 0 -> 938 bytes .../referencedata/prob_C_F1_0.roff | Bin 0 -> 938 bytes .../referencedata/prob_C_F1_3.roff | Bin 0 -> 938 bytes .../referencedata/prob_C_F2_0.roff | Bin 0 -> 938 bytes .../referencedata/prob_C_F2_3.roff | Bin 0 -> 938 bytes .../referencedata/prob_C_F3_0.roff | Bin 0 -> 938 bytes .../referencedata/prob_C_F3_3.roff | Bin 0 -> 938 bytes .../referencedata/stdev_A_P1_0.roff | Bin 0 -> 939 bytes .../referencedata/stdev_A_P1_3.roff | Bin 0 -> 939 bytes .../referencedata/stdev_A_P2_0.roff | Bin 0 -> 939 bytes .../referencedata/stdev_A_P2_3.roff | Bin 0 -> 939 bytes .../referencedata/stdev_B_P1_0.roff | Bin 0 -> 939 bytes .../referencedata/stdev_B_P1_3.roff | Bin 0 -> 939 bytes .../referencedata/stdev_C_P2_0.roff | Bin 0 -> 939 bytes .../referencedata/stdev_C_P2_3.roff | Bin 0 -> 939 bytes .../ert/model/0readme | 1 + .../fmuconfig/output/global_variables.yml | 18 + 52 files changed, 1831 insertions(+) create mode 100644 src/subscript/field_statistics/WF_FIELD_PARAM_STATISTICS create mode 100644 src/subscript/field_statistics/__init__.py create mode 100644 src/subscript/field_statistics/field_param_stat.yml create mode 100644 src/subscript/field_statistics/field_statistics.py create mode 100644 src/subscript/field_statistics/wf_field_param_statistics create mode 100644 tests/test_field_statistics.py create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/ERTBOX.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/Geogrid.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/files.txt create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_A_P1_0.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_A_P1_3.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_A_P2_0.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_A_P2_3.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_B_P1_0.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_B_P1_3.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_C_P2_0.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/mean_C_P2_3.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_A_0.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_A_3.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_B_0.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_B_3.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_C_0.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/nactive_C_3.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F1_0.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F1_3.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F2_0.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F2_3.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F3_0.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_A_F3_3.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F1_0.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F1_3.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F2_0.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F2_3.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F3_0.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_B_F3_3.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F1_0.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F1_3.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F2_0.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F2_3.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F3_0.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/prob_C_F3_3.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_A_P1_0.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_A_P1_3.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_A_P2_0.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_A_P2_3.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_B_P1_0.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_B_P1_3.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_C_P2_0.roff create mode 100644 tests/testdata_field_statistics/ensemble/share/grid_statistics/referencedata/stdev_C_P2_3.roff create mode 100644 tests/testdata_field_statistics/ert/model/0readme create mode 100644 tests/testdata_field_statistics/fmuconfig/output/global_variables.yml 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 0000000000000000000000000000000000000000..8a29f26549e6a2a468e71bc2834a1a57e5d56173 GIT binary patch literal 6000 zcmeI0y^hmB6ov2d*YOUK0v+roSt!_sk$|m`ke~rNZ{jRQS$k!hg+v0)3v5e0kH8zO zTWEO^9)N;#$G*~KzX}-|zs_voj0Vm1pqLfSMtdBd% za#Xq~zQ|6}T&JvJCVwthyLkRWu9jU%!d_uI1*%fBF0j+(o|iu@AWK^5>lw|Ka^7_z?NlYoD*+P1nb~_;wb-(m8dm zxP0r|Ik&#gJIccwZ|7|D>WzDN>#yE;Ag^ZBEU^-{5f{Ue)Kd z_7JWPGOpgZC$8RjAgrzfu7@`1Q| z;|+23%DrB(w7+@dHm7o%GiFftd3eh^;_8j-cPt;j@7reu;_5Bm5Ld4}-iOsb=8fB& z%5Bb=LEY!!E$@h{H|~k6Hy((qH{K9eFP{DzHfPD|&7b}kzFkIALi4jcYL-LI64Wez zfjmiPVg=+`9yQCMW(jH*_g2rnn;7pi2JjRaQUBH=^(RuKdS~-hCwV eIy+0dSE5Hnv6A>k^doj?=bv0Ods-~*MfDGew0Xq< literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..354e6271fb56400d324b390a0eca2a76a565bb0a GIT binary patch literal 13440 zcmeI3L2e^O6oy|$zyb;3BeWFQ!AvIu3uZ$}z^st4U<0h&lXM0n$8OopKq7(a0&|Bx z0ymf%cCh6FJ_rlG*LK-|7!CkZwe-dBd;MIlyzZ*r*z48R)%#E8OFRAYi^q?vtNEfm zwbM`5ZPTyTkE%7N=d1UsXYG13UoES%`)Bv-Q|p_j=B1ZS-`ITF+moBV-Tc^e{O#?; z%r3rb)^_2;@U~BzzP0m*59*H|oY&_cRA&z#eONy_f4Hmif%==SwWsU(rM1gTAKlr1 z==cHG&G)Od-D70`xa;{6^Fzun=hyAhXSo?`mS5ZJ`|{;C_C^lBwYTrStzS3G&BAAR zwOBR1{lve#X?u!npSgJ@b1NB{!RDe_9ERD6S^Ri6?#(|Fw}-?F8RKHLUbgGc=gW3u z&3fJ3>?bj{`mb?T`A>YABVObA+4Fb&)8OSX_WWD#az5GK+oxQlo5JjRFmAoavKznkZ--s7B) z#Cd#=bAFJ#$2p%Q@3H62UpdBphA}+k^E|y~{2wCciAVST@8dj9!1FjB=X{jB$2mVp z-s7B4lK0s2!CyIi{2b?eo~P&YJjWXMTu+?q$GUxftc$xbqtttx>j%kuobyTY9(z9e zD~Eq>j&nZG)AM`2KzU`2NT#&JU2s@i^Bf$$RYi@w|tOljEGv z^Ynb4=UC&O>xpxHki5q^A0_W`&JU9JIOmh(J+>%&!|k(X@x#CE3-L`Vtz%xw3|FSO z5^$x)l?qoTxMH}nO=e2Ml>t{Gt^`~$T!}BJ7rbXm$9$9-u1s+y;7W}v6|PKh#c*Yt z%KxuDNc2M)a3$hOz!k$4z4jjKwfDH=dMGnonc_;ol^RznT$$jC;mS6Z>#sdX^g|hN zCE`lJ6~mQs{pnZP{|-GU>!0-zk96!u$_!VgxDs%s#+3?JCb(j_vQ1_G(;g)Hp$xbZ zaV6l2;fk*P^8BMW@h%_T+WrizLTnV^R<4T1q6I?M|*`~7pX%7GiBCZ5nFiYozEYFw#sWr8b)E8A4=f9*k{AIg9$5my4P7_OB2pMI6+|DgwE{j)yek&gXH znc>P5R|2loxKiQD1Xm1KwyErY+Ji(tlmS;Ft^`~$T+y{(p8xbF-lgMyRA#s`#g%|7 zHLg^+GQkzYm2E2bzxE)}4`slWh${hC3|GqiPru6Z|ImZ7{#hUKNXLGp%y4CjD*;z( zT&Zwnf-8nA+f?>H?Lnd+%77~oR|2jWuISo-_4&X1vbcW{-!|Q1-hZ~dwc(!GYvm{% s_z!nLzTezj^z&!!{$~$QR;z`Lzk2xX#O$OT|ML#Wb#7Nz`G-4y1KqlxNB{r; literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..975f8f497b2a50298039c10a3ac02dbcba961e37 GIT binary patch literal 938 zcmeHF%TB{E5FC);#79`l71})LfgE@U)GG)sz$Mx?8!&3@Xk&r$51jcb_$O?)6*+UR zrM1@U*`2X9Vw>h%Xmg&VH}Et9FP xs=KU8K_WDuSy^(3kNyh0z-3W9T>N|=`S?6vmv*(?zeFee+Z!WavTe5R{soo93qb$? literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..01da7ca184d2180fd145cff3be056748a88e0928 GIT binary patch literal 938 zcmeHFu};G<5PcxQ#79KR3T+b8fh(Gy181v^KZP}d*ScjBr1lYG9&xmVn+w9!^3pC0LyZ`_I literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..eed38b9c12b2ec67ffc3a80d272cfd732a9119f5 GIT binary patch literal 938 zcmeHF%TB{E5FC);#79`l71})LfgE@U)GG)sz$Mx?8!&3@Xk&r$51jcb_$O?)6*+UR zrM1@U*`2X9Vw>h%Xmg&VH}Et9FP xs=K^OK_WDuSy^(3kNyh0z-3W9T>N|=`S?6vmv*(?zeFee+Z!WavTe5R{sovz3qk+@ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..17a197d24cb2d1622b850684d4e13c15894a62af GIT binary patch literal 938 zcmeHFu};G<5PcxQ#79KR3T+b8fh(Gy181v^KZP}d*ScjBr1lYG9&xmVn+w9!^3pY^ir>T;T^{36MUa;D0B_J@i@zHb>V2{r6lz_yMO zac`9em4@D&;5g7n&BoY}#WjN0rePaL?FNi}#K{(JpK!VhpNHzGZ7)3xZo?dCnC#+e xahEL=NQ8RmwIh$=(Vu}z>#yqP`=io&Qx(NS1*i}oYKK4gpASa9WT$uT{spi6H>>~v literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a9079036a0b9b1fcf97be07776233ce4b67b1c08 GIT binary patch literal 938 zcmeHF%TB{E5FC);J0j&u+dSxj+ybaq5L|#uv~AYKsIjAsRg{0=%#kmGf5Hx}nmgB8 zvbDROompEWR;kHS8^jNHi$$dLhGWDx!sM(Jk@#zEFQP3=r>%+7X*x|}I9ek>YjOl_ z9G1P~?wwkHI-CGly^>%RXnfl(IYySxlB-#kWS3Ez7xSdZ=6|R_I7I*e literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f9e7adf47f6296584705bb037fd52d8824b0c16b GIT binary patch literal 938 zcmeHF%TB{E5FC);&JUnSIRb4SRD#^1qFzCqD3@s4tih9jRbI!Px<3`b80(3%`U z8;4czxO=14uMP(Q*3TqZ2O8gYLynQ<)8u-ZCD~<^=EW>2ve_>x5V^i(v?SE9V*#5w zNQAjn9#k57v4TTSA2b_n{3(nPyw(kyV6_`C<^e}rxLM+Ow|yL{qqaZk!MO20&@h>5 yUfpF?0urGfYGcV^c=Wrc`u@5qKR?b&rB2H{KPiE>+}HMh@ZTJaa>-Wh()|PdSR!}; literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..747e19ce1326e173b209b132c47d5ff1e0b49400 GIT binary patch literal 938 zcmeHF%TB{E5FC);&JUnSIRb4SRDv8+)GLS+5hVB~_$Tbpirl%@ zk}Z2ZJ2SRMtWp;%Z4lqzE*6o}8;%jrg~?edBJtPSo=2}Nowg=QC+Q@K;phngT9YGa z1C)!_iZ`k4glK;zqP$T6~fnp{t_B)g2#yqG0LHv2^dBGaygGxg$R&eO)gJz?RKZP-Z*ScX7taby&Jm6>xH%lDvwvR(~)b=Mm7&qPr8YWZC ytGleqK_b*cZ7ewqkAC-5-(Oed=f_#8)M=ULCneC9``Z2w{+okQF4?MGx_cJuS`pUHwRG0` z{O-N8^~BfpWMdtY=cm=m)ON=Sk_TZ@FNKkCZT*eevvk%wlTEW}nn39t5n7QFtW#`y z2IYWtHO26tf&I4afddA(ex mAQ9?O?>s4nM}J3MeBA?P`9IFJ^S?w-obH2>FWJ{ecmDvX6snT| literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cf4dbfed514e2d194241537ef724c438422d4f54 GIT binary patch literal 938 zcmeHF%TB{E5FC);J1pf2Z65SME~oe3+WITAW9hATCYxomG=b6^BD5wYSf|(w z%KayGVON|1sNP9XMVip|K+4FAd3rm~)BMI{MY%}JeDRBlL^^bgj)YoP7O<$S0N&l$2uVbUO#k$COGjXALNE_hSSi+P?w=>rMckP@6%YzO83 zi@G=~E&$X!64Z$%wmp$D%H<-zTayH%Bynqi%LW~bc~LKT2>aYZ<0i+qw=WI z(q|A{M*5`LIv;mwOyG@c*(a<0i1DwuI>P-Mu20MN@pRJmCp{YX(FZ)$>JGel*yJD) d8esQ<6yu}6lP=8txz2y^-x-+Xl7l^^`v-SrpO63m literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6b9a4b4e95091045d25a49dd07d0504c4e05484a GIT binary patch literal 938 zcmeHF%TB{E5FC);J1pf2Z65SMZama05+~qNZId+^HFmUF73D|xUhL3{d;rHwry*AeEI79XzOd6y%60cpjGJBT61#gN)vB)zhy(2+eQiAh}?WjC_ zQ6Fc;8Gz=41WlreZI7gkvRdZ1%Q7!-Oi|UVye?P2s6?b=&*(|0V`TyRHc6y9C{HRK zeFnjKq)(ci^KqBP4BonqeX=@C82^Hc1Khvj^0<7PPG@a@(vxu?eZW(rZqJ+hO$8F6 d5q1|yF+KV_>B8Kd>huTyoq<^{IoM;me*k{;pOXLp literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..71253c00d7912cb23a362b88cd5933d37026d51d GIT binary patch literal 938 zcmeHFJx{|h5PcxQ@352=+9ahzvmjK|6^RL0s%vtgMvWa^PDR=H75rZ8(28_qucfo@ z%`af?Z!GJ&rhqBsqKyvBuimZFNKj%w*JQKSvu>T$tKw(O`!CS2(3s7)+si< za`#DXxD^)w$}I`XNE6x~Ng4TUnl7e!n%|jhR?O2PpZ}sFk@hX4C836u1?;LQ5$&Kn zs5JE91($(7YBtt|D4r3#vJJa9YBylq3$6}u_loPI`!-}pZGY0k;4aJoN2S_5ub1l- mBtkvvohQZc=2b9(joosBWGPJQr8E-C)?b+&OK-h1*({r-36$Otp*1PNI>lyC z?mwvux8e*y^-h8+(uB4LQbu0P)7yET<~JrQ%0*h{i(gbE(xGE?B-FC9fL$FWqV1JO zm6krd;5^Vr&DOdQ#WR7|wq+Mb?MIAz#>F1)UT}GEU&rjE?N53f+=V&ds8YM*^>ST+ mL});>^`sad{T+2-?vL&3?l_M1Q||N!|J#GfUb1fv+x-ItajKXA literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3444f5034e8d8a851eddda6fdba6b2dad02c3872 GIT binary patch literal 938 zcmeHF!A`?441FNMcU0;XwzgvjdH^9%Pe@3dfJ^1BE(EPf z*G_pf>k#e`m)maVqhj~x4gvmM>O;(YJEjnPTkL3(W5hCa}VtIE5w sNXrByLIaw$BaiXXKdQUM?eFICCb+?F)GCg#;~)Hc4kmNSuGvrbAKNMfUjP6A literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7de49009bb2ba0711b7aa20b83b43d23d9f10f8f GIT binary patch literal 938 zcmeHFO;5ux41FNM@2J!(Z2e#ddH^9%Pe@3dfXn2rE(EPfTlvzo`qm#|ePd2MJbMCZs)(XJq+2xt?c9b`_<0u}F$+v1Mr?KXi5r7gY?+A4Sk>!S1s?B tWxC8kA~c{`JMtJG{iC{D-2QGJZ-N`_My=u)JO07H=U_6I?3(>_{{iB;1Y!UH literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..eaa3cc52a05d6b47c446b632b68a1b704f1aa910 GIT binary patch literal 938 zcmeHFO-sW-5Pb^zI|8|Co22$&Kt+^X1y9yXm^2ezX?DY8QSwKG`kU&1a(CO(+`V>S z_G9MFyxHZ6X`0JQX~fS@MG-Y>#WCUsVX{W@NL*T(i|CW3S4KzaB%LHNEWN=)YqA8T zEvmtC|3#f!ElvO|-$}6aZCu$SSw=RSCfCy}$*!Vwmd}$sn{QFRksUflM?x)I7O<{8 zh4&qEU}@>Y2u@vl&)F*NZr(=lTD7eGsC~fb7o6?jdV%x(@ijz8We4goxOQ`(6H}G< tWs#K$NQ4G7D?=9H(chle+3#j^n>_4?NPF?Rfe|LS0*OE%43-G8ruHy!{0 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ddee2fa9e8ee76c20eda0011d5c0cd5799403956 GIT binary patch literal 938 zcmeHFO-sW-5Pb^zI|8|Cn-6<1pdw1Hf+zJ7Ce1`wn%yv2l>8B){-*k$++ACmyVnlP ze$2d?H@iGBZF^oTjrj4QETdMfI7WOYOx8#diAyVU7JaY`%IGMarL!c4rB`@pLzbYl zMLk*`KB;%B#Swtz8wr-ajVrq+%gFM1ayieE>>^6@Vv!Wt;t%B;*|BHzB-F8G0qe$7 zc;7MymX6+y;MleIoSo9{=4}FRRL9znIs}Y5q*KQ8+19ffXnrVnRf7Zp*u5(HQ3rrb_bjZBkr8d(O$_vTmC5N-Bg8_eBvjaw9^7H!R3#&H}d2QlACyf_te|5HI3I6vEI89JD4w zkjkJMjOah8v#Y@ofb}ae){e%e-I5VV(q(kHOrqo>h|_EpWy$In<%rDC33SBN3L_Xo z)lS0Aow8SH=}j|^ExprhrLs3SX7F0JLOH9w$EZh~?BMDNr~BixkIvHmq`h%veV}7q ymDgpFmJx`V1~eN@2L97uP8Xk7+iA+b$1&edcYZ&_#{5u&|J7jDmqIuD?*0Kp#D-S@ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8a9d76b7cad9762f9e445386fbe4b7aced648687 GIT binary patch literal 938 zcmeHF%SyyB6g>+19ffXnrVnRf7Zp*u5(HQ3rrb_bjZBkr8d(O$_vTmC5N-Bg8_eBvjaw9^7H!R3#&H}d2QlACyf_te|5HI3I6vEI89JD4w zkjkJMjOah8v#Y@ofb}ae){e%e-I5VV(q(kHOrqo>h|_EpWy$In<%rDC33SBN3L_Xo z)lS0Aow8SH=}j|^ExprhrLs3SX7F0JLOH9w$EZh~?BMDNr~BixkIvHmq`h%veV}7q ymDgpFmMMst1~eN@2L97uP8Xk7+iA+b$1&edcYZ&_#{5u&|J7jDmqIuD?*0KxQ-)dq literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c6ae7ffd728b792d4dd9dbb21fab81f8df4f1b9a GIT binary patch literal 938 zcmeHFK~BRk5FC);9hP#1Hc9D$9C`qhD-tK*5^b6d7&UgZu|W9IG4jPcy`JZ3exYs9VU!<#MvHho^XB$pQmh8_9s0xZhRl;#n#nL vRc2KR5}^_8#*)MI=uc1ez1yw6w%b+MBjd9h52Y`I5yB0Khso`gEKEMVPu z2|o|YlS)VLMlf^qUb9o$o%{&kjp|tYR)-0rpKyMF>t|dX!JFhjceBjdNFl% wTa{^*gG6XVyD?-jJ^IU0{p@zD@9lQw_b@)?PX7}6A&mdyjlh>|+M~OF0S>*+5dZ)H literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..99ea631ae67a252e6db3006602a4fa3d3f274aae GIT binary patch literal 938 zcmeHFO-sW-5Pb^zI|8}dc9YtJ0X?YXBnY0=OPDm1y3*{1$)e_E1wE+rBnY0=OPOw`8rddg8kGJKVf{_(e=>=?Y_Hxo zFkwF4yf+CWrftt_r4c_qlx5Va6~~Bgg~=KzB5`G9&Y};NK^YyTvviiku=ENKZO9Uo zwx~zT!zcCbwm1T?d?Ufq)3~-fvWzUBCztaq$u6QaFBVCWE&foR$c{auC!vll3s^T^ z!rN9ksC4vh1jmlvYj#Sz$e#(kQ5|bP>JTvc2`5{)e#Ys}eF@n~+g>^huH78y#njbJ uRc2KV5}^_8%8*5P^p~Uh+1y|6ZFBX#UQc3QJLC?3@c%uS>?NCax81*k6SCp} literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5b2a2546ec9dce550aeaa3a686506911f0f1cbf4 GIT binary patch literal 938 zcmeHFOHRZv41FNMIU;onGyO0Nx)>xt-6F99HkCVdA!wRZZUpK@I90h5Ntuc+Ilx3p z?8MLSIhMsX&1IzxqQ{4#P>tSmgy>F~oRwUO4{Lj&K3IBfjY?L@Dvsdj6#-h4BWUAL z4UYRy>ip?&0$}$>f?c5TY4_w9X||59*J+$ysU*ucah`5|QGv(}9itxt-6F99HkCVdA!wRZZUpK@I90h5Ntuc+Ilx3p z?8MLSIhMsX&1IzxqQ{4#P>tSmgy>F~oRwUO4{Lj&K3IBfjY?L@Dvsdj6#-h4BWUAL z4UYRy>ip?&0$}$>f?c5TY4_w9X||59*J+$ysU*ucah`5|QGv(}9itRG`{Sf93z`eliO*QWH(Vd%jZd+&Hqq=$n`CwC835L3)s{_ zBG_JeP-*DR3eG)!&}_8vURWb|ts6F>)o#F;XI$*z<^`9B>+295l^vyr#*Oa-t=Otu umPJ}7AQ9@Jww4@*M}K;%uib9(^Zl?0r+4qXonogy`1c%)=8~;CO!qHmIVe2< literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2a9a66d447696ca0c1d75c98ac6652b6556438d9 GIT binary patch literal 938 zcmeHFO;5ux41FNM@2J!(Z2e#ddKibHo{*3@0hh{M9SB;J%8fw%BS_;n!G9tto7Owm zMRF3`&+j>r#VU1KYlHaVzAPi9Hyk5g36rx@MB>xhUPSLKy|yMwXXz}7;phbc+K?k? z<4_Nd`w!~;?r;KN{Yrv$pz&q5-y=K_WCjZ7eyAkN)&jU)$~S=lgCMPVe4$JI0QG@b5X8%q3g3pYC67$S6Pn literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..11a574f693f08356f1dfc199c9228d92e90857fc GIT binary patch literal 938 zcmeHF%TB{E5FC);J0j%@ZIaRhIpk856A}_9;1X?`H5fH^w6Q?>5v1}><)5(AR^-mL zmi*eCof%szR;lY+8^n(fWf>{G;TZ8sn4Fb75}(%gD*9mQwKY*XOJ_+8N3RIbh8#f~ zhk9__e^TdHhcf`{HxjG^jW4?+$H*4*?CHC0lju?q5ZyZ}tEH literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7af1c2b4e5b546a3849e761cc3b7ba536fc4af07 GIT binary patch literal 938 zcmeHF%TB{E5FC);J0j&u+dSxj9C9hj2?>c4aEUg}h8i_?w6Q?>5v1}><)5(AR^-mL zmi*eCof%szrfFuC(g>d(iy~;$mP3RqVX{WDKwMgxo8W_`S4Ic%Jf24(EWN=)YqA8T zEvmtC|4E%&EiM48HzZhl8dr8tmXV~3=x&ij$!!p)*)qzKx~cvOnq3xOP6!iK)s} tStMl&5}^Uj){w>c=r2e0z1!tK`+a`cA95Eu$IgH7?>?CHC7b5d-M>vYZ}|WK literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..444b9c015a9d3465927039b7ed25f5dc9baca725 GIT binary patch literal 938 zcmeHFJx{|h5PcxQ?}(I@w)vm~8Ol(U6^RL0qDyn3MvWa^EKoLn1ycD<mpB!2qZ#1>YX8r;nAOt>ick5jrO(QuYix+(f8*mcKU<=%)w+X+0^57{{Yc`r$+z) literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..09ec45601a1f4c88ea5b4fcd3e7156c959962794 GIT binary patch literal 938 zcmeHFJx{|h5PcxQ?}(I@w)vm~8Ol(U6^RL0qDyn3MvWa^EKoLn1ycD<mpB!6eL1D>YX8r;nAOt>ick5jrO(QuYix+(f8*mcKU<=%)w+X+0^57{{Yzyr%C_- literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dabe78467902e4e9fc31e6ea3fe6018401a98ae4 GIT binary patch literal 939 zcmeHFK~BRk5F8MNM{tRhE3`>U59Ck@fqDgT0xr?E*?>`FN1IhqKfn{X@Ijt|owOon z&RuCOdp)}|wnki4T`rA9{CK}uM3q@{jQCcV)JYzRzc%h7+OV|7*(e>S<0OXCD+076 zB^aw%Zk5{)YW%7=0u3C3!aMQDKm78%9GyH7g6) zmVqMVj=5v0>D>vA{rJFH8|zOY2Jq6p-g| zZ;G4ZAuSS+2yIcV9Vxn3e+6P6XP-aw(aE>Z^Lbx6)DHjBJK?|H7#NdXwYT>#Je9x4 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..68a872e55ed450584f02c1eb672b59173311ceb4 GIT binary patch literal 939 zcmeHFOHRWu5FHSOL$I-wEwuU21=&;x)Gde=u!*M47>pV_nv9Ba0gk|i3vvu>rxjVV z?nq0AT%w4C^q)x80!-wr#b z+EzE!Lsk`l%-Dn8I24Gl{tCoC&OUz@lap_s=Zm3os2%=gaKeARF)}7Oy|?!-wEn;( literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..47646404c97fe1ace19309ea63875f8665b5de13 GIT binary patch literal 939 zcmeHFK~BRk5F8MNM{tRhE3`>U59Ck@fqDgT0xr?E*?>`FN1IhqKfn{X@Ijt|owOon z&RuCOdp)}|wnki4T`rA9{CK}uM3q@{jQCcV)JYzRzc%h7+OV|7*(e>S<0OXCD+076 zB^aw%Zk5{)YW%7=0u3C3!aMQDKm78%9GyH7g6) zmVqMVj=5v0>D>vA{rJFH8|zOY2Jq6p-g| zZ;G4ZAuAG)2yIcV9Vxn3e+6P6XP-aw(aE>Z^Lbx6)DHjBJK?|H7#NdXwYT>#KODcv literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..be6a09e9d21ca56574ad02c67374d3d997a10358 GIT binary patch literal 939 zcmeHFu};G<5Iqouk6^Ks723q116e8r>Iz~4mgw4?!KksL%c&?oz$dWqgM0>d(u&N? zJ?Sj_{O;bf_1GJ8xv~zD$NS|{8M~nbl3OMe&xPV}ZT*GX61Ubnl})p0nt*XC7p1m6#y!Dg2Y1hKy5GL^)lu6YbuZin9neZe z+w!J-&}9zDj2)PbM}hv;Uy(S(+2_w_V)e-w}rqZ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..973d70e083a97a26e4ab58306e409428725eccde GIT binary patch literal 939 zcmeHFK~BRk5L^&#AHgM3u8=w@J&;QyK)r%E6)w@X*?>`FN1Ihq?x;`T1H6O-f8iO} zp%uAvttDIbdUj@P4coFjUupy4{q1}nlzL4egx5?cmeYXUwYF!$hPctz1aTBcDg?nN z@X!JUK^uW(E7ZJV?N)&!09ZXE!^+dRwi^_JWHMHl<3uGFK|D#PDov)pC{HA9O;96a zMM4Z@3NPX3PPtR5@WnD5J9@8KY2!|Q^x%cA$at$whcOEn?BM1BhWqVfSM9a^Nq5GL z^8t+%w8^jXyEs>X%-BM?wkXiO`rUDDizpkMoMvwyqinSOo(;b~XNTJ15B{5jUNFg) H`*{BVbulip literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..093a6ad9a6eed6981f5e243db8eee57701165094 GIT binary patch literal 939 zcmeHFOHRWu5FHS055XofEVxszQCCi zkH9vq$i3HEvSp8FXU5jDt?RS7HW1%jm1R`x6~z$Gm{2U|5xZ+`Pop()r>%+7Q94Ru z5L|+%Dij251m?X^_l&Ju1r7jU@qi2qFXPHCQ3$fhIJp>SNp>EklYE-w+4PU)g`{r@ zT4ZcUh=Gjq7JhD=gG+;tmf_IJd(TE2ck*LESGpnNqjrH~ZsBM{H+OKn-QEXuQ1)*f zf*Y3uI#IN)X4Q38C4kJ>L%p&n5MKT2w92Q?;``^Lcv0`g$=6%qpgp$#gPr?e(3oWF HZF~O$pC@0> literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cc810a1ef6bbeb659f92c401b0d44243e64dd7fe GIT binary patch literal 939 zcmeHFJ5Izf5FL=<9FZbA*v*G6NEagyOh}*k2oW9Ic5v<7|{p(n%77 z;2i|2LP0QAV9^V8PuTiZU=IM6_sFmeGQR8_g&@nP$=NhZvePKdi&;`+vtL#alD;Kq zk+C5m2C^zxgt>7ZTpE0E4EtU_cs9oRQy3$