From 4a837905ee51428f8ace7921b98320d6bd5e7312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Herje?= <82032112+jorgenherje@users.noreply.github.com> Date: Mon, 25 Sep 2023 08:54:55 +0200 Subject: [PATCH] Refactor WellCompletions back-end code (#317) --- backend/src/backend/primary/main.py | 4 +- .../primary/routers/well_completion/router.py | 30 -- .../routers/well_completion/schemas.py | 7 - .../routers/well_completions/router.py | 29 ++ .../sumo_access/well_completion_access.py | 70 ----- .../sumo_access/well_completions_access.py | 277 ++++++++++++++++++ .../sumo_access/well_completions_types.py | 45 +++ .../services/types/well_completion_types.py | 48 --- .../services/utils/well_completion_utils.py | 187 ------------ frontend/src/api/ApiService.ts | 6 +- frontend/src/api/index.ts | 13 +- frontend/src/api/models/WellCompletionData.ts | 10 - .../src/api/models/WellCompletionDataSet.ts | 19 -- .../src/api/models/WellCompletionUnits.ts | 10 - .../src/api/models/WellCompletionsData.ts | 19 ++ ...UnitInfo.ts => WellCompletionsUnitInfo.ts} | 2 +- .../src/api/models/WellCompletionsUnits.ts | 10 + ...mpletionWell.ts => WellCompletionsWell.ts} | 2 +- ...mpletionZone.ts => WellCompletionsZone.ts} | 4 +- ...onService.ts => WellCompletionsService.ts} | 14 +- .../loadModule.tsx | 2 +- .../queryHooks.tsx | 10 +- .../registerModule.ts | 2 +- .../settings.tsx | 22 +- .../state.ts | 0 .../utils/stringUtils.ts | 2 +- .../utils/wellCompletionsDataAccessor.ts | 49 ++-- .../view.tsx | 2 +- frontend/src/modules/registerAllModules.ts | 2 +- 29 files changed, 447 insertions(+), 450 deletions(-) delete mode 100644 backend/src/backend/primary/routers/well_completion/router.py delete mode 100644 backend/src/backend/primary/routers/well_completion/schemas.py create mode 100644 backend/src/backend/primary/routers/well_completions/router.py delete mode 100644 backend/src/services/sumo_access/well_completion_access.py create mode 100644 backend/src/services/sumo_access/well_completions_access.py create mode 100644 backend/src/services/sumo_access/well_completions_types.py delete mode 100644 backend/src/services/types/well_completion_types.py delete mode 100644 backend/src/services/utils/well_completion_utils.py delete mode 100644 frontend/src/api/models/WellCompletionData.ts delete mode 100644 frontend/src/api/models/WellCompletionDataSet.ts delete mode 100644 frontend/src/api/models/WellCompletionUnits.ts create mode 100644 frontend/src/api/models/WellCompletionsData.ts rename frontend/src/api/models/{WellCompletionUnitInfo.ts => WellCompletionsUnitInfo.ts} (74%) create mode 100644 frontend/src/api/models/WellCompletionsUnits.ts rename frontend/src/api/models/{WellCompletionWell.ts => WellCompletionsWell.ts} (87%) rename frontend/src/api/models/{WellCompletionZone.ts => WellCompletionsZone.ts} (56%) rename frontend/src/api/services/{WellCompletionService.ts => WellCompletionsService.ts} (72%) rename frontend/src/modules/{WellCompletion => WellCompletions}/loadModule.tsx (81%) rename frontend/src/modules/{WellCompletion => WellCompletions}/queryHooks.tsx (58%) rename frontend/src/modules/{WellCompletion => WellCompletions}/registerModule.ts (78%) rename frontend/src/modules/{WellCompletion => WellCompletions}/settings.tsx (95%) rename frontend/src/modules/{WellCompletion => WellCompletions}/state.ts (100%) rename frontend/src/modules/{WellCompletion => WellCompletions}/utils/stringUtils.ts (92%) rename frontend/src/modules/{WellCompletion => WellCompletions}/utils/wellCompletionsDataAccessor.ts (86%) rename frontend/src/modules/{WellCompletion => WellCompletions}/view.tsx (93%) diff --git a/backend/src/backend/primary/main.py b/backend/src/backend/primary/main.py index 9386e9b9b..36c29c12e 100644 --- a/backend/src/backend/primary/main.py +++ b/backend/src/backend/primary/main.py @@ -17,7 +17,7 @@ from .routers.correlations.router import router as correlations_router from .routers.grid.router import router as grid_router from .routers.pvt.router import router as pvt_router -from .routers.well_completion.router import router as well_completion_router +from .routers.well_completions.router import router as well_completions_router from .routers.well.router import router as well_router from .routers.surface_polygons.router import router as surface_polygons_router @@ -53,7 +53,7 @@ def custom_generate_unique_id(route: APIRoute) -> str: app.include_router(correlations_router, prefix="/correlations", tags=["correlations"]) app.include_router(grid_router, prefix="/grid", tags=["grid"]) app.include_router(pvt_router, prefix="/pvt", tags=["pvt"]) -app.include_router(well_completion_router, prefix="/well_completion", tags=["well_completion"]) +app.include_router(well_completions_router, prefix="/well_completions", tags=["well_completions"]) app.include_router(well_router, prefix="/well", tags=["well"]) app.include_router(surface_polygons_router, prefix="/surface_polygons", tags=["surface_polygons"]) diff --git a/backend/src/backend/primary/routers/well_completion/router.py b/backend/src/backend/primary/routers/well_completion/router.py deleted file mode 100644 index f9044792a..000000000 --- a/backend/src/backend/primary/routers/well_completion/router.py +++ /dev/null @@ -1,30 +0,0 @@ -from typing import Optional - -from fastapi import APIRouter, Depends, Query - -from src.backend.auth.auth_helper import AuthHelper -from src.services.utils.authenticated_user import AuthenticatedUser - -from src.services.sumo_access.well_completion_access import WellCompletionAccess -from src.services.utils.well_completion_utils import WellCompletionDataModel - -from . import schemas - -router = APIRouter() - - -@router.get("/well_completion_data/") -def get_well_completion_data( - # fmt:off - authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), - case_uuid: str = Query(description="Sumo case uuid"), - ensemble_name: str = Query(description="Ensemble name"), - realization: Optional[int] = Query(None, description="Optional realization to include. If not specified, all realizations will be returned."), - # fmt:on -) -> schemas.WellCompletionData: - access = WellCompletionAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) - - well_completion_df = access.get_well_completion_data(realization=realization) - well_completion_data_model = WellCompletionDataModel(well_completion_df) - - return schemas.WellCompletionData(json_data=well_completion_data_model.create_well_completion_dataset()) diff --git a/backend/src/backend/primary/routers/well_completion/schemas.py b/backend/src/backend/primary/routers/well_completion/schemas.py deleted file mode 100644 index e04e65187..000000000 --- a/backend/src/backend/primary/routers/well_completion/schemas.py +++ /dev/null @@ -1,7 +0,0 @@ -from pydantic import BaseModel - -from src.services.utils.well_completion_utils import WellCompletionDataSet - - -class WellCompletionData(BaseModel): - json_data: WellCompletionDataSet diff --git a/backend/src/backend/primary/routers/well_completions/router.py b/backend/src/backend/primary/routers/well_completions/router.py new file mode 100644 index 000000000..de3bb50a6 --- /dev/null +++ b/backend/src/backend/primary/routers/well_completions/router.py @@ -0,0 +1,29 @@ +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Query + +from src.backend.auth.auth_helper import AuthHelper +from src.services.utils.authenticated_user import AuthenticatedUser + +from src.services.sumo_access.well_completions_access import WellCompletionsAccess +from src.services.sumo_access.well_completions_types import WellCompletionsData + +router = APIRouter() + + +@router.get("/well_completions_data/") +def get_well_completions_data( + # fmt:off + authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), + case_uuid: str = Query(description="Sumo case uuid"), + ensemble_name: str = Query(description="Ensemble name"), + realization: Optional[int] = Query(None, description="Optional realization to include. If not specified, all realizations will be returned."), + # fmt:on +) -> WellCompletionsData: + access = WellCompletionsAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) + well_completions_data = access.get_well_completions_data(realization=realization) + + if not well_completions_data: + raise HTTPException(status_code=404, detail="Well completions data not found") + + return well_completions_data diff --git a/backend/src/services/sumo_access/well_completion_access.py b/backend/src/services/sumo_access/well_completion_access.py deleted file mode 100644 index d226d0ee5..000000000 --- a/backend/src/services/sumo_access/well_completion_access.py +++ /dev/null @@ -1,70 +0,0 @@ -from typing import Optional - -import pandas as pd - -from fmu.sumo.explorer.explorer import CaseCollection, Case, SumoClient -from ._helpers import create_sumo_client_instance - - -class WellCompletionAccess: - """ - Class for accessing and retrieving well completion data - """ - - def __init__(self, access_token: str, case_uuid: str, iteration_name: str) -> None: - sumo_client: SumoClient = create_sumo_client_instance(access_token) - case_collection = CaseCollection(sumo_client).filter(uuid=case_uuid) - if len(case_collection) > 1: - raise ValueError(f"Multiple sumo cases found {case_uuid=}") - if len(case_collection) < 1: - raise ValueError(f"No sumo cases found {case_uuid=}") - - self._case: Case = case_collection[0] - self._iteration_name = iteration_name - self._tagname = str("wellcompletiondata") # Should tagname be hard coded? - - def get_well_completion_data(self, realization: Optional[int]) -> pd.DataFrame: - """Get well completion data for case and iteration""" - - # With single realization, return the table including additional column REAL - if realization is not None: - well_completion_tables = self._case.tables.filter( - tagname=self._tagname, realization=realization, iteration=self._iteration_name - ) - well_completion_df = well_completion_tables[0].to_pandas if len(well_completion_tables) > 0 else None - if well_completion_df is None: - return {} - - well_completion_df["REAL"] = realization - return well_completion_df - - # With multiple realizations, retrieve each column and concatenate - # Expect one table with aggregated OP/SH and one with aggregate KH data - well_completion_tables = self._case.tables.filter( - tagname=self._tagname, aggregation="collection", iteration=self._iteration_name - ) - - # Improve code (iterate over tables and concatenate) - concat gives issue? See jupyter-notebook - if len(well_completion_tables) < 2: - return {} - - first_df = well_completion_tables[0].to_pandas - second_df = well_completion_tables[1].to_pandas - - expected_columns = set(["WELL", "DATE", "ZONE", "REAL"]) - if not set(first_df.columns).issuperset(expected_columns) or not set(second_df.columns).issuperset( - expected_columns - ): - raise ValueError( - f"Expected df columns to be superset of columns: {expected_columns} - got: {first_df.columns} and {second_df.columns}" - ) - - if "OP/SH" in first_df.columns and "KH" in second_df.columns: - first_df["KH"] = second_df["KH"] - return first_df - - if "OP/SH" in second_df.columns and "KH" in first_df.columns: - second_df["KH"] = first_df["KH"] - return second_df - - raise ValueError('Expected columns "OP/SH" and "KH" not found in tables') diff --git a/backend/src/services/sumo_access/well_completions_access.py b/backend/src/services/sumo_access/well_completions_access.py new file mode 100644 index 000000000..276e03140 --- /dev/null +++ b/backend/src/services/sumo_access/well_completions_access.py @@ -0,0 +1,277 @@ +import itertools +from typing import Dict, Iterator, List, Optional, Set, Tuple + +import pandas as pd + +from fmu.sumo.explorer.explorer import CaseCollection, Case, SumoClient +from ._helpers import create_sumo_client_instance + +from .well_completions_types import ( + Completions, + WellCompletionsAttributeType, + WellCompletionsWell, + WellCompletionsData, + WellCompletionsZone, + WellCompletionsUnitInfo, + WellCompletionsUnits, +) + + +class WellCompletionsAccess: + """ + Class for accessing and retrieving well completions data + """ + + def __init__(self, access_token: str, case_uuid: str, iteration_name: str) -> None: + sumo_client: SumoClient = create_sumo_client_instance(access_token) + case_collection = CaseCollection(sumo_client).filter(uuid=case_uuid) + if len(case_collection) > 1: + raise ValueError(f"Multiple sumo cases found {case_uuid=}") + if len(case_collection) < 1: + raise ValueError(f"No sumo cases found {case_uuid=}") + + self._case: Case = case_collection[0] + self._iteration_name = iteration_name + self._tagname = str("wellcompletiondata") # Should tagname be hard coded? + + def get_well_completions_data(self, realization: Optional[int]) -> Optional[WellCompletionsData]: + """Get well completions data for case and iteration""" + + # With single realization, filter on realization + if realization is not None: + well_completions_tables = self._case.tables.filter( + tagname=self._tagname, realization=realization, iteration=self._iteration_name + ) + well_completions_df = well_completions_tables[0].to_pandas if len(well_completions_tables) > 0 else None + if well_completions_df is None: + return None + + return WellCompletionDataConverter(well_completions_df).create_data() + + # With multiple realizations, expect one table with aggregated OP/SH and one with aggregate KH data + well_completions_tables = self._case.tables.filter( + tagname=self._tagname, aggregation="collection", iteration=self._iteration_name + ) + + # As of now, two tables are expected - one with OP/SH and one with KH + if len(well_completions_tables) < 2: + return None + + expected_common_columns = set(["WELL", "DATE", "ZONE", "REAL"]) + first_df = well_completions_tables[0].to_pandas + second_df = well_completions_tables[1].to_pandas + + # Validate columns and ensure equal column content in both tables + self._validate_common_dataframe_columns(expected_common_columns, first_df, second_df) + + # Assign "KH" column to the dataframe with missing column + if "OP/SH" in first_df.columns and "KH" in second_df.columns: + first_df["KH"] = second_df["KH"] + return WellCompletionDataConverter(first_df).create_data() + if "OP/SH" in second_df.columns and "KH" in first_df.columns: + second_df["KH"] = first_df["KH"] + return WellCompletionDataConverter(second_df).create_data() + + raise ValueError('Expected columns "OP/SH" and "KH" not found in tables') + + def _validate_common_dataframe_columns( + self, common_column_names: Set[str], first_df: pd.DataFrame, second_df: pd.DataFrame + ) -> None: + """ + Validates that the two dataframes contains same common columns and that the columns have the same content, + raises value error if not matching. + """ + # Ensure expected columns are present + if not common_column_names.issubset(first_df.columns): + raise ValueError(f"Expected columns of first table: {common_column_names} - got: {first_df.columns}") + if not common_column_names.issubset(second_df.columns): + raise ValueError(f"Expected columns of second table: {common_column_names} - got: {second_df.columns}") + + # Verify equal columns in both tables + for column_name in common_column_names: + if not (first_df[column_name] == second_df[column_name]).all(): + raise ValueError(f'Expected equal column content, "{column_name}", in first and second dataframe') + + +class WellCompletionDataConverter: + """ + Class for converter into WellCompletionData type from a pandas dataframe with well completions data + + Accessor retrieves well completions data from Sumo as table data. This converter class handles + the pandas dataframe and provides a data structure for API to consume. + """ + + def __init__(self, well_completions_df: pd.DataFrame) -> None: + # NOTE: Which level of verification? + # - Only columns names? + # - Verify dtype of columns? + # - Verify dimension of columns - only 2D df? + + # Based on realization filtering in Accessor, the "REAL" column is optional - not expected + expected_columns = set(["WELL", "DATE", "ZONE", "OP/SH", "KH"]) + + if not expected_columns.issubset(well_completions_df.columns): + raise ValueError(f"Expected columns: {expected_columns} - got: {well_completions_df.columns}") + + self._well_completions_df = well_completions_df + + # NOTE: Metadata should be provided by Sumo? + # _kh_unit = ( + # kh_metadata.unit + # if kh_metadata is not None and kh_metadata.unit is not None + # else "" + # ) + self._kh_unit = "mDm" # NOTE: How to find metadata? + self._kh_decimal_places = 2 + self._datemap = {dte: i for i, dte in enumerate(sorted(self._well_completions_df["DATE"].unique()))} + self._zones = list(sorted(self._well_completions_df["ZONE"].unique())) + + self._well_completions_df["TIMESTEP"] = self._well_completions_df["DATE"].map(self._datemap) + + # NOTE: + # - How to handle well attributes? Should be provided by Sumo? + # - How to handle theme colors? + self._well_attributes: Dict[ + str, Dict[str, WellCompletionsAttributeType] + ] = {} # Each well has dict of attributes + self._theme_colors = ["#6EA35A", "#EDAF4C", "#CA413D"] # Hard coded + + def _dummy_stratigraphy(self) -> List[WellCompletionsZone]: + """ + Returns a default stratigraphy for TESTING, should be provided by Sumo + """ + return [ + WellCompletionsZone( + name="TopVolantis_BaseVolantis", + color="#6EA35A", + subzones=[ + WellCompletionsZone(name="Valysar", color="#6EA35A"), + WellCompletionsZone(name="Therys", color="#EDAF4C"), + WellCompletionsZone(name="Volon", color="#CA413D"), + ], + ), + ] + + def create_data(self) -> WellCompletionsData: + """Creates well completions dataset for front-end""" + + return WellCompletionsData( + version="1.1.0", + units=WellCompletionsUnits( + kh=WellCompletionsUnitInfo(unit=self._kh_unit, decimalPlaces=self._kh_decimal_places) + ), + stratigraphy=self._extract_stratigraphy(self._dummy_stratigraphy(), self._zones), + timeSteps=[pd.to_datetime(str(dte)).strftime("%Y-%m-%d") for dte in self._datemap.keys()], + wells=self._extract_wells(), + ) + + def _extract_wells(self) -> List[WellCompletionsWell]: + """Generates the wells part of the dataset to front-end""" + # Optional "REAL" column, i.e. no column implies only one realization + no_real = self._well_completions_df["REAL"].nunique() if "REAL" in self._well_completions_df.columns else 1 + + well_list = [] + for well_name, well_group in self._well_completions_df.groupby("WELL"): + well_data = self._extract_well(well_group, well_name, no_real) + well_data.attributes = self._well_attributes[well_name] if well_name in self._well_attributes else {} + well_list.append(well_data) + return well_list + + def _extract_well(self, well_group: pd.DataFrame, well_name: str, no_real: int) -> WellCompletionsWell: + """Extract completions events and kh values for a single well""" + well: WellCompletionsWell = WellCompletionsWell(name=well_name, attributes={}, completions={}) + + completions: Dict[str, Completions] = {} + for (zone, timestep), group_df in well_group.groupby(["ZONE", "TIMESTEP"]): + data = group_df["OP/SH"].value_counts() + if zone not in completions: + completions[zone] = Completions(t=[], open=[], shut=[], kh_mean=[], kh_min=[], kh_max=[]) + + zone_completions = completions[zone] + zone_completions.t.append(int(timestep)) + zone_completions.open.append(float(data["OPEN"] / no_real if "OPEN" in data else 0)) + zone_completions.shut.append(float(data["SHUT"] / no_real if "SHUT" in data else 0)) + zone_completions.kh_mean.append(round(float(group_df["KH"].mean()), 2)) + zone_completions.kh_min.append(round(float(group_df["KH"].min()), 2)) + zone_completions.kh_max.append(round(float(group_df["KH"].max()), 2)) + + well.completions = completions + return well + + def _extract_stratigraphy( + self, stratigraphy: Optional[List[WellCompletionsZone]], zones: List[str] + ) -> List[WellCompletionsZone]: + """Returns the stratigraphy part of the dataset to front-end""" + color_iterator = itertools.cycle(self._theme_colors) + + # If no stratigraphy file is found then the stratigraphy is + # created from the unique zones in the well completions data input. + # They will then probably not come in the correct order. + if stratigraphy is None: + return [WellCompletionsZone(name=zone, color=next(color_iterator)) for zone in zones] + + # If stratigraphy is not None the following is done: + stratigraphy, remaining_valid_zones = self._filter_valid_nodes(stratigraphy, zones) + + if remaining_valid_zones: + raise ValueError( + "The following zones are defined in the well completions data, " + f"but not in the stratigraphy: {remaining_valid_zones}" + ) + + return self._add_colors_to_stratigraphy(stratigraphy, color_iterator) + + def _add_colors_to_stratigraphy( + self, + stratigraphy: List[WellCompletionsZone], + color_iterator: Iterator, + zone_color_mapping: Optional[Dict[str, str]] = None, + ) -> List[WellCompletionsZone]: + """Add colors to the stratigraphy tree. The function will recursively parse the tree. + + There are tree sources of color: + 1. The color is given in the stratigraphy list, in which case nothing is done to the node + 2. The color is the optional the zone->color map + 3. If none of the above applies, the color will be taken from the theme color iterable for \ + the leaves. For other levels, a dummy color grey is used + """ + for zone in stratigraphy: + if zone.color == "": + if zone_color_mapping is not None and zone.name in zone_color_mapping: + zone.color = zone_color_mapping[zone.name] + elif zone.subzones is None: + zone = next(color_iterator) # theme colors only applied on leaves + else: + zone.color = "#808080" # grey + if zone.subzones is not None: + zone.subzones = self._add_colors_to_stratigraphy( + zone.subzones, + color_iterator, + zone_color_mapping=zone_color_mapping, + ) + return stratigraphy + + def _filter_valid_nodes( + self, stratigraphy: List[WellCompletionsZone], valid_zone_names: List[str] + ) -> Tuple[List[WellCompletionsZone], List[str]]: + """Returns the stratigraphy tree with only valid nodes. + A node is considered valid if it self or one of it's subzones are in the + valid zone names list (passed from the lyr file) + + The function recursively parses the tree to add valid nodes. + """ + + output = [] + remaining_valid_zones = valid_zone_names + for zone in stratigraphy: + if zone.subzones is not None: + zone.subzones, remaining_valid_zones = self._filter_valid_nodes(zone.subzones, remaining_valid_zones) + if zone.name in remaining_valid_zones: + output.append(zone) + remaining_valid_zones = [ + elm for elm in remaining_valid_zones if elm != zone.name + ] # remove zone name from valid zones if it is found in the stratigraphy + elif zone.subzones is not None: + output.append(zone) + + return output, remaining_valid_zones diff --git a/backend/src/services/sumo_access/well_completions_types.py b/backend/src/services/sumo_access/well_completions_types.py new file mode 100644 index 000000000..4e23be989 --- /dev/null +++ b/backend/src/services/sumo_access/well_completions_types.py @@ -0,0 +1,45 @@ +from typing import Dict, List, Optional, Union +from pydantic import BaseModel + + +WellCompletionsAttributeType = Union[str, int, bool] + + +class Completions(BaseModel): + t: List[int] + open: List[float] + shut: List[float] + kh_mean: List[float] + kh_min: List[float] + kh_max: List[float] + + +class WellCompletionsWell(BaseModel): + name: str + attributes: Dict[str, WellCompletionsAttributeType] + completions: Dict[str, Completions] + + +class WellCompletionsZone(BaseModel): + name: str + color: str + subzones: Optional[List["WellCompletionsZone"]] = None + + +class WellCompletionsUnitInfo(BaseModel): + unit: str + decimalPlaces: int + + +class WellCompletionsUnits(BaseModel): + kh: WellCompletionsUnitInfo + + +class WellCompletionsData(BaseModel): + """Type definition for well completions data""" + + version: str + units: WellCompletionsUnits + stratigraphy: List[WellCompletionsZone] + timeSteps: List[str] + wells: List[WellCompletionsWell] diff --git a/backend/src/services/types/well_completion_types.py b/backend/src/services/types/well_completion_types.py deleted file mode 100644 index 0df76ca7a..000000000 --- a/backend/src/services/types/well_completion_types.py +++ /dev/null @@ -1,48 +0,0 @@ -from pydantic import BaseModel -from typing import Dict, List, Optional, Union - - -WellCompletionAttributeType = Union[str, int, bool] - - -class Completions(BaseModel): - t: List[int] - open: List[float] - shut: List[float] - kh_mean: List[float] - kh_min: List[float] - kh_max: List[float] - - -class WellCompletionWellInfo(BaseModel): - name: str - attributes: Dict[str, WellCompletionAttributeType] - - -class WellCompletionWell(WellCompletionWellInfo): - completions: Dict[str, Completions] - - -class WellCompletionZone(BaseModel): - name: str - color: str - subzones: Optional[List["WellCompletionZone"]] = None - - -class WellCompletionUnitInfo(BaseModel): - unit: str - decimalPlaces: int - - -class WellCompletionUnits(BaseModel): - kh: WellCompletionUnitInfo - - -class WellCompletionDataSet(BaseModel): - """Type definition for well completion data set""" - - version: str - units: WellCompletionUnits - stratigraphy: List[WellCompletionZone] - timeSteps: List[str] - wells: List[WellCompletionWell] diff --git a/backend/src/services/utils/well_completion_utils.py b/backend/src/services/utils/well_completion_utils.py deleted file mode 100644 index 7f51e26e4..000000000 --- a/backend/src/services/utils/well_completion_utils.py +++ /dev/null @@ -1,187 +0,0 @@ -import itertools -from typing import Dict, Iterator, List, Optional, Tuple - -import pandas as pd - -from src.services.types.well_completion_types import ( - Completions, - WellCompletionAttributeType, - WellCompletionWell, - WellCompletionDataSet, - WellCompletionZone, - WellCompletionUnitInfo, - WellCompletionUnits, -) - - -class WellCompletionDataModel: - def __init__(self, well_completion_data: pd.DataFrame) -> None: - # NOTE: Which level of verification? - # - Only columns names? - # - Verify dtype of columns? - # - Verify dimension of columns - only 2D df? - - expected_columns = set(["WELL", "DATE", "ZONE", "REAL", "OP/SH", "KH"]) - if expected_columns != set(well_completion_data.columns): - raise ValueError(f"Expected columns: {expected_columns} - got: {well_completion_data.columns}") - - self._well_completion_df = well_completion_data - - # NOTE: Metadata should be provided by Sumo? - # _kh_unit = ( - # kh_metadata.unit - # if kh_metadata is not None and kh_metadata.unit is not None - # else "" - # ) - self._kh_unit = "mDm" # NOTE: How to find metadata? - self._kh_decimal_places = 2 - self._datemap = {dte: i for i, dte in enumerate(sorted(self._well_completion_df["DATE"].unique()))} - self._zones = list(sorted(self._well_completion_df["ZONE"].unique())) - - self._well_completion_df["TIMESTEP"] = self._well_completion_df["DATE"].map(self._datemap) - - # NOTE: - # - How to handle well attributes? Should be provided by Sumo? - # - How to handle theme colors? - self._well_attributes: Dict[ - str, Dict[str, WellCompletionAttributeType] - ] = {} # Each well has dict of attributes - self._theme_colors = ["#6EA35A", "#EDAF4C", "#CA413D"] # Hard coded - - def _dummy_stratigraphy(self) -> List[WellCompletionZone]: - """ - Returns a default stratigraphy for TESTING, should be provided by Sumo - """ - return [ - WellCompletionZone( - name="TopVolantis_BaseVolantis", - color="#6EA35A", - subzones=[ - WellCompletionZone(name="Valysar", color="#6EA35A"), - WellCompletionZone(name="Therys", color="#EDAF4C"), - WellCompletionZone(name="Volon", color="#CA413D"), - ], - ), - ] - - def create_well_completion_dataset(self) -> WellCompletionDataSet: - """Creates well completion dataset for front-end""" - - return WellCompletionDataSet( - version="1.1.0", - units=WellCompletionUnits( - kh=WellCompletionUnitInfo(unit=self._kh_unit, decimalPlaces=self._kh_decimal_places) - ), - stratigraphy=self._extract_stratigraphy(self._dummy_stratigraphy(), self._zones), - timeSteps=[pd.to_datetime(str(dte)).strftime("%Y-%m-%d") for dte in self._datemap.keys()], - wells=self._extract_wells(), - ) - - def _extract_wells(self) -> List[WellCompletionWell]: - """Generates the wells part of the dataset to front-end""" - well_list = [] - no_real = self._well_completion_df["REAL"].nunique() - for well_name, well_group in self._well_completion_df.groupby("WELL"): - well_data = self._extract_well(well_group, well_name, no_real) - well_data.attributes = self._well_attributes[well_name] if well_name in self._well_attributes else {} - well_list.append(well_data) - return well_list - - def _extract_well(self, well_group: pd.DataFrame, well_name: str, no_real: int) -> WellCompletionWell: - """Extract completion events and kh values for a single well""" - well: WellCompletionWell = WellCompletionWell(name=well_name, attributes={}, completions={}) - - completions: Dict[str, Completions] = {} - for (zone, timestep), group_df in well_group.groupby(["ZONE", "TIMESTEP"]): - data = group_df["OP/SH"].value_counts() - if zone not in completions: - completions[zone] = Completions(t=[], open=[], shut=[], kh_mean=[], kh_min=[], kh_max=[]) - - zone_completions = completions[zone] - zone_completions.t.append(int(timestep)) - zone_completions.open.append(float(data["OPEN"] / no_real if "OPEN" in data else 0)) - zone_completions.shut.append(float(data["SHUT"] / no_real if "SHUT" in data else 0)) - zone_completions.kh_mean.append(round(float(group_df["KH"].mean()), 2)) - zone_completions.kh_min.append(round(float(group_df["KH"].min()), 2)) - zone_completions.kh_max.append(round(float(group_df["KH"].max()), 2)) - - well.completions = completions - return well - - def _extract_stratigraphy( - self, stratigraphy: Optional[List[WellCompletionZone]], zones: List[str] - ) -> List[WellCompletionZone]: - """Returns the stratigraphy part of the dataset to front-end""" - color_iterator = itertools.cycle(self._theme_colors) - - # If no stratigraphy file is found then the stratigraphy is - # created from the unique zones in the wellcompletiondata input. - # They will then probably not come in the correct order. - if stratigraphy is None: - return [WellCompletionZone(name=zone, color=next(color_iterator)) for zone in zones] - - # If stratigraphy is not None the following is done: - stratigraphy, remaining_valid_zones = self._filter_valid_nodes(stratigraphy, zones) - - if remaining_valid_zones: - raise ValueError( - "The following zones are defined in the well completion data, " - f"but not in the stratigraphy: {remaining_valid_zones}" - ) - - return self._add_colors_to_stratigraphy(stratigraphy, color_iterator) - - def _add_colors_to_stratigraphy( - self, - stratigraphy: List[WellCompletionZone], - color_iterator: Iterator, - zone_color_mapping: Optional[Dict[str, str]] = None, - ) -> List[WellCompletionZone]: - """Add colors to the stratigraphy tree. The function will recursively parse the tree. - - There are tree sources of color: - 1. The color is given in the stratigraphy list, in which case nothing is done to the node - 2. The color is the optional the zone->color map - 3. If none of the above applies, the color will be taken from the theme color iterable for \ - the leaves. For other levels, a dummy color grey is used - """ - for zone in stratigraphy: - if zone.color == "": - if zone_color_mapping is not None and zone.name in zone_color_mapping: - zone.color = zone_color_mapping[zone.name] - elif zone.subzones is None: - zone = next(color_iterator) # theme colors only applied on leaves - else: - zone.color = "#808080" # grey - if zone.subzones is not None: - zone.subzones = self._add_colors_to_stratigraphy( - zone.subzones, - color_iterator, - zone_color_mapping=zone_color_mapping, - ) - return stratigraphy - - def _filter_valid_nodes( - self, stratigraphy: List[WellCompletionZone], valid_zone_names: List[str] - ) -> Tuple[List[WellCompletionZone], List[str]]: - """Returns the stratigraphy tree with only valid nodes. - A node is considered valid if it self or one of it's subzones are in the - valid zone names list (passed from the lyr file) - - The function recursively parses the tree to add valid nodes. - """ - - output = [] - remaining_valid_zones = valid_zone_names - for zone in stratigraphy: - if zone.subzones is not None: - zone.subzones, remaining_valid_zones = self._filter_valid_nodes(zone.subzones, remaining_valid_zones) - if zone.name in remaining_valid_zones: - output.append(zone) - remaining_valid_zones = [ - elm for elm in remaining_valid_zones if elm != zone.name - ] # remove zone name from valid zones if it is found in the stratigraphy - elif zone.subzones is not None: - output.append(zone) - - return output, remaining_valid_zones diff --git a/frontend/src/api/ApiService.ts b/frontend/src/api/ApiService.ts index 44c7fe0f9..d0fab3543 100644 --- a/frontend/src/api/ApiService.ts +++ b/frontend/src/api/ApiService.ts @@ -15,7 +15,7 @@ import { SurfaceService } from './services/SurfaceService'; import { SurfacePolygonsService } from './services/SurfacePolygonsService'; import { TimeseriesService } from './services/TimeseriesService'; import { WellService } from './services/WellService'; -import { WellCompletionService } from './services/WellCompletionService'; +import { WellCompletionsService } from './services/WellCompletionsService'; type HttpRequestConstructor = new (config: OpenAPIConfig) => BaseHttpRequest; @@ -31,7 +31,7 @@ export class ApiService { public readonly surfacePolygons: SurfacePolygonsService; public readonly timeseries: TimeseriesService; public readonly well: WellService; - public readonly wellCompletion: WellCompletionService; + public readonly wellCompletions: WellCompletionsService; public readonly request: BaseHttpRequest; @@ -58,7 +58,7 @@ export class ApiService { this.surfacePolygons = new SurfacePolygonsService(this.request); this.timeseries = new TimeseriesService(this.request); this.well = new WellService(this.request); - this.wellCompletion = new WellCompletionService(this.request); + this.wellCompletions = new WellCompletionsService(this.request); } } diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 1923f75da..174ba475c 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -47,12 +47,11 @@ export type { VectorStatisticData as VectorStatisticData_api } from './models/Ve export type { VectorStatisticSensitivityData as VectorStatisticSensitivityData_api } from './models/VectorStatisticSensitivityData'; export type { WellBoreHeader as WellBoreHeader_api } from './models/WellBoreHeader'; export type { WellBoreTrajectory as WellBoreTrajectory_api } from './models/WellBoreTrajectory'; -export type { WellCompletionData as WellCompletionData_api } from './models/WellCompletionData'; -export type { WellCompletionDataSet as WellCompletionDataSet_api } from './models/WellCompletionDataSet'; -export type { WellCompletionUnitInfo as WellCompletionUnitInfo_api } from './models/WellCompletionUnitInfo'; -export type { WellCompletionUnits as WellCompletionUnits_api } from './models/WellCompletionUnits'; -export type { WellCompletionWell as WellCompletionWell_api } from './models/WellCompletionWell'; -export type { WellCompletionZone as WellCompletionZone_api } from './models/WellCompletionZone'; +export type { WellCompletionsData as WellCompletionsData_api } from './models/WellCompletionsData'; +export type { WellCompletionsUnitInfo as WellCompletionsUnitInfo_api } from './models/WellCompletionsUnitInfo'; +export type { WellCompletionsUnits as WellCompletionsUnits_api } from './models/WellCompletionsUnits'; +export type { WellCompletionsWell as WellCompletionsWell_api } from './models/WellCompletionsWell'; +export type { WellCompletionsZone as WellCompletionsZone_api } from './models/WellCompletionsZone'; export { DefaultService } from './services/DefaultService'; export { ExploreService } from './services/ExploreService'; @@ -64,4 +63,4 @@ export { SurfaceService } from './services/SurfaceService'; export { SurfacePolygonsService } from './services/SurfacePolygonsService'; export { TimeseriesService } from './services/TimeseriesService'; export { WellService } from './services/WellService'; -export { WellCompletionService } from './services/WellCompletionService'; +export { WellCompletionsService } from './services/WellCompletionsService'; diff --git a/frontend/src/api/models/WellCompletionData.ts b/frontend/src/api/models/WellCompletionData.ts deleted file mode 100644 index 9d9d24caf..000000000 --- a/frontend/src/api/models/WellCompletionData.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ - -import type { WellCompletionDataSet } from './WellCompletionDataSet'; - -export type WellCompletionData = { - json_data: WellCompletionDataSet; -}; - diff --git a/frontend/src/api/models/WellCompletionDataSet.ts b/frontend/src/api/models/WellCompletionDataSet.ts deleted file mode 100644 index add799c58..000000000 --- a/frontend/src/api/models/WellCompletionDataSet.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ - -import type { WellCompletionUnits } from './WellCompletionUnits'; -import type { WellCompletionWell } from './WellCompletionWell'; -import type { WellCompletionZone } from './WellCompletionZone'; - -/** - * Type definition for well completion data set - */ -export type WellCompletionDataSet = { - version: string; - units: WellCompletionUnits; - stratigraphy: Array; - timeSteps: Array; - wells: Array; -}; - diff --git a/frontend/src/api/models/WellCompletionUnits.ts b/frontend/src/api/models/WellCompletionUnits.ts deleted file mode 100644 index 9aa8c07cd..000000000 --- a/frontend/src/api/models/WellCompletionUnits.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ - -import type { WellCompletionUnitInfo } from './WellCompletionUnitInfo'; - -export type WellCompletionUnits = { - kh: WellCompletionUnitInfo; -}; - diff --git a/frontend/src/api/models/WellCompletionsData.ts b/frontend/src/api/models/WellCompletionsData.ts new file mode 100644 index 000000000..272ec94d2 --- /dev/null +++ b/frontend/src/api/models/WellCompletionsData.ts @@ -0,0 +1,19 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { WellCompletionsUnits } from './WellCompletionsUnits'; +import type { WellCompletionsWell } from './WellCompletionsWell'; +import type { WellCompletionsZone } from './WellCompletionsZone'; + +/** + * Type definition for well completions data + */ +export type WellCompletionsData = { + version: string; + units: WellCompletionsUnits; + stratigraphy: Array; + timeSteps: Array; + wells: Array; +}; + diff --git a/frontend/src/api/models/WellCompletionUnitInfo.ts b/frontend/src/api/models/WellCompletionsUnitInfo.ts similarity index 74% rename from frontend/src/api/models/WellCompletionUnitInfo.ts rename to frontend/src/api/models/WellCompletionsUnitInfo.ts index b3b6e2512..8eafff910 100644 --- a/frontend/src/api/models/WellCompletionUnitInfo.ts +++ b/frontend/src/api/models/WellCompletionsUnitInfo.ts @@ -2,7 +2,7 @@ /* tslint:disable */ /* eslint-disable */ -export type WellCompletionUnitInfo = { +export type WellCompletionsUnitInfo = { unit: string; decimalPlaces: number; }; diff --git a/frontend/src/api/models/WellCompletionsUnits.ts b/frontend/src/api/models/WellCompletionsUnits.ts new file mode 100644 index 000000000..26ea69dfa --- /dev/null +++ b/frontend/src/api/models/WellCompletionsUnits.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { WellCompletionsUnitInfo } from './WellCompletionsUnitInfo'; + +export type WellCompletionsUnits = { + kh: WellCompletionsUnitInfo; +}; + diff --git a/frontend/src/api/models/WellCompletionWell.ts b/frontend/src/api/models/WellCompletionsWell.ts similarity index 87% rename from frontend/src/api/models/WellCompletionWell.ts rename to frontend/src/api/models/WellCompletionsWell.ts index d0ee31077..d9afb14c2 100644 --- a/frontend/src/api/models/WellCompletionWell.ts +++ b/frontend/src/api/models/WellCompletionsWell.ts @@ -4,7 +4,7 @@ import type { Completions } from './Completions'; -export type WellCompletionWell = { +export type WellCompletionsWell = { name: string; attributes: Record; completions: Record; diff --git a/frontend/src/api/models/WellCompletionZone.ts b/frontend/src/api/models/WellCompletionsZone.ts similarity index 56% rename from frontend/src/api/models/WellCompletionZone.ts rename to frontend/src/api/models/WellCompletionsZone.ts index 15202c456..02047e005 100644 --- a/frontend/src/api/models/WellCompletionZone.ts +++ b/frontend/src/api/models/WellCompletionsZone.ts @@ -2,9 +2,9 @@ /* tslint:disable */ /* eslint-disable */ -export type WellCompletionZone = { +export type WellCompletionsZone = { name: string; color: string; - subzones: (Array | null); + subzones: (Array | null); }; diff --git a/frontend/src/api/services/WellCompletionService.ts b/frontend/src/api/services/WellCompletionsService.ts similarity index 72% rename from frontend/src/api/services/WellCompletionService.ts rename to frontend/src/api/services/WellCompletionsService.ts index 99c919b59..23bf1ecf6 100644 --- a/frontend/src/api/services/WellCompletionService.ts +++ b/frontend/src/api/services/WellCompletionsService.ts @@ -1,31 +1,31 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -import type { WellCompletionData } from '../models/WellCompletionData'; +import type { WellCompletionsData } from '../models/WellCompletionsData'; import type { CancelablePromise } from '../core/CancelablePromise'; import type { BaseHttpRequest } from '../core/BaseHttpRequest'; -export class WellCompletionService { +export class WellCompletionsService { constructor(public readonly httpRequest: BaseHttpRequest) {} /** - * Get Well Completion Data + * Get Well Completions Data * @param caseUuid Sumo case uuid * @param ensembleName Ensemble name * @param realization Optional realization to include. If not specified, all realizations will be returned. - * @returns WellCompletionData Successful Response + * @returns WellCompletionsData Successful Response * @throws ApiError */ - public getWellCompletionData( + public getWellCompletionsData( caseUuid: string, ensembleName: string, realization?: (number | null), - ): CancelablePromise { + ): CancelablePromise { return this.httpRequest.request({ method: 'GET', - url: '/well_completion/well_completion_data/', + url: '/well_completions/well_completions_data/', query: { 'case_uuid': caseUuid, 'ensemble_name': ensembleName, diff --git a/frontend/src/modules/WellCompletion/loadModule.tsx b/frontend/src/modules/WellCompletions/loadModule.tsx similarity index 81% rename from frontend/src/modules/WellCompletion/loadModule.tsx rename to frontend/src/modules/WellCompletions/loadModule.tsx index 715bc51e8..ce61ed7ad 100644 --- a/frontend/src/modules/WellCompletion/loadModule.tsx +++ b/frontend/src/modules/WellCompletions/loadModule.tsx @@ -10,7 +10,7 @@ const initialState: State = { plotData: null, }; -const module = ModuleRegistry.initModule("WellCompletion", initialState); +const module = ModuleRegistry.initModule("WellCompletions", initialState); module.viewFC = view; module.settingsFC = settings; diff --git a/frontend/src/modules/WellCompletion/queryHooks.tsx b/frontend/src/modules/WellCompletions/queryHooks.tsx similarity index 58% rename from frontend/src/modules/WellCompletion/queryHooks.tsx rename to frontend/src/modules/WellCompletions/queryHooks.tsx index dbb4915a7..77e3689d0 100644 --- a/frontend/src/modules/WellCompletion/queryHooks.tsx +++ b/frontend/src/modules/WellCompletions/queryHooks.tsx @@ -1,19 +1,19 @@ -import { WellCompletionData_api } from "@api"; +import { WellCompletionsData_api } from "@api"; import { apiService } from "@framework/ApiService"; import { UseQueryResult, useQuery } from "@tanstack/react-query"; const STALE_TIME = 60 * 1000; const CACHE_TIME = 60 * 1000; -export function useWellCompletionQuery( +export function useWellCompletionsQuery( caseUuid: string | undefined, ensembleName: string | undefined, realizationNumber: number | undefined -): UseQueryResult { +): UseQueryResult { return useQuery({ - queryKey: ["getWellCompletion", caseUuid, ensembleName, realizationNumber], + queryKey: ["getWellCompletions", caseUuid, ensembleName, realizationNumber], queryFn: () => - apiService.wellCompletion.getWellCompletionData(caseUuid ?? "", ensembleName ?? "", realizationNumber), + apiService.wellCompletions.getWellCompletionsData(caseUuid ?? "", ensembleName ?? "", realizationNumber), staleTime: STALE_TIME, cacheTime: CACHE_TIME, enabled: caseUuid && ensembleName ? true : false, diff --git a/frontend/src/modules/WellCompletion/registerModule.ts b/frontend/src/modules/WellCompletions/registerModule.ts similarity index 78% rename from frontend/src/modules/WellCompletion/registerModule.ts rename to frontend/src/modules/WellCompletions/registerModule.ts index c0c559d90..ed66141d7 100644 --- a/frontend/src/modules/WellCompletion/registerModule.ts +++ b/frontend/src/modules/WellCompletions/registerModule.ts @@ -2,4 +2,4 @@ import { ModuleRegistry } from "@framework/ModuleRegistry"; import { State } from "./state"; -ModuleRegistry.registerModule({ moduleName: "WellCompletion", defaultTitle: "Well Completion" }); +ModuleRegistry.registerModule({ moduleName: "WellCompletions", defaultTitle: "Well Completions" }); diff --git a/frontend/src/modules/WellCompletion/settings.tsx b/frontend/src/modules/WellCompletions/settings.tsx similarity index 95% rename from frontend/src/modules/WellCompletion/settings.tsx rename to frontend/src/modules/WellCompletions/settings.tsx index e4839f37c..c57ca179c 100644 --- a/frontend/src/modules/WellCompletion/settings.tsx +++ b/frontend/src/modules/WellCompletions/settings.tsx @@ -18,7 +18,7 @@ import { resolveClassNames } from "@lib/utils/resolveClassNames"; import { isEqual } from "lodash"; -import { useWellCompletionQuery } from "./queryHooks"; +import { useWellCompletionsQuery } from "./queryHooks"; import { DataLoadingStatus, State } from "./state"; import { TimeAggregationType, WellCompletionsDataAccessor } from "./utils/wellCompletionsDataAccessor"; @@ -62,7 +62,7 @@ export const settings = ({ moduleContext, workbenchSession, workbenchServices }: setSelectedEnsembleIdent(computedEnsembleIdent, acceptInvalidState); } - const wellCompletionQuery = useWellCompletionQuery( + const wellCompletionsQuery = useWellCompletionsQuery( selectedEnsembleIdent?.getCaseUuid(), selectedEnsembleIdent?.getEnsembleName(), realizationSelection === RealizationSelection.Single ? selectedRealizationNumber : undefined @@ -73,14 +73,14 @@ export const settings = ({ moduleContext, workbenchSession, workbenchServices }: React.useEffect( function handleNewQueryData() { - if (!wellCompletionQuery.data) { + if (!wellCompletionsQuery.data) { wellCompletionsDataAccessor.current.clearWellCompletionsData(); setAvailableTimeSteps(null); setPlotData(null); return; } - wellCompletionsDataAccessor.current.parseWellCompletionsData(wellCompletionQuery.data); + wellCompletionsDataAccessor.current.parseWellCompletionsData(wellCompletionsQuery.data); // Update available time steps const allTimeSteps = wellCompletionsDataAccessor.current.getTimeSteps(); @@ -116,20 +116,20 @@ export const settings = ({ moduleContext, workbenchSession, workbenchServices }: } createAndSetPlotData(allTimeSteps, timeStepIndex, selectedTimeStepOptions.timeAggregationType); }, - [wellCompletionQuery.data, selectedTimeStepOptions] + [wellCompletionsQuery.data, selectedTimeStepOptions] ); React.useEffect( function handleQueryStateChange() { - if (wellCompletionQuery.status === "loading" && wellCompletionQuery.fetchStatus === "fetching") { + if (wellCompletionsQuery.status === "loading" && wellCompletionsQuery.fetchStatus === "fetching") { setDataLoadingStatus(DataLoadingStatus.Loading); - } else if (wellCompletionQuery.status === "error") { + } else if (wellCompletionsQuery.status === "error") { setDataLoadingStatus(DataLoadingStatus.Error); - } else if (wellCompletionQuery.status === "success") { + } else if (wellCompletionsQuery.status === "success") { setDataLoadingStatus(DataLoadingStatus.Idle); } }, - [wellCompletionQuery.status, wellCompletionQuery.fetchStatus] + [wellCompletionsQuery.status, wellCompletionsQuery.fetchStatus] ); function createAndSetPlotData( @@ -279,14 +279,14 @@ export const settings = ({ moduleContext, workbenchSession, workbenchServices }: /> {
- {wellCompletionQuery.isError + {wellCompletionsQuery.isError ? "Current ensemble does not contain well completions data" : ""}
} -
+