From 2ec1484dd41e92884b45102874994199891ee578 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Tue, 5 Nov 2024 21:41:51 +0100 Subject: [PATCH 1/8] wip --- backend_py/primary/primary/main.py | 2 + .../primary/routers/relperm/converters.py | 17 ++++ .../primary/primary/routers/relperm/router.py | 27 ++++++ .../primary/routers/relperm/schemas.py | 7 ++ .../services/sumo_access/queries/relperm.py | 83 +++++++++++++++++ .../services/sumo_access/relperm_access.py | 93 +++++++++++++++++++ .../services/sumo_access/relperm_types.py | 15 +++ 7 files changed, 244 insertions(+) create mode 100644 backend_py/primary/primary/routers/relperm/converters.py create mode 100644 backend_py/primary/primary/routers/relperm/router.py create mode 100644 backend_py/primary/primary/routers/relperm/schemas.py create mode 100644 backend_py/primary/primary/services/sumo_access/queries/relperm.py create mode 100644 backend_py/primary/primary/services/sumo_access/relperm_access.py create mode 100644 backend_py/primary/primary/services/sumo_access/relperm_types.py diff --git a/backend_py/primary/primary/main.py b/backend_py/primary/primary/main.py index d2e9fa22e..098821038 100644 --- a/backend_py/primary/primary/main.py +++ b/backend_py/primary/primary/main.py @@ -23,6 +23,7 @@ from primary.routers.parameters.router import router as parameters_router from primary.routers.polygons.router import router as polygons_router from primary.routers.pvt.router import router as pvt_router +from primary.routers.relperm.router import router as relperm_router from primary.routers.rft.router import router as rft_router from primary.routers.seismic.router import router as seismic_router from primary.routers.surface.router import router as surface_router @@ -79,6 +80,7 @@ def custom_generate_unique_id(route: APIRoute) -> str: app.include_router(grid3d_router, prefix="/grid3d", tags=["grid3d"]) app.include_router(group_tree_router, prefix="/group_tree", tags=["group_tree"]) app.include_router(pvt_router, prefix="/pvt", tags=["pvt"]) +app.include_router(relperm_router, prefix="/relperm", tags=["relperm"]) app.include_router(well_completions_router, prefix="/well_completions", tags=["well_completions"]) app.include_router(well_router, prefix="/well", tags=["well"]) app.include_router(seismic_router, prefix="/seismic", tags=["seismic"]) diff --git a/backend_py/primary/primary/routers/relperm/converters.py b/backend_py/primary/primary/routers/relperm/converters.py new file mode 100644 index 000000000..d47e2ad55 --- /dev/null +++ b/backend_py/primary/primary/routers/relperm/converters.py @@ -0,0 +1,17 @@ +import base64 + +import numpy as np +import xtgeo +from numpy.typing import NDArray +from webviz_pkg.core_utils.b64 import b64_encode_float_array_as_float32 + +from primary.services.sumo_access.relperm_access import RelPermAccess, RelPermTableInfo + +from . import schemas + +def to_api_relperm_table_info(table_info: RelPermTableInfo) -> schemas.RelPermTableInfo: + + + return schemas.RelPermTableInfo( + table_name=table_info.table_name, + column_names=table_info.column_names) \ No newline at end of file diff --git a/backend_py/primary/primary/routers/relperm/router.py b/backend_py/primary/primary/routers/relperm/router.py new file mode 100644 index 000000000..e628056c5 --- /dev/null +++ b/backend_py/primary/primary/routers/relperm/router.py @@ -0,0 +1,27 @@ +import logging +from typing import Annotated, List + +from fastapi import APIRouter, Depends, Query + +from primary.auth.auth_helper import AuthHelper +from primary.services.sumo_access.relperm_access import RelPermAccess +from primary.services.utils.authenticated_user import AuthenticatedUser + +from . import schemas +from . import converters + +LOGGER = logging.getLogger(__name__) + +router = APIRouter() + + +@router.get("/table_definition") +async def get_table_definition( + authenticated_user: Annotated[AuthenticatedUser, Depends(AuthHelper.get_authenticated_user)], + case_uuid: Annotated[str, Query(description="Sumo case uuid")], + ensemble_name: Annotated[str, Query(description="Ensemble name")], +) -> List[schemas.RelPermTableInfo]: + access = await RelPermAccess.from_case_uuid_async(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) + relperm_tables_info = await access.get_relperm_tables_info() + return [converters.to_api_relperm_table_info(table_info) for table_info in relperm_tables_info] + diff --git a/backend_py/primary/primary/routers/relperm/schemas.py b/backend_py/primary/primary/routers/relperm/schemas.py new file mode 100644 index 000000000..6545dc6bb --- /dev/null +++ b/backend_py/primary/primary/routers/relperm/schemas.py @@ -0,0 +1,7 @@ +from typing import List + +from pydantic import BaseModel + +class RelPermTableInfo(BaseModel): + table_name: str + column_names: List[str] diff --git a/backend_py/primary/primary/services/sumo_access/queries/relperm.py b/backend_py/primary/primary/services/sumo_access/queries/relperm.py new file mode 100644 index 000000000..29a7cdc3d --- /dev/null +++ b/backend_py/primary/primary/services/sumo_access/queries/relperm.py @@ -0,0 +1,83 @@ +from typing import List +from dataclasses import dataclass +from sumo.wrapper import SumoClient +from ..relperm_types import RelPermTableInfo, RealizationBlobid + + +async def get_relperm_table_names_and_columns( + sumo_client: SumoClient, case_id: str, iteration_name: str +) -> List[RelPermTableInfo]: + query = { + "size": 0, + "query": { + "bool": { + "must": [ + {"term": {"_sumo.parent_object.keyword": case_id}}, + {"term": {"fmu.iteration.name.keyword": iteration_name}}, + {"term": {"class.keyword": "table"}}, + {"term": {"fmu.context.stage.keyword": "realization"}}, + {"term": {"data.content.keyword": "relperm"}}, + ] + } + }, + "aggs": { + "table_names": { + "terms": {"field": "data.name.keyword", "size": 1000}, + "aggs": {"column_names": {"terms": {"field": "data.spec.columns.keyword"}}}, + } + }, + } + response = await sumo_client.post_async("/search", json=query) + result = response.json() + aggs = result.get("aggregations", {}) + table_names = aggs.get("table_names", {}).get("buckets", []) + table_infos: List[RelPermTableInfo] = [] + for table_name in table_names: + column_names_aggs = table_name.get("column_names", {}).get("buckets", []) + column_names = [column_name.get("key") for column_name in column_names_aggs] + table_info = RelPermTableInfo(table_name=table_name.get("key"), column_names=column_names) + table_infos.append(table_info) + return table_infos + + +async def get_relperm_realization_table_blob_uuids( + sumo_client: SumoClient, case_id: str, iteration_name: str, table_name: str +) -> List[RealizationBlobid]: + query = { + "size": 1, + "query": { + "bool": { + "must": [ + {"term": {"_sumo.parent_object.keyword": case_id}}, + {"term": {"fmu.iteration.name.keyword": iteration_name}}, + {"term": {"class.keyword": "table"}}, + {"term": {"fmu.context.stage.keyword": "realization"}}, + {"term": {"data.content.keyword": "relperm"}}, + {"term": {"data.name.keyword": table_name}}, + ] + } + }, + "aggs": { + "key_combinations": { + "composite": { + "size": 65535, + "sources": [ + {"k_blob_names": {"terms": {"field": "_sumo.blob_name.keyword"}}}, + {"k_realizations": {"terms": {"field": "fmu.realization.id"}}}, + ], + } + } + }, + } + response = await sumo_client.post_async("/search", json=query) + result = response.json() + aggs = result.get("aggregations", {}) + + key_combinations = aggs.get("key_combinations", {}).get("buckets", []) + realization_blobids = [] + for key_combination in key_combinations: + blob_name = key_combination.get("key").get("k_blob_names") + realization_id = key_combination.get("key").get("k_realizations") + realization_blobid = RealizationBlobid(blob_name=blob_name, realization_id=realization_id) + realization_blobids.append(realization_blobid) + return realization_blobids diff --git a/backend_py/primary/primary/services/sumo_access/relperm_access.py b/backend_py/primary/primary/services/sumo_access/relperm_access.py new file mode 100644 index 000000000..69dd05568 --- /dev/null +++ b/backend_py/primary/primary/services/sumo_access/relperm_access.py @@ -0,0 +1,93 @@ +from enum import Enum +import logging +from io import BytesIO +import asyncio +from typing import List, Optional, Dict +from dataclasses import dataclass +from fmu.sumo.explorer.objects import Case, TableCollection +import polars as pl +import pyarrow as pa + +from webviz_pkg.core_utils.perf_timer import PerfTimer + +from ._helpers import create_sumo_client, create_sumo_case_async +from ..service_exceptions import ( + Service, + NoDataError, + InvalidDataError, +) + +from .queries.relperm import get_relperm_table_names_and_columns, get_relperm_realization_table_blob_uuids +from .relperm_types import RelPermTableInfo, RealizationBlobid + + +class RelPermFamily(str, Enum): + """Enumeration of relative permeability keyword families""" + + FAMILY_1 = "family_1" # SWOF, SGOF, SLGOF family + FAMILY_2 = "family_2" # SWFN, SGFN, SOF3 family + + +LOGGER = logging.getLogger(__name__) + + +@dataclass +class RelPermSaturationInfo: + name: str + relperm_curve_names: List[str] + capillary_pressure_curve_names: List[str] + + +class RelPermAccess: + def __init__(self, case: Case, case_uuid: str, iteration_name: str): + self._case: Case = case + self._case_uuid: str = case_uuid + self._iteration_name: str = iteration_name + + @classmethod + async def from_case_uuid_async(cls, access_token: str, case_uuid: str, iteration_name: str) -> "RelPermAccess": + sumo_client = create_sumo_client(access_token) + case: Case = await create_sumo_case_async(client=sumo_client, case_uuid=case_uuid, want_keepalive_pit=False) + return RelPermAccess(case=case, case_uuid=case_uuid, iteration_name=iteration_name) + + async def get_relperm_tables_info(self) -> List[RelPermTableInfo]: + table_names_and_columns = await get_relperm_table_names_and_columns( + self._case._sumo, self._case_uuid, self._iteration_name + ) + + valid_table_names_and_columns = [] + for table_info in table_names_and_columns: + if validate_relperm_columns(table_info): + valid_table_names_and_columns.append(table_info) + test = await self.get_relperm_table("DROGON") + return valid_table_names_and_columns + + async def get_relperm_table(self, table_name: str) -> TableCollection: + realization_blob_ids = await get_relperm_realization_table_blob_uuids( + self._case._sumo, self._case_uuid, self._iteration_name, "DROGON" + ) + tasks = [asyncio.create_task(self.fetch_realization_table(table)) for table in realization_blob_ids] + realization_tables = await asyncio.gather(*tasks) + table = pl.concat(realization_tables) + print(table) + + async def fetch_realization_table(self, realization_blob_id: RealizationBlobid) -> pl.DataFrame: + res = await self._case._sumo.get_async(f"/objects('{realization_blob_id.blob_name}')/blob") + blob = BytesIO(res.content) + real_df = pl.read_parquet(blob) + # Add realization id to the dataframe + real_df = real_df.with_columns(pl.lit(realization_blob_id.realization_id).alias("REAL")) + return real_df + + +def validate_relperm_columns(table_info: RelPermTableInfo) -> bool: + if "KEYWORD" not in table_info.column_names: + LOGGER.warning(f"Missing 'KEYWORD' column in table '{table_info.table_name}'") + return False + if "SATNUM" not in table_info.column_names: + LOGGER.warning(f"Missing 'SATNUM' column in table '{table_info.table_name}'") + return False + if not any(saturation in table_info.column_names for saturation in ["SW", "SO", "SG", "SL"]): + LOGGER.warning(f"Missing saturation columns in table '{table_info.table_name}'") + return False + return True diff --git a/backend_py/primary/primary/services/sumo_access/relperm_types.py b/backend_py/primary/primary/services/sumo_access/relperm_types.py new file mode 100644 index 000000000..76f394c71 --- /dev/null +++ b/backend_py/primary/primary/services/sumo_access/relperm_types.py @@ -0,0 +1,15 @@ +from typing import List + +from dataclasses import dataclass + + +@dataclass +class RelPermTableInfo: + table_name: str + column_names: List[str] + + +@dataclass +class RealizationBlobid: + blob_name: str + realization_id: str From ce735d7b5e1e4865dd5245d33f80c4e9aa2ec080 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Thu, 7 Nov 2024 09:11:17 +0100 Subject: [PATCH 2/8] wip --- .../primary/routers/relperm/converters.py | 40 ++- .../primary/primary/routers/relperm/router.py | 53 +++- .../primary/routers/relperm/schemas.py | 21 +- .../relperm_assembler/relperm_assembler.py | 229 ++++++++++++++++++ .../services/sumo_access/queries/relperm.py | 14 +- .../services/sumo_access/relperm_access.py | 83 +++++-- .../services/sumo_access/relperm_types.py | 15 +- frontend/src/api/ApiService.ts | 3 + frontend/src/api/index.ts | 5 + .../src/api/models/RelPermRealizationData.ts | 11 + frontend/src/api/models/RelPermSatNumData.ts | 9 + .../src/api/models/RelPermSaturationAxis.ts | 10 + frontend/src/api/models/RelPermTableInfo.ts | 11 + frontend/src/api/services/RelpermService.ts | 95 ++++++++ frontend/src/framework/ModuleDataTags.ts | 3 +- frontend/src/modules/RelPerm/interfaces.ts | 17 ++ frontend/src/modules/RelPerm/loadModule.tsx | 10 + .../src/modules/RelPerm/registerModule.tsx | 16 ++ frontend/src/modules/RelPerm/settings.tsx | 177 ++++++++++++++ .../RelPerm/settings/atoms/baseAtoms.ts | 21 ++ .../RelPerm/settings/atoms/derivedAtoms.ts | 110 +++++++++ .../RelPerm/settings/atoms/queryAtoms.ts | 103 ++++++++ frontend/src/modules/RelPerm/typesAndEnums.ts | 11 + frontend/src/modules/RelPerm/view.tsx | 159 ++++++++++++ frontend/src/modules/registerAllModules.ts | 4 +- 25 files changed, 1178 insertions(+), 52 deletions(-) create mode 100644 backend_py/primary/primary/services/relperm_assembler/relperm_assembler.py create mode 100644 frontend/src/api/models/RelPermRealizationData.ts create mode 100644 frontend/src/api/models/RelPermSatNumData.ts create mode 100644 frontend/src/api/models/RelPermSaturationAxis.ts create mode 100644 frontend/src/api/models/RelPermTableInfo.ts create mode 100644 frontend/src/api/services/RelpermService.ts create mode 100644 frontend/src/modules/RelPerm/interfaces.ts create mode 100644 frontend/src/modules/RelPerm/loadModule.tsx create mode 100644 frontend/src/modules/RelPerm/registerModule.tsx create mode 100644 frontend/src/modules/RelPerm/settings.tsx create mode 100644 frontend/src/modules/RelPerm/settings/atoms/baseAtoms.ts create mode 100644 frontend/src/modules/RelPerm/settings/atoms/derivedAtoms.ts create mode 100644 frontend/src/modules/RelPerm/settings/atoms/queryAtoms.ts create mode 100644 frontend/src/modules/RelPerm/typesAndEnums.ts create mode 100644 frontend/src/modules/RelPerm/view.tsx diff --git a/backend_py/primary/primary/routers/relperm/converters.py b/backend_py/primary/primary/routers/relperm/converters.py index d47e2ad55..d8cba3e24 100644 --- a/backend_py/primary/primary/routers/relperm/converters.py +++ b/backend_py/primary/primary/routers/relperm/converters.py @@ -1,17 +1,37 @@ -import base64 - -import numpy as np -import xtgeo -from numpy.typing import NDArray -from webviz_pkg.core_utils.b64 import b64_encode_float_array_as_float32 - -from primary.services.sumo_access.relperm_access import RelPermAccess, RelPermTableInfo +from primary.services.relperm_assembler.relperm_assembler import ( + RelPermTableInfo, + RelPermSaturationAxis, + RelPermRealizationData, +) from . import schemas + def to_api_relperm_table_info(table_info: RelPermTableInfo) -> schemas.RelPermTableInfo: - return schemas.RelPermTableInfo( table_name=table_info.table_name, - column_names=table_info.column_names) \ No newline at end of file + saturation_axes=[to_api_relperm_saturation_axis(axis) for axis in table_info.saturation_axes], + satnums=table_info.satnums, + ) + + +def to_api_relperm_saturation_axis(axis: RelPermSaturationAxis) -> schemas.RelPermSaturationAxis: + + return schemas.RelPermSaturationAxis( + saturation_name=axis.saturation_name, + relperm_curve_names=axis.relperm_curve_names, + capillary_pressure_curve_names=axis.capillary_pressure_curve_names, + ) + + +def to_api_relperm_ensemble_data(data: RelPermRealizationData) -> schemas.RelPermRealizationData: + + return schemas.RelPermRealizationData( + saturation_axis_data=data.saturation_axis_data, + satnum_data=[ + schemas.RelPermSatNumData(satnum=satnum_data.satnum, relperm_curves_data=satnum_data.relperm_curves_data) + for satnum_data in data.satnum_data + ], + realization=data.realization, + ) diff --git a/backend_py/primary/primary/routers/relperm/router.py b/backend_py/primary/primary/routers/relperm/router.py index e628056c5..4017e27b5 100644 --- a/backend_py/primary/primary/routers/relperm/router.py +++ b/backend_py/primary/primary/routers/relperm/router.py @@ -1,10 +1,11 @@ import logging -from typing import Annotated, List +from typing import Annotated, List, Any from fastapi import APIRouter, Depends, Query from primary.auth.auth_helper import AuthHelper from primary.services.sumo_access.relperm_access import RelPermAccess +from primary.services.relperm_assembler.relperm_assembler import RelPermAssembler from primary.services.utils.authenticated_user import AuthenticatedUser from . import schemas @@ -15,13 +16,49 @@ router = APIRouter() -@router.get("/table_definition") -async def get_table_definition( +@router.get("/table_names") +async def get_table_names( authenticated_user: Annotated[AuthenticatedUser, Depends(AuthHelper.get_authenticated_user)], case_uuid: Annotated[str, Query(description="Sumo case uuid")], ensemble_name: Annotated[str, Query(description="Ensemble name")], -) -> List[schemas.RelPermTableInfo]: - access = await RelPermAccess.from_case_uuid_async(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) - relperm_tables_info = await access.get_relperm_tables_info() - return [converters.to_api_relperm_table_info(table_info) for table_info in relperm_tables_info] - +) -> List[str]: + access = await RelPermAccess.from_case_uuid_async( + authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name + ) + return await access.get_relperm_table_names() + + +@router.get("/table_info") +async def get_table_info( + authenticated_user: Annotated[AuthenticatedUser, Depends(AuthHelper.get_authenticated_user)], + case_uuid: Annotated[str, Query(description="Sumo case uuid")], + ensemble_name: Annotated[str, Query(description="Ensemble name")], + table_name: Annotated[str, Query(description="Table name")], +) -> schemas.RelPermTableInfo: + access = await RelPermAccess.from_case_uuid_async( + authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name + ) + assembler = RelPermAssembler(access) + relperm_table_info = await assembler.get_relperm_table_info(table_name) + + return converters.to_api_relperm_table_info(relperm_table_info) + + +@router.get("/saturation_and_curve_data") +async def get_saturation_and_curve_data( + authenticated_user: Annotated[AuthenticatedUser, Depends(AuthHelper.get_authenticated_user)], + case_uuid: Annotated[str, Query(description="Sumo case uuid")], + ensemble_name: Annotated[str, Query(description="Ensemble name")], + table_name: Annotated[str, Query(description="Table name")], + saturation_axis_name: Annotated[str, Query(description="Saturation axis name")], + curve_names: Annotated[List[str], Query(description="Curve names")], + satnums: Annotated[List[int], Query(description="Satnums")], +) -> List[schemas.RelPermRealizationData]: + + access = await RelPermAccess.from_case_uuid_async( + authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name + ) + assembler = RelPermAssembler(access) + relperm_data = await assembler.get_relperm_ensemble_data(table_name, saturation_axis_name, curve_names, satnums) + + return [converters.to_api_relperm_ensemble_data(data) for data in relperm_data] diff --git a/backend_py/primary/primary/routers/relperm/schemas.py b/backend_py/primary/primary/routers/relperm/schemas.py index 6545dc6bb..c002785af 100644 --- a/backend_py/primary/primary/routers/relperm/schemas.py +++ b/backend_py/primary/primary/routers/relperm/schemas.py @@ -2,6 +2,25 @@ from pydantic import BaseModel + +class RelPermSaturationAxis(BaseModel): + saturation_name: str + relperm_curve_names: List[str] + capillary_pressure_curve_names: List[str] + + class RelPermTableInfo(BaseModel): table_name: str - column_names: List[str] + saturation_axes: List[RelPermSaturationAxis] + satnums: List[int] + + +class RelPermSatNumData(BaseModel): + satnum: int + relperm_curves_data: List[List[float]] + + +class RelPermRealizationData(BaseModel): + saturation_axis_data: List[float] + satnum_data: List[RelPermSatNumData] + realization: int diff --git a/backend_py/primary/primary/services/relperm_assembler/relperm_assembler.py b/backend_py/primary/primary/services/relperm_assembler/relperm_assembler.py new file mode 100644 index 000000000..f3a2d1de2 --- /dev/null +++ b/backend_py/primary/primary/services/relperm_assembler/relperm_assembler.py @@ -0,0 +1,229 @@ +from enum import Enum +from typing import List +import logging +from dataclasses import dataclass +import numpy as np +from scipy.interpolate import interp1d +import polars as pl +from primary.services.sumo_access.relperm_access import RelPermAccess +from primary.services.service_exceptions import ( + Service, + NoDataError, + InvalidDataError, +) + +LOGGER = logging.getLogger(__name__) + + +class RelPermFamily(str, Enum): + """Enumeration of relative permeability keyword families""" + + FAMILY_1 = "family_1" # SWOF, SGOF, SLGOF family + FAMILY_2 = "family_2" # SWFN, SGFN, SOF3 family + + +RELPERM_FAMILIES = { + 1: ["SWOF", "SGOF", "SLGOF"], + 2: ["SWFN", "SGFN", "SOF3"], +} + + +@dataclass +class RelPermSaturationAxis: + saturation_name: str + relperm_curve_names: List[str] + capillary_pressure_curve_names: List[str] + + +@dataclass +class RelPermTableInfo: + table_name: str + saturation_axes: List[RelPermSaturationAxis] + satnums: List[int] + + +@dataclass +class RelPermSatNumData: + satnum: int + relperm_curves_data: List[List[float]] + + +@dataclass +class RelPermRealizationData: + saturation_axis_data: List[float] + satnum_data: List[RelPermSatNumData] + realization: int + + +class RelPermAssembler: + def __init__(self, relperm_access: RelPermAccess): + self._relperm_access = relperm_access + + async def get_relperm_table_info(self, relperm_table_name: str): + single_realization_table = await self._relperm_access.get_single_realization_table(relperm_table_name) + table_columns = single_realization_table.columns + satnums = extract_satnums_from_relperm_table(single_realization_table) + all_keywords = extract_keywords_from_relperm_table(single_realization_table) + family = extract_familiy_info_from_keywords(all_keywords) + saturation_infos = extract_saturation_axes_from_relperm_table(table_columns, family) + + return RelPermTableInfo( + table_name=relperm_table_name, saturation_axes=saturation_infos, satnums=sorted(satnums) + ) + + async def get_relperm_ensemble_data( + self, relperm_table_name: str, saturation_axis_name: str, curve_names: List[str], satnums: List[int] + ) -> List[RelPermRealizationData]: + realizations_table: pl.DataFrame = await self._relperm_access.get_relperm_table(relperm_table_name) + table_columns = realizations_table.columns + + if saturation_axis_name not in table_columns: + raise NoDataError( + f"Saturation axis {saturation_axis_name} not found in table {relperm_table_name}", + Service.GENERAL, + ) + + for curve_name in curve_names: + if curve_name not in table_columns: + raise NoDataError( + f"Curve {curve_name} not found in saturation axis {saturation_axis_name} in table {relperm_table_name}", + Service.GENERAL, + ) + + columns_to_use = [saturation_axis_name] + curve_names + ["REAL", "SATNUM"] + filtered_table = ( + realizations_table.select(columns_to_use) + .filter((realizations_table["SATNUM"].cast(pl.Int32).is_in(satnums))) + .drop_nulls() + .sort(saturation_axis_name) + ) + shared_saturation_axis = np.linspace(0, 1, 100) + real_data: List[RelPermRealizationData] = [] + for _real, real_table in filtered_table.group_by("REAL"): + satnum_data = [] + for _satnum, satnum_table in real_table.group_by("SATNUM"): + table_to = satnum_table.sort(saturation_axis_name) + original_saturation = table_to[saturation_axis_name].to_numpy() + + # Interpolate to get shared axis + interpolated_curves = [] + for curve_name in curve_names: + original_values = table_to[curve_name] + + interpolator = interp1d( + original_saturation, + original_values, + kind="cubic", + bounds_error=False, + fill_value=(original_values[0], original_values[-1]), + ) + + # Interpolate to shared axis + interpolated_values = interpolator(shared_saturation_axis) + interpolated_curves.append(interpolated_values.tolist()) + satnum_data.append( + RelPermSatNumData( + satnum=table_to["SATNUM"][0], + relperm_curves_data=[table_to[curve_name].to_list() for curve_name in curve_names], + ) + ) + real_data.append( + RelPermRealizationData( + saturation_axis_data=table_to[saturation_axis_name].to_list(), + satnum_data=satnum_data, + realization=table_to["REAL"][0], + ) + ) + return real_data + + +def extract_keywords_from_relperm_table(relperm_table: pl.DataFrame) -> List[str]: + return relperm_table["KEYWORD"].unique().to_list() + + +def extract_satnums_from_relperm_table(relperm_table: pl.DataFrame) -> List[int]: + return relperm_table["SATNUM"].cast(pl.Int32).unique().to_list() + + +def extract_familiy_info_from_keywords(keywords: List[str]) -> RelPermFamily: + + if any(keyword in RELPERM_FAMILIES[1] for keyword in keywords): + if any(keyword in RELPERM_FAMILIES[2] for keyword in keywords): + raise InvalidDataError( + "Mix of keyword family 1 and 2, currently only support one family at this time.", + Service.GENERAL, + ) + return RelPermFamily.FAMILY_1 + + elif not all(keyword in RELPERM_FAMILIES[2] for keyword in keywords): + raise InvalidDataError( + "Unrecognized saturation table keyword in data. This should not occur unless " + "there has been changes to res2df. Update of this plugin might be required.", + Service.GENERAL, + ) + else: + return RelPermFamily.FAMILY_2 + + +def extract_saturation_axes_from_relperm_table( + relperm_table_columns: List[str], relperm_family: RelPermFamily +) -> List[RelPermSaturationAxis]: + saturation_infos = [] + if relperm_family == RelPermFamily.FAMILY_1: + if "SW" in relperm_table_columns: + saturation_infos.append( + RelPermSaturationAxis( + saturation_name="SW", + relperm_curve_names=[ + curve_name for curve_name in ["KRWO", "KRW"] if curve_name in relperm_table_columns + ], + capillary_pressure_curve_names=[ + curve_name for curve_name in ["PCOW"] if curve_name in relperm_table_columns + ], + ) + ) + if "SG" in relperm_table_columns: + saturation_infos.append( + RelPermSaturationAxis( + saturation_name="SG", + relperm_curve_names=[ + curve_name for curve_name in ["KRG", "KROG"] if curve_name in relperm_table_columns + ], + capillary_pressure_curve_names=[ + curve_name for curve_name in ["PCOG"] if curve_name in relperm_table_columns + ], + ) + ) + + if relperm_family == RelPermFamily.FAMILY_2: + if "SW" in relperm_table_columns: + saturation_infos.append( + RelPermSaturationAxis( + saturation_name="SW", + relperm_curve_names=[curve_name for curve_name in ["KRW"] if curve_name in relperm_table_columns], + capillary_pressure_curve_names=[ + curve_name for curve_name in ["PCOW"] if curve_name in relperm_table_columns + ], + ) + ) + if "SG" in relperm_table_columns: + saturation_infos.append( + RelPermSaturationAxis( + saturation_name="SG", + relperm_curve_names=[curve_name for curve_name in ["KRG"] if curve_name in relperm_table_columns], + capillary_pressure_curve_names=[ + curve_name for curve_name in ["PCOG"] if curve_name in relperm_table_columns + ], + ) + ) + if "SO" in relperm_table_columns: + saturation_infos.append( + RelPermSaturationAxis( + saturation_name="SO", + relperm_curve_names=[ + curve_name for curve_name in ["KROW", "KROG"] if curve_name in relperm_table_columns + ], + capillary_pressure_curve_names=[], + ) + ) + return saturation_infos diff --git a/backend_py/primary/primary/services/sumo_access/queries/relperm.py b/backend_py/primary/primary/services/sumo_access/queries/relperm.py index 29a7cdc3d..abfed1321 100644 --- a/backend_py/primary/primary/services/sumo_access/queries/relperm.py +++ b/backend_py/primary/primary/services/sumo_access/queries/relperm.py @@ -1,12 +1,18 @@ from typing import List from dataclasses import dataclass from sumo.wrapper import SumoClient -from ..relperm_types import RelPermTableInfo, RealizationBlobid +from ..relperm_types import RealizationBlobid + + +@dataclass +class TableInfo: + table_name: str + column_names: List[str] async def get_relperm_table_names_and_columns( sumo_client: SumoClient, case_id: str, iteration_name: str -) -> List[RelPermTableInfo]: +) -> List[TableInfo]: query = { "size": 0, "query": { @@ -31,11 +37,11 @@ async def get_relperm_table_names_and_columns( result = response.json() aggs = result.get("aggregations", {}) table_names = aggs.get("table_names", {}).get("buckets", []) - table_infos: List[RelPermTableInfo] = [] + table_infos: List[TableInfo] = [] for table_name in table_names: column_names_aggs = table_name.get("column_names", {}).get("buckets", []) column_names = [column_name.get("key") for column_name in column_names_aggs] - table_info = RelPermTableInfo(table_name=table_name.get("key"), column_names=column_names) + table_info = TableInfo(table_name=table_name.get("key"), column_names=column_names) table_infos.append(table_info) return table_infos diff --git a/backend_py/primary/primary/services/sumo_access/relperm_access.py b/backend_py/primary/primary/services/sumo_access/relperm_access.py index 69dd05568..b7eeb221c 100644 --- a/backend_py/primary/primary/services/sumo_access/relperm_access.py +++ b/backend_py/primary/primary/services/sumo_access/relperm_access.py @@ -2,13 +2,13 @@ import logging from io import BytesIO import asyncio -from typing import List, Optional, Dict +from typing import List, Optional, Dict, Sequence from dataclasses import dataclass from fmu.sumo.explorer.objects import Case, TableCollection import polars as pl import pyarrow as pa -from webviz_pkg.core_utils.perf_timer import PerfTimer +from webviz_pkg.core_utils.perf_metrics import PerfMetrics from ._helpers import create_sumo_client, create_sumo_case_async from ..service_exceptions import ( @@ -17,15 +17,14 @@ InvalidDataError, ) -from .queries.relperm import get_relperm_table_names_and_columns, get_relperm_realization_table_blob_uuids +from .queries.relperm import ( + get_relperm_table_names_and_columns, + get_relperm_realization_table_blob_uuids, +) from .relperm_types import RelPermTableInfo, RealizationBlobid -class RelPermFamily(str, Enum): - """Enumeration of relative permeability keyword families""" - - FAMILY_1 = "family_1" # SWOF, SGOF, SLGOF family - FAMILY_2 = "family_2" # SWFN, SGFN, SOF3 family +SATURATIONS = ["SW", "SO", "SG", "SL"] LOGGER = logging.getLogger(__name__) @@ -38,6 +37,19 @@ class RelPermSaturationInfo: capillary_pressure_curve_names: List[str] +@dataclass +class RelpermCurveData: + curve_name: str + curve_data: List[float] + + +@dataclass +class RelPermEnsembleSaturationData: + saturation_curve_data: List[float] + relperm_curves_data: List[RelpermCurveData] + realizations: List[int] + + class RelPermAccess: def __init__(self, case: Case, case_uuid: str, iteration_name: str): self._case: Case = case @@ -50,26 +62,43 @@ async def from_case_uuid_async(cls, access_token: str, case_uuid: str, iteration case: Case = await create_sumo_case_async(client=sumo_client, case_uuid=case_uuid, want_keepalive_pit=False) return RelPermAccess(case=case, case_uuid=case_uuid, iteration_name=iteration_name) - async def get_relperm_tables_info(self) -> List[RelPermTableInfo]: + async def get_relperm_table_names(self) -> List[str]: table_names_and_columns = await get_relperm_table_names_and_columns( self._case._sumo, self._case_uuid, self._iteration_name ) - - valid_table_names_and_columns = [] + table_names: List[str] = [] for table_info in table_names_and_columns: - if validate_relperm_columns(table_info): - valid_table_names_and_columns.append(table_info) - test = await self.get_relperm_table("DROGON") - return valid_table_names_and_columns + if has_required_relperm_table_columns(table_info.table_name, table_info.column_names): + table_names.append(table_info.table_name) + return table_names - async def get_relperm_table(self, table_name: str) -> TableCollection: + async def get_single_realization_table(self, table_name: str) -> pl.DataFrame: + realization_blob_ids = await get_relperm_realization_table_blob_uuids( + self._case._sumo, self._case_uuid, self._iteration_name, table_name + ) + single_realization_blob_id = realization_blob_ids[0] + return await self.fetch_realization_table(single_realization_blob_id) + + async def get_relperm_table( + self, + table_name: str, + realizations: Sequence[int] | None = None, + ) -> pl.DataFrame: + perf_metrics = PerfMetrics() realization_blob_ids = await get_relperm_realization_table_blob_uuids( - self._case._sumo, self._case_uuid, self._iteration_name, "DROGON" + self._case._sumo, self._case_uuid, self._iteration_name, table_name ) + perf_metrics.record_lap("get_relperm_realization_table_blob_uuids") + tasks = [asyncio.create_task(self.fetch_realization_table(table)) for table in realization_blob_ids] realization_tables = await asyncio.gather(*tasks) + perf_metrics.record_lap("fetch_realization_tables") + table = pl.concat(realization_tables) - print(table) + perf_metrics.record_lap("concat_realization_tables") + + LOGGER.debug(f"RelPermAccess.get_relperm_table: {perf_metrics.to_string()}") + return table async def fetch_realization_table(self, realization_blob_id: RealizationBlobid) -> pl.DataFrame: res = await self._case._sumo.get_async(f"/objects('{realization_blob_id.blob_name}')/blob") @@ -80,14 +109,18 @@ async def fetch_realization_table(self, realization_blob_id: RealizationBlobid) return real_df -def validate_relperm_columns(table_info: RelPermTableInfo) -> bool: - if "KEYWORD" not in table_info.column_names: - LOGGER.warning(f"Missing 'KEYWORD' column in table '{table_info.table_name}'") +def has_required_relperm_table_columns(table_name: str, column_names: List[str]) -> bool: + if "KEYWORD" not in column_names: + LOGGER.warning(f"Missing 'KEYWORD' column in table '{table_name}'") return False - if "SATNUM" not in table_info.column_names: - LOGGER.warning(f"Missing 'SATNUM' column in table '{table_info.table_name}'") + if "SATNUM" not in column_names: + LOGGER.warning(f"Missing 'SATNUM' column in table '{table_name}'") return False - if not any(saturation in table_info.column_names for saturation in ["SW", "SO", "SG", "SL"]): - LOGGER.warning(f"Missing saturation columns in table '{table_info.table_name}'") + if not any(saturation in column_names for saturation in ["SW", "SO", "SG", "SL"]): + LOGGER.warning(f"Missing saturation columns in table '{table_name}'") return False return True + + +def get_saturation_names(column_names: List[str]) -> List[str]: + return [sat for sat in SATURATIONS if sat in column_names] diff --git a/backend_py/primary/primary/services/sumo_access/relperm_types.py b/backend_py/primary/primary/services/sumo_access/relperm_types.py index 76f394c71..c87a7eba4 100644 --- a/backend_py/primary/primary/services/sumo_access/relperm_types.py +++ b/backend_py/primary/primary/services/sumo_access/relperm_types.py @@ -6,10 +6,23 @@ @dataclass class RelPermTableInfo: table_name: str - column_names: List[str] + saturation_names: List[str] @dataclass class RealizationBlobid: blob_name: str realization_id: str + + +@dataclass +class RelpermCurveData: + curve_name: str + curve_data: List[float] + + +@dataclass +class RelPermEnsembleSaturationData: + saturation_curve_data: List[float] + relperm_curves_data: List[RelpermCurveData] + realizations: List[int] diff --git a/frontend/src/api/ApiService.ts b/frontend/src/api/ApiService.ts index bfce0c929..830f78ba7 100644 --- a/frontend/src/api/ApiService.ts +++ b/frontend/src/api/ApiService.ts @@ -15,6 +15,7 @@ import { ObservationsService } from './services/ObservationsService'; import { ParametersService } from './services/ParametersService'; import { PolygonsService } from './services/PolygonsService'; import { PvtService } from './services/PvtService'; +import { RelpermService } from './services/RelpermService'; import { RftService } from './services/RftService'; import { SeismicService } from './services/SeismicService'; import { SurfaceService } from './services/SurfaceService'; @@ -34,6 +35,7 @@ export class ApiService { public readonly parameters: ParametersService; public readonly polygons: PolygonsService; public readonly pvt: PvtService; + public readonly relperm: RelpermService; public readonly rft: RftService; public readonly seismic: SeismicService; public readonly surface: SurfaceService; @@ -64,6 +66,7 @@ export class ApiService { this.parameters = new ParametersService(this.request); this.polygons = new PolygonsService(this.request); this.pvt = new PvtService(this.request); + this.relperm = new RelpermService(this.request); this.rft = new RftService(this.request); this.seismic = new SeismicService(this.request); this.surface = new SurfaceService(this.request); diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 883499f0c..c16461744 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -65,6 +65,10 @@ export { PolygonsAttributeType as PolygonsAttributeType_api } from './models/Pol export type { PolygonsMeta as PolygonsMeta_api } from './models/PolygonsMeta'; export type { PolylineIntersection as PolylineIntersection_api } from './models/PolylineIntersection'; export type { PvtData as PvtData_api } from './models/PvtData'; +export type { RelPermRealizationData as RelPermRealizationData_api } from './models/RelPermRealizationData'; +export type { RelPermSatNumData as RelPermSatNumData_api } from './models/RelPermSatNumData'; +export type { RelPermSaturationAxis as RelPermSaturationAxis_api } from './models/RelPermSaturationAxis'; +export type { RelPermTableInfo as RelPermTableInfo_api } from './models/RelPermTableInfo'; export type { RepeatedTableColumnData as RepeatedTableColumnData_api } from './models/RepeatedTableColumnData'; export type { RftObservation as RftObservation_api } from './models/RftObservation'; export type { RftObservations as RftObservations_api } from './models/RftObservations'; @@ -132,6 +136,7 @@ export { ObservationsService } from './services/ObservationsService'; export { ParametersService } from './services/ParametersService'; export { PolygonsService } from './services/PolygonsService'; export { PvtService } from './services/PvtService'; +export { RelpermService } from './services/RelpermService'; export { RftService } from './services/RftService'; export { SeismicService } from './services/SeismicService'; export { SurfaceService } from './services/SurfaceService'; diff --git a/frontend/src/api/models/RelPermRealizationData.ts b/frontend/src/api/models/RelPermRealizationData.ts new file mode 100644 index 000000000..890771a5a --- /dev/null +++ b/frontend/src/api/models/RelPermRealizationData.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { RelPermSatNumData } from './RelPermSatNumData'; +export type RelPermRealizationData = { + saturation_axis_data: Array; + satnum_data: Array; + realization: number; +}; + diff --git a/frontend/src/api/models/RelPermSatNumData.ts b/frontend/src/api/models/RelPermSatNumData.ts new file mode 100644 index 000000000..feb6d4e7b --- /dev/null +++ b/frontend/src/api/models/RelPermSatNumData.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type RelPermSatNumData = { + satnum: number; + relperm_curves_data: Array>; +}; + diff --git a/frontend/src/api/models/RelPermSaturationAxis.ts b/frontend/src/api/models/RelPermSaturationAxis.ts new file mode 100644 index 000000000..79e70568d --- /dev/null +++ b/frontend/src/api/models/RelPermSaturationAxis.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type RelPermSaturationAxis = { + saturation_name: string; + relperm_curve_names: Array; + capillary_pressure_curve_names: Array; +}; + diff --git a/frontend/src/api/models/RelPermTableInfo.ts b/frontend/src/api/models/RelPermTableInfo.ts new file mode 100644 index 000000000..e704b1380 --- /dev/null +++ b/frontend/src/api/models/RelPermTableInfo.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { RelPermSaturationAxis } from './RelPermSaturationAxis'; +export type RelPermTableInfo = { + table_name: string; + saturation_axes: Array; + satnums: Array; +}; + diff --git a/frontend/src/api/services/RelpermService.ts b/frontend/src/api/services/RelpermService.ts new file mode 100644 index 000000000..0bc976b09 --- /dev/null +++ b/frontend/src/api/services/RelpermService.ts @@ -0,0 +1,95 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { RelPermRealizationData } from '../models/RelPermRealizationData'; +import type { RelPermTableInfo } from '../models/RelPermTableInfo'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import type { BaseHttpRequest } from '../core/BaseHttpRequest'; +export class RelpermService { + constructor(public readonly httpRequest: BaseHttpRequest) {} + /** + * Get Table Names + * @param caseUuid Sumo case uuid + * @param ensembleName Ensemble name + * @returns string Successful Response + * @throws ApiError + */ + public getTableNames( + caseUuid: string, + ensembleName: string, + ): CancelablePromise> { + return this.httpRequest.request({ + method: 'GET', + url: '/relperm/table_names', + query: { + 'case_uuid': caseUuid, + 'ensemble_name': ensembleName, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Get Table Info + * @param caseUuid Sumo case uuid + * @param ensembleName Ensemble name + * @param tableName Table name + * @returns RelPermTableInfo Successful Response + * @throws ApiError + */ + public getTableInfo( + caseUuid: string, + ensembleName: string, + tableName: string, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'GET', + url: '/relperm/table_info', + query: { + 'case_uuid': caseUuid, + 'ensemble_name': ensembleName, + 'table_name': tableName, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Get Saturation And Curve Data + * @param caseUuid Sumo case uuid + * @param ensembleName Ensemble name + * @param tableName Table name + * @param saturationAxisName Saturation axis name + * @param curveNames Curve names + * @param satnums Satnums + * @returns RelPermRealizationData Successful Response + * @throws ApiError + */ + public getSaturationAndCurveData( + caseUuid: string, + ensembleName: string, + tableName: string, + saturationAxisName: string, + curveNames: Array, + satnums: Array, + ): CancelablePromise> { + return this.httpRequest.request({ + method: 'GET', + url: '/relperm/saturation_and_curve_data', + query: { + 'case_uuid': caseUuid, + 'ensemble_name': ensembleName, + 'table_name': tableName, + 'saturation_axis_name': saturationAxisName, + 'curve_names': curveNames, + 'satnums': satnums, + }, + errors: { + 422: `Validation Error`, + }, + }); + } +} diff --git a/frontend/src/framework/ModuleDataTags.ts b/frontend/src/framework/ModuleDataTags.ts index b7251cc7c..408c044cb 100644 --- a/frontend/src/framework/ModuleDataTags.ts +++ b/frontend/src/framework/ModuleDataTags.ts @@ -10,7 +10,8 @@ export enum ModuleDataTagId { OBSERVATIONS = "observations", SEISMIC = "seismic", WELL_COMPLETIONS = "well-completions", - VFP = "vfp" + VFP = "vfp", + RELPERM = "relperm", } export type ModuleDataTag = { diff --git a/frontend/src/modules/RelPerm/interfaces.ts b/frontend/src/modules/RelPerm/interfaces.ts new file mode 100644 index 000000000..c86d0dce9 --- /dev/null +++ b/frontend/src/modules/RelPerm/interfaces.ts @@ -0,0 +1,17 @@ +import { RelPermRealizationData_api, RftRealizationData_api } from "@api"; +import { InterfaceInitialization } from "@framework/UniDirectionalModuleComponentsInterface"; +import { UseQueryResult } from "@tanstack/react-query"; + +import { validRealizationNumbersAtom } from "./settings/atoms/baseAtoms"; +import { relPermDataQueryAtom } from "./settings/atoms/queryAtoms"; + +type SettingsToViewInterface = { relPermDataQuery: UseQueryResult }; +export type Interfaces = { + settingsToView: SettingsToViewInterface; +}; + +export const settingsToViewInterfaceInitialization: InterfaceInitialization = { + relPermDataQuery: (get) => { + return get(relPermDataQueryAtom); + }, +}; diff --git a/frontend/src/modules/RelPerm/loadModule.tsx b/frontend/src/modules/RelPerm/loadModule.tsx new file mode 100644 index 000000000..101893c39 --- /dev/null +++ b/frontend/src/modules/RelPerm/loadModule.tsx @@ -0,0 +1,10 @@ +import { ModuleRegistry } from "@framework/ModuleRegistry"; + +import { Interfaces, settingsToViewInterfaceInitialization } from "./interfaces"; +import { Settings } from "./settings"; +import { View } from "./view"; + +const module = ModuleRegistry.initModule("RelPerm", { settingsToViewInterfaceInitialization }); + +module.viewFC = View; +module.settingsFC = Settings; diff --git a/frontend/src/modules/RelPerm/registerModule.tsx b/frontend/src/modules/RelPerm/registerModule.tsx new file mode 100644 index 000000000..577047f18 --- /dev/null +++ b/frontend/src/modules/RelPerm/registerModule.tsx @@ -0,0 +1,16 @@ +import { ModuleCategory, ModuleDevState } from "@framework/Module"; +import { ModuleDataTagId } from "@framework/ModuleDataTags"; +import { ModuleRegistry } from "@framework/ModuleRegistry"; + +import { Interfaces } from "./interfaces"; + +const description = "Plotting of relative permeability results."; + +ModuleRegistry.registerModule({ + moduleName: "RelPerm", + defaultTitle: "Relative Permeability", + category: ModuleCategory.MAIN, + devState: ModuleDevState.DEV, + dataTagIds: [ModuleDataTagId.RELPERM], + description, +}); diff --git a/frontend/src/modules/RelPerm/settings.tsx b/frontend/src/modules/RelPerm/settings.tsx new file mode 100644 index 000000000..0cc719c86 --- /dev/null +++ b/frontend/src/modules/RelPerm/settings.tsx @@ -0,0 +1,177 @@ +import React from "react"; + +import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { ModuleSettingsProps } from "@framework/Module"; +import { useSettingsStatusWriter } from "@framework/StatusWriter"; +import { useEnsembleRealizationFilterFunc, useEnsembleSet } from "@framework/WorkbenchSession"; +import { EnsembleDropdown } from "@framework/components/EnsembleDropdown"; +import { timestampUtcMsToCompactIsoString } from "@framework/utils/timestampUtils"; +import { CollapsibleGroup } from "@lib/components/CollapsibleGroup"; +import { Dropdown } from "@lib/components/Dropdown"; +import { PendingWrapper } from "@lib/components/PendingWrapper"; +import { RadioGroup } from "@lib/components/RadioGroup"; +import { Select, SelectOption } from "@lib/components/Select"; +import { usePropagateApiErrorToStatusWriter } from "@modules/_shared/hooks/usePropagateApiErrorToStatusWriter"; + +import { useAtom, useAtomValue, useSetAtom } from "jotai"; +import { set } from "lodash"; + +import { Interfaces } from "./interfaces"; +import { + selectedColorByAtom, + userSelectedEnsembleIdentAtom, + userSelectedRelPermCurveNamesAtom, + userSelectedSatNumsAtom, + userSelectedSaturationAxisAtom, + userSelectedTableNameAtom, + validRealizationNumbersAtom, +} from "./settings/atoms/baseAtoms"; +import { + availableRelPermCurveNamesAtom, + availableRelPermSaturationAxesAtom, + availableRelPermTableNamesAtom, + availableSatNumsAtom, + selectedEnsembleIdentAtom, + selectedRelPermCurveNamesAtom, + selectedRelPermSaturationAxisAtom, + selectedRelPermTableNameAtom, + selectedSatNumsAtom, +} from "./settings/atoms/derivedAtoms"; +import { relPermTableInfoQueryAtom, relPermTableNamesQueryAtom } from "./settings/atoms/queryAtoms"; +import { ColorBy } from "./typesAndEnums"; + +//Helpers to populate dropdowns +const stringToOptions = (strings: string[]): SelectOption[] => { + return strings.map((string) => ({ label: string, value: string })); +}; + +export function Settings({ settingsContext, workbenchSession }: ModuleSettingsProps) { + const ensembleSet = useEnsembleSet(workbenchSession); + const statusWriter = useSettingsStatusWriter(settingsContext); + + const selectedEnsembleIdent = useAtomValue(selectedEnsembleIdentAtom); + const setUserSelectedEnsembleIdent = useSetAtom(userSelectedEnsembleIdentAtom); + const filterEnsembleRealizationsFunc = useEnsembleRealizationFilterFunc(workbenchSession); + + const setValidRealizationNumbersAtom = useSetAtom(validRealizationNumbersAtom); + const validRealizations = selectedEnsembleIdent ? [...filterEnsembleRealizationsFunc(selectedEnsembleIdent)] : null; + setValidRealizationNumbersAtom(validRealizations); + + const [selectedColorBy, setSelectedColorBy] = useAtom(selectedColorByAtom); + + const relPermTableNamesQuery = useAtomValue(relPermTableNamesQueryAtom); + const relPermTableInfoQuery = useAtomValue(relPermTableInfoQueryAtom); + const availableRelPermTableNames = useAtomValue(availableRelPermTableNamesAtom); + const selecedRelPermTableName = useAtomValue(selectedRelPermTableNameAtom); + const setUserSelectedRelPermTableName = useSetAtom(userSelectedTableNameAtom); + + const availableRelPermSaturationAxes = useAtomValue(availableRelPermSaturationAxesAtom); + const selectedRelPermSaturationAxis = useAtomValue(selectedRelPermSaturationAxisAtom); + const setUserSelectedRelPermSaturationAxis = useSetAtom(userSelectedSaturationAxisAtom); + + const availableRelPermCurveNames = useAtomValue(availableRelPermCurveNamesAtom); + const selectedRelPermCurveNames = useAtomValue(selectedRelPermCurveNamesAtom); + const setUserSelectedRelPermCurveNames = useSetAtom(userSelectedRelPermCurveNamesAtom); + + const availableSatNums = useAtomValue(availableSatNumsAtom); + const selectedSatNums = useAtomValue(selectedSatNumsAtom); + const setUserSelectedSatNums = useSetAtom(userSelectedSatNumsAtom); + + const relPermTableNamesQueryErrorMessage = + usePropagateApiErrorToStatusWriter(relPermTableNamesQuery, statusWriter) ?? ""; + const relPermTableInfoQueryErrorMessage = + usePropagateApiErrorToStatusWriter(relPermTableInfoQuery, statusWriter) ?? ""; + + function handleEnsembleSelectionChange(ensembleIdent: EnsembleIdent | null) { + setUserSelectedEnsembleIdent(ensembleIdent); + } + + const [selectedMultiSatNums, setSelectedMultiSatNums] = React.useState(selectedSatNums); + + function handleSatNumsChange(values: string[]) { + const newSatNums = values.map((value) => parseInt(value) as number); + setUserSelectedSatNums(newSatNums); + setSelectedMultiSatNums(newSatNums); + } + function handleColorByChange(_: React.ChangeEvent, colorBy: ColorBy) { + setSelectedColorBy(colorBy); + if (colorBy === ColorBy.SATNUM) { + setUserSelectedSatNums(selectedMultiSatNums); + } else { + setUserSelectedSatNums([selectedMultiSatNums[0]]); + } + + // setSelectedEnsembleIdents([selectedMultiEnsembleIdents[0]]); + // setSelectedRealizations([selectedMultiRealizations[0]]); + // setSelectedPvtNums(selectedMultiPvtNums); + // } else { + // setSelectedEnsembleIdents(selectedMultiEnsembleIdents); + // setSelectedRealizations(selectedMultiRealizations); + // setSelectedPvtNums([selectedMultiPvtNums[0]]); + // } + } + return ( +
+ + + + + + + + + + + + + + + + + num.toString()))} + value={selectedSatNums ? selectedSatNums.map((num) => num.toString()) : []} + onChange={handleSatNumsChange} + size={10} + multiple={selectedColorBy === ColorBy.SATNUM} + /> + + +
+ ); +} diff --git a/frontend/src/modules/RelPerm/settings/atoms/baseAtoms.ts b/frontend/src/modules/RelPerm/settings/atoms/baseAtoms.ts new file mode 100644 index 000000000..8f8627aca --- /dev/null +++ b/frontend/src/modules/RelPerm/settings/atoms/baseAtoms.ts @@ -0,0 +1,21 @@ +import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { atomWithCompare } from "@framework/utils/atomUtils"; +import { ColorBy } from "@modules/RelPerm/typesAndEnums"; + +import { atom } from "jotai"; +import { isEqual } from "lodash"; + +function areEnsembleIdentsEqual(a: EnsembleIdent | null, b: EnsembleIdent | null) { + if (a === null) { + return b === null; + } + return a.equals(b); +} + +export const userSelectedEnsembleIdentAtom = atomWithCompare(null, areEnsembleIdentsEqual); +export const validRealizationNumbersAtom = atom(null); +export const userSelectedTableNameAtom = atom(null); +export const userSelectedSaturationAxisAtom = atom(null); +export const userSelectedSatNumsAtom = atomWithCompare([], isEqual); +export const userSelectedRelPermCurveNamesAtom = atom(null); +export const selectedColorByAtom = atom(ColorBy.ENSEMBLE); diff --git a/frontend/src/modules/RelPerm/settings/atoms/derivedAtoms.ts b/frontend/src/modules/RelPerm/settings/atoms/derivedAtoms.ts new file mode 100644 index 000000000..27387cca3 --- /dev/null +++ b/frontend/src/modules/RelPerm/settings/atoms/derivedAtoms.ts @@ -0,0 +1,110 @@ +import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { EnsembleSetAtom } from "@framework/GlobalAtoms"; +import { fixupEnsembleIdent } from "@framework/utils/ensembleUiHelpers"; + +import { atom } from "jotai"; + +import { + userSelectedEnsembleIdentAtom, + userSelectedRelPermCurveNamesAtom, + userSelectedSatNumsAtom, + userSelectedSaturationAxisAtom, + userSelectedTableNameAtom, +} from "./baseAtoms"; +import { relPermTableInfoQueryAtom, relPermTableNamesQueryAtom } from "./queryAtoms"; + +function fixupSelectedOrFirstValue(selectedValue: T | null, values: T[]): T | null { + const includes = (value: T | null): value is T => { + return value !== null && values.includes(value); + }; + + if (includes(selectedValue)) { + return selectedValue; + } + if (values.length) { + return values[0]; + } + return null; +} + +export const selectedEnsembleIdentAtom = atom((get) => { + const ensembleSet = get(EnsembleSetAtom); + const userSelectedEnsembleIdent = get(userSelectedEnsembleIdentAtom); + + const validEnsembleIdent = fixupEnsembleIdent(userSelectedEnsembleIdent, ensembleSet); + return validEnsembleIdent; +}); + +export const availableRelPermTableNamesAtom = atom((get) => { + const tableNames = get(relPermTableNamesQueryAtom).data; + return tableNames ?? []; +}); +export const selectedRelPermTableNameAtom = atom((get) => { + const availableRelPermTableNames = get(availableRelPermTableNamesAtom); + const userSelectedTableName = get(userSelectedTableNameAtom); + return fixupSelectedOrFirstValue(userSelectedTableName, availableRelPermTableNames); +}); + +export const availableRelPermSaturationAxesAtom = atom((get) => { + const tableInfo = get(relPermTableInfoQueryAtom).data; + if (!tableInfo) { + return []; + } + return tableInfo.saturation_axes.map((axis) => axis.saturation_name); +}); + +export const selectedRelPermSaturationAxisAtom = atom((get) => { + const availableSaturationAxes = get(availableRelPermSaturationAxesAtom); + const userSelectedSaturationAxis = get(userSelectedSaturationAxisAtom); + return fixupSelectedOrFirstValue(userSelectedSaturationAxis, availableSaturationAxes); +}); + +export const availableRelPermCurveNamesAtom = atom((get) => { + const tableInfo = get(relPermTableInfoQueryAtom).data; + if (!tableInfo) { + return []; + } + const selectedSaturationAxis = get(selectedRelPermSaturationAxisAtom); + const selectedSaturationAxisInfo = tableInfo.saturation_axes.find( + (axis) => axis.saturation_name === selectedSaturationAxis + ); + return selectedSaturationAxisInfo?.relperm_curve_names ?? []; +}); + +export const selectedRelPermCurveNamesAtom = atom((get) => { + const availableRelPermCurveNames = get(availableRelPermCurveNamesAtom); + const userSelectedRelPermCurveNames = get(userSelectedRelPermCurveNamesAtom); + let computedRelPermCurveNames = userSelectedRelPermCurveNames?.filter((name) => + availableRelPermCurveNames.includes(name) + ); + if (!computedRelPermCurveNames || computedRelPermCurveNames.length === 0) { + computedRelPermCurveNames = availableRelPermCurveNames; + } + return computedRelPermCurveNames; +}); + +export const availableSatNumsAtom = atom((get) => { + const tableInfo = get(relPermTableInfoQueryAtom).data; + if (!tableInfo) { + return []; + } + + return tableInfo.satnums; +}); + +export const selectedSatNumsAtom = atom((get) => { + const availableSatNums = get(availableSatNumsAtom); + const userSelectedSatNums = get(userSelectedSatNumsAtom); + + let computedSatNums = userSelectedSatNums.filter((el) => availableSatNums.includes(el)); + + if (computedSatNums.length === 0) { + if (availableSatNums.length > 0) { + computedSatNums = [availableSatNums[0]]; + } else { + computedSatNums = []; + } + } + + return computedSatNums; +}); diff --git a/frontend/src/modules/RelPerm/settings/atoms/queryAtoms.ts b/frontend/src/modules/RelPerm/settings/atoms/queryAtoms.ts new file mode 100644 index 000000000..faef97258 --- /dev/null +++ b/frontend/src/modules/RelPerm/settings/atoms/queryAtoms.ts @@ -0,0 +1,103 @@ +import { apiService } from "@framework/ApiService"; + +import { atomWithQuery } from "jotai-tanstack-query"; + +import { + selectedEnsembleIdentAtom, + selectedRelPermCurveNamesAtom, + selectedRelPermSaturationAxisAtom, + selectedRelPermTableNameAtom, + selectedSatNumsAtom, +} from "./derivedAtoms"; + +const STALE_TIME = 60 * 1000; +const CACHE_TIME = 60 * 1000; + +export const relPermTableNamesQueryAtom = atomWithQuery((get) => { + const selectedEnsembleIdent = get(selectedEnsembleIdentAtom); + + const query = { + queryKey: [ + "getRelPermTableNames", + selectedEnsembleIdent?.getCaseUuid(), + selectedEnsembleIdent?.getEnsembleName(), + ], + queryFn: () => + apiService.relperm.getTableNames( + selectedEnsembleIdent?.getCaseUuid() ?? "", + selectedEnsembleIdent?.getEnsembleName() ?? "" + ), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + enabled: !!(selectedEnsembleIdent?.getCaseUuid() && selectedEnsembleIdent?.getEnsembleName()), + }; + return query; +}); + +export const relPermTableInfoQueryAtom = atomWithQuery((get) => { + const selectedEnsembleIdent = get(selectedEnsembleIdentAtom); + const selectedTableName = get(selectedRelPermTableNameAtom); + + const query = { + queryKey: [ + "getRelPermTableInfo", + selectedEnsembleIdent?.getCaseUuid(), + selectedEnsembleIdent?.getEnsembleName(), + selectedTableName, + ], + queryFn: () => + apiService.relperm.getTableInfo( + selectedEnsembleIdent?.getCaseUuid() ?? "", + selectedEnsembleIdent?.getEnsembleName() ?? "", + selectedTableName ?? "" + ), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + enabled: !!( + selectedEnsembleIdent?.getCaseUuid() && + selectedEnsembleIdent?.getEnsembleName() && + selectedTableName + ), + }; + return query; +}); + +export const relPermDataQueryAtom = atomWithQuery((get) => { + const selectedEnsembleIdent = get(selectedEnsembleIdentAtom); + const selectedTableName = get(selectedRelPermTableNameAtom); + const selectedRelPermSaturationAxis = get(selectedRelPermSaturationAxisAtom); + const selectedSatNums = get(selectedSatNumsAtom); + const selectedRelPermCurveNames = get(selectedRelPermCurveNamesAtom); + + const query = { + queryKey: [ + "getRelPermData", + selectedEnsembleIdent?.getCaseUuid(), + selectedEnsembleIdent?.getEnsembleName(), + selectedTableName, + selectedRelPermSaturationAxis, + selectedSatNums, + selectedRelPermCurveNames, + ], + queryFn: () => + apiService.relperm.getSaturationAndCurveData( + selectedEnsembleIdent?.getCaseUuid() ?? "", + selectedEnsembleIdent?.getEnsembleName() ?? "", + selectedTableName ?? "", + selectedRelPermSaturationAxis ?? "", + selectedRelPermCurveNames ?? [], + selectedSatNums ?? [] + ), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + enabled: !!( + selectedEnsembleIdent?.getCaseUuid() && + selectedEnsembleIdent?.getEnsembleName() && + selectedTableName && + selectedRelPermSaturationAxis && + selectedSatNums && + selectedRelPermCurveNames + ), + }; + return query; +}); diff --git a/frontend/src/modules/RelPerm/typesAndEnums.ts b/frontend/src/modules/RelPerm/typesAndEnums.ts new file mode 100644 index 000000000..a49543982 --- /dev/null +++ b/frontend/src/modules/RelPerm/typesAndEnums.ts @@ -0,0 +1,11 @@ +export enum ColorBy { + ENSEMBLE = "ensemble", + CURVE = "curve", + SATNUM = "satnum", +} + +export const COLOR_BY_TO_DISPLAY_NAME: Record = { + [ColorBy.ENSEMBLE]: "Ensemble", + [ColorBy.CURVE]: "Curve", + [ColorBy.SATNUM]: "Satnum", +}; diff --git a/frontend/src/modules/RelPerm/view.tsx b/frontend/src/modules/RelPerm/view.tsx new file mode 100644 index 000000000..11faf2b23 --- /dev/null +++ b/frontend/src/modules/RelPerm/view.tsx @@ -0,0 +1,159 @@ +import React from "react"; +import Plot from "react-plotly.js"; + +import { RftRealizationData_api } from "@api"; +import { ModuleViewProps } from "@framework/Module"; +import { useViewStatusWriter } from "@framework/StatusWriter"; +import { timestampUtcMsToCompactIsoString } from "@framework/utils/timestampUtils"; +import { CircularProgress } from "@lib/components/CircularProgress"; +import { useElementSize } from "@lib/hooks/useElementSize"; +import { ContentMessage, ContentMessageType } from "@modules/_shared/components/ContentMessage/contentMessage"; +import { usePropagateApiErrorToStatusWriter } from "@modules/_shared/hooks/usePropagateApiErrorToStatusWriter"; + +import { PlotData } from "plotly.js"; + +import { Interfaces } from "./interfaces"; + +export const View = ({ viewContext }: ModuleViewProps) => { + const wrapperDivRef = React.useRef(null); + const wrapperDivSize = useElementSize(wrapperDivRef); + + const relPermDataQuery = viewContext.useSettingsToViewInterfaceValue("relPermDataQuery"); + // const realizationNums = viewContext.useSettingsToViewInterfaceValue("realizationNums"); + // const responseName = viewContext.useSettingsToViewInterfaceValue("responseName"); + // const wellName = viewContext.useSettingsToViewInterfaceValue("wellName"); + // const timeStampUtcMs = viewContext.useSettingsToViewInterfaceValue("timeStampsUtcMs"); + + const statusWriter = useViewStatusWriter(viewContext); + const statusError = usePropagateApiErrorToStatusWriter(relPermDataQuery, statusWriter); + + let content = null; + + if (relPermDataQuery.isFetching) { + content = ( + + + + ); + } else if (statusError !== null) { + content =
{statusError}
; + } else if (relPermDataQuery.isError || relPermDataQuery.data === undefined) { + content =
Could not load RFT data
; + } else { + // const filteredRftData = rftDataQuery.data.filter((realizationData) => + // realizationNums?.includes(realizationData.realization) + // ); + // const [minValue, maxValue] = getResponseValueRange(filteredRftData); + const plotData: Partial[] = []; + const colors = [ + "red", + "blue", + "green", + "yellow", + "purple", + "orange", + "pink", + "brown", + "black", + "gray", + "cyan", + "magenta", + "purple", + "lime", + "teal", + "indigo", + "maroon", + "navy", + "olive", + "silver", + "aqua", + "fuchsia", + "white", + ]; + + let totalPoints = 0; + relPermDataQuery.data.forEach((realizationData) => { + realizationData.satnum_data.forEach((satNumData) => { + satNumData.relperm_curves_data.forEach((curveData) => { + totalPoints += curveData.length; + }); + }); + }); + const useGl: boolean = totalPoints > 1000; + relPermDataQuery.data.forEach((realizationData) => { + realizationData.satnum_data.forEach((satNumData, idx) => { + satNumData.relperm_curves_data.forEach((curveData) => { + plotData.push( + createRelPermRealizationTrace( + realizationData.realization, + realizationData.saturation_axis_data, + curveData, + colors[idx], + useGl + ) + ); + }); + }); + }); + // const title = `RFT for ${wellName}, ${timeStampUtcMs && timestampUtcMsToCompactIsoString(timeStampUtcMs)}`; + content = ( + + ); + } + return ( +
+ {content} +
+ ); +}; + +function createRelPermRealizationTrace( + realization: number, + saturationValues: number[], + curveValues: number[], + color: string, + useGl: boolean +): Partial { + const trace: Partial = { + x: saturationValues, + y: curveValues, + + type: useGl ? "scattergl" : "scatter", + mode: "lines", + showlegend: false, + line: { + color: color, + width: 2, + }, + }; + return trace; +} + +// function getResponseValueRange(rftRealizationData: RftRealizationData_api[] | null): [number, number] { +// let minValue = Number.POSITIVE_INFINITY; +// let maxValue = Number.NEGATIVE_INFINITY; +// if (rftRealizationData !== null && rftRealizationData.length) { +// rftRealizationData.forEach((realizationData) => { +// realizationData.value_arr.forEach((value) => { +// if (value < minValue) { +// minValue = value; +// } +// if (value > maxValue) { +// maxValue = value; +// } +// }); +// }); +// } +// return [minValue, maxValue]; +// } diff --git a/frontend/src/modules/registerAllModules.ts b/frontend/src/modules/registerAllModules.ts index f16f380d4..1001df229 100644 --- a/frontend/src/modules/registerAllModules.ts +++ b/frontend/src/modules/registerAllModules.ts @@ -9,14 +9,14 @@ import "./Intersection/registerModule"; import "./Map/registerModule"; import "./ParameterDistributionMatrix/registerModule"; import "./Pvt/registerModule"; +import "./RelPerm/registerModule"; import "./Rft/registerModule"; import "./SimulationTimeSeries/registerModule"; import "./SimulationTimeSeriesSensitivity/registerModule"; import "./SubsurfaceMap/registerModule"; import "./TornadoChart/registerModule"; -import "./WellCompletions/registerModule"; import "./Vfp/registerModule"; - +import "./WellCompletions/registerModule"; if (isDevMode()) { await import("./MyModule/registerModule"); From 6bccc045a86e595cdc08df0b7e4b7cd79a8eaa55 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Thu, 7 Nov 2024 09:27:26 +0100 Subject: [PATCH 3/8] wip --- .../services/sumo_access/relperm_access.py | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/backend_py/primary/primary/services/sumo_access/relperm_access.py b/backend_py/primary/primary/services/sumo_access/relperm_access.py index b7eeb221c..f0cd0e293 100644 --- a/backend_py/primary/primary/services/sumo_access/relperm_access.py +++ b/backend_py/primary/primary/services/sumo_access/relperm_access.py @@ -2,7 +2,7 @@ import logging from io import BytesIO import asyncio -from typing import List, Optional, Dict, Sequence +from typing import List, Optional, Dict, Sequence, Any from dataclasses import dataclass from fmu.sumo.explorer.objects import Case, TableCollection import polars as pl @@ -77,7 +77,12 @@ async def get_single_realization_table(self, table_name: str) -> pl.DataFrame: self._case._sumo, self._case_uuid, self._iteration_name, table_name ) single_realization_blob_id = realization_blob_ids[0] - return await self.fetch_realization_table(single_realization_blob_id) + res = await self.fetch_realization_table(single_realization_blob_id) + blob = BytesIO(res.content) + real_df = pl.read_parquet(blob) + # Add realization id to the dataframe + real_df = real_df.with_columns(pl.lit(single_realization_blob_id.realization_id).alias("REAL")) + return real_df async def get_relperm_table( self, @@ -91,8 +96,16 @@ async def get_relperm_table( perf_metrics.record_lap("get_relperm_realization_table_blob_uuids") tasks = [asyncio.create_task(self.fetch_realization_table(table)) for table in realization_blob_ids] - realization_tables = await asyncio.gather(*tasks) + + realization_tables_res = await asyncio.gather(*tasks) perf_metrics.record_lap("fetch_realization_tables") + realization_tables = [] + for res, realization_blob_id in zip(realization_tables_res, realization_blob_ids): + blob = BytesIO(res.content) + real_df = pl.read_parquet(blob) + # Add realization id to the dataframe + real_df = real_df.with_columns(pl.lit(realization_blob_id.realization_id).alias("REAL")) + realization_tables.append(real_df) table = pl.concat(realization_tables) perf_metrics.record_lap("concat_realization_tables") @@ -100,13 +113,9 @@ async def get_relperm_table( LOGGER.debug(f"RelPermAccess.get_relperm_table: {perf_metrics.to_string()}") return table - async def fetch_realization_table(self, realization_blob_id: RealizationBlobid) -> pl.DataFrame: + async def fetch_realization_table(self, realization_blob_id: RealizationBlobid) -> Any: res = await self._case._sumo.get_async(f"/objects('{realization_blob_id.blob_name}')/blob") - blob = BytesIO(res.content) - real_df = pl.read_parquet(blob) - # Add realization id to the dataframe - real_df = real_df.with_columns(pl.lit(realization_blob_id.realization_id).alias("REAL")) - return real_df + return res def has_required_relperm_table_columns(table_name: str, column_names: List[str]) -> bool: From 421977d65370b453f897cc4be19de706f80dba35 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Fri, 8 Nov 2024 11:09:16 +0100 Subject: [PATCH 4/8] wip --- frontend/src/modules/RelPerm/interfaces.ts | 10 +++++++- frontend/src/modules/RelPerm/settings.tsx | 23 ++++++++++++++++--- .../RelPerm/settings/atoms/baseAtoms.ts | 3 ++- frontend/src/modules/RelPerm/typesAndEnums.ts | 9 ++++---- 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/frontend/src/modules/RelPerm/interfaces.ts b/frontend/src/modules/RelPerm/interfaces.ts index c86d0dce9..e644bc8dd 100644 --- a/frontend/src/modules/RelPerm/interfaces.ts +++ b/frontend/src/modules/RelPerm/interfaces.ts @@ -3,9 +3,14 @@ import { InterfaceInitialization } from "@framework/UniDirectionalModuleComponen import { UseQueryResult } from "@tanstack/react-query"; import { validRealizationNumbersAtom } from "./settings/atoms/baseAtoms"; +import { selectedVisualizationTypeAtom } from "./settings/atoms/baseAtoms"; import { relPermDataQueryAtom } from "./settings/atoms/queryAtoms"; +import { VisualizationType } from "./typesAndEnums"; -type SettingsToViewInterface = { relPermDataQuery: UseQueryResult }; +type SettingsToViewInterface = { + relPermDataQuery: UseQueryResult; + visualizationType: VisualizationType; +}; export type Interfaces = { settingsToView: SettingsToViewInterface; }; @@ -14,4 +19,7 @@ export const settingsToViewInterfaceInitialization: InterfaceInitialization { return get(relPermDataQueryAtom); }, + visualizationType: (get) => { + return get(selectedVisualizationTypeAtom); + }, }; diff --git a/frontend/src/modules/RelPerm/settings.tsx b/frontend/src/modules/RelPerm/settings.tsx index 0cc719c86..8296021e0 100644 --- a/frontend/src/modules/RelPerm/settings.tsx +++ b/frontend/src/modules/RelPerm/settings.tsx @@ -14,11 +14,11 @@ import { Select, SelectOption } from "@lib/components/Select"; import { usePropagateApiErrorToStatusWriter } from "@modules/_shared/hooks/usePropagateApiErrorToStatusWriter"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; -import { set } from "lodash"; import { Interfaces } from "./interfaces"; import { selectedColorByAtom, + selectedVisualizationTypeAtom, userSelectedEnsembleIdentAtom, userSelectedRelPermCurveNamesAtom, userSelectedSatNumsAtom, @@ -38,7 +38,7 @@ import { selectedSatNumsAtom, } from "./settings/atoms/derivedAtoms"; import { relPermTableInfoQueryAtom, relPermTableNamesQueryAtom } from "./settings/atoms/queryAtoms"; -import { ColorBy } from "./typesAndEnums"; +import { ColorBy, VisualizationType } from "./typesAndEnums"; //Helpers to populate dropdowns const stringToOptions = (strings: string[]): SelectOption[] => { @@ -58,7 +58,7 @@ export function Settings({ settingsContext, workbenchSession }: ModuleSettingsPr setValidRealizationNumbersAtom(validRealizations); const [selectedColorBy, setSelectedColorBy] = useAtom(selectedColorByAtom); - + const [selectedVisualizationType, setSelectedVisualizationType] = useAtom(selectedVisualizationTypeAtom); const relPermTableNamesQuery = useAtomValue(relPermTableNamesQueryAtom); const relPermTableInfoQuery = useAtomValue(relPermTableInfoQueryAtom); const availableRelPermTableNames = useAtomValue(availableRelPermTableNamesAtom); @@ -110,6 +110,13 @@ export function Settings({ settingsContext, workbenchSession }: ModuleSettingsPr // setSelectedPvtNums([selectedMultiPvtNums[0]]); // } } + function handleVisualizationTypeChange( + _: React.ChangeEvent, + visualizationType: VisualizationType + ) { + setSelectedVisualizationType(visualizationType); + } + return (
@@ -171,6 +178,16 @@ export function Settings({ settingsContext, workbenchSession }: ModuleSettingsPr multiple={selectedColorBy === ColorBy.SATNUM} /> + + +
); diff --git a/frontend/src/modules/RelPerm/settings/atoms/baseAtoms.ts b/frontend/src/modules/RelPerm/settings/atoms/baseAtoms.ts index 8f8627aca..bef194ac0 100644 --- a/frontend/src/modules/RelPerm/settings/atoms/baseAtoms.ts +++ b/frontend/src/modules/RelPerm/settings/atoms/baseAtoms.ts @@ -1,6 +1,6 @@ import { EnsembleIdent } from "@framework/EnsembleIdent"; import { atomWithCompare } from "@framework/utils/atomUtils"; -import { ColorBy } from "@modules/RelPerm/typesAndEnums"; +import { ColorBy, VisualizationType } from "@modules/RelPerm/typesAndEnums"; import { atom } from "jotai"; import { isEqual } from "lodash"; @@ -19,3 +19,4 @@ export const userSelectedSaturationAxisAtom = atom(null); export const userSelectedSatNumsAtom = atomWithCompare([], isEqual); export const userSelectedRelPermCurveNamesAtom = atom(null); export const selectedColorByAtom = atom(ColorBy.ENSEMBLE); +export const selectedVisualizationTypeAtom = atom(VisualizationType.STATISTICAL_FANCHART); diff --git a/frontend/src/modules/RelPerm/typesAndEnums.ts b/frontend/src/modules/RelPerm/typesAndEnums.ts index a49543982..77c3fb6a3 100644 --- a/frontend/src/modules/RelPerm/typesAndEnums.ts +++ b/frontend/src/modules/RelPerm/typesAndEnums.ts @@ -4,8 +4,7 @@ export enum ColorBy { SATNUM = "satnum", } -export const COLOR_BY_TO_DISPLAY_NAME: Record = { - [ColorBy.ENSEMBLE]: "Ensemble", - [ColorBy.CURVE]: "Curve", - [ColorBy.SATNUM]: "Satnum", -}; +export enum VisualizationType { + STATISTICAL_FANCHART = "statisticalFanchart", + INDIVIDUAL_REALIZATIONS = "individualRealizations", +} From 83388f979531b4498ef887238de0b0717e473d64 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Mon, 11 Nov 2024 10:06:00 +0100 Subject: [PATCH 5/8] wip --- .../primary/routers/relperm/converters.py | 23 ++- .../primary/primary/routers/relperm/router.py | 30 ++- .../primary/routers/relperm/schemas.py | 20 +- .../relperm_assembler/relperm_assembler.py | 183 +++++++++++++++--- frontend/src/api/index.ts | 5 +- .../{RelPermSatNumData.ts => CurveData.ts} | 7 +- .../src/api/models/RelPermRealizationData.ts | 11 -- .../RelPermRealizationDataForSaturation.ts | 10 + .../api/models/SaturationRealizationData.ts | 12 ++ frontend/src/api/services/RelpermService.ts | 47 ++++- frontend/src/modules/RelPerm/interfaces.ts | 47 ++++- frontend/src/modules/RelPerm/loadModule.tsx | 10 +- .../RelPerm/settings/atoms/baseAtoms.ts | 5 +- .../RelPerm/settings/atoms/queryAtoms.ts | 40 ---- .../RelPerm/{ => settings}/settings.tsx | 11 +- .../modules/RelPerm/view/atoms/baseAtoms.ts | 22 +++ .../RelPerm/view/atoms/interfaceEffects.ts | 50 +++++ .../modules/RelPerm/view/atoms/queryAtoms.ts | 102 ++++++++++ .../src/modules/RelPerm/{ => view}/view.tsx | 52 ++--- 19 files changed, 533 insertions(+), 154 deletions(-) rename frontend/src/api/models/{RelPermSatNumData.ts => CurveData.ts} (55%) delete mode 100644 frontend/src/api/models/RelPermRealizationData.ts create mode 100644 frontend/src/api/models/RelPermRealizationDataForSaturation.ts create mode 100644 frontend/src/api/models/SaturationRealizationData.ts rename frontend/src/modules/RelPerm/{ => settings}/settings.tsx (97%) create mode 100644 frontend/src/modules/RelPerm/view/atoms/baseAtoms.ts create mode 100644 frontend/src/modules/RelPerm/view/atoms/interfaceEffects.ts create mode 100644 frontend/src/modules/RelPerm/view/atoms/queryAtoms.ts rename frontend/src/modules/RelPerm/{ => view}/view.tsx (74%) diff --git a/backend_py/primary/primary/routers/relperm/converters.py b/backend_py/primary/primary/routers/relperm/converters.py index d8cba3e24..27423877f 100644 --- a/backend_py/primary/primary/routers/relperm/converters.py +++ b/backend_py/primary/primary/routers/relperm/converters.py @@ -1,7 +1,8 @@ from primary.services.relperm_assembler.relperm_assembler import ( RelPermTableInfo, RelPermSaturationAxis, - RelPermRealizationData, + SaturationRealizationData, + CurveData, ) from . import schemas @@ -25,13 +26,23 @@ def to_api_relperm_saturation_axis(axis: RelPermSaturationAxis) -> schemas.RelPe ) -def to_api_relperm_ensemble_data(data: RelPermRealizationData) -> schemas.RelPermRealizationData: +def to_api_relperm_realization_data(data: SaturationRealizationData) -> schemas.SaturationRealizationData: - return schemas.RelPermRealizationData( - saturation_axis_data=data.saturation_axis_data, + return schemas.SaturationRealizationData( + saturation_axis_data=schemas.CurveData( + curve_name=data.saturation_axis_data.curve_name, + curve_values=data.saturation_axis_data.curve_values, + unit=data.saturation_axis_data.unit, + ), satnum_data=[ - schemas.RelPermSatNumData(satnum=satnum_data.satnum, relperm_curves_data=satnum_data.relperm_curves_data) + schemas.RelPermRealizationDataForSaturation( + saturation_number=satnum_data.saturation_number, + relperm_curve_data=[ + schemas.CurveData(curve_name=curve.curve_name, curve_values=curve.curve_values) + for curve in satnum_data.relperm_curve_data + ], + ) for satnum_data in data.satnum_data ], - realization=data.realization, + realization_id=data.realization_id, ) diff --git a/backend_py/primary/primary/routers/relperm/router.py b/backend_py/primary/primary/routers/relperm/router.py index 4017e27b5..3c6a8d848 100644 --- a/backend_py/primary/primary/routers/relperm/router.py +++ b/backend_py/primary/primary/routers/relperm/router.py @@ -44,8 +44,8 @@ async def get_table_info( return converters.to_api_relperm_table_info(relperm_table_info) -@router.get("/saturation_and_curve_data") -async def get_saturation_and_curve_data( +@router.get("/realizations_curve_data") +async def get_realizations_curve_data( authenticated_user: Annotated[AuthenticatedUser, Depends(AuthHelper.get_authenticated_user)], case_uuid: Annotated[str, Query(description="Sumo case uuid")], ensemble_name: Annotated[str, Query(description="Ensemble name")], @@ -53,12 +53,32 @@ async def get_saturation_and_curve_data( saturation_axis_name: Annotated[str, Query(description="Saturation axis name")], curve_names: Annotated[List[str], Query(description="Curve names")], satnums: Annotated[List[int], Query(description="Satnums")], -) -> List[schemas.RelPermRealizationData]: +) -> List[schemas.SaturationRealizationData]: access = await RelPermAccess.from_case_uuid_async( authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name ) assembler = RelPermAssembler(access) - relperm_data = await assembler.get_relperm_ensemble_data(table_name, saturation_axis_name, curve_names, satnums) + relperm_data = await assembler.get_relperm_realization_data(table_name, saturation_axis_name, curve_names, satnums) - return [converters.to_api_relperm_ensemble_data(data) for data in relperm_data] + return [converters.to_api_relperm_realization_data(data) for data in relperm_data] + + +@router.get("/statistical_curve_data") +async def get_statistical_curve_data( + authenticated_user: Annotated[AuthenticatedUser, Depends(AuthHelper.get_authenticated_user)], + case_uuid: Annotated[str, Query(description="Sumo case uuid")], + ensemble_name: Annotated[str, Query(description="Ensemble name")], + table_name: Annotated[str, Query(description="Table name")], + saturation_axis_name: Annotated[str, Query(description="Saturation axis name")], + curve_names: Annotated[List[str], Query(description="Curve names")], + satnums: Annotated[List[int], Query(description="Satnums")], +) -> List[schemas.SaturationRealizationData]: + + access = await RelPermAccess.from_case_uuid_async( + authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name + ) + assembler = RelPermAssembler(access) + relperm_data = await assembler.get_relperm_realization_data(table_name, saturation_axis_name, curve_names, satnums) + + return [converters.to_api_relperm_realization_data(data) for data in relperm_data] diff --git a/backend_py/primary/primary/routers/relperm/schemas.py b/backend_py/primary/primary/routers/relperm/schemas.py index c002785af..d1bb59785 100644 --- a/backend_py/primary/primary/routers/relperm/schemas.py +++ b/backend_py/primary/primary/routers/relperm/schemas.py @@ -15,12 +15,18 @@ class RelPermTableInfo(BaseModel): satnums: List[int] -class RelPermSatNumData(BaseModel): - satnum: int - relperm_curves_data: List[List[float]] +class CurveData(BaseModel): + curve_name: str + curve_values: List[float] + unit: str | None = None -class RelPermRealizationData(BaseModel): - saturation_axis_data: List[float] - satnum_data: List[RelPermSatNumData] - realization: int +class RelPermRealizationDataForSaturation(BaseModel): + saturation_number: int + relperm_curve_data: List[CurveData] + + +class SaturationRealizationData(BaseModel): + saturation_axis_data: CurveData + satnum_data: List[RelPermRealizationDataForSaturation] + realization_id: int diff --git a/backend_py/primary/primary/services/relperm_assembler/relperm_assembler.py b/backend_py/primary/primary/services/relperm_assembler/relperm_assembler.py index f3a2d1de2..d633c4844 100644 --- a/backend_py/primary/primary/services/relperm_assembler/relperm_assembler.py +++ b/backend_py/primary/primary/services/relperm_assembler/relperm_assembler.py @@ -43,16 +43,50 @@ class RelPermTableInfo: @dataclass -class RelPermSatNumData: - satnum: int - relperm_curves_data: List[List[float]] +class CurveData: + curve_name: str + curve_values: List[float] + unit: str | None = None @dataclass -class RelPermRealizationData: - saturation_axis_data: List[float] - satnum_data: List[RelPermSatNumData] - realization: int +class RelPermRealizationDataForSaturation: + saturation_number: int + relperm_curve_data: List[CurveData] + + +@dataclass +class SaturationRealizationData: + saturation_axis_data: CurveData + satnum_data: List[RelPermRealizationDataForSaturation] + realization_id: int + + +class Statistic(str, Enum): + """ + Definition of possible statistics for a result column in an inplace volumetrics table + """ + + MEAN = "mean" + STD_DEV = "stddev" + MAX = "max" + MIN = "min" + P10 = "p10" + P90 = "p90" + + +@dataclass +class SaturationStatisticalData: + saturation_axis_data: CurveData + satnum_data: List[RelPermRealizationDataForSaturation] + statistics: Statistic + + +@dataclass +class RealizationCurveData: + curve_name: str + curve_values: np.ndarray + realization_id: int class RelPermAssembler: @@ -71,9 +105,80 @@ async def get_relperm_table_info(self, relperm_table_name: str): table_name=relperm_table_name, saturation_axes=saturation_infos, satnums=sorted(satnums) ) - async def get_relperm_ensemble_data( + # async def get_relperm_realization_data( + # self, relperm_table_name: str, saturation_axis_name: str, curve_names: List[str], satnums: List[int] + # ) -> List[SaturationRealizationData]: + # realizations_table: pl.DataFrame = await self._relperm_access.get_relperm_table(relperm_table_name) + # table_columns = realizations_table.columns + + # if saturation_axis_name not in table_columns: + # raise NoDataError( + # f"Saturation axis {saturation_axis_name} not found in table {relperm_table_name}", + # Service.GENERAL, + # ) + + # for curve_name in curve_names: + # if curve_name not in table_columns: + # raise NoDataError( + # f"Curve {curve_name} not found in saturation axis {saturation_axis_name} in table {relperm_table_name}", + # Service.GENERAL, + # ) + + # columns_to_use = [saturation_axis_name] + curve_names + ["REAL", "SATNUM"] + # filtered_table = ( + # realizations_table.select(columns_to_use) + # .filter((realizations_table["SATNUM"].cast(pl.Int32).is_in(satnums))) + # .drop_nulls() + # .sort(saturation_axis_name) + # ) + # # shared_saturation_axis = np.linspace(0, 1, 100) + # real_data: List[SaturationRealizationData] = [] + # for _real, real_table in filtered_table.group_by("REAL"): + # satnum_data = [] + # for _satnum, satnum_table in real_table.group_by("SATNUM"): + # sorted_satnum_table = satnum_table.sort(saturation_axis_name) + # # original_saturation = sorted_satnum_table[saturation_axis_name].to_numpy() + + # # Interpolate to get shared axis + # # interpolated_curves = [] + # # for curve_name in curve_names: + # # original_values = sorted_satnum_table[curve_name] + + # # interpolator = interp1d( + # # original_saturation, + # # original_values, + # # kind="cubic", + # # bounds_error=False, + # # fill_value=(original_values[0], original_values[-1]), + # # ) + + # # # Interpolate to shared axis + # # interpolated_values = interpolator(shared_saturation_axis) + # # interpolated_curves.append(interpolated_values.tolist()) + # satnum_data.append( + # RelPermRealizationDataForSaturation( + # saturation_number=sorted_satnum_table["SATNUM"][0], + # relperm_curve_data=[ + # CurveData(curve_values=sorted_satnum_table[curve_name].to_list(), curve_name=curve_name) + # for curve_name in curve_names + # ], + # ) + # ) + # real_data.append( + # SaturationRealizationData( + # saturation_axis_data=CurveData( + # curve_values=sorted_satnum_table[saturation_axis_name].to_list(), + # curve_name=saturation_axis_name, + # ), + # satnum_data=satnum_data, + # realization_id=sorted_satnum_table["REAL"][0], + # ) + # ) + # return real_data + + async def get_relperm_realization_data( self, relperm_table_name: str, saturation_axis_name: str, curve_names: List[str], satnums: List[int] - ) -> List[RelPermRealizationData]: + ) -> List[SaturationRealizationData]: realizations_table: pl.DataFrame = await self._relperm_access.get_relperm_table(relperm_table_name) table_columns = realizations_table.columns @@ -98,45 +203,63 @@ async def get_relperm_ensemble_data( .sort(saturation_axis_name) ) shared_saturation_axis = np.linspace(0, 1, 100) - real_data: List[RelPermRealizationData] = [] + real_data: List[SaturationRealizationData] = [] for _real, real_table in filtered_table.group_by("REAL"): satnum_data = [] + realization = real_table["REAL"][0] for _satnum, satnum_table in real_table.group_by("SATNUM"): - table_to = satnum_table.sort(saturation_axis_name) - original_saturation = table_to[saturation_axis_name].to_numpy() + sorted_satnum_table = satnum_table.sort(saturation_axis_name) + original_saturation = sorted_satnum_table[saturation_axis_name].to_numpy() + saturation_number = sorted_satnum_table["SATNUM"][0] # Interpolate to get shared axis - interpolated_curves = [] + interpolated_curves: dict = {} + for curve_name in curve_names: - original_values = table_to[curve_name] - - interpolator = interp1d( - original_saturation, - original_values, - kind="cubic", - bounds_error=False, - fill_value=(original_values[0], original_values[-1]), + original_values = sorted_satnum_table[curve_name] + + interpolated_curve = interpolate_curve_values_to_shared_axis( + original_saturation, original_values.to_numpy(), shared_saturation_axis ) + interpolated_curves[curve_name] = interpolated_curve - # Interpolate to shared axis - interpolated_values = interpolator(shared_saturation_axis) - interpolated_curves.append(interpolated_values.tolist()) satnum_data.append( - RelPermSatNumData( - satnum=table_to["SATNUM"][0], - relperm_curves_data=[table_to[curve_name].to_list() for curve_name in curve_names], + RelPermRealizationDataForSaturation( + saturation_number=saturation_number, + relperm_curve_data=[ + CurveData(curve_values=interpolated_curves[curve_name], curve_name=curve_name) + for curve_name in curve_names + ], ) ) real_data.append( - RelPermRealizationData( - saturation_axis_data=table_to[saturation_axis_name].to_list(), + SaturationRealizationData( + saturation_axis_data=CurveData( + curve_values=shared_saturation_axis.tolist(), + curve_name=saturation_axis_name, + ), satnum_data=satnum_data, - realization=table_to["REAL"][0], + realization_id=sorted_satnum_table["REAL"][0], ) ) return real_data +def interpolate_curve_values_to_shared_axis( + original_axis_values: np.ndarray, original_curve_values: np.ndarray, shared_axis_values: np.ndarray +) -> np.ndarray: + interpolator = interp1d( + original_axis_values, + original_curve_values, + kind="cubic", + bounds_error=False, + fill_value=np.nan, + ) + + interpolated_values = interpolator(shared_axis_values) + return interpolated_values + + def extract_keywords_from_relperm_table(relperm_table: pl.DataFrame) -> List[str]: return relperm_table["KEYWORD"].unique().to_list() diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index c16461744..7619ff84e 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -23,6 +23,7 @@ export type { BoundingBox2d as BoundingBox2d_api } from './models/BoundingBox2d' export type { BoundingBox3d as BoundingBox3d_api } from './models/BoundingBox3d'; export type { CaseInfo as CaseInfo_api } from './models/CaseInfo'; export type { Completions as Completions_api } from './models/Completions'; +export type { CurveData as CurveData_api } from './models/CurveData'; export type { DatedTree as DatedTree_api } from './models/DatedTree'; export type { EnsembleDetails as EnsembleDetails_api } from './models/EnsembleDetails'; export type { EnsembleInfo as EnsembleInfo_api } from './models/EnsembleInfo'; @@ -65,8 +66,7 @@ export { PolygonsAttributeType as PolygonsAttributeType_api } from './models/Pol export type { PolygonsMeta as PolygonsMeta_api } from './models/PolygonsMeta'; export type { PolylineIntersection as PolylineIntersection_api } from './models/PolylineIntersection'; export type { PvtData as PvtData_api } from './models/PvtData'; -export type { RelPermRealizationData as RelPermRealizationData_api } from './models/RelPermRealizationData'; -export type { RelPermSatNumData as RelPermSatNumData_api } from './models/RelPermSatNumData'; +export type { RelPermRealizationDataForSaturation as RelPermRealizationDataForSaturation_api } from './models/RelPermRealizationDataForSaturation'; export type { RelPermSaturationAxis as RelPermSaturationAxis_api } from './models/RelPermSaturationAxis'; export type { RelPermTableInfo as RelPermTableInfo_api } from './models/RelPermTableInfo'; export type { RepeatedTableColumnData as RepeatedTableColumnData_api } from './models/RepeatedTableColumnData'; @@ -75,6 +75,7 @@ export type { RftObservations as RftObservations_api } from './models/RftObserva export type { RftRealizationData as RftRealizationData_api } from './models/RftRealizationData'; export type { RftTableDefinition as RftTableDefinition_api } from './models/RftTableDefinition'; export type { RftWellInfo as RftWellInfo_api } from './models/RftWellInfo'; +export type { SaturationRealizationData as SaturationRealizationData_api } from './models/SaturationRealizationData'; export type { SeismicCubeMeta as SeismicCubeMeta_api } from './models/SeismicCubeMeta'; export type { SeismicFenceData as SeismicFenceData_api } from './models/SeismicFenceData'; export type { SeismicFencePolyline as SeismicFencePolyline_api } from './models/SeismicFencePolyline'; diff --git a/frontend/src/api/models/RelPermSatNumData.ts b/frontend/src/api/models/CurveData.ts similarity index 55% rename from frontend/src/api/models/RelPermSatNumData.ts rename to frontend/src/api/models/CurveData.ts index feb6d4e7b..f22cfd9e9 100644 --- a/frontend/src/api/models/RelPermSatNumData.ts +++ b/frontend/src/api/models/CurveData.ts @@ -2,8 +2,9 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -export type RelPermSatNumData = { - satnum: number; - relperm_curves_data: Array>; +export type CurveData = { + curve_name: string; + curve_values: Array; + unit: (string | null); }; diff --git a/frontend/src/api/models/RelPermRealizationData.ts b/frontend/src/api/models/RelPermRealizationData.ts deleted file mode 100644 index 890771a5a..000000000 --- a/frontend/src/api/models/RelPermRealizationData.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do no edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { RelPermSatNumData } from './RelPermSatNumData'; -export type RelPermRealizationData = { - saturation_axis_data: Array; - satnum_data: Array; - realization: number; -}; - diff --git a/frontend/src/api/models/RelPermRealizationDataForSaturation.ts b/frontend/src/api/models/RelPermRealizationDataForSaturation.ts new file mode 100644 index 000000000..2b0463057 --- /dev/null +++ b/frontend/src/api/models/RelPermRealizationDataForSaturation.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CurveData } from './CurveData'; +export type RelPermRealizationDataForSaturation = { + saturation_number: number; + relperm_curve_data: Array; +}; + diff --git a/frontend/src/api/models/SaturationRealizationData.ts b/frontend/src/api/models/SaturationRealizationData.ts new file mode 100644 index 000000000..5e98333aa --- /dev/null +++ b/frontend/src/api/models/SaturationRealizationData.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CurveData } from './CurveData'; +import type { RelPermRealizationDataForSaturation } from './RelPermRealizationDataForSaturation'; +export type SaturationRealizationData = { + saturation_axis_data: CurveData; + satnum_data: Array; + realization_id: number; +}; + diff --git a/frontend/src/api/services/RelpermService.ts b/frontend/src/api/services/RelpermService.ts index 0bc976b09..42b6f8a66 100644 --- a/frontend/src/api/services/RelpermService.ts +++ b/frontend/src/api/services/RelpermService.ts @@ -2,8 +2,8 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -import type { RelPermRealizationData } from '../models/RelPermRealizationData'; import type { RelPermTableInfo } from '../models/RelPermTableInfo'; +import type { SaturationRealizationData } from '../models/SaturationRealizationData'; import type { CancelablePromise } from '../core/CancelablePromise'; import type { BaseHttpRequest } from '../core/BaseHttpRequest'; export class RelpermService { @@ -58,27 +58,62 @@ export class RelpermService { }); } /** - * Get Saturation And Curve Data + * Get Realizations Curve Data * @param caseUuid Sumo case uuid * @param ensembleName Ensemble name * @param tableName Table name * @param saturationAxisName Saturation axis name * @param curveNames Curve names * @param satnums Satnums - * @returns RelPermRealizationData Successful Response + * @returns SaturationRealizationData Successful Response * @throws ApiError */ - public getSaturationAndCurveData( + public getRealizationsCurveData( caseUuid: string, ensembleName: string, tableName: string, saturationAxisName: string, curveNames: Array, satnums: Array, - ): CancelablePromise> { + ): CancelablePromise> { return this.httpRequest.request({ method: 'GET', - url: '/relperm/saturation_and_curve_data', + url: '/relperm/realizations_curve_data', + query: { + 'case_uuid': caseUuid, + 'ensemble_name': ensembleName, + 'table_name': tableName, + 'saturation_axis_name': saturationAxisName, + 'curve_names': curveNames, + 'satnums': satnums, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Get Statistical Curve Data + * @param caseUuid Sumo case uuid + * @param ensembleName Ensemble name + * @param tableName Table name + * @param saturationAxisName Saturation axis name + * @param curveNames Curve names + * @param satnums Satnums + * @returns SaturationRealizationData Successful Response + * @throws ApiError + */ + public getStatisticalCurveData( + caseUuid: string, + ensembleName: string, + tableName: string, + saturationAxisName: string, + curveNames: Array, + satnums: Array, + ): CancelablePromise> { + return this.httpRequest.request({ + method: 'GET', + url: '/relperm/statistical_curve_data', query: { 'case_uuid': caseUuid, 'ensemble_name': ensembleName, diff --git a/frontend/src/modules/RelPerm/interfaces.ts b/frontend/src/modules/RelPerm/interfaces.ts index e644bc8dd..ce35f8fb9 100644 --- a/frontend/src/modules/RelPerm/interfaces.ts +++ b/frontend/src/modules/RelPerm/interfaces.ts @@ -1,14 +1,25 @@ -import { RelPermRealizationData_api, RftRealizationData_api } from "@api"; +import { EnsembleIdent } from "@framework/EnsembleIdent"; import { InterfaceInitialization } from "@framework/UniDirectionalModuleComponentsInterface"; -import { UseQueryResult } from "@tanstack/react-query"; -import { validRealizationNumbersAtom } from "./settings/atoms/baseAtoms"; +import { selectedColorByAtom, validRealizationNumbersAtom } from "./settings/atoms/baseAtoms"; import { selectedVisualizationTypeAtom } from "./settings/atoms/baseAtoms"; -import { relPermDataQueryAtom } from "./settings/atoms/queryAtoms"; -import { VisualizationType } from "./typesAndEnums"; +import { + selectedEnsembleIdentAtom, + selectedRelPermCurveNamesAtom, + selectedRelPermSaturationAxisAtom, + selectedRelPermTableNameAtom, + selectedSatNumsAtom, +} from "./settings/atoms/derivedAtoms"; +import { ColorBy, VisualizationType } from "./typesAndEnums"; -type SettingsToViewInterface = { - relPermDataQuery: UseQueryResult; +export type SettingsToViewInterface = { + ensembleIdent: EnsembleIdent | null; + realizationNumbers: number[] | null; + tableName: string | null; + saturationAxis: string | null; + satNums: number[]; + relPermCurveNames: string[] | null; + colorBy: ColorBy; visualizationType: VisualizationType; }; export type Interfaces = { @@ -16,8 +27,26 @@ export type Interfaces = { }; export const settingsToViewInterfaceInitialization: InterfaceInitialization = { - relPermDataQuery: (get) => { - return get(relPermDataQueryAtom); + ensembleIdent: (get) => { + return get(selectedEnsembleIdentAtom); + }, + realizationNumbers: (get) => { + return get(validRealizationNumbersAtom); + }, + tableName: (get) => { + return get(selectedRelPermTableNameAtom); + }, + saturationAxis: (get) => { + return get(selectedRelPermSaturationAxisAtom); + }, + satNums: (get) => { + return get(selectedSatNumsAtom); + }, + relPermCurveNames: (get) => { + return get(selectedRelPermCurveNamesAtom); + }, + colorBy: (get) => { + return get(selectedColorByAtom); }, visualizationType: (get) => { return get(selectedVisualizationTypeAtom); diff --git a/frontend/src/modules/RelPerm/loadModule.tsx b/frontend/src/modules/RelPerm/loadModule.tsx index 101893c39..af17fea0b 100644 --- a/frontend/src/modules/RelPerm/loadModule.tsx +++ b/frontend/src/modules/RelPerm/loadModule.tsx @@ -1,10 +1,14 @@ import { ModuleRegistry } from "@framework/ModuleRegistry"; import { Interfaces, settingsToViewInterfaceInitialization } from "./interfaces"; -import { Settings } from "./settings"; -import { View } from "./view"; +import { Settings } from "./settings/settings"; +import { settingsToViewInterfaceEffects } from "./view/atoms/interfaceEffects"; +import { View } from "./view/view"; -const module = ModuleRegistry.initModule("RelPerm", { settingsToViewInterfaceInitialization }); +const module = ModuleRegistry.initModule("RelPerm", { + settingsToViewInterfaceInitialization, + settingsToViewInterfaceEffects, +}); module.viewFC = View; module.settingsFC = Settings; diff --git a/frontend/src/modules/RelPerm/settings/atoms/baseAtoms.ts b/frontend/src/modules/RelPerm/settings/atoms/baseAtoms.ts index bef194ac0..e3af1d21f 100644 --- a/frontend/src/modules/RelPerm/settings/atoms/baseAtoms.ts +++ b/frontend/src/modules/RelPerm/settings/atoms/baseAtoms.ts @@ -11,12 +11,13 @@ function areEnsembleIdentsEqual(a: EnsembleIdent | null, b: EnsembleIdent | null } return a.equals(b); } +export const selectedColorByAtom = atom(ColorBy.ENSEMBLE); +export const selectedVisualizationTypeAtom = atom(VisualizationType.STATISTICAL_FANCHART); export const userSelectedEnsembleIdentAtom = atomWithCompare(null, areEnsembleIdentsEqual); export const validRealizationNumbersAtom = atom(null); + export const userSelectedTableNameAtom = atom(null); export const userSelectedSaturationAxisAtom = atom(null); export const userSelectedSatNumsAtom = atomWithCompare([], isEqual); export const userSelectedRelPermCurveNamesAtom = atom(null); -export const selectedColorByAtom = atom(ColorBy.ENSEMBLE); -export const selectedVisualizationTypeAtom = atom(VisualizationType.STATISTICAL_FANCHART); diff --git a/frontend/src/modules/RelPerm/settings/atoms/queryAtoms.ts b/frontend/src/modules/RelPerm/settings/atoms/queryAtoms.ts index faef97258..330b594ed 100644 --- a/frontend/src/modules/RelPerm/settings/atoms/queryAtoms.ts +++ b/frontend/src/modules/RelPerm/settings/atoms/queryAtoms.ts @@ -61,43 +61,3 @@ export const relPermTableInfoQueryAtom = atomWithQuery((get) => { }; return query; }); - -export const relPermDataQueryAtom = atomWithQuery((get) => { - const selectedEnsembleIdent = get(selectedEnsembleIdentAtom); - const selectedTableName = get(selectedRelPermTableNameAtom); - const selectedRelPermSaturationAxis = get(selectedRelPermSaturationAxisAtom); - const selectedSatNums = get(selectedSatNumsAtom); - const selectedRelPermCurveNames = get(selectedRelPermCurveNamesAtom); - - const query = { - queryKey: [ - "getRelPermData", - selectedEnsembleIdent?.getCaseUuid(), - selectedEnsembleIdent?.getEnsembleName(), - selectedTableName, - selectedRelPermSaturationAxis, - selectedSatNums, - selectedRelPermCurveNames, - ], - queryFn: () => - apiService.relperm.getSaturationAndCurveData( - selectedEnsembleIdent?.getCaseUuid() ?? "", - selectedEnsembleIdent?.getEnsembleName() ?? "", - selectedTableName ?? "", - selectedRelPermSaturationAxis ?? "", - selectedRelPermCurveNames ?? [], - selectedSatNums ?? [] - ), - staleTime: STALE_TIME, - gcTime: CACHE_TIME, - enabled: !!( - selectedEnsembleIdent?.getCaseUuid() && - selectedEnsembleIdent?.getEnsembleName() && - selectedTableName && - selectedRelPermSaturationAxis && - selectedSatNums && - selectedRelPermCurveNames - ), - }; - return query; -}); diff --git a/frontend/src/modules/RelPerm/settings.tsx b/frontend/src/modules/RelPerm/settings/settings.tsx similarity index 97% rename from frontend/src/modules/RelPerm/settings.tsx rename to frontend/src/modules/RelPerm/settings/settings.tsx index 8296021e0..1501c08c4 100644 --- a/frontend/src/modules/RelPerm/settings.tsx +++ b/frontend/src/modules/RelPerm/settings/settings.tsx @@ -15,7 +15,6 @@ import { usePropagateApiErrorToStatusWriter } from "@modules/_shared/hooks/usePr import { useAtom, useAtomValue, useSetAtom } from "jotai"; -import { Interfaces } from "./interfaces"; import { selectedColorByAtom, selectedVisualizationTypeAtom, @@ -25,7 +24,7 @@ import { userSelectedSaturationAxisAtom, userSelectedTableNameAtom, validRealizationNumbersAtom, -} from "./settings/atoms/baseAtoms"; +} from "./atoms/baseAtoms"; import { availableRelPermCurveNamesAtom, availableRelPermSaturationAxesAtom, @@ -36,9 +35,11 @@ import { selectedRelPermSaturationAxisAtom, selectedRelPermTableNameAtom, selectedSatNumsAtom, -} from "./settings/atoms/derivedAtoms"; -import { relPermTableInfoQueryAtom, relPermTableNamesQueryAtom } from "./settings/atoms/queryAtoms"; -import { ColorBy, VisualizationType } from "./typesAndEnums"; +} from "./atoms/derivedAtoms"; +import { relPermTableInfoQueryAtom, relPermTableNamesQueryAtom } from "./atoms/queryAtoms"; + +import { Interfaces } from "../interfaces"; +import { ColorBy, VisualizationType } from "../typesAndEnums"; //Helpers to populate dropdowns const stringToOptions = (strings: string[]): SelectOption[] => { diff --git a/frontend/src/modules/RelPerm/view/atoms/baseAtoms.ts b/frontend/src/modules/RelPerm/view/atoms/baseAtoms.ts new file mode 100644 index 000000000..543ebe9ef --- /dev/null +++ b/frontend/src/modules/RelPerm/view/atoms/baseAtoms.ts @@ -0,0 +1,22 @@ +import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { atomWithCompare } from "@framework/utils/atomUtils"; +import { ColorBy, VisualizationType } from "@modules/RelPerm/typesAndEnums"; + +import { atom } from "jotai"; +import { isEqual } from "lodash"; + +function areEnsembleIdentsEqual(a: EnsembleIdent | null, b: EnsembleIdent | null) { + if (a === null) { + return b === null; + } + return a.equals(b); +} + +export const selectedEnsembleIdentAtom = atom(null); +export const selectedRealizationNumbersAtom = atom(null); +export const selectedTableNameAtom = atom(null); +export const selectedSaturationAxisAtom = atom(null); +export const selectedSatNumsAtom = atomWithCompare([], isEqual); +export const selectedRelPermCurveNamesAtom = atom(null); +export const selectedColorByAtom = atom(ColorBy.ENSEMBLE); +export const selectedVisualizationTypeAtom = atom(VisualizationType.STATISTICAL_FANCHART); diff --git a/frontend/src/modules/RelPerm/view/atoms/interfaceEffects.ts b/frontend/src/modules/RelPerm/view/atoms/interfaceEffects.ts new file mode 100644 index 000000000..0dde815cd --- /dev/null +++ b/frontend/src/modules/RelPerm/view/atoms/interfaceEffects.ts @@ -0,0 +1,50 @@ +import { InterfaceEffects } from "@framework/Module"; + + +import { + selectedColorByAtom, + selectedEnsembleIdentAtom, + selectedRealizationNumbersAtom, + selectedRelPermCurveNamesAtom, + selectedSatNumsAtom, + selectedSaturationAxisAtom, + selectedTableNameAtom, + selectedVisualizationTypeAtom, +} from "./baseAtoms"; + +import { SettingsToViewInterface } from "../../interfaces"; + +export const settingsToViewInterfaceEffects: InterfaceEffects = [ + (getInterfaceValue, setAtomValue) => { + const ensembleIdent = getInterfaceValue("ensembleIdent"); + setAtomValue(selectedEnsembleIdentAtom, ensembleIdent); + }, + (getInterfaceValue, setAtomValue) => { + const realizationNumbers = getInterfaceValue("realizationNumbers"); + setAtomValue(selectedRealizationNumbersAtom, realizationNumbers); + }, + (getInterfaceValue, setAtomValue) => { + const tableName = getInterfaceValue("tableName"); + setAtomValue(selectedTableNameAtom, tableName); + }, + (getInterfaceValue, setAtomValue) => { + const saturationAxis = getInterfaceValue("saturationAxis"); + setAtomValue(selectedSaturationAxisAtom, saturationAxis); + }, + (getInterfaceValue, setAtomValue) => { + const satNums = getInterfaceValue("satNums"); + setAtomValue(selectedSatNumsAtom, satNums); + }, + (getInterfaceValue, setAtomValue) => { + const relPermCurveNames = getInterfaceValue("relPermCurveNames"); + setAtomValue(selectedRelPermCurveNamesAtom, relPermCurveNames); + }, + (getInterfaceValue, setAtomValue) => { + const colorBy = getInterfaceValue("colorBy"); + setAtomValue(selectedColorByAtom, colorBy); + }, + (getInterfaceValue, setAtomValue) => { + const visualizationType = getInterfaceValue("visualizationType"); + setAtomValue(selectedVisualizationTypeAtom, visualizationType); + }, +]; diff --git a/frontend/src/modules/RelPerm/view/atoms/queryAtoms.ts b/frontend/src/modules/RelPerm/view/atoms/queryAtoms.ts new file mode 100644 index 000000000..2ba210fb2 --- /dev/null +++ b/frontend/src/modules/RelPerm/view/atoms/queryAtoms.ts @@ -0,0 +1,102 @@ +import { apiService } from "@framework/ApiService"; +import { + selectedRelPermSaturationAxisAtom, + selectedRelPermTableNameAtom, +} from "@modules/RelPerm/settings/atoms/derivedAtoms"; +import { VisualizationType } from "@modules/RelPerm/typesAndEnums"; + +import { atomWithQuery } from "jotai-tanstack-query"; + +import { + selectedEnsembleIdentAtom, + selectedRelPermCurveNamesAtom, + selectedSatNumsAtom, + selectedVisualizationTypeAtom, +} from "./baseAtoms"; + +const STALE_TIME = 60 * 1000; +const CACHE_TIME = 60 * 1000; + +export const relPermRealizationDataQueryAtom = atomWithQuery((get) => { + const selectedEnsembleIdent = get(selectedEnsembleIdentAtom); + const selectedTableName = get(selectedRelPermTableNameAtom); + const selectedRelPermSaturationAxis = get(selectedRelPermSaturationAxisAtom); + const selectedSatNums = get(selectedSatNumsAtom); + const selectedRelPermCurveNames = get(selectedRelPermCurveNamesAtom); + const visualizationType = get(selectedVisualizationTypeAtom); + + const query = { + queryKey: [ + "getRelPermRealizationData", + selectedEnsembleIdent?.getCaseUuid(), + selectedEnsembleIdent?.getEnsembleName(), + selectedTableName, + selectedRelPermSaturationAxis, + selectedSatNums, + selectedRelPermCurveNames, + ], + queryFn: () => + apiService.relperm.getRealizationsCurveData( + selectedEnsembleIdent?.getCaseUuid() ?? "", + selectedEnsembleIdent?.getEnsembleName() ?? "", + selectedTableName ?? "", + selectedRelPermSaturationAxis ?? "", + selectedRelPermCurveNames ?? [], + selectedSatNums ?? [] + ), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + enabled: !!( + selectedEnsembleIdent?.getCaseUuid() && + selectedEnsembleIdent?.getEnsembleName() && + selectedTableName && + selectedRelPermSaturationAxis && + selectedSatNums && + selectedRelPermCurveNames && + visualizationType === VisualizationType.INDIVIDUAL_REALIZATIONS + ), + }; + return query; +}); + +export const relPermStatisticalDataQueryAtom = atomWithQuery((get) => { + const selectedEnsembleIdent = get(selectedEnsembleIdentAtom); + const selectedTableName = get(selectedRelPermTableNameAtom); + const selectedRelPermSaturationAxis = get(selectedRelPermSaturationAxisAtom); + const selectedSatNums = get(selectedSatNumsAtom); + const selectedRelPermCurveNames = get(selectedRelPermCurveNamesAtom); + const visualizationType = get(selectedVisualizationTypeAtom); + + const query = { + queryKey: [ + "getRelPermStatisticalData", + selectedEnsembleIdent?.getCaseUuid(), + selectedEnsembleIdent?.getEnsembleName(), + selectedTableName, + selectedRelPermSaturationAxis, + selectedSatNums, + selectedRelPermCurveNames, + ], + queryFn: () => + apiService.relperm.getStatisticalCurveData( + selectedEnsembleIdent?.getCaseUuid() ?? "", + selectedEnsembleIdent?.getEnsembleName() ?? "", + selectedTableName ?? "", + selectedRelPermSaturationAxis ?? "", + selectedRelPermCurveNames ?? [], + selectedSatNums ?? [] + ), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + enabled: !!( + selectedEnsembleIdent?.getCaseUuid() && + selectedEnsembleIdent?.getEnsembleName() && + selectedTableName && + selectedRelPermSaturationAxis && + selectedSatNums && + selectedRelPermCurveNames && + visualizationType === VisualizationType.STATISTICAL_FANCHART + ), + }; + return query; +}); diff --git a/frontend/src/modules/RelPerm/view.tsx b/frontend/src/modules/RelPerm/view/view.tsx similarity index 74% rename from frontend/src/modules/RelPerm/view.tsx rename to frontend/src/modules/RelPerm/view/view.tsx index 11faf2b23..a81bb118b 100644 --- a/frontend/src/modules/RelPerm/view.tsx +++ b/frontend/src/modules/RelPerm/view/view.tsx @@ -10,40 +10,38 @@ import { useElementSize } from "@lib/hooks/useElementSize"; import { ContentMessage, ContentMessageType } from "@modules/_shared/components/ContentMessage/contentMessage"; import { usePropagateApiErrorToStatusWriter } from "@modules/_shared/hooks/usePropagateApiErrorToStatusWriter"; +import { useAtomValue } from "jotai"; import { PlotData } from "plotly.js"; -import { Interfaces } from "./interfaces"; +import { relPermRealizationDataQueryAtom, relPermStatisticalDataQueryAtom } from "./atoms/queryAtoms"; + +import { Interfaces } from "../interfaces"; export const View = ({ viewContext }: ModuleViewProps) => { const wrapperDivRef = React.useRef(null); const wrapperDivSize = useElementSize(wrapperDivRef); - const relPermDataQuery = viewContext.useSettingsToViewInterfaceValue("relPermDataQuery"); - // const realizationNums = viewContext.useSettingsToViewInterfaceValue("realizationNums"); - // const responseName = viewContext.useSettingsToViewInterfaceValue("responseName"); - // const wellName = viewContext.useSettingsToViewInterfaceValue("wellName"); - // const timeStampUtcMs = viewContext.useSettingsToViewInterfaceValue("timeStampsUtcMs"); + const visualizationType = viewContext.useSettingsToViewInterfaceValue("visualizationType"); + const relPermRealizationsDataQuery = useAtomValue(relPermRealizationDataQueryAtom); + const relPermStatisticalDataQuery = useAtomValue(relPermStatisticalDataQueryAtom); const statusWriter = useViewStatusWriter(viewContext); - const statusError = usePropagateApiErrorToStatusWriter(relPermDataQuery, statusWriter); + const statusErrorRealizations = usePropagateApiErrorToStatusWriter(relPermRealizationsDataQuery, statusWriter); + const statusErrorStatistical = usePropagateApiErrorToStatusWriter(relPermStatisticalDataQuery, statusWriter); let content = null; - if (relPermDataQuery.isFetching) { + if (relPermRealizationsDataQuery.isFetching) { content = ( ); - } else if (statusError !== null) { - content =
{statusError}
; - } else if (relPermDataQuery.isError || relPermDataQuery.data === undefined) { + } else if (statusErrorRealizations !== null) { + content =
{statusErrorRealizations}
; + } else if (relPermRealizationsDataQuery.isError || relPermRealizationsDataQuery.data === undefined) { content =
Could not load RFT data
; } else { - // const filteredRftData = rftDataQuery.data.filter((realizationData) => - // realizationNums?.includes(realizationData.realization) - // ); - // const [minValue, maxValue] = getResponseValueRange(filteredRftData); const plotData: Partial[] = []; const colors = [ "red", @@ -72,22 +70,22 @@ export const View = ({ viewContext }: ModuleViewProps) => { ]; let totalPoints = 0; - relPermDataQuery.data.forEach((realizationData) => { + relPermRealizationsDataQuery.data.forEach((realizationData) => { realizationData.satnum_data.forEach((satNumData) => { - satNumData.relperm_curves_data.forEach((curveData) => { - totalPoints += curveData.length; + satNumData.relperm_curve_data.forEach((curveData) => { + totalPoints += curveData.curve_values.length; }); }); }); const useGl: boolean = totalPoints > 1000; - relPermDataQuery.data.forEach((realizationData) => { + relPermRealizationsDataQuery.data.forEach((realizationData) => { realizationData.satnum_data.forEach((satNumData, idx) => { - satNumData.relperm_curves_data.forEach((curveData) => { + satNumData.relperm_curve_data.forEach((curveData) => { plotData.push( createRelPermRealizationTrace( - realizationData.realization, - realizationData.saturation_axis_data, - curveData, + realizationData.realization_id, + realizationData.saturation_axis_data.curve_values, + curveData.curve_values, colors[idx], useGl ) @@ -130,11 +128,15 @@ function createRelPermRealizationTrace( y: curveValues, type: useGl ? "scattergl" : "scatter", - mode: "lines", + mode: "markers+lines", showlegend: false, line: { color: color, - width: 2, + width: 1, + }, + marker: { + color: "blue", + size: 5, }, }; return trace; From 9c5d90895f8863c08c1bb99049e543ffaa44cc54 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:53:07 +0100 Subject: [PATCH 6/8] wip --- .../primary/routers/relperm/converters.py | 25 +- .../primary/primary/routers/relperm/router.py | 8 +- .../primary/routers/relperm/schemas.py | 13 +- .../relperm_assembler/relperm_assembler.py | 262 ++++++++++++++---- frontend/src/api/index.ts | 2 +- .../src/api/models/RealizationCurveData.ts | 10 + .../RelPermRealizationDataForSaturation.ts | 4 +- .../api/models/SaturationRealizationData.ts | 12 - frontend/src/api/services/RelpermService.ts | 10 +- frontend/src/modules/RelPerm/view/view.tsx | 42 ++- 10 files changed, 267 insertions(+), 121 deletions(-) create mode 100644 frontend/src/api/models/RealizationCurveData.ts delete mode 100644 frontend/src/api/models/SaturationRealizationData.ts diff --git a/backend_py/primary/primary/routers/relperm/converters.py b/backend_py/primary/primary/routers/relperm/converters.py index 27423877f..31885dce9 100644 --- a/backend_py/primary/primary/routers/relperm/converters.py +++ b/backend_py/primary/primary/routers/relperm/converters.py @@ -1,8 +1,9 @@ from primary.services.relperm_assembler.relperm_assembler import ( RelPermTableInfo, RelPermSaturationAxis, - SaturationRealizationData, + RelPermRealizationDataForSaturation, CurveData, + RealizationCurveData, ) from . import schemas @@ -26,23 +27,23 @@ def to_api_relperm_saturation_axis(axis: RelPermSaturationAxis) -> schemas.RelPe ) -def to_api_relperm_realization_data(data: SaturationRealizationData) -> schemas.SaturationRealizationData: +def to_api_relperm_realization_data( + data: RelPermRealizationDataForSaturation, +) -> schemas.RelPermRealizationDataForSaturation: - return schemas.SaturationRealizationData( + return schemas.RelPermRealizationDataForSaturation( saturation_axis_data=schemas.CurveData( curve_name=data.saturation_axis_data.curve_name, curve_values=data.saturation_axis_data.curve_values, unit=data.saturation_axis_data.unit, ), - satnum_data=[ - schemas.RelPermRealizationDataForSaturation( - saturation_number=satnum_data.saturation_number, - relperm_curve_data=[ - schemas.CurveData(curve_name=curve.curve_name, curve_values=curve.curve_values) - for curve in satnum_data.relperm_curve_data - ], + saturation_number=data.saturation_number, + relperm_curve_data=[ + schemas.RealizationCurveData( + curve_name=curve_data.curve_name, + curve_values=curve_data.curve_values.tolist(), + realization_id=curve_data.realization_id, ) - for satnum_data in data.satnum_data + for curve_data in data.relperm_curve_data ], - realization_id=data.realization_id, ) diff --git a/backend_py/primary/primary/routers/relperm/router.py b/backend_py/primary/primary/routers/relperm/router.py index 3c6a8d848..bf5c9f0b9 100644 --- a/backend_py/primary/primary/routers/relperm/router.py +++ b/backend_py/primary/primary/routers/relperm/router.py @@ -53,7 +53,7 @@ async def get_realizations_curve_data( saturation_axis_name: Annotated[str, Query(description="Saturation axis name")], curve_names: Annotated[List[str], Query(description="Curve names")], satnums: Annotated[List[int], Query(description="Satnums")], -) -> List[schemas.SaturationRealizationData]: +) -> schemas.RelPermRealizationDataForSaturation: access = await RelPermAccess.from_case_uuid_async( authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name @@ -61,7 +61,7 @@ async def get_realizations_curve_data( assembler = RelPermAssembler(access) relperm_data = await assembler.get_relperm_realization_data(table_name, saturation_axis_name, curve_names, satnums) - return [converters.to_api_relperm_realization_data(data) for data in relperm_data] + return converters.to_api_relperm_realization_data(relperm_data) @router.get("/statistical_curve_data") @@ -73,7 +73,7 @@ async def get_statistical_curve_data( saturation_axis_name: Annotated[str, Query(description="Saturation axis name")], curve_names: Annotated[List[str], Query(description="Curve names")], satnums: Annotated[List[int], Query(description="Satnums")], -) -> List[schemas.SaturationRealizationData]: +) -> schemas.RelPermRealizationDataForSaturation: access = await RelPermAccess.from_case_uuid_async( authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name @@ -81,4 +81,4 @@ async def get_statistical_curve_data( assembler = RelPermAssembler(access) relperm_data = await assembler.get_relperm_realization_data(table_name, saturation_axis_name, curve_names, satnums) - return [converters.to_api_relperm_realization_data(data) for data in relperm_data] + return converters.to_api_relperm_realization_data(relperm_data) diff --git a/backend_py/primary/primary/routers/relperm/schemas.py b/backend_py/primary/primary/routers/relperm/schemas.py index d1bb59785..cd81f69a3 100644 --- a/backend_py/primary/primary/routers/relperm/schemas.py +++ b/backend_py/primary/primary/routers/relperm/schemas.py @@ -21,12 +21,13 @@ class CurveData(BaseModel): unit: str | None = None -class RelPermRealizationDataForSaturation(BaseModel): - saturation_number: int - relperm_curve_data: List[CurveData] +class RealizationCurveData(BaseModel): + curve_name: str + curve_values: List[float] + realization_id: int -class SaturationRealizationData(BaseModel): +class RelPermRealizationDataForSaturation(BaseModel): + saturation_number: int saturation_axis_data: CurveData - satnum_data: List[RelPermRealizationDataForSaturation] - realization_id: int + relperm_curve_data: List[RealizationCurveData] diff --git a/backend_py/primary/primary/services/relperm_assembler/relperm_assembler.py b/backend_py/primary/primary/services/relperm_assembler/relperm_assembler.py index d633c4844..9a2d197a8 100644 --- a/backend_py/primary/primary/services/relperm_assembler/relperm_assembler.py +++ b/backend_py/primary/primary/services/relperm_assembler/relperm_assembler.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import List +from typing import List, Callable import logging from dataclasses import dataclass import numpy as np @@ -50,15 +50,9 @@ class CurveData: @dataclass -class RelPermRealizationDataForSaturation: - saturation_number: int - relperm_curve_data: List[CurveData] - - -@dataclass -class SaturationRealizationData: - saturation_axis_data: CurveData - satnum_data: List[RelPermRealizationDataForSaturation] +class RealizationCurveData: + curve_name: str + curve_values: np.ndarray realization_id: int @@ -76,17 +70,23 @@ class Statistic(str, Enum): @dataclass -class SaturationStatisticalData: - saturation_axis_data: CurveData - satnum_data: List[RelPermRealizationDataForSaturation] +class StatisticalCurveData: + curve_name: str + curve_values: List[float] statistics: Statistic @dataclass -class RealizationCurveData: - curve_name: str - curve_values: np.ndarray - realization_id: int +class RelPermRealizationDataForSaturation: + saturation_number: int + saturation_axis_data: CurveData + relperm_curve_data: List[RealizationCurveData] + + +@dataclass +class SaturationStatisticalData: + saturation_axis_data: CurveData + relperm_curve_data: List[StatisticalCurveData] class RelPermAssembler: @@ -178,8 +178,9 @@ async def get_relperm_table_info(self, relperm_table_name: str): async def get_relperm_realization_data( self, relperm_table_name: str, saturation_axis_name: str, curve_names: List[str], satnums: List[int] - ) -> List[SaturationRealizationData]: + ) -> RelPermRealizationDataForSaturation: realizations_table: pl.DataFrame = await self._relperm_access.get_relperm_table(relperm_table_name) + satnum = satnums[0] table_columns = realizations_table.columns if saturation_axis_name not in table_columns: @@ -198,51 +199,130 @@ async def get_relperm_realization_data( columns_to_use = [saturation_axis_name] + curve_names + ["REAL", "SATNUM"] filtered_table = ( realizations_table.select(columns_to_use) - .filter((realizations_table["SATNUM"].cast(pl.Int32).is_in(satnums))) + .filter((realizations_table["SATNUM"].cast(pl.Int32) == satnum)) .drop_nulls() .sort(saturation_axis_name) ) shared_saturation_axis = np.linspace(0, 1, 100) - real_data: List[SaturationRealizationData] = [] - for _real, real_table in filtered_table.group_by("REAL"): - satnum_data = [] + interpolated_realizations_table = interpolate_realizations_satnum_table_on_shared_saturation_axis( + filtered_table, shared_saturation_axis, saturation_axis_name, curve_names + ) + + real_data: List[RealizationCurveData] = [] + + for _real, real_table in interpolated_realizations_table.group_by("REAL"): + realization = real_table["REAL"][0] - for _satnum, satnum_table in real_table.group_by("SATNUM"): - sorted_satnum_table = satnum_table.sort(saturation_axis_name) - original_saturation = sorted_satnum_table[saturation_axis_name].to_numpy() - saturation_number = sorted_satnum_table["SATNUM"][0] - - # Interpolate to get shared axis - interpolated_curves: dict = {} - - for curve_name in curve_names: - original_values = sorted_satnum_table[curve_name] - - interpolated_curve = interpolate_curve_values_to_shared_axis( - original_saturation, original_values.to_numpy(), shared_saturation_axis - ) - interpolated_curves[curve_name] = interpolated_curve - - satnum_data.append( - RelPermRealizationDataForSaturation( - saturation_number=saturation_number, - relperm_curve_data=[ - CurveData(curve_values=interpolated_curves[curve_name], curve_name=curve_name) - for curve_name in curve_names - ], - ) - ) - real_data.append( - SaturationRealizationData( - saturation_axis_data=CurveData( - curve_values=shared_saturation_axis.tolist(), - curve_name=saturation_axis_name, - ), - satnum_data=satnum_data, - realization_id=sorted_satnum_table["REAL"][0], + for curve_name in curve_names: + curve_values = real_table[curve_name].to_numpy() + real_data.append( + RealizationCurveData(curve_name=curve_name, curve_values=curve_values, realization_id=realization) ) + test = await self.get_relperm_statistics_data( + relperm_table_name=relperm_table_name, + saturation_axis_name=saturation_axis_name, + curve_names=curve_names, + satnums=satnums, + ) + return RelPermRealizationDataForSaturation( + saturation_axis_data=CurveData( + curve_values=shared_saturation_axis.tolist(), + curve_name=saturation_axis_name, + ), + relperm_curve_data=real_data, + saturation_number=satnum, + ) + + async def get_relperm_statistics_data( + self, relperm_table_name: str, saturation_axis_name: str, curve_names: List[str], satnums: List[int] + ) -> None: + realizations_table: pl.DataFrame = await self._relperm_access.get_relperm_table(relperm_table_name) + satnum = satnums[0] + table_columns = realizations_table.columns + + if saturation_axis_name not in table_columns: + raise NoDataError( + f"Saturation axis {saturation_axis_name} not found in table {relperm_table_name}", + Service.GENERAL, ) - return real_data + + for curve_name in curve_names: + if curve_name not in table_columns: + raise NoDataError( + f"Curve {curve_name} not found in saturation axis {saturation_axis_name} in table {relperm_table_name}", + Service.GENERAL, + ) + + columns_to_use = [saturation_axis_name] + curve_names + ["REAL", "SATNUM"] + filtered_table = ( + realizations_table.select(columns_to_use) + .filter((realizations_table["SATNUM"].cast(pl.Int32) == satnum)) + .drop_nulls() + .sort(saturation_axis_name) + ) + shared_saturation_axis = np.linspace(0, 1, 100) + interpolated_realizations_table = interpolate_realizations_satnum_table_on_shared_saturation_axis( + filtered_table, shared_saturation_axis, saturation_axis_name, curve_names + ) + requested_statistics = [ + Statistic.MEAN, + Statistic.STD_DEV, + Statistic.MIN, + Statistic.MAX, + Statistic.P10, + Statistic.P90, + ] + statistic_aggregation_expressions = _create_statistic_aggregation_expressions(curve_names, requested_statistics) + per_group_statistical_df = ( + interpolated_realizations_table.select([saturation_axis_name] + curve_names) + .group_by(saturation_axis_name) + .agg(statistic_aggregation_expressions) + ) + print(per_group_statistical_df) + + +def _get_statistical_function_expression(statistic: Statistic) -> Callable[[pl.Expr], pl.Expr] | None: + """ + Get statistical function Polars expression based on statistic enum + + Note: Inverted P10 and P90 according to oil industry standards + """ + statistical_function_expression_map: dict[Statistic, Callable[[pl.Expr], pl.Expr]] = { + Statistic.MEAN: lambda col: col.mean(), + Statistic.MIN: lambda col: col.min(), + Statistic.MAX: lambda col: col.max(), + Statistic.STD_DEV: lambda col: col.std(), + Statistic.P10: lambda col: col.quantile(0.9, "linear"), # Inverted P10 and P90 + Statistic.P90: lambda col: col.quantile(0.1, "linear"), # Inverted P10 and P90 + } + + return statistical_function_expression_map.get(statistic) + + +def _create_statistical_expression(statistic: Statistic, column_name: str, drop_nans: bool = True) -> pl.Expr: + """ + Generate the Polars expression for the given statistic. + """ + base_col = pl.col(column_name) + if drop_nans: + base_col = base_col.drop_nans() + stat_func_expr = _get_statistical_function_expression(statistic) + if stat_func_expr is None: + raise ValueError(f"Unsupported statistic: {statistic}") + return stat_func_expr(base_col).alias(f"{column_name}_{statistic}") + + +def _create_statistic_aggregation_expressions( + result_columns: list[str], statistics: list[Statistic], drop_nans: bool = True +) -> list[pl.Expr]: + """ + Create Polars expressions for aggregation of result columns + """ + expressions = [] + for column_name in result_columns: + for statistic in statistics: + expressions.append(_create_statistical_expression(statistic, column_name, drop_nans)) + return expressions def interpolate_curve_values_to_shared_axis( @@ -253,7 +333,7 @@ def interpolate_curve_values_to_shared_axis( original_curve_values, kind="cubic", bounds_error=False, - fill_value=np.nan, + fill_value=[original_curve_values[0], original_curve_values[-1]], ) interpolated_values = interpolator(shared_axis_values) @@ -298,7 +378,7 @@ def extract_saturation_axes_from_relperm_table( RelPermSaturationAxis( saturation_name="SW", relperm_curve_names=[ - curve_name for curve_name in ["KRWO", "KRW"] if curve_name in relperm_table_columns + curve_name for curve_name in ["KROW", "KRW"] if curve_name in relperm_table_columns ], capillary_pressure_curve_names=[ curve_name for curve_name in ["PCOW"] if curve_name in relperm_table_columns @@ -350,3 +430,69 @@ def extract_saturation_axes_from_relperm_table( ) ) return saturation_infos + + +def interpolate_realizations_satnum_table_on_shared_saturation_axis( + satnum_table: pl.DataFrame, shared_saturation_axis: np.ndarray, saturation_axis_name: str, curve_names: List[str] +) -> pl.DataFrame: + shared_saturation_axis = np.linspace(0, 1, 100) + interpolated_tables = [] + for _real, real_table in satnum_table.group_by("REAL"): + realization = real_table["REAL"][0] + + # Sort by saturation + real_table_sorted = real_table.sort(saturation_axis_name) + original_saturation = real_table_sorted[saturation_axis_name].to_numpy() + + interpolated_realization_table = pl.DataFrame() + + # Ensure shared_saturation_axis is within bounds of original data + valid_mask = (shared_saturation_axis >= np.min(original_saturation)) & ( + shared_saturation_axis <= np.max(original_saturation) + ) + shared_saturation_filtered = shared_saturation_axis[valid_mask] + + # Interpolate each curve + for curve_name in curve_names: + original_values = real_table_sorted[curve_name].to_numpy() + + # Determine if values should be non-negative + is_non_negative = all(original_values >= 0) + + # Create interpolator with appropriate bounds handling + interpolator = interp1d( + original_saturation, + original_values, + kind="cubic", + bounds_error=False, + fill_value=(original_values[0], original_values[-1]), # Use endpoint values instead of NaN + ) + + # Interpolate to shared axis + interpolated_values = interpolator(shared_saturation_filtered) + + # Enforce non-negativity if original data was non-negative + if is_non_negative: + interpolated_values = np.maximum(interpolated_values, 0) + + # Create full-length array with NaN padding + full_interpolated = np.full_like(shared_saturation_axis, np.nan) + full_interpolated[valid_mask] = interpolated_values + + # Add to table + interpolated_realization_table = interpolated_realization_table.with_columns( + **{curve_name: pl.Series(full_interpolated)} + ) + + # Add saturation axis and realization number + interpolated_realization_table = interpolated_realization_table.with_columns( + **{ + saturation_axis_name: pl.Series(shared_saturation_axis), + "REAL": pl.Series([realization] * len(shared_saturation_axis)), + } + ) + + interpolated_tables.append(interpolated_realization_table) + + # Concatenate all realizations + return pl.concat(interpolated_tables) diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 7619ff84e..2fd960a13 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -66,6 +66,7 @@ export { PolygonsAttributeType as PolygonsAttributeType_api } from './models/Pol export type { PolygonsMeta as PolygonsMeta_api } from './models/PolygonsMeta'; export type { PolylineIntersection as PolylineIntersection_api } from './models/PolylineIntersection'; export type { PvtData as PvtData_api } from './models/PvtData'; +export type { RealizationCurveData as RealizationCurveData_api } from './models/RealizationCurveData'; export type { RelPermRealizationDataForSaturation as RelPermRealizationDataForSaturation_api } from './models/RelPermRealizationDataForSaturation'; export type { RelPermSaturationAxis as RelPermSaturationAxis_api } from './models/RelPermSaturationAxis'; export type { RelPermTableInfo as RelPermTableInfo_api } from './models/RelPermTableInfo'; @@ -75,7 +76,6 @@ export type { RftObservations as RftObservations_api } from './models/RftObserva export type { RftRealizationData as RftRealizationData_api } from './models/RftRealizationData'; export type { RftTableDefinition as RftTableDefinition_api } from './models/RftTableDefinition'; export type { RftWellInfo as RftWellInfo_api } from './models/RftWellInfo'; -export type { SaturationRealizationData as SaturationRealizationData_api } from './models/SaturationRealizationData'; export type { SeismicCubeMeta as SeismicCubeMeta_api } from './models/SeismicCubeMeta'; export type { SeismicFenceData as SeismicFenceData_api } from './models/SeismicFenceData'; export type { SeismicFencePolyline as SeismicFencePolyline_api } from './models/SeismicFencePolyline'; diff --git a/frontend/src/api/models/RealizationCurveData.ts b/frontend/src/api/models/RealizationCurveData.ts new file mode 100644 index 000000000..dd74162ea --- /dev/null +++ b/frontend/src/api/models/RealizationCurveData.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type RealizationCurveData = { + curve_name: string; + curve_values: Array; + realization_id: number; +}; + diff --git a/frontend/src/api/models/RelPermRealizationDataForSaturation.ts b/frontend/src/api/models/RelPermRealizationDataForSaturation.ts index 2b0463057..9780b99d5 100644 --- a/frontend/src/api/models/RelPermRealizationDataForSaturation.ts +++ b/frontend/src/api/models/RelPermRealizationDataForSaturation.ts @@ -3,8 +3,10 @@ /* tslint:disable */ /* eslint-disable */ import type { CurveData } from './CurveData'; +import type { RealizationCurveData } from './RealizationCurveData'; export type RelPermRealizationDataForSaturation = { saturation_number: number; - relperm_curve_data: Array; + saturation_axis_data: CurveData; + relperm_curve_data: Array; }; diff --git a/frontend/src/api/models/SaturationRealizationData.ts b/frontend/src/api/models/SaturationRealizationData.ts deleted file mode 100644 index 5e98333aa..000000000 --- a/frontend/src/api/models/SaturationRealizationData.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* generated using openapi-typescript-codegen -- do no edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { CurveData } from './CurveData'; -import type { RelPermRealizationDataForSaturation } from './RelPermRealizationDataForSaturation'; -export type SaturationRealizationData = { - saturation_axis_data: CurveData; - satnum_data: Array; - realization_id: number; -}; - diff --git a/frontend/src/api/services/RelpermService.ts b/frontend/src/api/services/RelpermService.ts index 42b6f8a66..e3c3e6478 100644 --- a/frontend/src/api/services/RelpermService.ts +++ b/frontend/src/api/services/RelpermService.ts @@ -2,8 +2,8 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { RelPermRealizationDataForSaturation } from '../models/RelPermRealizationDataForSaturation'; import type { RelPermTableInfo } from '../models/RelPermTableInfo'; -import type { SaturationRealizationData } from '../models/SaturationRealizationData'; import type { CancelablePromise } from '../core/CancelablePromise'; import type { BaseHttpRequest } from '../core/BaseHttpRequest'; export class RelpermService { @@ -65,7 +65,7 @@ export class RelpermService { * @param saturationAxisName Saturation axis name * @param curveNames Curve names * @param satnums Satnums - * @returns SaturationRealizationData Successful Response + * @returns RelPermRealizationDataForSaturation Successful Response * @throws ApiError */ public getRealizationsCurveData( @@ -75,7 +75,7 @@ export class RelpermService { saturationAxisName: string, curveNames: Array, satnums: Array, - ): CancelablePromise> { + ): CancelablePromise { return this.httpRequest.request({ method: 'GET', url: '/relperm/realizations_curve_data', @@ -100,7 +100,7 @@ export class RelpermService { * @param saturationAxisName Saturation axis name * @param curveNames Curve names * @param satnums Satnums - * @returns SaturationRealizationData Successful Response + * @returns RelPermRealizationDataForSaturation Successful Response * @throws ApiError */ public getStatisticalCurveData( @@ -110,7 +110,7 @@ export class RelpermService { saturationAxisName: string, curveNames: Array, satnums: Array, - ): CancelablePromise> { + ): CancelablePromise { return this.httpRequest.request({ method: 'GET', url: '/relperm/statistical_curve_data', diff --git a/frontend/src/modules/RelPerm/view/view.tsx b/frontend/src/modules/RelPerm/view/view.tsx index a81bb118b..721e58cf4 100644 --- a/frontend/src/modules/RelPerm/view/view.tsx +++ b/frontend/src/modules/RelPerm/view/view.tsx @@ -9,6 +9,7 @@ import { CircularProgress } from "@lib/components/CircularProgress"; import { useElementSize } from "@lib/hooks/useElementSize"; import { ContentMessage, ContentMessageType } from "@modules/_shared/components/ContentMessage/contentMessage"; import { usePropagateApiErrorToStatusWriter } from "@modules/_shared/hooks/usePropagateApiErrorToStatusWriter"; +import { ConstructionOutlined } from "@mui/icons-material"; import { useAtomValue } from "jotai"; import { PlotData } from "plotly.js"; @@ -70,30 +71,27 @@ export const View = ({ viewContext }: ModuleViewProps) => { ]; let totalPoints = 0; - relPermRealizationsDataQuery.data.forEach((realizationData) => { - realizationData.satnum_data.forEach((satNumData) => { - satNumData.relperm_curve_data.forEach((curveData) => { - totalPoints += curveData.curve_values.length; - }); - }); + relPermRealizationsDataQuery.data.relperm_curve_data.forEach((realizationData) => { + totalPoints += realizationData.curve_values.length; }); + const useGl: boolean = totalPoints > 1000; - relPermRealizationsDataQuery.data.forEach((realizationData) => { - realizationData.satnum_data.forEach((satNumData, idx) => { - satNumData.relperm_curve_data.forEach((curveData) => { - plotData.push( - createRelPermRealizationTrace( - realizationData.realization_id, - realizationData.saturation_axis_data.curve_values, - curveData.curve_values, - colors[idx], - useGl - ) - ); - }); - }); + const curveNames = new Set(relPermRealizationsDataQuery.data.relperm_curve_data.map((data) => data.curve_name)); + + relPermRealizationsDataQuery.data.relperm_curve_data.forEach((realizationData) => { + plotData.push( + createRelPermRealizationTrace( + realizationData.realization_id, + relPermRealizationsDataQuery.data.saturation_axis_data.curve_values, + realizationData.curve_values, + colors[Array.from(curveNames).indexOf(realizationData.curve_name)], + useGl + ) + ); }); + // const title = `RFT for ${wellName}, ${timeStampUtcMs && timestampUtcMsToCompactIsoString(timeStampUtcMs)}`; + console.log(plotData); content = ( Date: Thu, 14 Nov 2024 14:39:33 +0100 Subject: [PATCH 7/8] wip --- .../primary/routers/relperm/converters.py | 53 +++- .../primary/primary/routers/relperm/router.py | 6 +- .../primary/routers/relperm/schemas.py | 27 +- .../relperm_assembler/relperm_assembler.py | 43 ++- frontend/src/api/index.ts | 3 + .../RelPermStatisticalDataForSaturation.ts | 12 + frontend/src/api/models/Statistic.ts | 15 + .../src/api/models/StatisticalCurveData.ts | 9 + frontend/src/api/services/RelpermService.ts | 5 +- .../view/utils/createRelPermTracesUtils.ts | 211 ++++++++++++++ frontend/src/modules/RelPerm/view/view.tsx | 183 +++++++----- .../PlotlyTraceUtils/fanchartPlotting.ts | 275 ++++++++++++++++++ .../PlotlyTraceUtils/statisticsPlotting.ts | 240 +++++++++++++++ .../modules/_shared/PlotlyTraceUtils/types.ts | 9 + 14 files changed, 997 insertions(+), 94 deletions(-) create mode 100644 frontend/src/api/models/RelPermStatisticalDataForSaturation.ts create mode 100644 frontend/src/api/models/Statistic.ts create mode 100644 frontend/src/api/models/StatisticalCurveData.ts create mode 100644 frontend/src/modules/RelPerm/view/utils/createRelPermTracesUtils.ts create mode 100644 frontend/src/modules/_shared/PlotlyTraceUtils/fanchartPlotting.ts create mode 100644 frontend/src/modules/_shared/PlotlyTraceUtils/statisticsPlotting.ts create mode 100644 frontend/src/modules/_shared/PlotlyTraceUtils/types.ts diff --git a/backend_py/primary/primary/routers/relperm/converters.py b/backend_py/primary/primary/routers/relperm/converters.py index 31885dce9..6a6671ad1 100644 --- a/backend_py/primary/primary/routers/relperm/converters.py +++ b/backend_py/primary/primary/routers/relperm/converters.py @@ -2,8 +2,9 @@ RelPermTableInfo, RelPermSaturationAxis, RelPermRealizationDataForSaturation, - CurveData, + RelPermStatisticalDataForSaturation, RealizationCurveData, + Statistic, ) from . import schemas @@ -47,3 +48,53 @@ def to_api_relperm_realization_data( for curve_data in data.relperm_curve_data ], ) + + +def _convert_statistic_values_dict_to_schema( + statistic_values: dict[Statistic, list[float]], +) -> dict[schemas.Statistic, list[float]]: + """Converts the statistic values dictionary from the service layer format to API format""" + return { + _convert_statistic_enum_to_statistic_enum(statistic): values for statistic, values in statistic_values.items() + } + + +def _convert_statistic_enum_to_statistic_enum( + statistic: Statistic, +) -> schemas.Statistic: + """Converts the statistic enum from the service layer format to API enum""" + if statistic == Statistic.MEAN: + return schemas.Statistic.MEAN + if statistic == Statistic.STD_DEV: + return schemas.Statistic.STD_DEV + if statistic == Statistic.MIN: + return schemas.Statistic.MIN + if statistic == Statistic.MAX: + return schemas.Statistic.MAX + if statistic == Statistic.P10: + return schemas.Statistic.P10 + if statistic == Statistic.P90: + return schemas.Statistic.P90 + + raise ValueError(f"Unknown statistic value: {statistic.value}") + + +def to_api_relperm_statistical_data( + data: RelPermStatisticalDataForSaturation, +) -> schemas.RelPermStatisticalDataForSaturation: + print(data) + return schemas.RelPermStatisticalDataForSaturation( + saturation_axis_data=schemas.CurveData( + curve_name=data.saturation_axis_data.curve_name, + curve_values=data.saturation_axis_data.curve_values, + unit=data.saturation_axis_data.unit, + ), + saturation_number=data.saturation_number, + relperm_curve_data=[ + schemas.StatisticalCurveData( + curve_name=curve_data.curve_name, + curve_values=_convert_statistic_values_dict_to_schema(curve_data.curve_values), + ) + for curve_data in data.relperm_curve_data + ], + ) diff --git a/backend_py/primary/primary/routers/relperm/router.py b/backend_py/primary/primary/routers/relperm/router.py index bf5c9f0b9..55fc5819e 100644 --- a/backend_py/primary/primary/routers/relperm/router.py +++ b/backend_py/primary/primary/routers/relperm/router.py @@ -73,12 +73,12 @@ async def get_statistical_curve_data( saturation_axis_name: Annotated[str, Query(description="Saturation axis name")], curve_names: Annotated[List[str], Query(description="Curve names")], satnums: Annotated[List[int], Query(description="Satnums")], -) -> schemas.RelPermRealizationDataForSaturation: +) -> schemas.RelPermStatisticalDataForSaturation: access = await RelPermAccess.from_case_uuid_async( authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name ) assembler = RelPermAssembler(access) - relperm_data = await assembler.get_relperm_realization_data(table_name, saturation_axis_name, curve_names, satnums) + relperm_data = await assembler.get_relperm_statistics_data(table_name, saturation_axis_name, curve_names, satnums) - return converters.to_api_relperm_realization_data(relperm_data) + return converters.to_api_relperm_statistical_data(relperm_data) diff --git a/backend_py/primary/primary/routers/relperm/schemas.py b/backend_py/primary/primary/routers/relperm/schemas.py index cd81f69a3..2ae5857b2 100644 --- a/backend_py/primary/primary/routers/relperm/schemas.py +++ b/backend_py/primary/primary/routers/relperm/schemas.py @@ -1,4 +1,5 @@ -from typing import List +from enum import StrEnum +from typing import List, Dict from pydantic import BaseModel @@ -31,3 +32,27 @@ class RelPermRealizationDataForSaturation(BaseModel): saturation_number: int saturation_axis_data: CurveData relperm_curve_data: List[RealizationCurveData] + + +class Statistic(StrEnum): + """ + Definition of possible statistics + """ + + MEAN = "mean" + STD_DEV = "stddev" + MAX = "max" + MIN = "min" + P10 = "p10" + P90 = "p90" + + +class StatisticalCurveData(BaseModel): + curve_name: str + curve_values: Dict[Statistic, List[float]] + + +class RelPermStatisticalDataForSaturation(BaseModel): + saturation_axis_data: CurveData + saturation_number: int + relperm_curve_data: List[StatisticalCurveData] diff --git a/backend_py/primary/primary/services/relperm_assembler/relperm_assembler.py b/backend_py/primary/primary/services/relperm_assembler/relperm_assembler.py index 9a2d197a8..cc693e2dc 100644 --- a/backend_py/primary/primary/services/relperm_assembler/relperm_assembler.py +++ b/backend_py/primary/primary/services/relperm_assembler/relperm_assembler.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import List, Callable +from typing import List, Callable, Dict import logging from dataclasses import dataclass import numpy as np @@ -72,8 +72,7 @@ class Statistic(str, Enum): @dataclass class StatisticalCurveData: curve_name: str - curve_values: List[float] - statistics: Statistic + curve_values: Dict[Statistic, List[float]] @dataclass @@ -84,8 +83,9 @@ class RelPermRealizationDataForSaturation: @dataclass -class SaturationStatisticalData: +class RelPermStatisticalDataForSaturation: saturation_axis_data: CurveData + saturation_number: int relperm_curve_data: List[StatisticalCurveData] @@ -218,12 +218,8 @@ async def get_relperm_realization_data( real_data.append( RealizationCurveData(curve_name=curve_name, curve_values=curve_values, realization_id=realization) ) - test = await self.get_relperm_statistics_data( - relperm_table_name=relperm_table_name, - saturation_axis_name=saturation_axis_name, - curve_names=curve_names, - satnums=satnums, - ) + test = await self.get_relperm_statistics_data(relperm_table_name, saturation_axis_name, curve_names, satnums) + print(test) return RelPermRealizationDataForSaturation( saturation_axis_data=CurveData( curve_values=shared_saturation_axis.tolist(), @@ -235,7 +231,7 @@ async def get_relperm_realization_data( async def get_relperm_statistics_data( self, relperm_table_name: str, saturation_axis_name: str, curve_names: List[str], satnums: List[int] - ) -> None: + ) -> RelPermStatisticalDataForSaturation: realizations_table: pl.DataFrame = await self._relperm_access.get_relperm_table(relperm_table_name) satnum = satnums[0] table_columns = realizations_table.columns @@ -273,12 +269,33 @@ async def get_relperm_statistics_data( Statistic.P90, ] statistic_aggregation_expressions = _create_statistic_aggregation_expressions(curve_names, requested_statistics) - per_group_statistical_df = ( + statistical_df = ( interpolated_realizations_table.select([saturation_axis_name] + curve_names) .group_by(saturation_axis_name) .agg(statistic_aggregation_expressions) + .drop_nulls() + .sort(saturation_axis_name) + ) + + available_statistic_column_names = statistical_df.columns + statistical_curve_data = [] + for curve_name in curve_names: + stat_curve_values: Dict[Statistic, List[float]] = {} + for statistic in requested_statistics: + statistic_column_name = f"{curve_name}_{statistic}" + if statistic_column_name not in available_statistic_column_names: + raise ValueError(f"Column {statistic_column_name} not found in statistical table") + curve_values = statistical_df[statistic_column_name].to_list() + stat_curve_values[statistic] = curve_values + statistical_curve_data.append(StatisticalCurveData(curve_name=curve_name, curve_values=stat_curve_values)) + return RelPermStatisticalDataForSaturation( + saturation_axis_data=CurveData( + curve_values=statistical_df[saturation_axis_name].to_list(), + curve_name=saturation_axis_name, + ), + saturation_number=satnum, + relperm_curve_data=statistical_curve_data, ) - print(per_group_statistical_df) def _get_statistical_function_expression(statistic: Statistic) -> Callable[[pl.Expr], pl.Expr] | None: diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 2fd960a13..d2940df3e 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -69,6 +69,7 @@ export type { PvtData as PvtData_api } from './models/PvtData'; export type { RealizationCurveData as RealizationCurveData_api } from './models/RealizationCurveData'; export type { RelPermRealizationDataForSaturation as RelPermRealizationDataForSaturation_api } from './models/RelPermRealizationDataForSaturation'; export type { RelPermSaturationAxis as RelPermSaturationAxis_api } from './models/RelPermSaturationAxis'; +export type { RelPermStatisticalDataForSaturation as RelPermStatisticalDataForSaturation_api } from './models/RelPermStatisticalDataForSaturation'; export type { RelPermTableInfo as RelPermTableInfo_api } from './models/RelPermTableInfo'; export type { RepeatedTableColumnData as RepeatedTableColumnData_api } from './models/RepeatedTableColumnData'; export type { RftObservation as RftObservation_api } from './models/RftObservation'; @@ -80,6 +81,8 @@ export type { SeismicCubeMeta as SeismicCubeMeta_api } from './models/SeismicCub export type { SeismicFenceData as SeismicFenceData_api } from './models/SeismicFenceData'; export type { SeismicFencePolyline as SeismicFencePolyline_api } from './models/SeismicFencePolyline'; export { SensitivityType as SensitivityType_api } from './models/SensitivityType'; +export { Statistic as Statistic_api } from './models/Statistic'; +export type { StatisticalCurveData as StatisticalCurveData_api } from './models/StatisticalCurveData'; export { StatisticFunction as StatisticFunction_api } from './models/StatisticFunction'; export type { StatisticValueObject as StatisticValueObject_api } from './models/StatisticValueObject'; export type { StratigraphicUnit as StratigraphicUnit_api } from './models/StratigraphicUnit'; diff --git a/frontend/src/api/models/RelPermStatisticalDataForSaturation.ts b/frontend/src/api/models/RelPermStatisticalDataForSaturation.ts new file mode 100644 index 000000000..b163d9832 --- /dev/null +++ b/frontend/src/api/models/RelPermStatisticalDataForSaturation.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CurveData } from './CurveData'; +import type { StatisticalCurveData } from './StatisticalCurveData'; +export type RelPermStatisticalDataForSaturation = { + saturation_axis_data: CurveData; + saturation_number: number; + relperm_curve_data: Array; +}; + diff --git a/frontend/src/api/models/Statistic.ts b/frontend/src/api/models/Statistic.ts new file mode 100644 index 000000000..d468209a9 --- /dev/null +++ b/frontend/src/api/models/Statistic.ts @@ -0,0 +1,15 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * Definition of possible statistics + */ +export enum Statistic { + MEAN = 'mean', + STDDEV = 'stddev', + MAX = 'max', + MIN = 'min', + P10 = 'p10', + P90 = 'p90', +} diff --git a/frontend/src/api/models/StatisticalCurveData.ts b/frontend/src/api/models/StatisticalCurveData.ts new file mode 100644 index 000000000..98f08454f --- /dev/null +++ b/frontend/src/api/models/StatisticalCurveData.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type StatisticalCurveData = { + curve_name: string; + curve_values: Record>; +}; + diff --git a/frontend/src/api/services/RelpermService.ts b/frontend/src/api/services/RelpermService.ts index e3c3e6478..3ff079c62 100644 --- a/frontend/src/api/services/RelpermService.ts +++ b/frontend/src/api/services/RelpermService.ts @@ -3,6 +3,7 @@ /* tslint:disable */ /* eslint-disable */ import type { RelPermRealizationDataForSaturation } from '../models/RelPermRealizationDataForSaturation'; +import type { RelPermStatisticalDataForSaturation } from '../models/RelPermStatisticalDataForSaturation'; import type { RelPermTableInfo } from '../models/RelPermTableInfo'; import type { CancelablePromise } from '../core/CancelablePromise'; import type { BaseHttpRequest } from '../core/BaseHttpRequest'; @@ -100,7 +101,7 @@ export class RelpermService { * @param saturationAxisName Saturation axis name * @param curveNames Curve names * @param satnums Satnums - * @returns RelPermRealizationDataForSaturation Successful Response + * @returns RelPermStatisticalDataForSaturation Successful Response * @throws ApiError */ public getStatisticalCurveData( @@ -110,7 +111,7 @@ export class RelpermService { saturationAxisName: string, curveNames: Array, satnums: Array, - ): CancelablePromise { + ): CancelablePromise { return this.httpRequest.request({ method: 'GET', url: '/relperm/statistical_curve_data', diff --git a/frontend/src/modules/RelPerm/view/utils/createRelPermTracesUtils.ts b/frontend/src/modules/RelPerm/view/utils/createRelPermTracesUtils.ts new file mode 100644 index 000000000..e83763765 --- /dev/null +++ b/frontend/src/modules/RelPerm/view/utils/createRelPermTracesUtils.ts @@ -0,0 +1,211 @@ +import { + RelPermStatisticalDataForSaturation_api, + Statistic_api, + StatisticalCurveData_api, + SummaryVectorDateObservation_api, + VectorHistoricalData_api, + VectorRealizationData_api, + VectorStatisticData_api, +} from "@api"; +import { + FanchartData, + FreeLineData, + LowHighData, + MinMaxData, + createFanchartTraces, +} from "@modules/_shared/PlotlyTraceUtils/fanchartPlotting"; +import { LineData, StatisticsData, createStatisticsTraces } from "@modules/_shared/PlotlyTraceUtils/statisticsPlotting"; +import { PlotDataWithLegendRank } from "@modules/_shared/PlotlyTraceUtils/types"; + +import { PlotData } from "plotly.js"; + +/** + Get line shape - "vh" for rate data, "linear" for non-rate data + */ +export function getLineShape(isRate: boolean): "linear" | "vh" { + return isRate ? "vh" : "linear"; +} + +/** + Definition of base options for creating vector realization trace for a vector realization data. + */ +type CreateRealizationTraceBaseOptions = { + name?: string; + color: string; + legendGroup: string; + hoverTemplate?: string; + // lineShape?: "linear" | "spline" | "hv" | "vh" | "hvh" | "vhv"; + showLegend?: boolean; + yaxis?: string; + xaxis?: string; + type?: "scatter" | "scattergl"; +}; + +/** + Utility function for creating vector realization trace for a vector realization data object + for given vector. + */ +export type CreateVectorRealizationTraceOptions = CreateRealizationTraceBaseOptions & { + vectorRealizationData: VectorRealizationData_api; +}; +export function createVectorRealizationTrace({ + vectorRealizationData, + name, + color, + legendGroup, + hoverTemplate = "", + showLegend = false, + yaxis = "y", + xaxis = "x", + type = "scatter", +}: CreateVectorRealizationTraceOptions): Partial { + // TODO: + // - type: "scattergl" or "scatter"? Maximum 8 WebGL contexts in Chrome gives issues? + // "scattergl" hides traces when zooming and panning for Ruben on work computer. + // - lineShape - Each VectorRealizationData_api element has its own `is_rate` property. Should we + // use that to determine the line shape or provide a lineShape argument? + + return { + x: vectorRealizationData.timestamps_utc_ms, + y: vectorRealizationData.values, + line: { width: 1, color: color, shape: getLineShape(vectorRealizationData.is_rate) }, + mode: "lines", + type: type, + hovertemplate: `${hoverTemplate}Realization: ${vectorRealizationData.realization}`, + name: name, + legendgroup: legendGroup, + showlegend: vectorRealizationData.realization === 0 && showLegend ? true : false, + yaxis: yaxis, + xaxis: xaxis, + } as Partial; +} + +/** + Utility function for creating vector realization traces for an array of vector realization data + for given vector. + */ +export type CreateVectorRealizationTracesOptions = CreateRealizationTraceBaseOptions & { + vectorRealizationsData: VectorRealizationData_api[]; +}; +export function createVectorRealizationTraces({ + vectorRealizationsData, + name, + color, + legendGroup, + hoverTemplate = "", + showLegend = false, + yaxis = "y", + xaxis = "x", + type = "scatter", +}: CreateVectorRealizationTracesOptions): Partial[] { + // TODO: + // - lineShape - Each VectorRealizationData_api element has its own `is_rate` property. Should we + // use that to determine the line shape or provide a lineShape argument? + + return vectorRealizationsData.map((realization) => { + return createVectorRealizationTrace({ + vectorRealizationData: realization, + name, + color, + legendGroup, + hoverTemplate, + showLegend, + yaxis, + xaxis, + type, + }); + }); +} + +/** + Utility function for creating traces representing statistical fanchart for given statistics data. + + The function creates filled transparent area between P10 and P90, and between MIN and MAX, and a free line + for MEAN. + + NOTE: P10 and P90, and MIN and MAX are considered to work in pairs, therefore the pairs are neglected if + only one of the statistics in each pair is present in the data. I.e. P10/P90 is neglected if only P10 or P90 + is presented in the data. Similarly, MIN/MAX is neglected if only MIN or MAX is presented in the data. + */ +export type CreateRelPermFanchartTracesOptions = { + relPermStatisticsData: RelPermStatisticalDataForSaturation_api; + curveName: string; + hexColor: string; + legendGroup: string; + name?: string; + yaxis?: string; + // lineShape?: "vh" | "linear" | "spline" | "hv" | "hvh" | "vhv"; + hoverTemplate?: string; + showLegend?: boolean; + legendRank?: number; + type?: "scatter" | "scattergl"; +}; +export function createRelPermFanchartTraces({ + relPermStatisticsData, + curveName, + hexColor, + legendGroup, + name = undefined, + yaxis = "y", + hoverTemplate = "(%{x}, %{y})
", + showLegend = false, + type = "scatter", + legendRank, +}: CreateRelPermFanchartTracesOptions): Partial[] { + const curveData = relPermStatisticsData.relperm_curve_data.find((v) => v.curve_name === curveName); + if (!curveData) { + throw new Error(`Curve data for curve name ${curveName} not found in rel perm statistics data`); + } + const lowData = curveData.curve_values[Statistic_api.P90]; + const highData = curveData.curve_values[Statistic_api.P10]; + + let lowHighData: LowHighData | undefined = undefined; + if (lowData && highData) { + lowHighData = { + highName: Statistic_api.P10.toString(), + highData: highData, + lowName: Statistic_api.P90.toString(), + lowData: lowData, + }; + } + + const minData = curveData.curve_values[Statistic_api.MIN]; + const maxData = curveData.curve_values[Statistic_api.MAX]; + + let minMaxData: MinMaxData | undefined = undefined; + if (minData && maxData) { + minMaxData = { + maximum: maxData, + minimum: minData, + }; + } + + const meanData = curveData.curve_values[Statistic_api.MEAN]; + let meanFreeLineData: FreeLineData | undefined = undefined; + if (meanData) { + meanFreeLineData = { + name: Statistic_api.MEAN.toString(), + data: meanData, + }; + } + + const fanchartData: FanchartData = { + samples: relPermStatisticsData.saturation_axis_data.curve_values, + lowHigh: lowHighData, + minimumMaximum: minMaxData, + freeLine: meanFreeLineData, + }; + + return createFanchartTraces({ + data: fanchartData, + hexColor: hexColor, + legendGroup: legendGroup, + name: name, + lineShape: "linear", //getLineShape(relPermStatisticsData.is_rate), + showLegend: showLegend, + hoverTemplate: hoverTemplate, + legendRank: legendRank, + yaxis: yaxis, + type: type, + }); +} diff --git a/frontend/src/modules/RelPerm/view/view.tsx b/frontend/src/modules/RelPerm/view/view.tsx index 721e58cf4..82467d85a 100644 --- a/frontend/src/modules/RelPerm/view/view.tsx +++ b/frontend/src/modules/RelPerm/view/view.tsx @@ -13,10 +13,13 @@ import { ConstructionOutlined } from "@mui/icons-material"; import { useAtomValue } from "jotai"; import { PlotData } from "plotly.js"; +import { vi } from "vitest"; import { relPermRealizationDataQueryAtom, relPermStatisticalDataQueryAtom } from "./atoms/queryAtoms"; +import { createRelPermFanchartTraces } from "./utils/createRelPermTracesUtils"; import { Interfaces } from "../interfaces"; +import { VisualizationType } from "../typesAndEnums"; export const View = ({ viewContext }: ModuleViewProps) => { const wrapperDivRef = React.useRef(null); @@ -29,84 +32,116 @@ export const View = ({ viewContext }: ModuleViewProps) => { const statusWriter = useViewStatusWriter(viewContext); const statusErrorRealizations = usePropagateApiErrorToStatusWriter(relPermRealizationsDataQuery, statusWriter); const statusErrorStatistical = usePropagateApiErrorToStatusWriter(relPermStatisticalDataQuery, statusWriter); - + console.log("statData", relPermStatisticalDataQuery.data); let content = null; + const plotData: Partial[] = []; + + if (visualizationType === VisualizationType.INDIVIDUAL_REALIZATIONS) { + if (relPermRealizationsDataQuery.isFetching) { + content = ( + + + + ); + } else if (statusErrorRealizations !== null) { + content =
{statusErrorRealizations}
; + } else if (relPermRealizationsDataQuery.isError || relPermRealizationsDataQuery.data === undefined) { + content =
Could not load RFT data
; + } else { + const colors = [ + "red", + "blue", + "green", + "yellow", + "purple", + "orange", + "pink", + "brown", + "black", + "gray", + "cyan", + "magenta", + "purple", + "lime", + "teal", + "indigo", + "maroon", + "navy", + "olive", + "silver", + "aqua", + "fuchsia", + "white", + ]; + + let totalPoints = 0; + relPermRealizationsDataQuery.data.relperm_curve_data.forEach((realizationData) => { + totalPoints += realizationData.curve_values.length; + }); + + const useGl: boolean = totalPoints > 1000; + const curveNames = new Set( + relPermRealizationsDataQuery.data.relperm_curve_data.map((data) => data.curve_name) + ); - if (relPermRealizationsDataQuery.isFetching) { - content = ( - - - - ); - } else if (statusErrorRealizations !== null) { - content =
{statusErrorRealizations}
; - } else if (relPermRealizationsDataQuery.isError || relPermRealizationsDataQuery.data === undefined) { - content =
Could not load RFT data
; - } else { - const plotData: Partial[] = []; - const colors = [ - "red", - "blue", - "green", - "yellow", - "purple", - "orange", - "pink", - "brown", - "black", - "gray", - "cyan", - "magenta", - "purple", - "lime", - "teal", - "indigo", - "maroon", - "navy", - "olive", - "silver", - "aqua", - "fuchsia", - "white", - ]; - - let totalPoints = 0; - relPermRealizationsDataQuery.data.relperm_curve_data.forEach((realizationData) => { - totalPoints += realizationData.curve_values.length; - }); - - const useGl: boolean = totalPoints > 1000; - const curveNames = new Set(relPermRealizationsDataQuery.data.relperm_curve_data.map((data) => data.curve_name)); - - relPermRealizationsDataQuery.data.relperm_curve_data.forEach((realizationData) => { - plotData.push( - createRelPermRealizationTrace( - realizationData.realization_id, - relPermRealizationsDataQuery.data.saturation_axis_data.curve_values, - realizationData.curve_values, - colors[Array.from(curveNames).indexOf(realizationData.curve_name)], - useGl - ) + relPermRealizationsDataQuery.data.relperm_curve_data.forEach((realizationData) => { + plotData.push( + createRelPermRealizationTrace( + realizationData.realization_id, + relPermRealizationsDataQuery.data.saturation_axis_data.curve_values, + realizationData.curve_values, + colors[Array.from(curveNames).indexOf(realizationData.curve_name)], + useGl + ) + ); + }); + } + } + + if (visualizationType === VisualizationType.STATISTICAL_FANCHART) { + if (relPermStatisticalDataQuery.data) { + console.log("hello"); + const curveNames = Array.from( + new Set(relPermStatisticalDataQuery.data.relperm_curve_data.map((data) => data.curve_name)) ); - }); - - // const title = `RFT for ${wellName}, ${timeStampUtcMs && timestampUtcMsToCompactIsoString(timeStampUtcMs)}`; - console.log(plotData); - content = ( - - ); + console.log(curveNames); + for (let i = 0; i < curveNames.length; i++) { + const curve = curveNames[i]; + const test = createRelPermFanchartTraces({ + relPermStatisticsData: relPermStatisticalDataQuery.data, + curveName: curve, + hexColor: "blue", + legendGroup: "RelPerm", + }); + console.log(test); + plotData.push( + ...createRelPermFanchartTraces({ + relPermStatisticsData: relPermStatisticalDataQuery.data, + curveName: curve, + hexColor: "blue", + legendGroup: "RelPerm", + }) + ); + } + } } + console.log("plotData", plotData); + + content = ( + + ); + return (
{content} diff --git a/frontend/src/modules/_shared/PlotlyTraceUtils/fanchartPlotting.ts b/frontend/src/modules/_shared/PlotlyTraceUtils/fanchartPlotting.ts new file mode 100644 index 000000000..d8ed65cfe --- /dev/null +++ b/frontend/src/modules/_shared/PlotlyTraceUtils/fanchartPlotting.ts @@ -0,0 +1,275 @@ +import { formatRgb, modeRgb, useMode } from "culori"; +import { ScatterLine } from "plotly.js"; + +import { PlotDataWithLegendRank } from "./types"; + +/** + Definition of statistics data for free line trace in fanchart + + * `name` - Name of statistics data (e.g. mean, median, etc.) + * `data` - List of statistics value data + */ +export type FreeLineData = { + name: string; + data: number[]; +}; + +/** + Defining paired low and high percentile data for fanchart plotting + + * `lowData` - List of low percentile data + * `lowName` - Name of low percentile data (e.g. 10th percentile) + * `highData` - List of high percentile data + * `highName` - Name of high percentile data (e.g. 90th percentile) + */ +export type LowHighData = { + lowData: number[]; + lowName: string; + highData: number[]; + highName: string; +}; + +/** + Definition of paired minimum and maximum data for fanchart plotting + + * `minimum` - List of minimum value data + * `maximum` - List of maximum value data + */ +export type MinMaxData = { + minimum: number[]; + maximum: number[]; +}; + +/** + Type defining fanchart data utilized in creation of statistical fanchart traces + + * `samples` - Common sample point list for each following value list. Can be list of strings or numbers + * `freeLine` - Optional statistics with name and value data for free line trace in fanchart (e.g. + mean, median, etc.) + * `minimumMaximum` - Paired optional minimum and maximum data for fanchart plotting + * `lowHigh` - Paired optional low and high percentile names and data for fanchart plotting + */ +export type FanchartData = { + samples: string[] | number[]; + freeLine?: FreeLineData; + minimumMaximum?: MinMaxData; + lowHigh?: LowHighData; +}; + +/** + Direction of traces in fanchart + */ +enum TraceDirection { + HORIZONTAL = "horizontal", + VERTICAL = "vertical", +} + +/** + Validation of fanchart data + + Ensure equal length of all statistical fanchart data lists and x-axis data list + + Throw error if lengths are unequal +*/ +function validateFanchartData(data: FanchartData): void { + const samplesLength = data.samples.length; + + if (samplesLength <= 0) { + throw new Error("Empty x-axis data list in FanchartData"); + } + + if (data.freeLine !== undefined && samplesLength !== data.freeLine.data.length) { + throw new Error("Invalid fanchart mean value data length. data.samples.length !== freeLine.data.length"); + } + + if (data.minimumMaximum !== undefined && samplesLength !== data.minimumMaximum.minimum.length) { + throw new Error( + "Invalid fanchart minimum value data length. data.samples.length !== data.minimumMaximum.minimum.length" + ); + } + + if (data.minimumMaximum !== undefined && samplesLength !== data.minimumMaximum.maximum.length) { + throw new Error( + "Invalid fanchart maximum value data length. data.samples.length !== data.minimumMaximum.maximum.length" + ); + } + + if (data.lowHigh !== undefined && samplesLength !== data.lowHigh.lowData.length) { + throw new Error( + "Invalid fanchart low percentile value data length. data.samples.length !== data.lowHigh.lowData.length" + ); + } + + if (data.lowHigh !== undefined && samplesLength !== data.lowHigh.highData.length) { + throw new Error( + "Invalid fanchart high percentile value data length. data.samples.length !== data.lowHigh.highData.length" + ); + } +} + +/** + Definition of options for creating statistical fanchart traces + + To be used as input to createFanchartTraces function with default values for optional arguments. + */ +export type CreateFanchartTracesOptions = { + data: FanchartData; + hexColor: string; + legendGroup: string; + lineShape?: ScatterLine["shape"]; + showLegend?: boolean; + hoverTemplate?: string; + legendRank?: number; + yaxis?: string; + xaxis?: string; + direction?: TraceDirection; + showHoverInfo?: boolean; + hoverText?: string; + name?: string; + type?: "scatter" | "scattergl"; + // hovermode?: string, +}; + +/** + Utility function for creating statistical fanchart traces + + Takes `data` containing data for each statistical feature as input, and creates a list of traces + for each feature. Plotly plots traces from front to end of the list, thereby the last trace is + plotted on top. + + Note that min and max, and high and low percentile are paired optional statistics. This implies + that if minimum is provided, maximum must be provided as well, and vice versa. The same yields + for low and high percentile data. + + The function provides a list of traces: [trace0, tract1, ..., traceN] + + Fanchart is created by use of fill "tonexty" configuration for the traces. Fill "tonexty" is + misleading naming, as "tonexty" in trace1 fills to y in trace0, i.e y in previous trace. + + The order of traces are minimum, low, high, maximum and free line. Thus it is required that + values in minimum <= low, and low <= high, and high <= maximum. Fill is setting "tonexty" in + this function is set s.t. trace fillings are not stacked making colors in fills unchanged + when disabling trace statistics inputs (minimum and maximum or low and high). + + Free line is last trace and is plotted on top as a line - without filling to other traces. + + Note: + If hovertemplate is proved it overrides the hovertext + + Returns: + List of fanchart traces, one for each statistical feature in data input. + [trace0, tract1, ..., traceN]. + */ +export function createFanchartTraces({ + data, + hexColor, + legendGroup, + lineShape = "linear", + showLegend = true, + hoverTemplate = undefined, + legendRank = undefined, + yaxis = "y", + xaxis = "x", + direction = TraceDirection.HORIZONTAL, + showHoverInfo = true, + hoverText = "", + name = undefined, + type = "scatter", +}: CreateFanchartTracesOptions): Partial[] { + // NOTE: + // - hovermode? not exposed? + + // TODO: Remove unused default arguments? + + validateFanchartData(data); + + // False positive + // eslint-disable-next-line react-hooks/rules-of-hooks + const convertRgb = useMode(modeRgb); + const rgb = convertRgb(hexColor); + if (rgb === undefined) { + throw new Error("Invalid conversion of hex color string: " + hexColor + " to rgb."); + } + const fillColorLight = formatRgb({ ...rgb, alpha: 0.3 }); + const fillColorDark = formatRgb({ ...rgb, alpha: 0.6 }); + const lineColor = formatRgb({ ...rgb, alpha: 1.0 }); + + function getDefaultTrace(statisticsName: string, values: number[]): Partial { + const trace: Partial = { + name: name ?? legendGroup, + x: direction === TraceDirection.HORIZONTAL ? data.samples : values, + y: direction === TraceDirection.HORIZONTAL ? values : data.samples, + xaxis: xaxis, + yaxis: yaxis, + mode: "lines", + type: type, + line: { width: 0, color: lineColor, shape: lineShape }, + legendgroup: legendGroup, + showlegend: false, + }; + + if (legendRank !== undefined) { + trace.legendrank = legendRank; + } + if (!showHoverInfo) { + trace.hoverinfo = "skip"; + return trace; + } + if (hoverTemplate !== undefined) { + trace.hovertemplate = hoverTemplate + statisticsName; + } else { + trace.hovertext = statisticsName + " " + hoverText; + } + return trace; + } + + const traces: Partial[] = []; + + // Minimum + if (data.minimumMaximum !== undefined) { + traces.push(getDefaultTrace("Minimum", data.minimumMaximum.minimum)); + } + + // Low and high percentile + if (data.lowHigh !== undefined) { + const lowTrace = getDefaultTrace(data.lowHigh.lowName, data.lowHigh.lowData); + + // Add fill to previous trace + if (traces.length > 0) { + lowTrace.fill = "tonexty"; + lowTrace.fillcolor = fillColorLight; + } + traces.push(lowTrace); + + const highTrace = getDefaultTrace(data.lowHigh.highName, data.lowHigh.highData); + highTrace.fill = "tonexty"; + highTrace.fillcolor = fillColorDark; + traces.push(highTrace); + } + + // Maximum + if (data.minimumMaximum !== undefined) { + const maximumTrace = getDefaultTrace("Maximum", data.minimumMaximum.maximum); + + // Add fill to previous trace + if (traces.length > 0) { + maximumTrace.fill = "tonexty"; + maximumTrace.fillcolor = fillColorLight; + } + traces.push(maximumTrace); + } + + // Free line - solid line + if (data.freeLine !== undefined) { + const lineTrace = getDefaultTrace(data.freeLine.name, data.freeLine.data); + lineTrace.line = { color: lineColor, shape: lineShape }; + traces.push(lineTrace); + } + + // Set legend for last trace in list + if (traces.length > 0) { + traces[traces.length - 1].showlegend = showLegend; + } + console.log("traces", traces); + return traces; +} diff --git a/frontend/src/modules/_shared/PlotlyTraceUtils/statisticsPlotting.ts b/frontend/src/modules/_shared/PlotlyTraceUtils/statisticsPlotting.ts new file mode 100644 index 000000000..fe926cbf2 --- /dev/null +++ b/frontend/src/modules/_shared/PlotlyTraceUtils/statisticsPlotting.ts @@ -0,0 +1,240 @@ +import { ScatterLine } from "plotly.js"; + +import { PlotDataWithLegendRank } from "./types"; + +/** + Definition of line trace data for statistics plot + + * `data` - List of value data + * `name` - Name of line data + */ +export type LineData = { + data: number[]; + name: string; +}; + +/** + Definition of statistics data utilized in creation of statistical plot traces + + `Attributes:` + * `samples` - Common sample point list for each following value list. Can be list of strings or numbers + * `freeLine` - LineData with name and value data for free line trace in statistics plot + (e.g. mean, median, etc.) + * `minimum` - Optional list of minimum value data for statistics plot + * `maximum` - Optional list of maximum value data for statistics plot + * `lowPercentile` - Optional low percentile, name and data values for statistics plot + * `midPercentile` - Optional middle percentile, name and data values for statistics plot + * `highPercentile` - Optional high percentile, name and data values for statistics plot + */ +export type StatisticsData = { + samples: string[] | number[]; + freeLine?: LineData; + minimum?: number[]; + maximum?: number[]; + lowPercentile?: LineData; + highPercentile?: LineData; + midPercentile?: LineData; +}; + +/** + Validation of statistics data + + Ensure equal length of all statistical data lists and x-axis data list + + Throw error if lengths are unequal + */ +function validateStatisticsData(data: StatisticsData): void { + const samplesLength = data.samples.length; + + if (samplesLength <= 0) { + throw new Error("Empty x-axis data list in StatisticsData"); + } + + if (data.freeLine !== undefined && samplesLength !== data.freeLine.data.length) { + throw new Error( + `Invalid statistics mean value data length. data.samples.length (${samplesLength}) != data.freeLine.data.length (${data.freeLine.data.length})` + ); + } + + if (data.minimum !== undefined && samplesLength !== data.minimum.length) { + throw new Error( + `Invalid statistics minimum value data length. data.samples.length (${samplesLength}) != data.minimum.length (${data.minimum.length})` + ); + } + + if (data.maximum !== undefined && samplesLength !== data.maximum.length) { + throw new Error( + `Invalid statistics maximum value data length. data.samples.length (${samplesLength}) != data.maximum.length (${data.maximum.length})` + ); + } + + if (data.lowPercentile !== undefined && samplesLength !== data.lowPercentile.data.length) { + throw new Error( + `Invalid statistics low percentile value data length. data.samples.length (${samplesLength}) != data.lowPercentile.data.length (${data.lowPercentile.data.length})` + ); + } + + if (data.midPercentile !== undefined && samplesLength !== data.midPercentile.data.length) { + throw new Error( + `Invalid statistics middle percentile value data length. data.samples.length (${samplesLength}) != data.midPercentile.data.length (${data.midPercentile.data.length})` + ); + } + + if (data.highPercentile !== undefined && samplesLength !== data.highPercentile.data.length) { + throw new Error( + `Invalid statistics high percentile value data length. data.samples.length (${samplesLength}) != data.highPercentile.data.length (${data.highPercentile.data.length})` + ); + } +} + +/** + Definition of options for creating statistical plot trace + + To be used as input to createStatisticsTraces function with default values for optional arguments. + */ +export type CreateStatisticsTracesOptions = { + data: StatisticsData; + color: string; + legendGroup: string; + lineShape?: ScatterLine["shape"]; + showLegend?: boolean; + hoverTemplate?: string; + legendRank?: number; + xaxis?: string; + yaxis?: string; + lineWidth?: number; + showHoverInfo?: boolean; + hoverText?: string; + name?: string; + type?: "scatter" | "scattergl"; + // hovermode?: string, +}; + +/** + Utility function for creating statistical plot traces + + Takes `data` containing data for each statistical feature as input, and creates a list of traces + for each feature. Plotly plots traces from front to end of the list, thereby the last trace is + plotted on top. + + Note that the data is optional, which implies that only wanted statistical features needs to be + provided for trace plot generation. + + The function provides a list of traces: [trace0, tract1, ..., traceN] + + Note: + If hovertemplate is proved it overrides the hovertext + + Returns: + List of statistical line traces, one for each statistical feature in data input. + [trace0, tract1, ..., traceN]. + */ +export function createStatisticsTraces({ + data, + color, + legendGroup, + name = undefined, + lineShape = "linear", + lineWidth = 2, + xaxis = "x", + yaxis = "y", + showLegend = true, + showHoverInfo = true, + hoverText = "", + hoverTemplate = undefined, + legendRank = undefined, + type = "scatter", +}: CreateStatisticsTracesOptions): Partial[] { + // NOTE: + // - hovermode? not exposed? + + validateStatisticsData(data); + + function getDefaultTrace(statisticsName: string, values: number[]): Partial { + const trace: Partial = { + name: name ?? legendGroup, + x: data.samples, + y: values, + xaxis: xaxis, + yaxis: yaxis, + mode: "lines", + type: type, + line: { color: color, width: lineWidth, shape: lineShape }, + legendgroup: legendGroup, + showlegend: false, + }; + if (legendRank !== undefined) { + trace.legendrank = legendRank; + } + if (!showHoverInfo) { + trace.hoverinfo = "skip"; + return trace; + } + if (hoverTemplate !== undefined) { + trace.hovertemplate = hoverTemplate + statisticsName; + } else { + trace.hovertext = statisticsName + " " + hoverText; + } + return trace; + } + + const traces: Partial[] = []; + + // Minimum + if (data.minimum !== undefined) { + const minimumTrace = getDefaultTrace("Minimum", data.minimum); + if (minimumTrace.line) { + minimumTrace.line.dash = "longdash"; + } + traces.push(minimumTrace); + } + + // Low percentile + if (data.lowPercentile !== undefined) { + const lowPercentileTrace = getDefaultTrace(data.lowPercentile.name, data.lowPercentile.data); + if (lowPercentileTrace.line) { + lowPercentileTrace.line.dash = "dashdot"; + } + traces.push(lowPercentileTrace); + } + + // Mid percentile + if (data.midPercentile !== undefined) { + const midPercentileTrace = getDefaultTrace(data.midPercentile.name, data.midPercentile.data); + if (midPercentileTrace.line) { + midPercentileTrace.line.dash = "dot"; + } + traces.push(midPercentileTrace); + } + + // High percentile + if (data.highPercentile !== undefined) { + const highPercentileTrace = getDefaultTrace(data.highPercentile.name, data.highPercentile.data); + if (highPercentileTrace.line) { + highPercentileTrace.line.dash = "dashdot"; + } + traces.push(highPercentileTrace); + } + + // Maximum + if (data.maximum !== undefined) { + const maximumTrace = getDefaultTrace("Maximum", data.maximum); + if (maximumTrace.line) { + maximumTrace.line.dash = "longdash"; + } + traces.push(maximumTrace); + } + + // Free line + if (data.freeLine !== undefined) { + const freeLineTrace = getDefaultTrace(data.freeLine.name, data.freeLine.data); + traces.push(freeLineTrace); + } + + // Set legend for last trace in list + if (traces.length > 0) { + traces[traces.length - 1].showlegend = showLegend; + } + + return traces; +} diff --git a/frontend/src/modules/_shared/PlotlyTraceUtils/types.ts b/frontend/src/modules/_shared/PlotlyTraceUtils/types.ts new file mode 100644 index 000000000..77e781368 --- /dev/null +++ b/frontend/src/modules/_shared/PlotlyTraceUtils/types.ts @@ -0,0 +1,9 @@ +import { PlotData } from "plotly.js"; + +export interface PlotDataWithLegendRank extends Partial { + // TODO: Have realizationNumber? + //realizationNumber?: number | null; + + // Did they forget to expose this one + legendrank?: number; +} From 40ca692baa0c0609cd450a3c6a455ed9d51a7b59 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:44:37 +0100 Subject: [PATCH 8/8] wip --- .../relperm_assembler/relperm_assembler.py | 80 +------------------ 1 file changed, 4 insertions(+), 76 deletions(-) diff --git a/backend_py/primary/primary/services/relperm_assembler/relperm_assembler.py b/backend_py/primary/primary/services/relperm_assembler/relperm_assembler.py index cc693e2dc..d913bc29f 100644 --- a/backend_py/primary/primary/services/relperm_assembler/relperm_assembler.py +++ b/backend_py/primary/primary/services/relperm_assembler/relperm_assembler.py @@ -105,77 +105,6 @@ async def get_relperm_table_info(self, relperm_table_name: str): table_name=relperm_table_name, saturation_axes=saturation_infos, satnums=sorted(satnums) ) - # async def get_relperm_realization_data( - # self, relperm_table_name: str, saturation_axis_name: str, curve_names: List[str], satnums: List[int] - # ) -> List[SaturationRealizationData]: - # realizations_table: pl.DataFrame = await self._relperm_access.get_relperm_table(relperm_table_name) - # table_columns = realizations_table.columns - - # if saturation_axis_name not in table_columns: - # raise NoDataError( - # f"Saturation axis {saturation_axis_name} not found in table {relperm_table_name}", - # Service.GENERAL, - # ) - - # for curve_name in curve_names: - # if curve_name not in table_columns: - # raise NoDataError( - # f"Curve {curve_name} not found in saturation axis {saturation_axis_name} in table {relperm_table_name}", - # Service.GENERAL, - # ) - - # columns_to_use = [saturation_axis_name] + curve_names + ["REAL", "SATNUM"] - # filtered_table = ( - # realizations_table.select(columns_to_use) - # .filter((realizations_table["SATNUM"].cast(pl.Int32).is_in(satnums))) - # .drop_nulls() - # .sort(saturation_axis_name) - # ) - # # shared_saturation_axis = np.linspace(0, 1, 100) - # real_data: List[SaturationRealizationData] = [] - # for _real, real_table in filtered_table.group_by("REAL"): - # satnum_data = [] - # for _satnum, satnum_table in real_table.group_by("SATNUM"): - # sorted_satnum_table = satnum_table.sort(saturation_axis_name) - # # original_saturation = sorted_satnum_table[saturation_axis_name].to_numpy() - - # # Interpolate to get shared axis - # # interpolated_curves = [] - # # for curve_name in curve_names: - # # original_values = sorted_satnum_table[curve_name] - - # # interpolator = interp1d( - # # original_saturation, - # # original_values, - # # kind="cubic", - # # bounds_error=False, - # # fill_value=(original_values[0], original_values[-1]), - # # ) - - # # # Interpolate to shared axis - # # interpolated_values = interpolator(shared_saturation_axis) - # # interpolated_curves.append(interpolated_values.tolist()) - # satnum_data.append( - # RelPermRealizationDataForSaturation( - # saturation_number=sorted_satnum_table["SATNUM"][0], - # relperm_curve_data=[ - # CurveData(curve_values=sorted_satnum_table[curve_name].to_list(), curve_name=curve_name) - # for curve_name in curve_names - # ], - # ) - # ) - # real_data.append( - # SaturationRealizationData( - # saturation_axis_data=CurveData( - # curve_values=sorted_satnum_table[saturation_axis_name].to_list(), - # curve_name=saturation_axis_name, - # ), - # satnum_data=satnum_data, - # realization_id=sorted_satnum_table["REAL"][0], - # ) - # ) - # return real_data - async def get_relperm_realization_data( self, relperm_table_name: str, saturation_axis_name: str, curve_names: List[str], satnums: List[int] ) -> RelPermRealizationDataForSaturation: @@ -203,7 +132,7 @@ async def get_relperm_realization_data( .drop_nulls() .sort(saturation_axis_name) ) - shared_saturation_axis = np.linspace(0, 1, 100) + shared_saturation_axis = np.linspace(0, 1, 50) interpolated_realizations_table = interpolate_realizations_satnum_table_on_shared_saturation_axis( filtered_table, shared_saturation_axis, saturation_axis_name, curve_names ) @@ -218,8 +147,7 @@ async def get_relperm_realization_data( real_data.append( RealizationCurveData(curve_name=curve_name, curve_values=curve_values, realization_id=realization) ) - test = await self.get_relperm_statistics_data(relperm_table_name, saturation_axis_name, curve_names, satnums) - print(test) + return RelPermRealizationDataForSaturation( saturation_axis_data=CurveData( curve_values=shared_saturation_axis.tolist(), @@ -256,7 +184,7 @@ async def get_relperm_statistics_data( .drop_nulls() .sort(saturation_axis_name) ) - shared_saturation_axis = np.linspace(0, 1, 100) + shared_saturation_axis = np.linspace(0, 1, 50) interpolated_realizations_table = interpolate_realizations_satnum_table_on_shared_saturation_axis( filtered_table, shared_saturation_axis, saturation_axis_name, curve_names ) @@ -452,7 +380,7 @@ def extract_saturation_axes_from_relperm_table( def interpolate_realizations_satnum_table_on_shared_saturation_axis( satnum_table: pl.DataFrame, shared_saturation_axis: np.ndarray, saturation_axis_name: str, curve_names: List[str] ) -> pl.DataFrame: - shared_saturation_axis = np.linspace(0, 1, 100) + shared_saturation_axis = np.linspace(0, 1, 50) interpolated_tables = [] for _real, real_table in satnum_table.group_by("REAL"): realization = real_table["REAL"][0]