From 52f86801529eb423007cfd4a48e4d26d48c29e35 Mon Sep 17 00:00:00 2001 From: "Oddvar Lia (ST MSU GEO)" Date: Wed, 11 Dec 2024 10:46:01 +0100 Subject: [PATCH] Add function to combine multiple petro realizations using facies realization Add function to import and export field parameters to and from ERTBOX grid. Added documentation for update_petro_real and the export and import functions. --- docs/export_and_import_fields.rst | 110 ++++++++ docs/index.rst | 2 + docs/update_petro_real.rst | 157 +++++++++++ src/fmu/tools/rms/update_petro_real.py | 375 +++++++++++++++++++++++++ 4 files changed, 644 insertions(+) create mode 100644 docs/export_and_import_fields.rst create mode 100644 docs/update_petro_real.rst create mode 100644 src/fmu/tools/rms/update_petro_real.py diff --git a/docs/export_and_import_fields.rst b/docs/export_and_import_fields.rst new file mode 100644 index 00000000..0a118470 --- /dev/null +++ b/docs/export_and_import_fields.rst @@ -0,0 +1,110 @@ +rms.export_and_import_field_parameters +======================================= + +The export and import scripts desctibed below supports a workflow where +both facies and petrophysical properties are updated as field parameters in ERT. +The implemented functions in the module *fmu.tools.rms.update_petro_real* are:: + + export_initial_field_parameters + import_updated_field_parameters + +Both the export and import function the same set of petrophysical variables in the +same workflow. Therefore, they both read this information from a configuration file +in YAML format that is also shared with the function *generate_petro_jobs*. + +Export petrophysical parameters as field parameters to be used in ERT FIELD keyword +------------------------------------------------------------------------------------- + +Example of use of the export function + +.. code-block:: python + + from fmu.tools.rms.update_petro_real import export_initial_field_parameters + from fmu.tools.rms.generate_petro_jobs_for_field_update import read_specification_file + + DEBUG_PRINT=True + CONFIG_FILE_NAME_VALYSAR = "../input/config/field_update/generate_petro_jobs_valysar.yml" + CONFIG_FILE_NAME_THERYS = "../input/config/field_update/generate_petro_jobs_therys.yml" + CONFIG_FILE_NAME_VOLON = "../input/config/field_update/generate_petro_jobs_volon.yml" + ERTBOX_GRID = "ERTBOX" + + + def export_fields(): + spec_dict = read_specification_file(CONFIG_FILE_NAME_VALYSAR) + used_petro_dict = spec_dict["used_petro_var"] + export_initial_field_parameters( + project, + used_petro_dict, + grid_model_name=ERTBOX_GRID, + zone_name_for_single_zone_grid="Valysar", + debug_print= DEBUG_PRINT) + + spec_dict = read_specification_file(CONFIG_FILE_NAME_THERYS) + used_petro_dict = spec_dict["used_petro_var"] + export_initial_field_parameters( + project, + used_petro_dict, + grid_model_name=ERTBOX_GRID, + zone_name_for_single_zone_grid="Therys", + debug_print= DEBUG_PRINT) + + spec_dict = read_specification_file(CONFIG_FILE_NAME_VOLON) + used_petro_dict = spec_dict["used_petro_var"] + export_initial_field_parameters( + project, + used_petro_dict, + grid_model_name=ERTBOX_GRID, + zone_name_for_single_zone_grid="Volon", + debug_print= DEBUG_PRINT) + + if __name__ == "__main__": + export_fields() + + +Import petrophysical parameters used as field parameters to be used in ERT FIELD keyword +------------------------------------------------------------------------------------------ + +Example of use of the import function + +.. code-block:: python + + from fmu.tools.rms.update_petro_real import import_updated_field_parameters + from fmu.tools.rms.generate_petro_jobs_for_field_update import read_specification_file + + + DEBUG_PRINT = False + CONFIG_FILE_NAME_VALYSAR = "../input/config/field_update/generate_petro_jobs_valysar.yml" + CONFIG_FILE_NAME_THERYS = "../input/config/field_update/generate_petro_jobs_therys.yml" + CONFIG_FILE_NAME_VOLON = "../input/config/field_update/generate_petro_jobs_volon.yml" + ERTBOX_GRID = "ERTBOX" + + def import_fields(): + spec_dict = read_specification_file(CONFIG_FILE_NAME_VALYSAR) + used_petro_dict = spec_dict["used_petro_var"] + import_updated_field_parameters( + project, + used_petro_dict, + grid_model_name=ERTBOX_GRID, + zone_name_for_single_zone_grid="Valysar", + debug_print=DEBUG_PRINT) + + spec_dict = read_specification_file(CONFIG_FILE_NAME_THERYS) + used_petro_dict = spec_dict["used_petro_var"] + import_updated_field_parameters( + project, + used_petro_dict, + grid_model_name=ERTBOX_GRID, + zone_name_for_single_zone_grid="Therys", + debug_print=DEBUG_PRINT) + + spec_dict = read_specification_file(CONFIG_FILE_NAME_VOLON) + used_petro_dict = spec_dict["used_petro_var"] + import_updated_field_parameters( + project, + used_petro_dict, + grid_model_name=ERTBOX_GRID, + zone_name_for_single_zone_grid="Volon", + debug_print=DEBUG_PRINT) + + if __name__ == "__main__": + import_fields() diff --git a/docs/index.rst b/docs/index.rst index 22c6a21a..cfb544c3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,6 +17,8 @@ Contents: create_rft_ertobs rename_rms_scripts rms_import_localmodule + update_petro_real + export_and_import_fields utilities rmsvolumetrics2csv ensembles diff --git a/docs/update_petro_real.rst b/docs/update_petro_real.rst new file mode 100644 index 00000000..b70d1a01 --- /dev/null +++ b/docs/update_petro_real.rst @@ -0,0 +1,157 @@ +rms.update_petro_real +======================= + +When running FMU project where field parameters for both facies and +petrophysical properties are updated in ERT simultaneously, +some adjustments are needed in the RMS project to support this type +of workflow. It is necessary to have one petrosim job per facies. + +The module *update_petro_real* will combine values from realization +of a petrophysical property for individual facies into one common +petrophysical realization by using the facies realization as filter. + +Example describing the workflow +-------------------------------- + +The zone Valysar has four different facies:: + + Floodplain + Channel + Crevasse + Coal + +A petrophysical realization is made for PHIT and KLOGH for each of the three +first facies with property names:: + + Floodplain_PHIT + Floodplain_KLOGH + Channel_PHIT + Channel_KLOGH + Crevasse_PHIT + Crevasse_KLOGH + +Neither PHIT nor KLOGH are updated as field parameters for facies +Coal since they have constant (not spatially varying) values and +may even have 0 specified uncertainty. +The variables VSH and VPHYL are chosen not to be used as field parameters +in ERT in assisted histroy matching and therefore not included here, +but still used in the RMS workflow with the prior values. + +The original realizations created for PHIT and KLOGH are conditioned +to facies. In a workflow where we want to use both facies by applying +the Adaptive PluriGaussian Simulation method (APS) and PHIT and KLOG +for some selected facies as field parameters to be updated by ERT, +we will need the separate PHIT and KLOGH realizations for the selected +facies. + +The steps will be: + + * Use original petrophysical job to create initial version of a realization of + PHIT, KLOGH, VSH and VPHYL. + * Use separate petrophysical jobs to create separate version of realizations of + Floodplain_PHIT, Floodplain_KLOGH and so on. + * Use the script *update_petro_real* to copy the values for PHIT and KLOGH from + the individual realizations per facies into the initial version of PHIT and KLOG + by overwriting the values in the original version. In this process, + the facies realization is used as a filter to select which grid cell values + to get from the individual PHIT and KLOGH parameters that belongs to the + various facies. + +When running ERT iteration 0 which creates the initial ensemble, this looks a bit unnecessary, +since the initial PHIT and KLOGH already has taken facies into account. But, when running +ERT iteration larger than 0 (after ERT has updated the fields Floodplain_PHIT, +Floodplain_KLOGH and so on), then updated versions of the field parameters are imported +and updated version of facies realization is used to update the PHIT and KLOGH parameters +from the imported field parameters (Floodplain_PHIT, Floodplain_KLOGH ...). + +Also for iteration 0, to ensure consistency the same procedure +(copy from the field parameters Floodplain_PHIT, Floodplain_KLOG,..) is applied since this ensure +consistent handling of the realizations. + +Example of RMS python job running this module to combine the field parameters +------------------------------------------------------------------------------- + +The python job below will call the *update_petro_real* function that does the +combination of the field parameters into one petrophysical realization. +This job depends on a small config file ( the same config file that is used by the +function *generate_petro_jobs* from fmu.tools.rms module). Here the python job gets +the grid name and which of the petrophysical properties for which facies +to be updated as field parameters in ERT. The script also depends on the facies table +(which facies code and name belongs together). +This can be fetched from the *global_variables.yml* file as shown in the example below. + +.. code-block:: python + + from fmu.tools.rms.update_petro_real import update_petro_real + from fmu.config import utilities as ut + + DEBUG_PRINT = False + + # Choose either to use config file or input from global variable file + USE_CONFIG_FILES = True + + CFG = ut.yaml_load("../../fmuconfig/output/global_variables.yml")["global"] + FACIES_ZONE = CFG["FACIES_ZONE"] + + # Used if alternative 1 with config file is used + CONFIG_FILES = { + "Valysar": "../input/config/field_update/generate_petro_jobs_valysar.yml", + "Therys": "../input/config/field_update/generate_petro_jobs_therys.yml", + "Volon": "../input/config/field_update/generate_petro_jobs_volon.yml", + } + + # Used if alternative 2 without config file is used + # In this case the global master config must be updated + # to define the dictionary with used petro fields per zone per facies + USED_PETRO_FIELDS = CFG["USED_PETRO_FIELDS"] + GRID_NAMES = { + "Valysar": "Geogrid_Valysar", + "Therys": "Geogrid_Therys", + "Volon": "Geogrid_Volon", + } + FACIES_REAL_NAMES = { + "Valysar": "FACIES", + "Therys": "FACIES", + "Volon": "FACIES", + } + + # For multi zone grids also zone_param_name and zone_code_names must be specified + # Example: + # ZONE_PARAM_NAME = "Zone" + # ZONE_CODE_NAMES = { + # 1: "Valysar, + # 2: "Therys", + # 3: "Volon", + # } + + + if __name__ == "__main__": + # Drogon uses 3 different grids and single zone grids + if USE_CONFIG_FILES: + # Alternative 1 using config file common with generate_petro_jobs + for zone_name in ["Valysar", "Therys", "Volon"]: + facies_code_names = FACIES_ZONE[zone_name] + config_file = CONFIG_FILES[zone_name] + update_petro_real( + project, + facies_code_names, + zone_name_for_single_zone_grid=zone_name, + config_file=config_file, + debug_print=DEBUG_PRINT) + else: + # Alternative 2 specify input using global_variables file + for zone_name in ["Valysar", "Therys", "Volon"]: + facies_code_names = FACIES_ZONE[zone_name] + grid_name = GRID_NAMES[zone_name] + facies_real_name = FACIES_REAL_NAMES[zone_name] + used_petro_per_facies=USED_PETRO_FIELDS + update_petro_real( + project, + facies_code_names, + grid_name=grid_name, + facies_real_name=facies_real_name, + used_petro_dict=used_petro_per_facies, + zone_name_for_single_zone_grid=zone_name, + debug_print=DEBUG_PRINT) + + diff --git a/src/fmu/tools/rms/update_petro_real.py b/src/fmu/tools/rms/update_petro_real.py new file mode 100644 index 00000000..bcd96fcd --- /dev/null +++ b/src/fmu/tools/rms/update_petro_real.py @@ -0,0 +1,375 @@ +""" +Merge petrophysical realizations created individually per facies +into one realization using facies realization as filter +""" + +from typing import Dict, List + +import xtgeo + +from fmu.tools.rms.generate_petro_jobs_for_field_update import ( + get_original_job_settings, + read_specification_file, +) + + +def update_petro_real( + project, + facies_code_names: Dict[int, str], + config_file: str = "", + grid_name: str = "", + facies_real_name: str = "", + used_petro_dict: Dict = {}, + zone_name_for_single_zone_grid: str = "", + zone_code_names: Dict[int, str] = {}, + zone_param_name: str = "", + debug_print: bool = False, + ignore_missing_parameters: bool = False, +) -> None: + """Combine multiple petrophysical realizations (one per facies) into one parameter + using facies realization as filter. + + Description + + This function will read petrophysical realization for multiple facies + (where all grid cells have the same facies) and use the facies + realization to combine them into one realization conditioned to facies. + + Input + + Choose either to use a config file of the same type as for the function + generate_petro_real in fmu.tools.rms or choose to specify the + dictionary defining which petro variables to use as field parameters for + each zone and facies. In the last case also specify grid model + and facies realization name. + + If multi zone grid is used, also specify dictionary defining + zone name and zone code and zone parameter name. + + Output + + Updated version of petrophysical realizations + for the specified petrophysical variables. + + """ + if config_file: + spec_dict = read_specification_file(config_file) + used_petro_dict = spec_dict["used_petro_var"] + grid_name = spec_dict["grid_name"] + original_job_name = spec_dict["original_job_name"] + + # Get facies param name from the job settings + owner_string_list = ["Grid models", grid_name, "Grid"] + job_type = "Petrophysical Modeling" + petro_job_param = get_original_job_settings( + owner_string_list, job_type, original_job_name + ) + # Get facies realization name from the original petrophysics job + facies_real_name = petro_job_param["InputFaciesProperty"][2] + else: + # Check that necessary parameters are specified + if not grid_name: + raise ValueError( + "Need to specify grid name when config file is not specified." + ) + if not used_petro_dict: + raise ValueError( + "Need to specify the dict used_petro_dict when " + "config file is not specified." + ) + if not facies_real_name: + raise ValueError( + "Need to specify the facies realization name when " + "config file is not specified." + ) + grid = xtgeo.grid_from_roxar(project, grid_name) + subgrids = grid.subgrids + if subgrids: + # Multi zone grid is found + if not zone_code_names: + raise ValueError( + "Need to specify the zone_code_names when using multi zone grids." + ) + if not zone_param_name: + raise ValueError( + "Need to specify the zone_param_name when using multi zone grids." + ) + if zone_name_for_single_zone_grid: + raise ValueError( + "For multi zone grids the variable " + "'zone_name_for_single_zone_grid' should not be used" + ) + else: + if not zone_name_for_single_zone_grid: + raise ValueError( + "Need to specify zone_name_for_single_zone_grid " + "since input is a single zone grid." + ) + + combine_petro_real_from_multiple_facies( + project, + grid_name, + facies_real_name, + used_petro_dict, + facies_code_names, + zone_name_for_single_zone_grid=zone_name_for_single_zone_grid, + zone_param_name=zone_param_name, + zone_code_names=zone_code_names, + debug_print=debug_print, + ignore_missing_parameters=ignore_missing_parameters, + ) + + +def import_updated_field_parameters( + project, + used_petro_dict: Dict, + grid_model_name: str = "ERTBOX", + zone_name_for_single_zone_grid: str = "", + debug_print: bool = False, +) -> None: + """Import ROFF files with field parameters updated by ERT. + + Description + + This function will import ROFF format files generated by ERT when using + the FIELD keyword in ERT to update petrophysical field parameters. + The naming convention is files with the name of the form: + zonename_faciesname_petrovarname with suffix ".roff" + The files are assumed to be located at the top directory level + where updated fields are written by ERT. + + Input + + A dictionary specifying which petrophysical variables to use as field parameters + for each facies for each zone. + The grid model name to import the field parameters into (ERTBOX grid). + For singe zone grids, also a name of the zone for the single zone + must be specified. + + The result will be new petrophysical parameters in ERTBOX grid. + + """ + + for zone_name, petro_per_facies_dict in used_petro_dict.items(): + if len(used_petro_dict) == 1: + # Single zone grid, use specified zone name + zone_name = zone_name_for_single_zone_grid + if debug_print: + print(f"Zone name: {zone_name}") + for fname, petro_list in petro_per_facies_dict.items(): + if debug_print: + print(f"Facies name: {fname}") + for petro_name in petro_list: + if debug_print: + print(f"Petro variable: {petro_name}") + property_name = zone_name + "_" + fname + "_" + petro_name + file_name = "../../" + property_name + ".roff" + print(f"Import file: {file_name} into {grid_model_name}") + xtgeo_prop = xtgeo.gridproperty_from_file( + file_name, fformat="roff", name=property_name + ) + xtgeo_prop.to_roxar(project, grid_model_name, property_name) + + +def export_initial_field_parameters( + project, + used_petro_dict: Dict, + grid_model_name: str = "ERTBOX", + zone_name_for_single_zone_grid: str = "", + debug_print: bool = False, +) -> None: + """Export ROFF files with field parameters simulated by RMS to files to be + read by ERT and used as field parameters. + + Description + + This function will export ROFF format files generated by RMS in workflows + where field parameters are updated by ERT. + The parameter names will be in the format: zonename_faciesname_petroname + and the file names will have extension ".roff". + The files are assumed to be located in the directory: rms/output/aps + together with field parameters used by the facies modelling method APS. + + + Input + + A dictionary specifying which petrophysical variables to use as field parameters + for each facies for each zone. + The grid model name to export the field parameters from (ERTBOX grid). + For singe zone grids, also a name of the zone for the single zone must + be specified. + + The result will be files with petrophysical parameters per zone per facies with + size equal to the ERTBOX grid. + + """ + + for zone_name, petro_per_facies_dict in used_petro_dict.items(): + if len(used_petro_dict) == 1: + # Single zone grid, use specified zone name + zone_name = zone_name_for_single_zone_grid + if debug_print: + print(f"Zone name: {zone_name}") + for fname, petro_list in petro_per_facies_dict.items(): + if debug_print: + print(f"Facies name: {fname}") + for petro_name in petro_list: + if debug_print: + print(f"Petro variable: {petro_name}") + property_name = zone_name + "_" + fname + "_" + petro_name + file_name = "../../rms/output/aps/" + property_name + ".roff" + print(f"Export file: {file_name} into {grid_model_name}") + xtgeo_prop = xtgeo.gridproperty_from_roxar( + project, grid_model_name, property_name + ) + xtgeo_prop.to_file(file_name, fformat="roff", name=property_name) + + +def combine_petro_real_from_multiple_facies( + project, + grid_name: str, + facies_real_name: str, + used_petro_dict: Dict[str, Dict[str, List[str]]], + facies_code_names: Dict[int, str], + zone_name_for_single_zone_grid: str = "", + zone_param_name: str = "", + zone_code_names: Dict[int, str] = {}, + debug_print: bool = False, + ignore_missing_parameters: bool = False, +) -> None: + single_zone_grid = False + if len(zone_name_for_single_zone_grid) > 0: + single_zone_grid = True + + # Find all defined 3D grid parameters using rmsapi + properties = project.grid_models[grid_name].properties + property_names = [prop.name for prop in properties] + # print(f"Gridname: {grid_name} Properties: {property_names}") + + # Find all petro var names to use in any zone + petro_var_list = get_petro_var(used_petro_dict) + + # Get facies realization + prop_facies = xtgeo.gridproperty_from_roxar( + project, grid_name, facies_real_name, faciescodes=True + ) + prop_facies_values = prop_facies.values + + # Get zone realization for multi zone grid + if not single_zone_grid: + prop_zone = xtgeo.gridproperty_from_roxar(project, grid_name, zone_param_name) + prop_zone_values = prop_zone.values + + err_msg = [] + for zone_name, petro_per_facies_dict in used_petro_dict.items(): + if single_zone_grid: + # This is a single zone grid + if len(used_petro_dict) > 1 and zone_name_for_single_zone_grid != zone_name: + # Skip all but the one with correct zone name + continue + # Use the specified zone name + zone_name = zone_name_for_single_zone_grid + else: + if zone_code_names: + zone_code = code_per_name(zone_code_names, zone_name) + for pname in petro_var_list: + prop_petro_original = xtgeo.gridproperty_from_roxar( + project, grid_name, pname + ) + prop_petro_original_values = prop_petro_original.values + is_updated = False + for fname in petro_per_facies_dict: + petro_list_for_this_facies = petro_per_facies_dict[fname] + if pname in petro_list_for_this_facies: + petro_name_per_facies = f"{fname}_{pname}" + + # Get petro realization for this facies and this petro variable + if petro_name_per_facies not in property_names: + err_msg.append( + "Skip non-existing petro realization: " + f"{petro_name_per_facies}" + ) + continue + if ( + project.grid_models[grid_name] + .properties[petro_name_per_facies] + .is_empty() + ): + err_msg.append( + f"Skip empty petro realization: {petro_name_per_facies}" + ) + continue + + prop_petro = xtgeo.gridproperty_from_roxar( + project, grid_name, petro_name_per_facies + ) + prop_petro_values = prop_petro.values + + facies_code = code_per_name(facies_code_names, fname) + + if not single_zone_grid: + # Multi zone grid + if debug_print: + print( + f"Update values for {pname} " + f"in existing parameter for facies {fname} " + f"for zone {zone_name}" + ) + cells_selected = (prop_facies_values == facies_code) & ( + prop_zone_values == zone_code + ) + else: + if debug_print: + print( + f"Update values for {pname} " + f"in existing parameter for facies {fname}" + ) + cells_selected = prop_facies_values == facies_code + is_updated = True + prop_petro_original_values[cells_selected] = prop_petro_values[ + cells_selected + ] + prop_petro_original.values = prop_petro_original_values + if is_updated: + if not single_zone_grid: + # Multi zone grid + print( + f"Write updated petro param {pname} " + f"for zone {zone_name} to grid model {grid_name}" + ) + else: + print( + f"Write updated petro param {pname} for grid model {grid_name}" + ) + prop_petro_original.to_roxar(project, grid_name, pname) + if not ignore_missing_parameters and len(err_msg) > 0: + print( + f"Missing or empty petrophysical 3D parameters for grid model: {grid_name}:" + ) + for msg in err_msg: + print(f" {msg}") + raise ValueError("Missing or empty petrophysical parameters.") + print(f"Finished updating properties for grid model: {grid_name}") + print(" ") + if len(err_msg) > 0: + print( + "Warning: Some petrophysical parameters were not updated. Is that correct?" + ) + + +def code_per_name(code_name_dict: Dict[int, str], input_name: str) -> int: + # Since name is (must be) unique, get it if found or return -1 if not found + for code, name in code_name_dict.items(): + if input_name == name: + return code + return -1 + + +def get_petro_var(used_petro_dict: Dict[str, Dict[str, List[str]]]) -> List[str]: + petro_var_list = [] + for _, petro_var_per_facies_dict in used_petro_dict.items(): + for _, petro_list in petro_var_per_facies_dict.items(): + for petro_name in petro_list: + if petro_name not in petro_var_list: + petro_var_list.append(petro_name) + return petro_var_list