From 04aa69610cdc7ea8bbfb19862fcd39a781365385 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Tue, 26 Oct 2021 22:20:48 +0200 Subject: [PATCH] Utility to export data to qc well log upscaling in Webviz --- src/fmu/tools/rms/upscaling_qc/__init__.py | 0 src/fmu/tools/rms/upscaling_qc/_types.py | 43 +++++ .../tools/rms/upscaling_qc/upscaling_qc.py | 75 ++++++++ tests/rms/test_upscaling_qc.py | 170 ++++++++++++++++++ 4 files changed, 288 insertions(+) create mode 100644 src/fmu/tools/rms/upscaling_qc/__init__.py create mode 100644 src/fmu/tools/rms/upscaling_qc/_types.py create mode 100644 src/fmu/tools/rms/upscaling_qc/upscaling_qc.py create mode 100644 tests/rms/test_upscaling_qc.py diff --git a/src/fmu/tools/rms/upscaling_qc/__init__.py b/src/fmu/tools/rms/upscaling_qc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/fmu/tools/rms/upscaling_qc/_types.py b/src/fmu/tools/rms/upscaling_qc/_types.py new file mode 100644 index 00000000..d4a8812b --- /dev/null +++ b/src/fmu/tools/rms/upscaling_qc/_types.py @@ -0,0 +1,43 @@ +from typing import List, Union, Dict +from dataclasses import dataclass, field + + +@dataclass +class WellSource: + names: List[int] = field(default_factory=list) + trajectory: str = "Drilled trajectory" + logrun: str = "log" + + +@dataclass +class BlockedWellSource: + grid: str + bwname: str + names: List[int] = field(default_factory=list) + + +@dataclass +class Context: + properties: Union[List[str], Dict[str, str]] + selectors: Union[List[str], Dict[str, str]] + + +@dataclass +class WellContext(Context): + wells: dict = field(default_factory=dict) + + def __post_init__(self): + self.wells = WellSource(**self.wells) + + +@dataclass +class GridContext(Context): + grid: str + + +@dataclass +class BlockedWellContext(Context): + wells: dict = field(default_factory=dict) + + def __post_init__(self): + self.wells = BlockedWellSource(**self.wells) diff --git a/src/fmu/tools/rms/upscaling_qc/upscaling_qc.py b/src/fmu/tools/rms/upscaling_qc/upscaling_qc.py new file mode 100644 index 00000000..ab87ba37 --- /dev/null +++ b/src/fmu/tools/rms/upscaling_qc/upscaling_qc.py @@ -0,0 +1,75 @@ +from pathlib import Path +from typing import Optional, List, Union, Dict +from dataclasses import asdict + +import pandas as pd +from fmu.tools.qcproperties._grid2df import GridProps2df +from fmu.tools.qcproperties._well2df import WellLogs2df +from fmu.tools.qcdata import QCData + +from fmu.tools.rms.upscaling_qc._types import ( + WellContext, + GridContext, + BlockedWellContext, +) + + +class RMSUpscalingQC: + def __init__( + self, project, well_data: dict, grid_data: dict, bw_data: dict + ) -> None: + self._project = project + self._well_data = WellContext(**well_data) + self._grid_data = GridContext(**grid_data) + self._bw_data = BlockedWellContext(**bw_data) + self._validate_input() + self._set_well_names_in_context() + + def _validate_input(self) -> None: + if self._grid_data.grid != self._bw_data.wells.grid: + raise ValueError("Different grids given for blocked well and grid.") + + @property + def _well_names(self) -> List[str]: + try: + grid = self._project.grid_models[self._grid_data.grid] + return grid.blocked_wells_set[self._bw_data.wells.bwname].get_well_names() + except ValueError: + pass + + def _set_well_names_in_context(self) -> None: + self._well_data.wells.names = self._well_names + self._bw_data.wells.names = self._well_names + + def _get_well_data(self) -> pd.DataFrame: + _ = WellLogs2df( + project=self._project, data=asdict(self._well_data), xtgdata=QCData() + ) + return _.dataframe + + def _get_bw_data(self) -> pd.DataFrame: + _ = WellLogs2df( + project=self._project, + data=asdict(self._bw_data), + xtgdata=QCData(), + blockedwells=True, + ) + return _.dataframe + + def _get_grid_data(self) -> pd.DataFrame: + _ = GridProps2df( + project=self._project, data=asdict(self._grid_data), xtgdata=QCData() + ) + return _.dataframe + + def to_disk(self, path: str = "../../share/results/tables/upscaling_qc") -> None: + folder = Path(path) + + if not folder.parent.is_dir(): + print(f"Cannot create folder. Ensure that {folder.parent} exists.") + folder.mkdir(exist_ok=True) + print("Extracting data...") + self._get_well_data().to_csv(folder / "well.csv", index=False) + self._get_bw_data().to_csv(folder / "bw.csv", index=False) + self._get_grid_data().to_csv(folder / "grid.csv", index=False) + print(f"Done. Output written to {folder}.") diff --git a/tests/rms/test_upscaling_qc.py b/tests/rms/test_upscaling_qc.py new file mode 100644 index 00000000..de517699 --- /dev/null +++ b/tests/rms/test_upscaling_qc.py @@ -0,0 +1,170 @@ +"""Run tests in RMS. + +Creates a tmp RMS project in given version which is used as fixture for all other Roxar +API dependent tests. + +This requires a ROXAPI license, and to be ran in a "roxenvbash" environment; hence +the decorator "roxapilicense" + +""" +from pathlib import Path +from os.path import isdir +import shutil +import numpy as np +import pytest + +import xtgeo + +try: + import roxar + import roxar.jobs + +except ImportError: + pass + +from fmu.tools.rms.upscaling_qc.upscaling_qc import RMSUpscalingQC + +# ====================================================================================== +# settings to create RMS project! + +TMPD = Path("TMP") +TMPD.mkdir(parents=True, exist_ok=True) + +TPATH = Path("../xtgeo-testdata") +PROJNAME = "tmp_project_qcupscaling.rmsxxx" + +PRJ = str(TMPD / PROJNAME) + +GRIDDATA1 = TPATH / "3dgrids/reek/reek_sim_grid.roff" +PORODATA1 = TPATH / "3dgrids/reek/reek_sim_poro.roff" +ZONEDATA1 = TPATH / "3dgrids/reek/reek_sim_zone.roff" +GRIDNAME1 = "Simgrid" +PORONAME1 = "PORO" +ZONENAME1 = "Zone" + +WELLSFOLDER1 = TPATH / "wells/reek/1" +WELLS1 = ["OP_1.w", "OP_2.w", "OP_6.w"] + +BW_JOB_SPEC = { + "BlockedWellsName": "BW", + "Continuous Blocked Log": [ + { + "Name": "Poro", + } + ], + "Wells": [["Wells", "OP_1"], ["Wells", "OP_2"], ["Wells", "OP_6"]], + "Zone Blocked Log": [ + { + "Name": "Zonelog", + "ScaleUpType": "SUBGRID_BIAS", + "ThicknessWeighting": "MD_WEIGHT", + "ZoneLogArray": [1, 2], + } + ], +} + + +@pytest.mark.skipunlessroxar +@pytest.fixture(name="roxar_project") +def fixture_create_project(): + """Create a tmp RMS project for testing, populate with basic data. + + After the yield command, the teardown phase will remove the tmp RMS project. + """ + prj1 = str(PRJ) + + print("\n******** Setup RMS project!\n") + if isdir(prj1): + print("Remove existing project! (1)") + shutil.rmtree(prj1) + + project = roxar.Project.create() + + rox = xtgeo.RoxUtils(project) + print("Roxar version is", rox.roxversion) + print("RMS version is", rox.rmsversion(rox.roxversion)) + assert "1." in rox.roxversion + + for wfile in WELLS1: + wobj = xtgeo.well_from_file(WELLSFOLDER1 / wfile) + wobj.dataframe["Zonelog"] = 1 + + wobj.to_roxar(project, wobj.name, logrun="log", trajectory="Drilled trajectory") + + # populate with grid and props + grd = xtgeo.grid_from_file(GRIDDATA1) + grd.to_roxar(project, GRIDNAME1) + por = xtgeo.gridproperty_from_file(PORODATA1, name=PORONAME1) + por.to_roxar(project, GRIDNAME1, PORONAME1) + zon = xtgeo.gridproperty_from_file(ZONEDATA1, name=ZONENAME1) + zon.values = zon.values.astype(np.uint8) + zon.values = 1 + zon.to_roxar(project, GRIDNAME1, ZONENAME1) + bw_job = roxar.jobs.Job.create( + owner=["Grid models", "Simgrid", "Grid"], type="Block Wells", name="BW" + ) + bw_job.set_arguments(BW_JOB_SPEC) + bw_job.save() + + bw_job.execute(0) + for well in project.wells: + for traj in well.wellbore.trajectories: + print(traj.name) + for bw in project.grid_models["Simgrid"].blocked_wells_set["BW"].get_well_names(): + print(bw) + # save project (both an initla version and a work version) and exit + project.save_as(prj1) + project.close() + + yield prj1 + + print("\n******* Teardown RMS project!\n") + + if isdir(prj1): + print("Remove existing project! (1)") + shutil.rmtree(prj1) + + +@pytest.mark.skipunlessroxar +def test_upscaling_qc(roxar_project): + """Test qcreset metod in roxapi.""" + # ================================================================================== + # pylint: disable=invalid-name + from fmu.tools.rms import qcreset + + rox = xtgeo.RoxUtils(roxar_project, readonly=True) + project = rox.project + + well_data = { + "selectors": { + "ZONE": { + "name": "Zonelog", + } + }, + "properties": {"PORO": {"name": "Poro"}}, + } + bw_data = { + "selectors": { + "ZONE": { + "name": "Zonelog", + } + }, + "properties": {"PORO": {"name": "Poro"}}, + "wells": {"grid": "Simgrid", "bwname": "BW"}, + } + grid_data = { + "selectors": {"ZONE": {"name": "Zone"}}, + "properties": ["PORO"], + "grid": "Simgrid", + } + ups = RMSUpscalingQC( + project=project, well_data=well_data, bw_data=bw_data, grid_data=grid_data + ) + well_df = ups._get_well_data() + bw_df = ups._get_bw_data() + grid_df = ups._get_grid_data() + for data in [well_df, bw_df, grid_df]: + assert set(data.columns) == set(["ZONE", "PORO"]) + assert well_df["PORO"].mean() == pytest.approx(0.1725, abs=0.0001) + assert bw_df["PORO"].mean() == pytest.approx(0.1765, abs=0.0001) + assert grid_df["PORO"].mean() == pytest.approx(0.1677, abs=0.0001)