diff --git a/backend_py/primary/primary/main.py b/backend_py/primary/primary/main.py index 441fd3e8a..af023774d 100644 --- a/backend_py/primary/primary/main.py +++ b/backend_py/primary/primary/main.py @@ -28,6 +28,7 @@ from primary.routers.seismic.router import router as seismic_router from primary.routers.surface.router import router as surface_router from primary.routers.timeseries.router import router as timeseries_router +from primary.routers.vfp.router import router as vfp_router from primary.routers.well.router import router as well_router from primary.routers.well_completions.router import router as well_completions_router from primary.utils.azure_monitor_setup import setup_azure_monitor_telemetry @@ -87,6 +88,7 @@ def custom_generate_unique_id(route: APIRoute) -> str: app.include_router(graph_router, prefix="/graph", tags=["graph"]) app.include_router(observations_router, prefix="/observations", tags=["observations"]) app.include_router(rft_router, prefix="/rft", tags=["rft"]) +app.include_router(vfp_router, prefix="/vfp", tags=["vfp"]) app.include_router(dev_router, prefix="/dev", tags=["dev"], include_in_schema=False) auth_helper = AuthHelper() diff --git a/backend_py/primary/primary/routers/surface/router.py b/backend_py/primary/primary/routers/surface/router.py index 44d485578..02d74bab0 100644 --- a/backend_py/primary/primary/routers/surface/router.py +++ b/backend_py/primary/primary/routers/surface/router.py @@ -152,9 +152,6 @@ async def get_surface_data( raise HTTPException(status_code=404, detail="Could not get realization surface") elif addr.address_type == "STAT": - if addr.stat_realizations is not None: - raise HTTPException(status_code=501, detail="Statistics with specific realizations not yet supported") - service_stat_func_to_compute = StatisticFunction.from_string_value(addr.stat_function) if service_stat_func_to_compute is None: raise HTTPException(status_code=404, detail="Invalid statistic requested") @@ -164,6 +161,7 @@ async def get_surface_data( statistic_function=service_stat_func_to_compute, name=addr.name, attribute=addr.attribute, + realizations=addr.stat_realizations, time_or_interval_str=addr.iso_time_or_interval, ) perf_metrics.record_lap("sumo-calc") diff --git a/backend_py/primary/primary/routers/vfp/router.py b/backend_py/primary/primary/routers/vfp/router.py new file mode 100644 index 000000000..da4257e90 --- /dev/null +++ b/backend_py/primary/primary/routers/vfp/router.py @@ -0,0 +1,69 @@ +import logging +from typing import List + +from fastapi import APIRouter, Depends, Query, Response, HTTPException + +from primary.auth.auth_helper import AuthHelper +from primary.utils.response_perf_metrics import ResponsePerfMetrics +from primary.services.sumo_access.vfp_access import VfpAccess +from primary.services.sumo_access.vfp_types import VfpProdTable +from primary.services.utils.authenticated_user import AuthenticatedUser + +from . import schemas + +LOGGER = logging.getLogger(__name__) + +router = APIRouter() + + +@router.get("/vfp_table_names/") +async def get_vfp_table_names( + # fmt:off + response: Response, + authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), + case_uuid: str = Query(description="Sumo case uuid"), + ensemble_name: str = Query(description="Ensemble name"), + realization: int = Query(description="Realization"), + # fmt:on +) -> List[str]: + perf_metrics = ResponsePerfMetrics(response) + + vfp_access = await VfpAccess.from_case_uuid_async( + authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name + ) + perf_metrics.record_lap("get-access") + vfp_table_names = await vfp_access.get_all_vfp_table_names_for_realization(realization=realization) + perf_metrics.record_lap("get-available-vfp-table-names") + LOGGER.info(f"All Vfp table names loaded in: {perf_metrics.to_string()}") + + return vfp_table_names + + +@router.get("/vfp_table/") +async def get_vfp_table( + # fmt:off + response: Response, + authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), + case_uuid: str = Query(description="Sumo case uuid"), + ensemble_name: str = Query(description="Ensemble name"), + realization: int = Query(description="Realization"), + vfp_table_name: str = Query(description="VFP table name") + # fmt:on +) -> VfpProdTable: + perf_metrics = ResponsePerfMetrics(response) + + vfp_access = await VfpAccess.from_case_uuid_async( + authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name + ) + perf_metrics.record_lap("get-access") + try: + vfp_table: VfpProdTable = await vfp_access.get_vfpprod_table_from_tagname( + tagname=vfp_table_name, realization=realization + ) + except NotImplementedError as ex: + raise HTTPException(status_code=404, detail=ex) + + perf_metrics.record_lap("get-vfp-table") + LOGGER.info(f"VFP table loaded in: {perf_metrics.to_string()}") + + return vfp_table diff --git a/backend_py/primary/primary/routers/vfp/schemas.py b/backend_py/primary/primary/routers/vfp/schemas.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend_py/primary/primary/services/sumo_access/surface_access.py b/backend_py/primary/primary/services/sumo_access/surface_access.py index 4c75df55e..f781aaffe 100644 --- a/backend_py/primary/primary/services/sumo_access/surface_access.py +++ b/backend_py/primary/primary/services/sumo_access/surface_access.py @@ -1,7 +1,7 @@ import asyncio import logging from io import BytesIO -from typing import Optional +from typing import Sequence import xtgeo @@ -118,10 +118,11 @@ async def get_observed_surfaces_metadata_async(self) -> SurfaceMetaSet: return surf_meta_set async def get_realization_surface_data_async( - self, real_num: int, name: str, attribute: str, time_or_interval_str: Optional[str] = None - ) -> Optional[xtgeo.RegularSurface]: + self, real_num: int, name: str, attribute: str, time_or_interval_str: str | None = None + ) -> xtgeo.RegularSurface | None: """ Get surface data for a realization surface + If time_or_interval_str is None, only surfaces with no time information will be considered. """ if not self._iteration_name: raise InvalidParameterError("Iteration name must be set to get realization surface", Service.SUMO) @@ -148,7 +149,7 @@ async def get_realization_surface_data_async( f"Multiple ({surf_count}) surfaces found in Sumo for: {surf_str}", Service.SUMO ) if surf_count == 0: - LOGGER.warning(f"No realization surface found in Sumo for {surf_str}") + LOGGER.warning(f"No realization surface found in Sumo for: {surf_str}") return None sumo_surf: Surface = await surface_collection.getitem_async(0) @@ -170,7 +171,7 @@ async def get_realization_surface_data_async( async def get_observed_surface_data_async( self, name: str, attribute: str, time_or_interval_str: str - ) -> Optional[xtgeo.RegularSurface]: + ) -> xtgeo.RegularSurface | None: """ Get surface data for an observed surface """ @@ -218,17 +219,25 @@ async def get_statistical_surface_data_async( statistic_function: StatisticFunction, name: str, attribute: str, - time_or_interval_str: Optional[str] = None, - ) -> Optional[xtgeo.RegularSurface]: + realizations: Sequence[int] | None = None, + time_or_interval_str: str | None = None, + ) -> xtgeo.RegularSurface | None: """ Compute statistic and return surface data + If realizations is None this is interpreted as a wildcard and surfaces from all realizations will be included + in the statistics. The list of realizations cannon be empty. + If time_or_interval_str is None, only surfaces with no time information will be considered. """ if not self._iteration_name: raise InvalidParameterError("Iteration name must be set to get realization surfaces", Service.SUMO) + if realizations is not None: + if len(realizations) == 0: + raise InvalidParameterError("List of realizations cannot be empty", Service.SUMO) + perf_metrics = PerfMetrics() - surf_str = self._make_real_surf_log_str(-1, name, attribute, time_or_interval_str) + surf_str = self._make_stat_surf_log_str(name, attribute, time_or_interval_str) time_filter = _time_or_interval_str_to_time_filter(time_or_interval_str) @@ -238,28 +247,38 @@ async def get_statistical_surface_data_async( iteration=self._iteration_name, name=name, tagname=attribute, + realization=realizations, time=time_filter, ) surf_count = await surface_collection.length_async() if surf_count == 0: - LOGGER.warning(f"No statistical source surfaces found in Sumo for {surf_str}") + LOGGER.warning(f"No statistical source surfaces found in Sumo for: {surf_str}") return None perf_metrics.record_lap("locate") - realizations = await surface_collection.realizations_async + realizations_found = await surface_collection.realizations_async perf_metrics.record_lap("collect-reals") + # Ensure that we got data for all the requested realizations + if realizations is not None: + missing_reals = list(set(realizations) - set(realizations_found)) + if len(missing_reals) > 0: + raise InvalidParameterError( + f"Could not find source surfaces for realizations: {missing_reals} in Sumo for {surf_str}", + Service.SUMO, + ) + xtgeo_surf = await _compute_statistical_surface_async(statistic_function, surface_collection) perf_metrics.record_lap("calc-stat") if not xtgeo_surf: - LOGGER.warning(f"Could not calculate statistical surface using Sumo for {surf_str}") + LOGGER.warning(f"Could not calculate statistical surface using Sumo for: {surf_str}") return None LOGGER.debug( f"Calculated statistical surface using Sumo in: {perf_metrics.to_string()} " - f"({surf_str} {len(realizations)=})" + f"[{xtgeo_surf.ncol}x{xtgeo_surf.nrow}, real count: {len(realizations_found)}] ({surf_str})" ) return xtgeo_surf @@ -272,6 +291,10 @@ def _make_obs_surf_log_str(self, name: str, attribute: str, date_str: str) -> st addr_str = f"N={name}, A={attribute}, D={date_str}, C={self._case_uuid}" return addr_str + def _make_stat_surf_log_str(self, name: str, attribute: str, date_str: str | None) -> str: + addr_str = f"N={name}, A={attribute}, D={date_str}, C={self._case_uuid}, I={self._iteration_name}" + return addr_str + def _build_surface_meta_arr( src_surf_info_arr: list[SurfInfo], time_type: SurfTimeType, are_observations: bool diff --git a/backend_py/primary/primary/services/sumo_access/vfp_access.py b/backend_py/primary/primary/services/sumo_access/vfp_access.py new file mode 100644 index 000000000..d241c2533 --- /dev/null +++ b/backend_py/primary/primary/services/sumo_access/vfp_access.py @@ -0,0 +1,122 @@ +import logging +from io import BytesIO +from typing import Any, Dict, List + +import numpy as np +import pyarrow as pa +import pyarrow.parquet as pq +from fmu.sumo.explorer.objects import Case +from primary.services.service_exceptions import MultipleDataMatchesError, NoDataError, Service + +from ._helpers import create_sumo_case_async, create_sumo_client +from .vfp_types import ( + ALQ, + GFR, + WFR, + FlowRateTypeProd, + TabType, + UnitType, + VfpProdTable, + VfpType, + VfpParam, + VFPPROD_UNITS, + THP, +) + +LOGGER = logging.getLogger(__name__) + + +class VfpAccess: + """ + Class for accessing and retrieving Vfp tables + """ + + def __init__(self, case: Case, iteration_name: str): + self._case = case + self._iteration_name = iteration_name + + @classmethod + async def from_case_uuid_async(cls, access_token: str, case_uuid: str, iteration_name: str) -> "VfpAccess": + sumo_client = create_sumo_client(access_token) + case: Case = await create_sumo_case_async(sumo_client, case_uuid, want_keepalive_pit=False) + return VfpAccess(case, iteration_name) + + async def get_all_vfp_table_names_for_realization(self, realization: int) -> List[str]: + """Returns all VFP table names/tagnames for a realization.""" + table_collection = self._case.tables.filter( + content="lift_curves", realization=realization, iteration=self._iteration_name + ) + table_count = await table_collection.length_async() + if table_count == 0: + raise NoDataError(f"No VFP tables found for realization: {realization}", Service.SUMO) + return table_collection.tagnames + + async def get_vfp_table_from_tagname_as_pyarrow(self, tagname: str, realization: int) -> pa.Table: + """Returns a VFP table as a pyarrow table for a specific tagname (table name) + and realization. + """ + + table_collection = self._case.tables.filter( + tagname=tagname, realization=realization, iteration=self._iteration_name + ) + + table_count = await table_collection.length_async() + if table_count == 0: + raise NoDataError( + f"No VFP table found with tagname: {tagname} and realization: {realization}", Service.SUMO + ) + if table_count > 1: + raise MultipleDataMatchesError( + f"Multiple VFP tables found with tagname: {tagname} and realization: {realization}", Service.SUMO + ) + + sumo_table = await table_collection.getitem_async(0) + byte_stream: BytesIO = await sumo_table.blob_async + pa_table: pa.Table = pq.read_table(byte_stream) + + return pa_table + + async def get_vfpprod_table_from_tagname(self, tagname: str, realization: int) -> VfpProdTable: + """Returns a VFP table as a VFP table object for a specific tagname (table name) + and realization. + """ + if tagname.lower().startswith("vfpinj"): + raise NotImplementedError("VFPINJ not implemented.") + + pa_table = await self.get_vfp_table_from_tagname_as_pyarrow(tagname, realization) + + alq_type = ALQ.UNDEFINED + if pa_table.schema.metadata[b"ALQ_TYPE"].decode("utf-8") != "''": + alq_type = ALQ[pa_table.schema.metadata[b"ALQ_TYPE"].decode("utf-8")] + + unit_type = UnitType[pa_table.schema.metadata[b"UNIT_TYPE"].decode("utf-8")] + thp_type = THP[pa_table.schema.metadata[b"THP_TYPE"].decode("utf-8")] + wfr_type = WFR[pa_table.schema.metadata[b"WFR_TYPE"].decode("utf-8")] + gfr_type = GFR[pa_table.schema.metadata[b"GFR_TYPE"].decode("utf-8")] + flow_rate_type = FlowRateTypeProd[pa_table.schema.metadata[b"RATE_TYPE"].decode("utf-8")] + units: Dict[VfpParam, Any] = VFPPROD_UNITS[unit_type] + + return VfpProdTable( + vfp_type=VfpType[pa_table.schema.metadata[b"VFP_TYPE"].decode("utf-8")], + table_number=int(pa_table.schema.metadata[b"TABLE_NUMBER"].decode("utf-8")), + datum=float(pa_table.schema.metadata[b"DATUM"].decode("utf-8")), + thp_type=thp_type, + wfr_type=wfr_type, + gfr_type=gfr_type, + alq_type=alq_type, + flow_rate_type=flow_rate_type, + unit_type=unit_type, + tab_type=TabType[pa_table.schema.metadata[b"TAB_TYPE"].decode("utf-8")], + thp_values=np.frombuffer(pa_table.schema.metadata[b"THP_VALUES"], dtype=np.float64).tolist(), + wfr_values=np.frombuffer(pa_table.schema.metadata[b"WFR_VALUES"], dtype=np.float64).tolist(), + gfr_values=np.frombuffer(pa_table.schema.metadata[b"GFR_VALUES"], dtype=np.float64).tolist(), + alq_values=np.frombuffer(pa_table.schema.metadata[b"ALQ_VALUES"], dtype=np.float64).tolist(), + flow_rate_values=np.frombuffer(pa_table.schema.metadata[b"FLOW_VALUES"], dtype=np.float64).tolist(), + bhp_values=[val for sublist in np.array(pa_table.columns).tolist() for val in sublist], + flow_rate_unit=units[VfpParam.FLOWRATE][flow_rate_type], + thp_unit=units[VfpParam.THP][thp_type], + wfr_unit=units[VfpParam.WFR][wfr_type], + gfr_unit=units[VfpParam.GFR][gfr_type], + alq_unit=units[VfpParam.ALQ][alq_type], + bhp_unit=units[VfpParam.THP][thp_type], + ) diff --git a/backend_py/primary/primary/services/sumo_access/vfp_types.py b/backend_py/primary/primary/services/sumo_access/vfp_types.py new file mode 100644 index 000000000..e4eb2ee91 --- /dev/null +++ b/backend_py/primary/primary/services/sumo_access/vfp_types.py @@ -0,0 +1,348 @@ +from enum import Enum +from typing import Any, Dict, List + +from pydantic import BaseModel + + +# Type of VFP curve +class VfpType(Enum): + VFPPROD = "VFPPROD" + VFPINJ = "VFPINJ" + + +class VfpParam(Enum): + FLOWRATE = "FLOWRATE" + THP = "THP" + WFR = "WFR" + GFR = "GFR" + ALQ = "ALQ" + + +# Flow rate types +class FlowRateTypeProd(Enum): + OIL = "OIL" + LIQ = "LIQ" + GAS = "GAS" + WG = "WG" + TM = "TM" + + +# Flow rate types for injection curves +class FlowRateTypeInj(Enum): + OIL = "OIL" + WAT = "WAT" + GAS = "GAS" + WG = "WG" + TM = "TM" + + +# THP type +class THP(Enum): + THP = "THP" + + +# Water fraction types +class WFR(Enum): + WOR = "WOR" + WCT = "WCT" + WGR = "WGR" + WWR = "WWR" + WTF = "WTF" + + +# Gas fraction types +class GFR(Enum): + GOR = "GOR" + GLR = "GLR" + OGR = "OGR" + MMW = "MMW" + + +# Artificial lift types +class ALQ(Enum): + GRAT = "GRAT" + IGLR = "IGLR" + TGLR = "TGLR" + PUMP = "PUMP" + COMP = "COMP" + DENO = "DENO" + DENG = "DENG" + BEAN = "BEAN" + UNDEFINED = "''" + + +# Unit types +class UnitType(Enum): + METRIC = "METRIC" + FIELD = "FIELD" + LAB = "LAB" + PVTM = "PVT-M" + DEFAULT = "DEFAULT" + + +# Tabulated values type +class TabType(Enum): + BHP = "BHP" + THT = "TEMP" + + +# The length of bhp_values is len(thp_values)*len(wfr_values)*len(gfr_values)*len(alq_values)*len(flow_rate_values) +# The values are ordered so that the index of flow_rate_values moves fastest, and the index of thp_values moves +# slowest. The order is: THP, WFR, GFR, ALQ, Flow rates. +class VfpProdTable(BaseModel): + vfp_type: VfpType + table_number: int + datum: float + thp_type: THP + wfr_type: WFR + gfr_type: GFR + alq_type: ALQ + flow_rate_type: FlowRateTypeProd + unit_type: UnitType + tab_type: TabType + thp_values: List[float] + wfr_values: List[float] + gfr_values: List[float] + alq_values: List[float] + flow_rate_values: List[float] + bhp_values: List[float] + flow_rate_unit: str + thp_unit: str + wfr_unit: str + gfr_unit: str + alq_unit: str + bhp_unit: str + + +class VfpInjTable(BaseModel): + vfp_type: VfpType + table_number: int + datum: float + flow_rate_type: FlowRateTypeInj + unit_type: UnitType + tab_type: TabType + thp_values: List[float] + flow_rate_values: List[float] + bhp_values: List[float] + flow_rate_unit: str + thp_unit: str + bhp_unit: str + + +# Unit definitions for VFPPROD +VFPPROD_UNITS: Dict[UnitType, Dict[VfpParam, Any]] = { + UnitType.DEFAULT: { + VfpParam.FLOWRATE: { + FlowRateTypeProd.OIL: "", + FlowRateTypeProd.LIQ: "", + FlowRateTypeProd.GAS: "", + FlowRateTypeProd.WG: "", + FlowRateTypeProd.TM: "", + }, + VfpParam.THP: {THP.THP: "barsa"}, + VfpParam.WFR: { + WFR.WOR: "", + WFR.WCT: "", + WFR.WGR: "", + WFR.WWR: "", + WFR.WTF: "", + }, + VfpParam.GFR: { + GFR.GOR: "", + GFR.GLR: "", + GFR.OGR: "", + GFR.MMW: "", + }, + VfpParam.ALQ: { + ALQ.GRAT: "", + ALQ.IGLR: "", + ALQ.TGLR: "", + ALQ.DENO: "", + ALQ.DENG: "", + ALQ.BEAN: "", + ALQ.UNDEFINED: "", + }, + }, + UnitType.METRIC: { + VfpParam.FLOWRATE: { + FlowRateTypeProd.OIL: "sm³/day", + FlowRateTypeProd.LIQ: "sm³/day", + FlowRateTypeProd.GAS: "sm³/day", + FlowRateTypeProd.WG: "sm³/day", + FlowRateTypeProd.TM: "kg-M/day", + }, + VfpParam.THP: {THP.THP: "barsa"}, + VfpParam.WFR: { + WFR.WOR: "sm³/sm³", + WFR.WCT: "sm³/sm³", + WFR.WGR: "sm³/sm³", + WFR.WWR: "sm³/sm³", + WFR.WTF: "", + }, + VfpParam.GFR: { + GFR.GOR: "sm³/sm³", + GFR.GLR: "sm³/sm³", + GFR.OGR: "sm³/sm³", + GFR.MMW: "kg/kg-M", + }, + VfpParam.ALQ: { + ALQ.GRAT: "sm³/day", + ALQ.IGLR: "sm³/sm³", + ALQ.TGLR: "sm³/sm³", + ALQ.DENO: "kg/m3", + ALQ.DENG: "kg/m3", + ALQ.BEAN: "mm", + ALQ.UNDEFINED: "", + }, + }, + UnitType.FIELD: { + VfpParam.FLOWRATE: { + FlowRateTypeProd.OIL: "stb/day", + FlowRateTypeProd.LIQ: "stb/day", + FlowRateTypeProd.GAS: "Mscf/day", + FlowRateTypeProd.WG: "lb-M/day", + FlowRateTypeProd.TM: "lb-M/day", + }, + VfpParam.THP: {THP.THP: "psia"}, + VfpParam.WFR: { + WFR.WOR: "stb/stb", + WFR.WCT: "stb/stb", + WFR.WGR: "stb/Mscf", + WFR.WWR: "stb/Mscf", + WFR.WTF: "", + }, + VfpParam.GFR: { + GFR.GOR: "Mscf/stb", + GFR.GLR: "Mscf/stb", + GFR.OGR: "stb/Mscf", + GFR.MMW: "lb/lb-M", + }, + VfpParam.ALQ: { + ALQ.GRAT: "Mscf/day", + ALQ.IGLR: "Mscf/stb", + ALQ.TGLR: "Mscf/stb", + ALQ.DENO: "lb/ft3", + ALQ.DENG: "lb/ft3", + ALQ.BEAN: "1/64", + ALQ.UNDEFINED: "", + }, + }, + UnitType.LAB: { + VfpParam.FLOWRATE: { + FlowRateTypeProd.OIL: "scc/hr", + FlowRateTypeProd.LIQ: "scc/hr", + FlowRateTypeProd.GAS: "scc/hr", + FlowRateTypeProd.WG: "scc/hr", + FlowRateTypeProd.TM: "lb-M/day", + }, + VfpParam.THP: {THP.THP: "atma"}, + VfpParam.WFR: { + WFR.WOR: "scc/scc", + WFR.WCT: "scc/scc", + WFR.WGR: "scc/scc", + WFR.WWR: "scc/scc", + WFR.WTF: "", + }, + VfpParam.GFR: { + GFR.GOR: "scc/scc", + GFR.GLR: "scc/scc", + GFR.OGR: "scc/scc", + GFR.MMW: "lb/lb-M", + }, + VfpParam.ALQ: { + ALQ.GRAT: "scc/hr", + ALQ.IGLR: "scc/scc", + ALQ.TGLR: "scc/scc", + ALQ.DENO: "gm/cc", + ALQ.DENG: "gm/cc", + ALQ.BEAN: "mm", + ALQ.UNDEFINED: "", + }, + }, + UnitType.PVTM: { + VfpParam.FLOWRATE: { + FlowRateTypeProd.OIL: "sm³/day", + FlowRateTypeProd.LIQ: "sm³/day", + FlowRateTypeProd.GAS: "sm³/day", + FlowRateTypeProd.WG: "sm³/day", + FlowRateTypeProd.TM: "kg-M/day", + }, + VfpParam.THP: {THP.THP: "atma"}, + VfpParam.WFR: { + WFR.WOR: "sm³/sm³", + WFR.WCT: "sm³/sm³", + WFR.WGR: "sm³/sm³", + WFR.WWR: "sm³/sm³", + WFR.WTF: "", + }, + VfpParam.GFR: { + GFR.GOR: "sm³/sm³", + GFR.GLR: "sm³/sm³", + GFR.OGR: "sm³/sm³", + GFR.MMW: "kg/kg-M", + }, + VfpParam.ALQ: { + ALQ.GRAT: "sm³/day", + ALQ.IGLR: "sm³/sm³", + ALQ.TGLR: "sm³/sm³", + ALQ.DENO: "kg/m3", + ALQ.DENG: "kg/m3", + ALQ.BEAN: "mm", + ALQ.UNDEFINED: "", + }, + }, +} + +# # Unit definitions for VFPINJ +# VFPINJ_UNITS = { +# "DEFAULT": { +# "FLO": { +# "OIL": "", +# "WAT": "", +# "GAS": "", +# "WG": "", +# "TM": "", +# }, +# "THP": {"THP": ""}, +# }, +# "METRIC": { +# "FLO": { +# "OIL": "sm³/day", +# "WAT": "sm³/day", +# "GAS": "sm³/day", +# "WG": "sm³/day", +# "TM": "kg-M/day", +# }, +# "THP": {"THP": "barsa"}, +# }, +# "FIELD": { +# "FLO": { +# "OIL": "stb/day", +# "WAT": "stb/day", +# "GAS": "Mscf/day", +# "WG": "Mscf/day", +# "TM": "lb-M/day", +# }, +# "THP": {"THP": "psia"}, +# }, +# "LAB": { +# "FLO": { +# "OIL": "scc/hr", +# "WAT": "scc/hr", +# "GAS": "scc/hr", +# "WG": "scc/hr", +# "TM": "gm-M/hr", +# }, +# "THP": {"THP": "atma"}, +# }, +# "PVT-M": { +# "FLO": { +# "OIL": "sm³/day", +# "WAT": "sm³/day", +# "GAS": "sm³/day", +# "WG": "sm³/day", +# "TM": "kg-M/day", +# }, +# "THP": {"THP": "atma"}, +# }, +# } diff --git a/frontend/src/api/ApiService.ts b/frontend/src/api/ApiService.ts index 94a645d4c..bfce0c929 100644 --- a/frontend/src/api/ApiService.ts +++ b/frontend/src/api/ApiService.ts @@ -19,6 +19,7 @@ import { RftService } from './services/RftService'; import { SeismicService } from './services/SeismicService'; import { SurfaceService } from './services/SurfaceService'; import { TimeseriesService } from './services/TimeseriesService'; +import { VfpService } from './services/VfpService'; import { WellService } from './services/WellService'; import { WellCompletionsService } from './services/WellCompletionsService'; type HttpRequestConstructor = new (config: OpenAPIConfig) => BaseHttpRequest; @@ -37,6 +38,7 @@ export class ApiService { public readonly seismic: SeismicService; public readonly surface: SurfaceService; public readonly timeseries: TimeseriesService; + public readonly vfp: VfpService; public readonly well: WellService; public readonly wellCompletions: WellCompletionsService; public readonly request: BaseHttpRequest; @@ -66,6 +68,7 @@ export class ApiService { this.seismic = new SeismicService(this.request); this.surface = new SurfaceService(this.request); this.timeseries = new TimeseriesService(this.request); + this.vfp = new VfpService(this.request); this.well = new WellService(this.request); this.wellCompletions = new WellCompletionsService(this.request); } diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index af145a030..9881aadac 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -10,6 +10,7 @@ export { CancelablePromise, CancelError } from './core/CancelablePromise'; export { OpenAPI } from './core/OpenAPI'; export type { OpenAPIConfig } from './core/OpenAPI'; +export { ALQ as ALQ_api } from './models/ALQ'; export { B64FloatArray as B64FloatArray_api } from './models/B64FloatArray'; export { B64UintArray as B64UintArray_api } from './models/B64UintArray'; export type { Body_get_realizations_response as Body_get_realizations_response_api } from './models/Body_get_realizations_response'; @@ -31,7 +32,9 @@ export type { EnsembleSensitivity as EnsembleSensitivity_api } from './models/En export type { EnsembleSensitivityCase as EnsembleSensitivityCase_api } from './models/EnsembleSensitivityCase'; export type { FenceMeshSection as FenceMeshSection_api } from './models/FenceMeshSection'; export type { FieldInfo as FieldInfo_api } from './models/FieldInfo'; +export { FlowRateTypeProd as FlowRateTypeProd_api } from './models/FlowRateTypeProd'; export { Frequency as Frequency_api } from './models/Frequency'; +export { GFR as GFR_api } from './models/GFR'; export type { GraphUserPhoto as GraphUserPhoto_api } from './models/GraphUserPhoto'; export type { Grid3dDimensions as Grid3dDimensions_api } from './models/Grid3dDimensions'; export type { Grid3dGeometry as Grid3dGeometry_api } from './models/Grid3dGeometry'; @@ -77,7 +80,10 @@ export type { SurfaceMetaSet as SurfaceMetaSet_api } from './models/SurfaceMetaS export type { SurfaceRealizationSampleValues as SurfaceRealizationSampleValues_api } from './models/SurfaceRealizationSampleValues'; export { SurfaceStatisticFunction as SurfaceStatisticFunction_api } from './models/SurfaceStatisticFunction'; export { SurfaceTimeType as SurfaceTimeType_api } from './models/SurfaceTimeType'; +export { TabType as TabType_api } from './models/TabType'; +export type { THP as THP_api } from './models/THP'; export { TreeNode as TreeNode_api } from './models/TreeNode'; +export { UnitType as UnitType_api } from './models/UnitType'; export type { UserInfo as UserInfo_api } from './models/UserInfo'; export type { ValidationError as ValidationError_api } from './models/ValidationError'; export type { VectorDescription as VectorDescription_api } from './models/VectorDescription'; @@ -85,6 +91,8 @@ export type { VectorHistoricalData as VectorHistoricalData_api } from './models/ export type { VectorRealizationData as VectorRealizationData_api } from './models/VectorRealizationData'; export type { VectorStatisticData as VectorStatisticData_api } from './models/VectorStatisticData'; export type { VectorStatisticSensitivityData as VectorStatisticSensitivityData_api } from './models/VectorStatisticSensitivityData'; +export type { VfpProdTable as VfpProdTable_api } from './models/VfpProdTable'; +export { VfpType as VfpType_api } from './models/VfpType'; export type { WellboreCasing as WellboreCasing_api } from './models/WellboreCasing'; export type { WellboreCompletion as WellboreCompletion_api } from './models/WellboreCompletion'; export type { WellboreHeader as WellboreHeader_api } from './models/WellboreHeader'; @@ -99,6 +107,7 @@ export type { WellCompletionsUnitInfo as WellCompletionsUnitInfo_api } from './m export type { WellCompletionsUnits as WellCompletionsUnits_api } from './models/WellCompletionsUnits'; export type { WellCompletionsWell as WellCompletionsWell_api } from './models/WellCompletionsWell'; export type { WellCompletionsZone as WellCompletionsZone_api } from './models/WellCompletionsZone'; +export { WFR as WFR_api } from './models/WFR'; export { DefaultService } from './services/DefaultService'; export { ExploreService } from './services/ExploreService'; @@ -114,5 +123,6 @@ export { RftService } from './services/RftService'; export { SeismicService } from './services/SeismicService'; export { SurfaceService } from './services/SurfaceService'; export { TimeseriesService } from './services/TimeseriesService'; +export { VfpService } from './services/VfpService'; export { WellService } from './services/WellService'; export { WellCompletionsService } from './services/WellCompletionsService'; diff --git a/frontend/src/api/models/ALQ.ts b/frontend/src/api/models/ALQ.ts new file mode 100644 index 000000000..aeee0450d --- /dev/null +++ b/frontend/src/api/models/ALQ.ts @@ -0,0 +1,15 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export enum ALQ { + GRAT = 'GRAT', + IGLR = 'IGLR', + TGLR = 'TGLR', + PUMP = 'PUMP', + COMP = 'COMP', + DENO = 'DENO', + DENG = 'DENG', + BEAN = 'BEAN', + _ = '\'\'', +} diff --git a/frontend/src/api/models/FlowRateTypeProd.ts b/frontend/src/api/models/FlowRateTypeProd.ts new file mode 100644 index 000000000..db0f84c95 --- /dev/null +++ b/frontend/src/api/models/FlowRateTypeProd.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export enum FlowRateTypeProd { + OIL = 'OIL', + LIQ = 'LIQ', + GAS = 'GAS', + WG = 'WG', + TM = 'TM', +} diff --git a/frontend/src/api/models/GFR.ts b/frontend/src/api/models/GFR.ts new file mode 100644 index 000000000..4df9cf0f5 --- /dev/null +++ b/frontend/src/api/models/GFR.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export enum GFR { + GOR = 'GOR', + GLR = 'GLR', + OGR = 'OGR', + MMW = 'MMW', +} diff --git a/frontend/src/api/models/THP.ts b/frontend/src/api/models/THP.ts new file mode 100644 index 000000000..89ce3a47f --- /dev/null +++ b/frontend/src/api/models/THP.ts @@ -0,0 +1,7 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type THP = { +}; + diff --git a/frontend/src/api/models/TabType.ts b/frontend/src/api/models/TabType.ts new file mode 100644 index 000000000..29ce110d2 --- /dev/null +++ b/frontend/src/api/models/TabType.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export enum TabType { + BHP = 'BHP', + TEMP = 'TEMP', +} diff --git a/frontend/src/api/models/UnitType.ts b/frontend/src/api/models/UnitType.ts new file mode 100644 index 000000000..2ddad9125 --- /dev/null +++ b/frontend/src/api/models/UnitType.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export enum UnitType { + METRIC = 'METRIC', + FIELD = 'FIELD', + LAB = 'LAB', + PVT_M = 'PVT-M', + DEFAULT = 'DEFAULT', +} diff --git a/frontend/src/api/models/VfpProdTable.ts b/frontend/src/api/models/VfpProdTable.ts new file mode 100644 index 000000000..349bde27c --- /dev/null +++ b/frontend/src/api/models/VfpProdTable.ts @@ -0,0 +1,37 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ALQ } from './ALQ'; +import type { FlowRateTypeProd } from './FlowRateTypeProd'; +import type { GFR } from './GFR'; +import type { TabType } from './TabType'; +import type { THP } from './THP'; +import type { UnitType } from './UnitType'; +import type { VfpType } from './VfpType'; +import type { WFR } from './WFR'; +export type VfpProdTable = { + vfp_type: VfpType; + table_number: number; + datum: number; + thp_type: THP; + wfr_type: WFR; + gfr_type: GFR; + alq_type: ALQ; + flow_rate_type: FlowRateTypeProd; + unit_type: UnitType; + tab_type: TabType; + thp_values: Array; + wfr_values: Array; + gfr_values: Array; + alq_values: Array; + flow_rate_values: Array; + bhp_values: Array; + flow_rate_unit: string; + thp_unit: string; + wfr_unit: string; + gfr_unit: string; + alq_unit: string; + bhp_unit: string; +}; + diff --git a/frontend/src/api/models/VfpType.ts b/frontend/src/api/models/VfpType.ts new file mode 100644 index 000000000..bd79981f7 --- /dev/null +++ b/frontend/src/api/models/VfpType.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export enum VfpType { + VFPPROD = 'VFPPROD', + VFPINJ = 'VFPINJ', +} diff --git a/frontend/src/api/models/WFR.ts b/frontend/src/api/models/WFR.ts new file mode 100644 index 000000000..af2153c70 --- /dev/null +++ b/frontend/src/api/models/WFR.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export enum WFR { + WOR = 'WOR', + WCT = 'WCT', + WGR = 'WGR', + WWR = 'WWR', + WTF = 'WTF', +} diff --git a/frontend/src/api/services/VfpService.ts b/frontend/src/api/services/VfpService.ts new file mode 100644 index 000000000..7bcb86b8e --- /dev/null +++ b/frontend/src/api/services/VfpService.ts @@ -0,0 +1,65 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { VfpProdTable } from '../models/VfpProdTable'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import type { BaseHttpRequest } from '../core/BaseHttpRequest'; +export class VfpService { + constructor(public readonly httpRequest: BaseHttpRequest) {} + /** + * Get Vfp Table Names + * @param caseUuid Sumo case uuid + * @param ensembleName Ensemble name + * @param realization Realization + * @returns string Successful Response + * @throws ApiError + */ + public getVfpTableNames( + caseUuid: string, + ensembleName: string, + realization: number, + ): CancelablePromise> { + return this.httpRequest.request({ + method: 'GET', + url: '/vfp/vfp_table_names/', + query: { + 'case_uuid': caseUuid, + 'ensemble_name': ensembleName, + 'realization': realization, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Get Vfp Table + * @param caseUuid Sumo case uuid + * @param ensembleName Ensemble name + * @param realization Realization + * @param vfpTableName VFP table name + * @returns VfpProdTable Successful Response + * @throws ApiError + */ + public getVfpTable( + caseUuid: string, + ensembleName: string, + realization: number, + vfpTableName: string, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'GET', + url: '/vfp/vfp_table/', + query: { + 'case_uuid': caseUuid, + 'ensemble_name': ensembleName, + 'realization': realization, + 'vfp_table_name': vfpTableName, + }, + errors: { + 422: `Validation Error`, + }, + }); + } +} diff --git a/frontend/src/framework/ModuleDataTags.ts b/frontend/src/framework/ModuleDataTags.ts index 0fdbd01aa..b7251cc7c 100644 --- a/frontend/src/framework/ModuleDataTags.ts +++ b/frontend/src/framework/ModuleDataTags.ts @@ -10,6 +10,7 @@ export enum ModuleDataTagId { OBSERVATIONS = "observations", SEISMIC = "seismic", WELL_COMPLETIONS = "well-completions", + VFP = "vfp" } export type ModuleDataTag = { diff --git a/frontend/src/lib/components/Select/select.tsx b/frontend/src/lib/components/Select/select.tsx index 69f9905d9..b74f222de 100644 --- a/frontend/src/lib/components/Select/select.tsx +++ b/frontend/src/lib/components/Select/select.tsx @@ -68,7 +68,7 @@ export const Select = withDefaults()(defaultProps, (props) => { const [filteredOptions, setFilteredOptions] = React.useState(props.options); const [selectionAnchor, setSelectionAnchor] = React.useState(null); const [selectedOptionValues, setSelectedOptionValues] = React.useState([]); - const [prevPropsValue, setPrevPropsValue] = React.useState([]); + const [prevPropsValue, setPrevPropsValue] = React.useState(undefined); const [currentFocusIndex, setCurrentFocusIndex] = React.useState(0); const [virtualizationStartIndex, setVirtualizationStartIndex] = React.useState(0); const [reportedVirtualizationStartIndex, setReportedVirtualizationStartIndex] = React.useState(0); @@ -84,9 +84,10 @@ export const Select = withDefaults()(defaultProps, (props) => { filterOptions(newOptions, filterString); } - if (props.value && !isEqual(props.value, prevPropsValue)) { - setSelectedOptionValues([...props.value]); - setPrevPropsValue([...props.value]); + if (!isEqual(props.value, prevPropsValue)) { + setPrevPropsValue(props.value ? [...props.value] : undefined); + setSelectedOptionValues(props.value ? [...props.value] : []); + setSelectionAnchor(props.value ? filteredOptions.findIndex((option) => option.value === props.value[0]) : null); } const handleOnChange = React.useCallback( @@ -344,6 +345,7 @@ export const Select = withDefaults()(defaultProps, (props) => { setCurrentFocusIndex(newCurrentKeyboardFocusIndex); setVirtualizationStartIndex(newVirtualizationStartIndex); + setSelectionAnchor(newFilteredOptions.findIndex((option) => option.value === selectedOptionValues[0])); } function handleFilterChange(event: React.ChangeEvent) { diff --git a/frontend/src/modules/Vfp/interfaces.ts b/frontend/src/modules/Vfp/interfaces.ts new file mode 100644 index 000000000..cbb44d27b --- /dev/null +++ b/frontend/src/modules/Vfp/interfaces.ts @@ -0,0 +1,53 @@ +import { InterfaceInitialization } from "@framework/UniDirectionalModuleComponentsInterface"; +import { VfpParam,} from "./types"; + +import { + selectedAlqIndicesAtom, + selectedGfrIndicesAtom, + selectedPressureOptionAtom, + selectedThpIndicesAtom, + selectedWfrIndicesAtom, +} from "./settings/atoms/derivedAtoms"; +import { vfpTableQueryAtom } from "./settings/atoms/queryAtoms"; +import { PressureOption } from "./types"; +import { selectedColorByAtom } from "./settings/atoms/derivedAtoms"; +import { UseQueryResult } from "@tanstack/react-query"; +import { VfpProdTable_api } from "@api"; + +type SettingsToViewInterface = { + vfpDataQuery: UseQueryResult; + selectedThpIndices: number[] | null; + selectedWfrIndices: number[] | null; + selectedGfrIndices: number[] | null; + selectedAlqIndices: number[] | null; + selectedPressureOption: PressureOption; + selectedColorBy: VfpParam; +}; + +export type Interfaces = { + settingsToView: SettingsToViewInterface; +}; + +export const settingsToViewInterfaceInitialization: InterfaceInitialization = { + vfpDataQuery: (get) => { + return get(vfpTableQueryAtom); + }, + selectedThpIndices: (get) => { + return get(selectedThpIndicesAtom); + }, + selectedWfrIndices: (get) => { + return get(selectedWfrIndicesAtom); + }, + selectedGfrIndices: (get) => { + return get(selectedGfrIndicesAtom); + }, + selectedAlqIndices: (get) => { + return get(selectedAlqIndicesAtom); + }, + selectedPressureOption: (get) => { + return get(selectedPressureOptionAtom); + }, + selectedColorBy: (get) => { + return get(selectedColorByAtom); + }, +}; diff --git a/frontend/src/modules/Vfp/loadModule.tsx b/frontend/src/modules/Vfp/loadModule.tsx new file mode 100644 index 000000000..28c1dbd3c --- /dev/null +++ b/frontend/src/modules/Vfp/loadModule.tsx @@ -0,0 +1,11 @@ +import { ModuleRegistry } from "@framework/ModuleRegistry"; + +import { Interfaces, settingsToViewInterfaceInitialization } from "./interfaces"; +import { MODULE_NAME } from "./registerModule"; +import { Settings } from "./settings/settings"; +import { View } from "./view"; + +const module = ModuleRegistry.initModule(MODULE_NAME, { settingsToViewInterfaceInitialization }); + +module.viewFC = View; +module.settingsFC = Settings; diff --git a/frontend/src/modules/Vfp/preview.svg b/frontend/src/modules/Vfp/preview.svg new file mode 100644 index 000000000..af78f9bc2 --- /dev/null +++ b/frontend/src/modules/Vfp/preview.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/modules/Vfp/preview.tsx b/frontend/src/modules/Vfp/preview.tsx new file mode 100644 index 000000000..44948c66b --- /dev/null +++ b/frontend/src/modules/Vfp/preview.tsx @@ -0,0 +1,6 @@ +import { DrawPreviewFunc } from "@framework/Preview"; +import previewImg from "./preview.svg"; + +export const preview: DrawPreviewFunc = function (width: number, height: number) { + return +}; \ No newline at end of file diff --git a/frontend/src/modules/Vfp/registerModule.tsx b/frontend/src/modules/Vfp/registerModule.tsx new file mode 100644 index 000000000..74b694d82 --- /dev/null +++ b/frontend/src/modules/Vfp/registerModule.tsx @@ -0,0 +1,21 @@ +import { ModuleCategory, ModuleDevState } from "@framework/Module"; +import { ModuleDataTagId } from "@framework/ModuleDataTags"; +import { ModuleRegistry } from "@framework/ModuleRegistry"; + +import { Interfaces } from "./interfaces"; +import { preview } from "./preview"; + +export const MODULE_NAME = "Vfp"; + +const description = + "Visualizes Vfp tables from Eclipse."; + +ModuleRegistry.registerModule({ + moduleName: MODULE_NAME, + defaultTitle: "VFP", + category: ModuleCategory.MAIN, + devState: ModuleDevState.DEV, + dataTagIds: [ModuleDataTagId.VFP], + preview, + description, +}); diff --git a/frontend/src/modules/Vfp/settings/atoms/baseAtoms.ts b/frontend/src/modules/Vfp/settings/atoms/baseAtoms.ts new file mode 100644 index 000000000..143ea37d3 --- /dev/null +++ b/frontend/src/modules/Vfp/settings/atoms/baseAtoms.ts @@ -0,0 +1,32 @@ +import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { atomWithCompare } from "@framework/utils/atomUtils"; +import { PressureOption, VfpParam } from "@modules/Vfp/types"; + +import { atom } from "jotai"; + +function areEnsembleIdentsEqual(a: EnsembleIdent | null, b: EnsembleIdent | null) { + if (a === null) { + return b === null; + } + return a.equals(b); +} + +export const userSelectedRealizationNumberAtom = atom(null); + +export const validRealizationNumbersAtom = atom(null); + +export const userSelectedEnsembleIdentAtom = atomWithCompare(null, areEnsembleIdentsEqual); + +export const userSelectedVfpTableNameAtom = atom(null); + +export const userSelectedThpIndicesAtom = atom(null); + +export const userSelectedWfrIndicesAtom = atom(null); + +export const userSelectedGfrIndicesAtom = atom(null); + +export const userSelectedAlqIndicesAtom = atom(null); + +export const userSelectedPressureOptionAtom = atom(null); + +export const userSelectedColorByAtom = atom(null); diff --git a/frontend/src/modules/Vfp/settings/atoms/derivedAtoms.ts b/frontend/src/modules/Vfp/settings/atoms/derivedAtoms.ts new file mode 100644 index 000000000..3b266a2e8 --- /dev/null +++ b/frontend/src/modules/Vfp/settings/atoms/derivedAtoms.ts @@ -0,0 +1,152 @@ +import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { EnsembleSetAtom } from "@framework/GlobalAtoms"; +import { fixupEnsembleIdent } from "@framework/utils/ensembleUiHelpers"; + +import { atom } from "jotai"; + +import { + userSelectedAlqIndicesAtom, + userSelectedColorByAtom, + userSelectedEnsembleIdentAtom, + userSelectedGfrIndicesAtom, + userSelectedPressureOptionAtom, + userSelectedRealizationNumberAtom, + userSelectedThpIndicesAtom, + userSelectedVfpTableNameAtom, + userSelectedWfrIndicesAtom, + validRealizationNumbersAtom, +} from "./baseAtoms"; +import { vfpTableNamesQueryAtom, vfpTableQueryAtom } from "./queryAtoms"; + +import { PressureOption, VfpParam } from "../../types"; + +export const vfpTableNamesQueryResultAtom = atom((get) => { + return get(vfpTableNamesQueryAtom); +}); + +export const availableVfpTableNamesAtom = atom((get) => { + const vfpTableNamesQueryResult = get(vfpTableNamesQueryAtom); + return vfpTableNamesQueryResult.data?.map((item) => item) ?? []; +}); + +export const selectedEnsembleIdentAtom = atom((get) => { + const ensembleSet = get(EnsembleSetAtom); + const userSelectedEnsembleIdent = get(userSelectedEnsembleIdentAtom); + + const validEnsembleIdent = fixupEnsembleIdent(userSelectedEnsembleIdent, ensembleSet); + return validEnsembleIdent; +}); + +export const selectedRealizationNumberAtom = atom((get) => { + const userSelectedRealizationNumber = get(userSelectedRealizationNumberAtom); + const validRealizationNumbers = get(validRealizationNumbersAtom); + + if (!validRealizationNumbers) { + return null; + } + + if (userSelectedRealizationNumber === null) { + const firstRealization = validRealizationNumbers.length > 0 ? validRealizationNumbers[0] : null; + return firstRealization; + } + + const validRealizationNumber = validRealizationNumbers.includes(userSelectedRealizationNumber) + ? userSelectedRealizationNumber + : null; + return validRealizationNumber; +}); + +export const selectedVfpTableNameAtom = atom((get) => { + const userSelectedVfpTableName = get(userSelectedVfpTableNameAtom); + const validVfpTableNames = get(availableVfpTableNamesAtom); + + if (validVfpTableNames.length === 0) { + return null; + } + + if (userSelectedVfpTableName === null) { + const firstVfpTableName = validVfpTableNames.length > 0 ? validVfpTableNames[0] : null; + return firstVfpTableName; + } + + const validVfpTableName = validVfpTableNames.includes(userSelectedVfpTableName) ? userSelectedVfpTableName : null; + return validVfpTableName; +}); + +export const selectedThpIndicesAtom = atom((get) => { + const vfpTable = get(vfpTableQueryAtom).data; + const thp_values = vfpTable?.thp_values ?? []; + const userSelectedThpIndicies = get(userSelectedThpIndicesAtom); + + if (thp_values.length === 0) { + return null; + } + if (!userSelectedThpIndicies) { + return [0]; + } + + return userSelectedThpIndicies; +}); + +export const selectedWfrIndicesAtom = atom((get) => { + const vfpTable = get(vfpTableQueryAtom).data; + const wfr_values = vfpTable?.wfr_values ?? []; + const userSelectedWfrIndicies = get(userSelectedWfrIndicesAtom); + + if (wfr_values.length === 0) { + return null; + } + if (!userSelectedWfrIndicies) { + return [0]; + } + + return userSelectedWfrIndicies; +}); + +export const selectedGfrIndicesAtom = atom((get) => { + const vfpTable = get(vfpTableQueryAtom).data; + const wfr_values = vfpTable?.gfr_values ?? []; + const userSelectedGfrIndicies = get(userSelectedGfrIndicesAtom); + + if (wfr_values.length === 0) { + return null; + } + if (!userSelectedGfrIndicies) { + return [0]; + } + + return userSelectedGfrIndicies; +}); + +export const selectedAlqIndicesAtom = atom((get) => { + const vfpTable = get(vfpTableQueryAtom).data; + const wfr_values = vfpTable?.alq_values ?? []; + const userSelectedAlqIndicies = get(userSelectedAlqIndicesAtom); + + if (wfr_values.length === 0) { + return null; + } + if (!userSelectedAlqIndicies) { + return [0]; + } + + return userSelectedAlqIndicies; +}); + +export const selectedPressureOptionAtom = atom((get) => { + const userSelectedPressureOption = get(userSelectedPressureOptionAtom); + + if (userSelectedPressureOption === null) { + return PressureOption.BHP; + } + return userSelectedPressureOption; +}); + +export const selectedColorByAtom = atom((get) => { + const userSelectedColorBy = get(userSelectedColorByAtom); + + if (userSelectedColorBy === null) { + return VfpParam.THP + } + return userSelectedColorBy +}); diff --git a/frontend/src/modules/Vfp/settings/atoms/queryAtoms.ts b/frontend/src/modules/Vfp/settings/atoms/queryAtoms.ts new file mode 100644 index 000000000..6fe3fe6da --- /dev/null +++ b/frontend/src/modules/Vfp/settings/atoms/queryAtoms.ts @@ -0,0 +1,68 @@ +import { apiService } from "@framework/ApiService"; + +import { atomWithQuery } from "jotai-tanstack-query"; + +import { selectedEnsembleIdentAtom, selectedRealizationNumberAtom, selectedVfpTableNameAtom } from "./derivedAtoms"; + +const STALE_TIME = 60 * 1000; +const CACHE_TIME = 60 * 1000; + +export const vfpTableQueryAtom = atomWithQuery((get) => { + const selectedEnsembleIdent = get(selectedEnsembleIdentAtom); + const selectedRealizationNumber = get(selectedRealizationNumberAtom); + const selectedVfpTableName = get(selectedVfpTableNameAtom) + + const query = { + queryKey: [ + "getVfpTable", + selectedEnsembleIdent?.getCaseUuid(), + selectedEnsembleIdent?.getEnsembleName(), + selectedRealizationNumber, + selectedVfpTableName, + ], + queryFn: () => + apiService.vfp.getVfpTable( + selectedEnsembleIdent?.getCaseUuid() ?? "", + selectedEnsembleIdent?.getEnsembleName() ?? "", + selectedRealizationNumber ?? 0, + selectedVfpTableName ?? "", + ), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + enabled: !!( + selectedEnsembleIdent?.getCaseUuid() && + selectedEnsembleIdent?.getEnsembleName() && + selectedRealizationNumber !== null && + selectedVfpTableName + ), + }; + return query; +}); + +export const vfpTableNamesQueryAtom = atomWithQuery((get) => { + const selectedEnsembleIdent = get(selectedEnsembleIdentAtom); + const selectedRealizationNumber = get(selectedRealizationNumberAtom); + + const query = { + queryKey: [ + "getVfpTableNames", + selectedEnsembleIdent?.getCaseUuid(), + selectedEnsembleIdent?.getEnsembleName(), + selectedRealizationNumber, + ], + queryFn: () => + apiService.vfp.getVfpTableNames( + selectedEnsembleIdent?.getCaseUuid() ?? "", + selectedEnsembleIdent?.getEnsembleName() ?? "", + selectedRealizationNumber ?? 0, + ), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + enabled: !!( + selectedEnsembleIdent?.getCaseUuid() && + selectedEnsembleIdent?.getEnsembleName() && + selectedRealizationNumber !== null + ), + }; + return query; +}); \ No newline at end of file diff --git a/frontend/src/modules/Vfp/settings/settings.tsx b/frontend/src/modules/Vfp/settings/settings.tsx new file mode 100644 index 000000000..6ef4c4843 --- /dev/null +++ b/frontend/src/modules/Vfp/settings/settings.tsx @@ -0,0 +1,242 @@ +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 { CollapsibleGroup } from "@lib/components/CollapsibleGroup"; +import { Dropdown } from "@lib/components/Dropdown"; +import { Label } from "@lib/components/Label"; +import { RadioGroup } from "@lib/components/RadioGroup"; +import { Select, SelectOption } from "@lib/components/Select"; +import { usePropagateApiErrorToStatusWriter } from "@modules/_shared/hooks/usePropagateApiErrorToStatusWriter"; + +import { useAtomValue, useSetAtom } from "jotai"; + +import { + userSelectedAlqIndicesAtom, + userSelectedColorByAtom, + userSelectedEnsembleIdentAtom, + userSelectedGfrIndicesAtom, + userSelectedPressureOptionAtom, + userSelectedRealizationNumberAtom, + userSelectedThpIndicesAtom, + userSelectedVfpTableNameAtom, + userSelectedWfrIndicesAtom, + validRealizationNumbersAtom, +} from "./atoms/baseAtoms"; +import { + availableVfpTableNamesAtom, + selectedAlqIndicesAtom, + selectedColorByAtom, + selectedEnsembleIdentAtom, + selectedGfrIndicesAtom, + selectedPressureOptionAtom, + selectedRealizationNumberAtom, + selectedThpIndicesAtom, + selectedVfpTableNameAtom, + selectedWfrIndicesAtom, +} from "./atoms/derivedAtoms"; +import { vfpTableQueryAtom } from "./atoms/queryAtoms"; + +import { Interfaces } from "../interfaces"; +import { PressureOption, VfpParam } from "../types"; +import { VfpDataAccessor } from "../utils/VfpDataAccessor"; + +export function Settings({ workbenchSession, settingsContext }: ModuleSettingsProps) { + const statusWriter = useSettingsStatusWriter(settingsContext); + const ensembleSet = useEnsembleSet(workbenchSession); + + const vfpTableQuery = useAtomValue(vfpTableQueryAtom); + + const selectedEnsembleIdent = useAtomValue(selectedEnsembleIdentAtom); + const setUserSelectedEnsembleIdent = useSetAtom(userSelectedEnsembleIdentAtom); + + const selectedRealizationNumber = useAtomValue(selectedRealizationNumberAtom); + const setUserSelectedRealizationNumber = useSetAtom(userSelectedRealizationNumberAtom); + + const selectedVfpTableName = useAtomValue(selectedVfpTableNameAtom); + const setUserSelectedVfpName = useSetAtom(userSelectedVfpTableNameAtom); + + const setValidRealizationNumbersAtom = useSetAtom(validRealizationNumbersAtom); + const filterEnsembleRealizationsFunc = useEnsembleRealizationFilterFunc(workbenchSession); + const validRealizations = selectedEnsembleIdent ? [...filterEnsembleRealizationsFunc(selectedEnsembleIdent)] : null; + setValidRealizationNumbersAtom(validRealizations); + + const validVfpTableNames = useAtomValue(availableVfpTableNamesAtom); + + const selectedThpIndicies = useAtomValue(selectedThpIndicesAtom); + const setUserSelectedThpIndices = useSetAtom(userSelectedThpIndicesAtom); + + const selectedWfrIndicies = useAtomValue(selectedWfrIndicesAtom); + const setUserSelectedWfrIndices = useSetAtom(userSelectedWfrIndicesAtom); + + const selectedGfrIndicies = useAtomValue(selectedGfrIndicesAtom); + const setUserSelectedGfrIndices = useSetAtom(userSelectedGfrIndicesAtom); + + const selectedAlqIndicies = useAtomValue(selectedAlqIndicesAtom); + const setUserSelectedAlqIndices = useSetAtom(userSelectedAlqIndicesAtom); + + const selectedPressureOption = useAtomValue(selectedPressureOptionAtom); + const setUserSelectedPressureOption = useSetAtom(userSelectedPressureOptionAtom); + + const selectedColorBy = useAtomValue(selectedColorByAtom); + const setUserSelectedColorBy = useSetAtom(userSelectedColorByAtom) + + usePropagateApiErrorToStatusWriter(vfpTableQuery, statusWriter); + + function handleEnsembleSelectionChange(ensembleIdent: EnsembleIdent | null) { + setUserSelectedEnsembleIdent(ensembleIdent); + } + + function handleRealizationNumberChange(value: string) { + const realizationNumber = parseInt(value); + setUserSelectedRealizationNumber(realizationNumber); + } + + function handleVfpNameSelectionChange(value: string) { + const vfpName = value; + setUserSelectedVfpName(vfpName); + } + + function handleThpIndicesSelectionChange(thpIndices: string[]) { + const thpIndicesNumbers = thpIndices.map((value) => parseInt(value)); + setUserSelectedThpIndices(thpIndicesNumbers); + } + + function handleWfrIndicesSelectionChange(wfrIndices: string[]) { + const wfrIndicesNumbers = wfrIndices.map((value) => parseInt(value)); + setUserSelectedWfrIndices(wfrIndicesNumbers); + } + + function handleGfrIndicesSelectionChange(gfrIndices: string[]) { + const gfrIndicesNumbers = gfrIndices.map((value) => parseInt(value)); + setUserSelectedGfrIndices(gfrIndicesNumbers); + } + + function handleAlqIndicesSelectionChange(alqIndices: string[]) { + const alqIndicesNumbers = alqIndices.map((value) => parseInt(value)); + setUserSelectedAlqIndices(alqIndicesNumbers); + } + + function handlePressureOptionChange(_: React.ChangeEvent, pressureOption: PressureOption) { + setUserSelectedPressureOption(pressureOption); + } + + function handleColorByChange(vfpParam: string) { + setUserSelectedColorBy(vfpParam as VfpParam); + } + + let thpLabel = "THP"; + let wfrLabel = "WFR"; + let gfrLabel = "GFR"; + let alqLabel = "ALQ"; + const vfpTableData = vfpTableQuery?.data; + let vfpDataAccessor; + if (vfpTableData) { + vfpDataAccessor = new VfpDataAccessor(vfpTableData) + thpLabel = vfpDataAccessor.getVfpParamLabel(VfpParam.THP, true) + wfrLabel = vfpDataAccessor.getVfpParamLabel(VfpParam.WFR, true) + gfrLabel = vfpDataAccessor.getVfpParamLabel(VfpParam.GFR, true) + alqLabel = vfpDataAccessor.getVfpParamLabel(VfpParam.ALQ, true) + } + + return ( +
+ + + + + { + return { value: real.toString(), label: real.toString() }; + }) ?? [] + } + value={selectedRealizationNumber?.toString() ?? undefined} + onChange={handleRealizationNumberChange} + /> + + + { + return { value: name, label: name }; + }) ?? [] + } + value={selectedVfpTableName ?? undefined} + onChange={handleVfpNameSelectionChange} + /> + + +
+ + +
+
+ + + + + + +
+ ); +} + +function makeFilterOptions(values: number[] | undefined): SelectOption[] { + return values?.map((value, index) => ({ label: value.toString(), value: index.toString() })) ?? []; +} diff --git a/frontend/src/modules/Vfp/types.ts b/frontend/src/modules/Vfp/types.ts new file mode 100644 index 000000000..0048c2c65 --- /dev/null +++ b/frontend/src/modules/Vfp/types.ts @@ -0,0 +1,18 @@ + +export enum QueryStatus { + Loading = "Loading", + Error = "Error", + Idle = "Idle", +} + +export enum VfpParam { + THP = "THP", + WFR = "WFR", + GFR = "GFR", + ALQ = "ALQ", +} + +export enum PressureOption { + BHP = "BHP", + DP = "DP", +} diff --git a/frontend/src/modules/Vfp/utils/VfpDataAccessor.ts b/frontend/src/modules/Vfp/utils/VfpDataAccessor.ts new file mode 100644 index 000000000..5bddd3f6f --- /dev/null +++ b/frontend/src/modules/Vfp/utils/VfpDataAccessor.ts @@ -0,0 +1,130 @@ +import { VfpProdTable_api, FlowRateTypeProd_api, ALQ_api, WFR_api, GFR_api } from "@api"; +import { VfpParam } from "../types"; + +export class VfpDataAccessor { + private _vfpTable: VfpProdTable_api; + + + constructor(vfpTable: VfpProdTable_api) { + this._vfpTable = vfpTable + } + + getTableNumber(): number { + return this._vfpTable.table_number + } + + getTableType(): string { + return this._vfpTable.vfp_type + } + + getFlowRateLabel(): string { + const flowRateType = this._vfpTable.flow_rate_type + const flowRateUnit = this.getFlowRateUnit() + if (flowRateType == FlowRateTypeProd_api.OIL) { + return `Oil Rate (${flowRateUnit})` + } else if (flowRateType == FlowRateTypeProd_api.GAS) { + return `Gas Rate (${flowRateUnit})` + } else if (flowRateType == FlowRateTypeProd_api.LIQ) { + return `Liquid Rate (${flowRateUnit})` + } else if (flowRateType == FlowRateTypeProd_api.TM) { + return `TM (${flowRateUnit})` + } else if (flowRateType == FlowRateTypeProd_api.WG) { + return `WG (${flowRateUnit})` + } + return "Flow rate type unknown" + } + + getFlowRateUnit(): string { + return this._vfpTable.flow_rate_unit + } + + getBhpUnit(): string { + return this._vfpTable.bhp_unit + } + + getFlowRateValues(): number[] { + return this._vfpTable.flow_rate_values + } + + getVfpParamValues(vfpParam: VfpParam): number [] { + if (vfpParam == VfpParam.THP) { + return this._vfpTable.thp_values + } else if (vfpParam == VfpParam.WFR) { + return this._vfpTable.wfr_values + } else if (vfpParam == VfpParam.GFR) { + return this._vfpTable.gfr_values + } else if (vfpParam == VfpParam.ALQ) { + return this._vfpTable.alq_values + } + return [] + } + + getVfpParamUnit(vfpParam: VfpParam): string { + if (vfpParam == VfpParam.THP) { + return this._vfpTable.thp_unit + } else if (vfpParam == VfpParam.WFR) { + return this._vfpTable.wfr_unit + } else if (vfpParam == VfpParam.GFR) { + return this._vfpTable.gfr_unit + } else if (vfpParam == VfpParam.ALQ) { + return this._vfpTable.alq_unit + } + return "" + } + + getVfpParamLabel(vfpParam: VfpParam, includeUnit: boolean): string { + let label = "" + if (vfpParam == VfpParam.THP) { + label = "THP" + } else if (vfpParam == VfpParam.WFR) { + label = this._vfpTable.wfr_type + } else if (vfpParam == VfpParam.GFR) { + label = this._vfpTable.gfr_type + } else if (vfpParam == VfpParam.ALQ) { + if (this._vfpTable.alq_type === ALQ_api._) { + label = "ALQ" + } else { + label = "ALQ: " + this._vfpTable.alq_type + } + } + const unit = this.getVfpParamUnit(vfpParam) + if (includeUnit && unit != "") { + label += ` (${unit})` + } + return label + } + + getWfrType(): WFR_api { + return this._vfpTable.wfr_type + } + + getGfrType(): GFR_api { + return this._vfpTable.gfr_type + } + + getAlqType(): ALQ_api { + return this._vfpTable.alq_type + } + + getBhpValues(thpIndex: number, wfrIndex: number, gfrIndex: number, alqIndex: number) : number[] { + const nbWfrValues = this._vfpTable.wfr_values.length + const nbGfrValues = this._vfpTable.gfr_values.length + const nbAlqValues = this._vfpTable.alq_values.length + const nbFlowRates = this._vfpTable.flow_rate_values.length + const startIndex = nbFlowRates*(nbAlqValues*(nbGfrValues*(nbWfrValues*thpIndex+wfrIndex)+gfrIndex)+alqIndex) + return this._vfpTable.bhp_values.slice(startIndex, startIndex+nbFlowRates) + } + + getNumberOfValues(vfpParam: VfpParam): number { + if (vfpParam == VfpParam.THP) { + return this._vfpTable.thp_values.length + } else if (vfpParam == VfpParam.WFR) { + return this._vfpTable.wfr_values.length + } else if (vfpParam == VfpParam.GFR) { + return this._vfpTable.gfr_values.length + } else if (vfpParam == VfpParam.ALQ) { + return this._vfpTable.alq_values.length + } + return NaN + } +} diff --git a/frontend/src/modules/Vfp/utils/VfpPlotBuilder.ts b/frontend/src/modules/Vfp/utils/VfpPlotBuilder.ts new file mode 100644 index 000000000..763b2a5a0 --- /dev/null +++ b/frontend/src/modules/Vfp/utils/VfpPlotBuilder.ts @@ -0,0 +1,128 @@ +import { Size2D } from "@lib/utils/geometry"; +import { Layout, PlotData, PlotMarker } from "plotly.js"; +import { PressureOption, VfpParam } from "../types"; +import { VfpDataAccessor } from "./VfpDataAccessor"; +import { ColorScale } from "@lib/utils/ColorScale"; + +export class VfpPlotBuilder { + private _vfpDataAccessor: VfpDataAccessor; + private _colorScale: ColorScale; + + constructor(vfpDataAccessor: VfpDataAccessor, colorScale: ColorScale){ + this._vfpDataAccessor = vfpDataAccessor + this._colorScale = colorScale + } + + makeLayout(size: Size2D, pressureOption: PressureOption) : Partial { + return { + title: `VFP type: ${this._vfpDataAccessor.getTableType()}, table number: ${this._vfpDataAccessor.getTableNumber()}`, + xaxis: { title: this._vfpDataAccessor.getFlowRateLabel()}, + yaxis: { title: `${pressureOption} (${this._vfpDataAccessor.getBhpUnit()})`}, + width: size.width, + height: size.height, + }; + } + + makeTraces( + selectedThpIndices: number[] | null, + selectedWfrIndices: number[] | null, + selectedGfrIndices: number[] | null, + selectedAlqIndices: number[] | null, + pressureOption: PressureOption, + colorBy: VfpParam, + ) : Partial[] { + + const data: Partial[] = []; + + if (selectedThpIndices == null || selectedWfrIndices == null || selectedGfrIndices == null || selectedAlqIndices == null) { + return []; + } + + const colorByValues = this._vfpDataAccessor.getVfpParamValues(colorBy) + const colorByIndices = { + [VfpParam.THP]: selectedThpIndices, + [VfpParam.WFR]: selectedWfrIndices, + [VfpParam.GFR]: selectedGfrIndices, + [VfpParam.ALQ]: selectedAlqIndices, + }[colorBy] + + const selectedColorByValues = colorByIndices.map(index => colorByValues[index]) + const minValue = Math.min(...selectedColorByValues) + const maxValue = Math.max(...selectedColorByValues) + const midValue = minValue + (maxValue - minValue) / 2; + this._colorScale.setRangeAndMidPoint(minValue, maxValue, midValue) + + for (let i = 0; i < selectedThpIndices.length; i++) { + for (let j = 0; j < selectedWfrIndices.length; j++) { + for (let k = 0; k < selectedGfrIndices.length; k++) { + for (let l = 0; l < selectedAlqIndices.length; l++) { + const thpIndex = selectedThpIndices[i] + const wfrIndex = selectedWfrIndices[j] + const gfrIndex = selectedGfrIndices[k] + const alqIndex = selectedAlqIndices[l] + + const colorByParamIndex = { + [VfpParam.THP]: thpIndex, + [VfpParam.WFR]: wfrIndex, + [VfpParam.GFR]: gfrIndex, + [VfpParam.ALQ]: alqIndex + }[colorBy] + const color = this._colorScale.getColorForValue(colorByValues[colorByParamIndex]) + + const trace = this.getSingleVfpTrace(thpIndex, wfrIndex, gfrIndex, alqIndex, pressureOption, color) + data.push(trace) + } + } + } + } + + // Add color scale legend + const colorScaleMarker: Partial = { + ...this._colorScale.getAsPlotlyColorScaleMarkerObject(), + colorbar: { + title: this._vfpDataAccessor.getVfpParamLabel(colorBy, true), + titleside: "right", + ticks: "outside", + len: 0.75, + }, + }; + const parameterColorLegendTrace: Partial = { + x: [null], + y: [null], + marker: colorScaleMarker, + showlegend: false, + }; + data.push(parameterColorLegendTrace); + + return data + } + + private getSingleVfpTrace(thpIndex: number, wfrIndex: number, gfrIndex: number, alqIndex: number, pressureOption: PressureOption, color: string) : Partial { + const thpValue = this._vfpDataAccessor.getVfpParamValues(VfpParam.THP)[thpIndex] + const wfrValue = this._vfpDataAccessor.getVfpParamValues(VfpParam.WFR)[wfrIndex] + const gfrValue = this._vfpDataAccessor.getVfpParamValues(VfpParam.GFR)[gfrIndex] + const alqValue = this._vfpDataAccessor.getVfpParamValues(VfpParam.ALQ)[alqIndex] + + const hovertext = `THP=${thpValue}
${this._vfpDataAccessor.getWfrType()}=${wfrValue}
${this._vfpDataAccessor.getGfrType()}=${gfrValue}
ALQ=${alqValue}` + let bhpValues = this._vfpDataAccessor.getBhpValues(thpIndex, wfrIndex, gfrIndex, alqIndex) + + if (pressureOption === PressureOption.DP) { + bhpValues = bhpValues.map(bhp => bhp - thpValue) + } + + const trace: Partial = { + x: this._vfpDataAccessor.getFlowRateValues(), + y: bhpValues, + mode: "lines+markers", + line: { + color, + }, + showlegend: false, + hovertext: hovertext, + hoverinfo: "y+x+text", + }; + + return trace + } + +} \ No newline at end of file diff --git a/frontend/src/modules/Vfp/view.tsx b/frontend/src/modules/Vfp/view.tsx new file mode 100644 index 000000000..f7197dd0f --- /dev/null +++ b/frontend/src/modules/Vfp/view.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import Plot from "react-plotly.js"; + +import { ModuleViewProps } from "@framework/Module"; +import { useElementSize } from "@lib/hooks/useElementSize"; +import { CircularProgress } from "@lib/components/CircularProgress"; +import { ContentMessage, ContentMessageType } from "@modules/_shared/components/ContentMessage/contentMessage"; + +import { Interfaces } from "./interfaces"; +import { VfpDataAccessor } from "./utils/VfpDataAccessor"; +import { VfpPlotBuilder } from "./utils/VfpPlotBuilder"; +import { ColorScaleGradientType } from "@lib/utils/ColorScale"; +import { useViewStatusWriter } from "@framework/StatusWriter"; +import { usePropagateApiErrorToStatusWriter } from "@modules/_shared/hooks/usePropagateApiErrorToStatusWriter"; + + +export function View({ viewContext, workbenchSettings }: ModuleViewProps) { + const colorScale = workbenchSettings.useContinuousColorScale({gradientType: ColorScaleGradientType.Sequential}) + + const vfpDataQuery = viewContext.useSettingsToViewInterfaceValue("vfpDataQuery"); + const selectedThpIndices = viewContext.useSettingsToViewInterfaceValue("selectedThpIndices"); + const selectedWfrIndices = viewContext.useSettingsToViewInterfaceValue("selectedWfrIndices"); + const selectedGfrIndices = viewContext.useSettingsToViewInterfaceValue("selectedGfrIndices"); + const selectedAlqIndices = viewContext.useSettingsToViewInterfaceValue("selectedAlqIndices"); + const selectedPressureOption = viewContext.useSettingsToViewInterfaceValue("selectedPressureOption") + const selectedColorBy = viewContext.useSettingsToViewInterfaceValue("selectedColorBy") + + const wrapperDivRef = React.useRef(null); + const wrapperDivSize = useElementSize(wrapperDivRef); + + const statusWriter = useViewStatusWriter(viewContext); + const statusError = usePropagateApiErrorToStatusWriter(vfpDataQuery, statusWriter); + + let content = null; + + if (vfpDataQuery.isFetching) { + content = + + + } else if (statusError !== null) { + content =
{statusError}
; + } else if (vfpDataQuery.isError || vfpDataQuery.data === undefined) { + content =
Could not load VFP data
; + } else { + const vfpTable = vfpDataQuery.data + const vfpPlotBuilder = new VfpPlotBuilder(new VfpDataAccessor(vfpTable), colorScale); + + const layout = vfpPlotBuilder.makeLayout(wrapperDivSize, selectedPressureOption); + const data = vfpPlotBuilder.makeTraces( + selectedThpIndices, + selectedWfrIndices, + selectedGfrIndices, + selectedAlqIndices, + selectedPressureOption, + selectedColorBy, + ); + + content = ; + } + return ( +
+ {content} +
+ ); +} diff --git a/frontend/src/modules/_shared/Surface/SurfaceAddressBuilder.ts b/frontend/src/modules/_shared/Surface/SurfaceAddressBuilder.ts index 1f7b301fa..34ba220a6 100644 --- a/frontend/src/modules/_shared/Surface/SurfaceAddressBuilder.ts +++ b/frontend/src/modules/_shared/Surface/SurfaceAddressBuilder.ts @@ -15,6 +15,7 @@ export class SurfaceAddressBuilder { private _realizationNum: number | null = null; private _isoTimeOrInterval: string | null = null; private _statisticFunction: SurfaceStatisticFunction_api | null = null; + private _statisticRealizations: number[] | null = null; withType(addrType: SurfaceAddressType): this { this._addrType = addrType; @@ -52,6 +53,11 @@ export class SurfaceAddressBuilder { return this; } + withStatisticRealizations(realizations: number[]): this { + this._statisticRealizations = realizations; + return this; + } + buildRealizationAddress(): RealizationSurfaceAddress { if (this._addrType && this._addrType !== "REAL") { throw new Error("Address type is already set to another type than REAL"); @@ -114,7 +120,7 @@ export class SurfaceAddressBuilder { name: this._name!, attribute: this._attribute!, statFunction: this._statisticFunction, - statRealizations: null, + statRealizations: this._statisticRealizations, isoTimeOrInterval: this._isoTimeOrInterval, }; return retObj; diff --git a/frontend/src/modules/registerAllModules.ts b/frontend/src/modules/registerAllModules.ts index 6232b8e12..ec450f509 100644 --- a/frontend/src/modules/registerAllModules.ts +++ b/frontend/src/modules/registerAllModules.ts @@ -16,6 +16,8 @@ import "./SimulationTimeSeriesSensitivity/registerModule"; import "./SubsurfaceMap/registerModule"; import "./TornadoChart/registerModule"; import "./WellCompletions/registerModule"; +import "./Vfp/registerModule"; + if (isDevMode()) { await import("./MyModule/registerModule");