From 5ea52309bd14af3162f8caf122b6f3ca56de1e9a Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Mon, 11 Sep 2023 12:46:52 +0200 Subject: [PATCH 1/2] Refactor surface directory --- .../primary/routers/surface/converters.py | 67 +++- .../backend/primary/routers/surface/router.py | 210 ++++------ .../primary/routers/surface/schemas.py | 44 ++- .../_mocked_stratigraphy_access.py | 71 ++++ .../queries/get_stratigraphic_units.py | 2 +- .../smda_access/stratigraphy_access.py | 4 +- .../smda_access/stratigraphy_utils.py | 114 ++++++ backend/src/services/smda_access/types.py | 23 ++ .../dev_sumo_access_test_driver.py | 31 +- .../src/services/sumo_access/generic_types.py | 5 +- .../services/sumo_access/surface_access.py | 309 ++++----------- .../src/services/sumo_access/surface_types.py | 34 +- .../smda_access/test_stratigraphy_utils.py | 59 +++ frontend/src/api/index.ts | 6 +- .../src/api/models/DynamicSurfaceDirectory.ts | 10 - .../src/api/models/StaticSurfaceDirectory.ts | 10 - .../src/api/models/StratigraphicFeature.ts | 11 + frontend/src/api/models/SumoContent.ts | 13 - .../src/api/models/SurfaceAttributeType.ts | 23 ++ frontend/src/api/models/SurfaceMeta.ts | 22 ++ frontend/src/api/services/SurfaceService.ts | 169 ++------ .../src/framework/utils/timestampUtils.ts | 12 +- frontend/src/modules/Map/MapQueryHooks.ts | 148 ------- frontend/src/modules/Map/MapSettings.tsx | 367 ++++++++---------- frontend/src/modules/Map/MapState.ts | 4 +- frontend/src/modules/Map/MapView.tsx | 13 +- frontend/src/modules/Map/SurfAddr.ts | 110 ------ .../src/modules/SubsurfaceMap/queryHooks.tsx | 108 +----- .../src/modules/SubsurfaceMap/settings.tsx | 251 ++++++++++-- frontend/src/modules/SubsurfaceMap/state.ts | 7 +- .../SubsurfaceMap/surfaceDirectoryProvider.ts | 106 ----- frontend/src/modules/SubsurfaceMap/view.tsx | 14 +- frontend/src/modules/_shared/Surface/index.ts | 5 + .../src/modules/_shared/Surface/queryHooks.ts | 93 +++++ .../Surface/surfaceAddress.ts} | 34 +- .../_shared/Surface/surfaceDirectory.ts | 137 +++++++ 36 files changed, 1310 insertions(+), 1336 deletions(-) create mode 100644 backend/src/services/smda_access/mocked_drogon_smda_access/_mocked_stratigraphy_access.py create mode 100644 backend/src/services/smda_access/stratigraphy_utils.py create mode 100644 backend/tests/unit/services/smda_access/test_stratigraphy_utils.py delete mode 100644 frontend/src/api/models/DynamicSurfaceDirectory.ts delete mode 100644 frontend/src/api/models/StaticSurfaceDirectory.ts create mode 100644 frontend/src/api/models/StratigraphicFeature.ts delete mode 100644 frontend/src/api/models/SumoContent.ts create mode 100644 frontend/src/api/models/SurfaceAttributeType.ts create mode 100644 frontend/src/api/models/SurfaceMeta.ts delete mode 100644 frontend/src/modules/Map/MapQueryHooks.ts delete mode 100644 frontend/src/modules/Map/SurfAddr.ts delete mode 100644 frontend/src/modules/SubsurfaceMap/surfaceDirectoryProvider.ts create mode 100644 frontend/src/modules/_shared/Surface/index.ts create mode 100644 frontend/src/modules/_shared/Surface/queryHooks.ts rename frontend/src/modules/{SubsurfaceMap/SurfaceAddress.ts => _shared/Surface/surfaceAddress.ts} (50%) create mode 100644 frontend/src/modules/_shared/Surface/surfaceDirectory.ts diff --git a/backend/src/backend/primary/routers/surface/converters.py b/backend/src/backend/primary/routers/surface/converters.py index 591c15871..b61b64dd3 100644 --- a/backend/src/backend/primary/routers/surface/converters.py +++ b/backend/src/backend/primary/routers/surface/converters.py @@ -1,6 +1,9 @@ +from typing import List + import orjson import xtgeo - +from src.services.sumo_access.surface_types import SurfaceMeta as SumoSurfaceMeta +from src.services.smda_access.types import StratigraphicSurface from src.services.utils.surface_to_float32 import surface_to_float32_array from . import schemas @@ -40,3 +43,65 @@ def to_api_surface_data(xtgeo_surf: xtgeo.RegularSurface) -> schemas.SurfaceData rot_deg=xtgeo_surf.rotation, mesh_data=orjson.dumps(float32values).decode(), # pylint: disable=maybe-no-member ) + + +def to_api_surface_directory( + sumo_surface_dir: List[SumoSurfaceMeta], stratigraphical_names: List[StratigraphicSurface] +) -> List[schemas.SurfaceMeta]: + """ + Convert Sumo surface directory to API surface directory + """ + + surface_metas = _sort_by_stratigraphical_order(sumo_surface_dir, stratigraphical_names) + return surface_metas + + +def _sort_by_stratigraphical_order( + sumo_surface_metas: List[SumoSurfaceMeta], stratigraphic_surfaces: List[StratigraphicSurface] +) -> List[schemas.SurfaceMeta]: + """Sort the Sumo surface meta list by the order they appear in the stratigraphic column. + Non-stratigraphical surfaces are appended at the end of the list.""" + + surface_metas_with_official_strat_name = [] + surface_metas_with_custom_names = [] + + for strat_surface in stratigraphic_surfaces: + for sumo_surface_meta in sumo_surface_metas: + if sumo_surface_meta.name == strat_surface.name: + surface_meta = schemas.SurfaceMeta( + name=sumo_surface_meta.name, + is_observation=sumo_surface_meta.is_observation, + iso_date_or_interval=sumo_surface_meta.iso_date_or_interval, + value_min=sumo_surface_meta.zmin, + value_max=sumo_surface_meta.zmax, + name_is_stratigraphic_offical=True, + stratigraphic_feature=strat_surface.feature, + relative_stratigraphic_level=strat_surface.relative_strat_unit_level, + parent_stratigraphic_identifier=strat_surface.strat_unit_parent, + stratigraphic_identifier=strat_surface.strat_unit_identifier, + attribute_name=sumo_surface_meta.tagname, + attribute_type=schemas.SurfaceAttributeType(sumo_surface_meta.content.value), + ) + surface_metas_with_official_strat_name.append(surface_meta) + + # Append non-official strat names + for sumo_surface_meta in sumo_surface_metas: + if sumo_surface_meta.name not in [s.name for s in surface_metas_with_official_strat_name]: + surface_meta = schemas.SurfaceMeta( + name=sumo_surface_meta.name, + is_observation=sumo_surface_meta.is_observation, + iso_date_or_interval=sumo_surface_meta.iso_date_or_interval, + value_min=sumo_surface_meta.zmin, + value_max=sumo_surface_meta.zmax, + name_is_stratigraphic_offical=False, + stratigraphic_feature=None, + relative_stratigraphic_level=None, + parent_stratigraphic_identifier=None, + stratigraphic_identifier=None, + attribute_name=sumo_surface_meta.tagname, + attribute_type=schemas.SurfaceAttributeType(sumo_surface_meta.content.value), + ) + + surface_metas_with_custom_names.append(surface_meta) + + return surface_metas_with_official_strat_name + surface_metas_with_custom_names diff --git a/backend/src/backend/primary/routers/surface/router.py b/backend/src/backend/primary/routers/surface/router.py index dcc2ef6c5..24a94b745 100644 --- a/backend/src/backend/primary/routers/surface/router.py +++ b/backend/src/backend/primary/routers/surface/router.py @@ -1,87 +1,111 @@ import logging -from typing import List - -from fastapi import APIRouter, Depends, HTTPException, Query +from typing import List, Union, Optional +import numpy as np +from fastapi import APIRouter, Depends, HTTPException, Query, Body +import json from src.services.sumo_access.surface_access import SurfaceAccess +from src.services.sumo_access.case_inspector import CaseInspector +from src.services.smda_access.stratigraphy_access import StratigraphyAccess +from src.services.smda_access.stratigraphy_utils import sort_stratigraphic_names_by_hierarchy +from src.services.smda_access.mocked_drogon_smda_access import _mocked_stratigraphy_access from src.services.utils.statistic_function import StatisticFunction from src.services.utils.authenticated_user import AuthenticatedUser from src.services.utils.perf_timer import PerfTimer from src.backend.auth.auth_helper import AuthHelper -from src.services.sumo_access.generic_types import SumoContent + from . import converters from . import schemas + LOGGER = logging.getLogger(__name__) router = APIRouter() -@router.get("/dynamic_surface_directory/") -def get_dynamic_surface_directory( +@router.get("/surface_directory/") +def get_surface_directory( authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), case_uuid: str = Query(description="Sumo case uuid"), ensemble_name: str = Query(description="Ensemble name"), -) -> schemas.DynamicSurfaceDirectory: +) -> List[schemas.SurfaceMeta]: """ - Get a directory of surface names, attributes and time/interval strings for simulated dynamic surfaces. + Get a directory of surfaces in a Sumo ensemble """ - access = SurfaceAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) - surf_dir = access.get_dynamic_surf_dir() + surface_access = SurfaceAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) + sumo_surf_dir = surface_access.get_surface_directory() - ret_dir = schemas.DynamicSurfaceDirectory( - names=surf_dir.names, - attributes=surf_dir.attributes, - time_or_interval_strings=surf_dir.date_strings, - ) + case_inspector = CaseInspector(authenticated_user.get_sumo_access_token(), case_uuid) + strat_column_identifier = case_inspector.get_stratigraphic_column_identifier() + strat_access: Union[StratigraphyAccess, _mocked_stratigraphy_access.StratigraphyAccess] + + if strat_column_identifier == "DROGON_HAS_NO_STRATCOLUMN": + strat_access = _mocked_stratigraphy_access.StratigraphyAccess(authenticated_user.get_smda_access_token()) + else: + strat_access = StratigraphyAccess(authenticated_user.get_smda_access_token()) + strat_units = strat_access.get_stratigraphic_units(strat_column_identifier) + sorted_stratigraphic_surfaces = sort_stratigraphic_names_by_hierarchy(strat_units) - return ret_dir + return converters.to_api_surface_directory(sumo_surf_dir, sorted_stratigraphic_surfaces) -@router.get("/static_surface_directory/") -def get_static_surface_directory( +@router.get("/realization_surface_data/") +def get_realization_surface_data( authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), case_uuid: str = Query(description="Sumo case uuid"), ensemble_name: str = Query(description="Ensemble name"), - sumo_content_filter: List[SumoContent] = Query(default=None, description="Optional filter by Sumo content type"), -) -> schemas.StaticSurfaceDirectory: - """ - Get a directory of surface names and attributes for static surfaces. - These are the non-observed surfaces that do NOT have time stamps - """ - access = SurfaceAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) - surf_dir = access.get_static_surf_dir(content_filter=sumo_content_filter) + realization_num: int = Query(description="Realization number"), + name: str = Query(description="Surface name"), + attribute: str = Query(description="Surface attribute"), + time_or_interval: Optional[str] = Query(None, description="Time point or time interval string"), +) -> schemas.SurfaceData: + timer = PerfTimer() - ret_dir = schemas.StaticSurfaceDirectory( - names=surf_dir.names, - attributes=surf_dir.attributes, - valid_attributes_for_name=surf_dir.valid_attributes_for_name, + access = SurfaceAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) + xtgeo_surf = access.get_realization_surface_data( + real_num=realization_num, name=name, attribute=attribute, time_or_interval_str=time_or_interval ) - return ret_dir + if not xtgeo_surf: + raise HTTPException(status_code=404, detail="Surface not found") + + surf_data_response = converters.to_api_surface_data(xtgeo_surf) + + LOGGER.debug(f"Loaded static surface and created image, total time: {timer.elapsed_ms()}ms") + + return surf_data_response -@router.get("/static_surface_data/") -def get_static_surface_data( +@router.get("/statistical_surface_data/") +def get_statistical_surface_data( authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), case_uuid: str = Query(description="Sumo case uuid"), ensemble_name: str = Query(description="Ensemble name"), - realization_num: int = Query(description="Realization number"), + statistic_function: schemas.SurfaceStatisticFunction = Query(description="Statistics to calculate"), name: str = Query(description="Surface name"), attribute: str = Query(description="Surface attribute"), + time_or_interval: Optional[str] = Query(None, description="Time point or time interval string"), ) -> schemas.SurfaceData: timer = PerfTimer() access = SurfaceAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) - xtgeo_surf = access.get_static_surf(real_num=realization_num, name=name, attribute=attribute) + + service_stat_func_to_compute = StatisticFunction.from_string_value(statistic_function) + if service_stat_func_to_compute is not None: + xtgeo_surf = access.get_statistical_surface_data( + statistic_function=service_stat_func_to_compute, + name=name, + attribute=attribute, + time_or_interval_str=time_or_interval, + ) if not xtgeo_surf: - raise HTTPException(status_code=404, detail="Surface not found") + raise HTTPException(status_code=404, detail="Could not find or compute surface") surf_data_response = converters.to_api_surface_data(xtgeo_surf) - LOGGER.debug(f"Loaded static surface and created image, total time: {timer.elapsed_ms()}ms") + LOGGER.debug(f"Calculated statistical dynamic surface and created image, total time: {timer.elapsed_ms()}ms") return surf_data_response @@ -97,13 +121,19 @@ def get_property_surface_resampled_to_static_surface( realization_num_property: int = Query(description="Realization number"), name_property: str = Query(description="Surface name"), attribute_property: str = Query(description="Surface attribute"), + time_or_interval_property: Optional[str] = Query(None, description="Time point or time interval string"), ) -> schemas.SurfaceData: timer = PerfTimer() access = SurfaceAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) - xtgeo_surf_mesh = access.get_static_surf(real_num=realization_num_mesh, name=name_mesh, attribute=attribute_mesh) - xtgeo_surf_property = access.get_static_surf( - real_num=realization_num_property, name=name_property, attribute=attribute_property + xtgeo_surf_mesh = access.get_realization_surface_data( + real_num=realization_num_mesh, name=name_mesh, attribute=attribute_mesh + ) + xtgeo_surf_property = access.get_realization_surface_data( + real_num=realization_num_property, + name=name_property, + attribute=attribute_property, + time_or_interval_str=time_or_interval_property, ) if not xtgeo_surf_mesh or not xtgeo_surf_property: @@ -129,21 +159,23 @@ def get_property_surface_resampled_to_statistical_static_surface( # statistic_function_property: schemas.SurfaceStatisticFunction = Query(description="Statistics to calculate"), name_property: str = Query(description="Surface name"), attribute_property: str = Query(description="Surface attribute"), + time_or_interval_property: Optional[str] = Query(None, description="Time point or time interval string"), ) -> schemas.SurfaceData: timer = PerfTimer() access = SurfaceAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) service_stat_func_to_compute = StatisticFunction.from_string_value(statistic_function) if service_stat_func_to_compute is not None: - xtgeo_surf_mesh = access.get_statistical_static_surf( + xtgeo_surf_mesh = access.get_statistical_surface_data( statistic_function=service_stat_func_to_compute, name=name_mesh, attribute=attribute_mesh, ) - xtgeo_surf_property = access.get_statistical_static_surf( + xtgeo_surf_property = access.get_statistical_surface_data( statistic_function=service_stat_func_to_compute, name=name_property, attribute=attribute_property, + time_or_interval_str=time_or_interval_property, ) if not xtgeo_surf_mesh or not xtgeo_surf_property: @@ -156,97 +188,3 @@ def get_property_surface_resampled_to_statistical_static_surface( LOGGER.debug(f"Loaded property surface and created image, total time: {timer.elapsed_ms()}ms") return surf_data_response - - -@router.get("/dynamic_surface_data/") -def get_dynamic_surface_data( - authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), - case_uuid: str = Query(description="Sumo case uuid"), - ensemble_name: str = Query(description="Ensemble name"), - realization_num: int = Query(description="Realization number"), - name: str = Query(description="Surface name"), - attribute: str = Query(description="Surface attribute"), - time_or_interval: str = Query(description="Timestamp or time interval string"), -) -> schemas.SurfaceData: - timer = PerfTimer() - - access = SurfaceAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) - xtgeo_surf = access.get_dynamic_surf( - real_num=realization_num, - name=name, - attribute=attribute, - time_or_interval_str=time_or_interval, - ) - - if not xtgeo_surf: - raise HTTPException(status_code=404, detail="Surface not found") - - surf_data_response = converters.to_api_surface_data(xtgeo_surf) - - LOGGER.debug(f"Loaded dynamic surface and created image, total time: {timer.elapsed_ms()}ms") - - return surf_data_response - - -@router.get("/statistical_dynamic_surface_data/") -def get_statistical_dynamic_surface_data( - authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), - case_uuid: str = Query(description="Sumo case uuid"), - ensemble_name: str = Query(description="Ensemble name"), - statistic_function: schemas.SurfaceStatisticFunction = Query(description="Statistics to calculate"), - name: str = Query(description="Surface name"), - attribute: str = Query(description="Surface attribute"), - time_or_interval: str = Query(description="Timestamp or time interval string"), -) -> schemas.SurfaceData: - timer = PerfTimer() - - access = SurfaceAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) - - service_stat_func_to_compute = StatisticFunction.from_string_value(statistic_function) - if service_stat_func_to_compute is not None: - xtgeo_surf = access.get_statistical_dynamic_surf( - statistic_function=service_stat_func_to_compute, - name=name, - attribute=attribute, - time_or_interval_str=time_or_interval, - ) - - if not xtgeo_surf: - raise HTTPException(status_code=404, detail="Could not find or compute surface") - - surf_data_response = converters.to_api_surface_data(xtgeo_surf) - - LOGGER.debug(f"Calculated statistical dynamic surface and created image, total time: {timer.elapsed_ms()}ms") - - return surf_data_response - - -@router.get("/statistical_static_surface_data/") -def get_statistical_static_surface_data( - authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), - case_uuid: str = Query(description="Sumo case uuid"), - ensemble_name: str = Query(description="Ensemble name"), - statistic_function: schemas.SurfaceStatisticFunction = Query(description="Statistics to calculate"), - name: str = Query(description="Surface name"), - attribute: str = Query(description="Surface attribute"), -) -> schemas.SurfaceData: - timer = PerfTimer() - - access = SurfaceAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) - - service_stat_func_to_compute = StatisticFunction.from_string_value(statistic_function) - if service_stat_func_to_compute is not None: - xtgeo_surf = access.get_statistical_static_surf( - statistic_function=service_stat_func_to_compute, - name=name, - attribute=attribute, - ) - - if not xtgeo_surf: - raise HTTPException(status_code=404, detail="Could not find or compute surface") - - surf_data_response = converters.to_api_surface_data(xtgeo_surf) - - LOGGER.debug(f"Calculated statistical static surface and created image, total time: {timer.elapsed_ms()}ms") - - return surf_data_response diff --git a/backend/src/backend/primary/routers/surface/schemas.py b/backend/src/backend/primary/routers/surface/schemas.py index 15ab10714..869900c68 100644 --- a/backend/src/backend/primary/routers/surface/schemas.py +++ b/backend/src/backend/primary/routers/surface/schemas.py @@ -1,6 +1,6 @@ from enum import Enum -from typing import List - +from typing import Optional +from src.services.smda_access.types import StratigraphicFeature from pydantic import BaseModel @@ -14,16 +14,40 @@ class SurfaceStatisticFunction(str, Enum): P50 = "P50" -class DynamicSurfaceDirectory(BaseModel): - names: List[str] - attributes: List[str] - time_or_interval_strings: List[str] +class SurfaceAttributeType(str, Enum): + """A surface has a single array with values, e.g. depth, time, property, seismic, thickness. + Only surfaces with depth and time have z-values that can be plotted in 3D. + The other attributes are scalar values that can be plotted in 2D or used as colormapping for 3D surfaces. + + Ideally if the attribute is a scalar, there should be corresponding z-values, but this information is not + available in the metadata. + + To be revisited later when the metadata is more mature. + + """ + + DEPTH = "depth" # Values are depths + TIME = "time" # Values are time (ms) + PROPERTY = "property" # Values are generic, but typically extracted from a gridmodel + SEISMIC = "seismic" # Values are extracted from a seismic cube + THICKNESS = "thickness" # Values are isochores (real or conceptual difference between two depth surfaces) + ISOCHORE = "isochore" # Values are isochores (real or conceptual difference between two depth surfaces) + FLUID_CONTACT = "fluid_contact" # Values are fluid contacts (oil-water, gas-water, etc.) -class StaticSurfaceDirectory(BaseModel): - names: List[str] - attributes: List[str] - valid_attributes_for_name: List[List[int]] +class SurfaceMeta(BaseModel): + name: str # Svarte fm. top / Svarte fm. / Svarte fm. base + name_is_stratigraphic_offical: bool + stratigraphic_identifier: Optional[str] = None # Svarte fm. + relative_stratigraphic_level: Optional[int] = None + parent_stratigraphic_identifier: Optional[str] = None + stratigraphic_feature: Optional[StratigraphicFeature] = None # Distinguish between horizon and unit + attribute_name: str + attribute_type: SurfaceAttributeType + iso_date_or_interval: Optional[str] = None + is_observation: bool # Can only be true for seismic surfaces + value_min: Optional[float] = None + value_max: Optional[float] = None class SurfaceData(BaseModel): diff --git a/backend/src/services/smda_access/mocked_drogon_smda_access/_mocked_stratigraphy_access.py b/backend/src/services/smda_access/mocked_drogon_smda_access/_mocked_stratigraphy_access.py new file mode 100644 index 000000000..1fdf3bb98 --- /dev/null +++ b/backend/src/services/smda_access/mocked_drogon_smda_access/_mocked_stratigraphy_access.py @@ -0,0 +1,71 @@ +from typing import List + + +from ..types import StratigraphicUnit, StratigraphicSurface +from ..stratigraphy_utils import sort_stratigraphic_names_by_hierarchy, sort_stratigraphic_units_by_hierarchy + +DROGON_STRAT_UNITS = [ + StratigraphicUnit( + identifier="Valysar Fm.", + top="Valysar Fm. Top", + base="Valysar Fm. Base", + color_r=255, + color_g=165, + color_b=0, + strat_unit_level=2, + strat_unit_type="formation", + strat_unit_parent="VOLANTIS GP.", + top_age=1, + ), + StratigraphicUnit( + identifier="Therys Fm.", + top="Therys Fm. Top", + base="Therys Fm. Base", + color_r=255, + color_g=255, + color_b=0, + strat_unit_level=2, + strat_unit_type="formation", + strat_unit_parent="VOLANTIS GP.", + top_age=2, + ), + StratigraphicUnit( + identifier="Volon Fm.", + top="Volon Fm. Top", + base="Volon Fm. Base", + color_r=143, + color_g=188, + color_b=143, + strat_unit_level=2, + strat_unit_type="formation", + strat_unit_parent="VOLANTIS GP.", + top_age=3, + ), + StratigraphicUnit( + identifier="VOLANTIS GP.", + top="VOLANTIS GP. Top", + base="VOLANTIS GP. Base", + color_r=255, + color_g=165, + color_b=0, + strat_unit_level=1, + strat_unit_type="group", + strat_unit_parent=None, + top_age=4, + ), +] + + +class StratigraphyAccess: + def __init__(self, access_token: str): + self._smda_token = access_token + + # type: ignore + # pylint: disable=unused-argument + def get_stratigraphic_units(self, stratigraphic_column_identifier: str) -> List[StratigraphicUnit]: + return sort_stratigraphic_units_by_hierarchy(DROGON_STRAT_UNITS) + + # type: ignore + # pylint: disable=unused-argument + def get_stratigraphic_surfaces(self, stratigraphic_column_identifier: str) -> List[StratigraphicSurface]: + return sort_stratigraphic_names_by_hierarchy(DROGON_STRAT_UNITS) diff --git a/backend/src/services/smda_access/queries/get_stratigraphic_units.py b/backend/src/services/smda_access/queries/get_stratigraphic_units.py index 199730164..83360bda3 100644 --- a/backend/src/services/smda_access/queries/get_stratigraphic_units.py +++ b/backend/src/services/smda_access/queries/get_stratigraphic_units.py @@ -11,7 +11,7 @@ def get_stratigraphic_units(access_token: str, stratigraphic_column_identifier: params = { "strat_column_identifier": stratigraphic_column_identifier, "_sort": "top_age", - "_projection": "top,base,identifier", + "_projection": "top,base,identifier,strat_unit_level,strat_unit_level,strat_unit_type,strat_unit_parent,top_age,color_r,color_g,color_b", } results = get(access_token=access_token, endpoint=endpoint, params=params) timer = PerfTimer() diff --git a/backend/src/services/smda_access/stratigraphy_access.py b/backend/src/services/smda_access/stratigraphy_access.py index 3f599973f..9589f8060 100644 --- a/backend/src/services/smda_access/stratigraphy_access.py +++ b/backend/src/services/smda_access/stratigraphy_access.py @@ -9,5 +9,5 @@ def __init__(self, access_token: str): self._smda_token = access_token def get_stratigraphic_units(self, stratigraphic_column_identifier: str) -> List[StratigraphicUnit]: - stratigraphic_units = get_stratigraphic_units(self._smda_token, stratigraphic_column_identifier) - return stratigraphic_units + """Get stratigraphic units for a given stratigraphic column""" + return get_stratigraphic_units(self._smda_token, stratigraphic_column_identifier) diff --git a/backend/src/services/smda_access/stratigraphy_utils.py b/backend/src/services/smda_access/stratigraphy_utils.py new file mode 100644 index 000000000..d4ace889e --- /dev/null +++ b/backend/src/services/smda_access/stratigraphy_utils.py @@ -0,0 +1,114 @@ +import logging +from typing import List +from pydantic import BaseModel + +from .types import StratigraphicUnit, StratigraphicSurface, StratigraphicFeature + +LOGGER = logging.getLogger(__name__) + + +class HierarchicalStratigraphicUnit(BaseModel): + """A stratigraphic unit within a hierarchical structure, i.e a multi-level stratigraphical column""" + + unit: StratigraphicUnit + children: List["HierarchicalStratigraphicUnit"] = [] + + +def create_hierarchical_structure(strat_units: List[StratigraphicUnit]) -> List[HierarchicalStratigraphicUnit]: + """Create a hierarchical structure of stratigraphic units using strat_unit_parent. + On Drogon this will result in the following structure: + - VOLANTIS GP. + - Valysar Fm. + - Therys Fm. + - Volon Fm + """ + unit_by_id = {unit.identifier: HierarchicalStratigraphicUnit(unit=unit, children=[]) for unit in strat_units} + roots = [] + + for unit in strat_units: + if unit.strat_unit_parent and unit.strat_unit_parent in unit_by_id: + parent = unit_by_id[unit.strat_unit_parent] + parent.children.append(unit_by_id[unit.identifier]) + else: + roots.append(unit_by_id[unit.identifier]) + + return roots + + +def flatten_hierarchical_structure(units: List[HierarchicalStratigraphicUnit]) -> List[StratigraphicUnit]: + """Flattens the hierarchy of stratigraphic units to a list of stratigraphic units preserving the order.""" + flattened_list = [] + + for hierarchical_unit in units: + flattened_list.append(hierarchical_unit.unit) + flattened_list.extend(flatten_hierarchical_structure(hierarchical_unit.children)) + + return flattened_list + + +def flatten_hierarchical_structure_to_surface_name( + units: List[HierarchicalStratigraphicUnit], idx: int = 0 +) -> List[StratigraphicSurface]: + """Flattens the hierarchy of stratigraphic units to a list of stratigraphic surfaces preserving the order. + On Drogon this will result in the following list: + - Volantis GP. Top + - Volantis GP. + - Valysar Fm. Top + - Valysar Fm. + - Therys Fm. Top + - Therys Fm. + - Volon Fm. Top + - Volon Fm. + - Volantis GP. Base + """ + flattened_list = [] + + for hierarchical_unit in units: + unit = hierarchical_unit.unit + LOGGER.info(f"Ordered stratigraphic top: {idx * ' '}{unit.top}") + LOGGER.info(f"Ordered stratigraphic identifier: {idx * ' '}{unit.identifier}") + flattened_list.append( + StratigraphicSurface( + name=unit.top, + feature=StratigraphicFeature.HORIZON, + strat_unit_parent=unit.strat_unit_parent, + relative_strat_unit_level=idx, + strat_unit_identifier=unit.identifier, + ) + ) + flattened_list.append( + StratigraphicSurface( + name=unit.identifier, + feature=StratigraphicFeature.ZONE, + strat_unit_parent=unit.strat_unit_parent, + relative_strat_unit_level=idx, + strat_unit_identifier=unit.identifier, + ) + ) + flattened_list.extend(flatten_hierarchical_structure_to_surface_name(hierarchical_unit.children, idx=idx + 1)) + flattened_list.append( + StratigraphicSurface( + name=unit.base, + feature=StratigraphicFeature.HORIZON, + strat_unit_parent=unit.strat_unit_parent, + relative_strat_unit_level=idx, + strat_unit_identifier=unit.identifier, + ) + ) + LOGGER.info(f"Ordered stratigraphic base: {idx * ' '}{unit.base}") + + return flattened_list + + +def sort_stratigraphic_names_by_hierarchy(strat_units: List[StratigraphicUnit]) -> List[StratigraphicSurface]: + """Get a flatten list of top/unit/base surface names in lithostratigraphical order""" + hierarchical_units = create_hierarchical_structure(strat_units) + sorted_surfaces = flatten_hierarchical_structure_to_surface_name(hierarchical_units) + return sorted_surfaces + + +def sort_stratigraphic_units_by_hierarchy(strat_units: List[StratigraphicUnit]) -> List[StratigraphicUnit]: + """Get stratigraphic units for stratigraphic column in lithostratigraphical order.""" + hierarchical_units = create_hierarchical_structure(strat_units) + sorted_units = flatten_hierarchical_structure(hierarchical_units) + return sorted_units diff --git a/backend/src/services/smda_access/types.py b/backend/src/services/smda_access/types.py index aeedf9085..a545bb066 100644 --- a/backend/src/services/smda_access/types.py +++ b/backend/src/services/smda_access/types.py @@ -1,3 +1,4 @@ +from enum import Enum from typing import List, Optional from pydantic import BaseModel @@ -39,3 +40,25 @@ class StratigraphicUnit(BaseModel): identifier: str top: str base: str + strat_unit_level: int + strat_unit_type: str + top_age: Optional[int | float] + strat_unit_parent: Optional[str] + color_r: int + color_g: int + color_b: int + + +class StratigraphicFeature(str, Enum): + """The stratigraphic feature of a surface""" + + ZONE = "zone" # identifier + HORIZON = "horizon" # top/base + + +class StratigraphicSurface(BaseModel): + name: str + feature: StratigraphicFeature + relative_strat_unit_level: int = 0 + strat_unit_parent: Optional[str] = None + strat_unit_identifier: Optional[str] = None diff --git a/backend/src/services/sumo_access/dev_sumo_access_test_driver.py b/backend/src/services/sumo_access/dev_sumo_access_test_driver.py index 254622c56..80a05db80 100644 --- a/backend/src/services/sumo_access/dev_sumo_access_test_driver.py +++ b/backend/src/services/sumo_access/dev_sumo_access_test_driver.py @@ -11,7 +11,7 @@ compute_vector_statistics, ) from .summary_access import SummaryAccess, RealizationVector, Frequency -from .surface_access import SurfaceAccess + from .sumo_explore import SumoExplore @@ -72,30 +72,6 @@ def test_summary_access(summary_access: SummaryAccess) -> None: print(vec_stats) -def test_surface_access(surf_access: SurfaceAccess) -> None: - dynamic_surf_dir = surf_access.get_dynamic_surf_dir() - print(f"{dynamic_surf_dir=}") - - static_surf_dir = surf_access.get_static_surf_dir() - print(f"{static_surf_dir=}") - - name_idx = 0 - valid_attr_indices = static_surf_dir.valid_attributes_for_name[name_idx] - surf_name = static_surf_dir.names[name_idx] - surf_attr = static_surf_dir.attributes[valid_attr_indices[0]] - surf = surf_access.get_static_surf(real_num=0, name=surf_name, attribute=surf_attr) - print(f"{type(surf)=}") - - # surf = surf_access.get_static_surf(real_num=0, name=static_surf_dir.names[0], attribute=static_surf_dir.attributes[2]) - # print(f"{type(surf)=}") - - # dyn_surf = surf_access.get_dynamic_surf(real_num=0, name=dynamic_surf_dir.names[0], attribute=dynamic_surf_dir.attributes[0], time_or_interval_str=dynamic_surf_dir.date_strings[0]) - # print(f"{type(dyn_surf)=}") - - # dyn_surf = surf_access.get_statistical_dynamic_surf(statistic_function=StatisticFunction.MEAN, name=dynamic_surf_dir.names[0], attribute=dynamic_surf_dir.attributes[0], time_or_interval_str=dynamic_surf_dir.date_strings[0]) - # print(f"{type(dyn_surf)=}") - - def main() -> None: print("## Running dev_sumo_access_test_driver") print("## =================================================") @@ -130,14 +106,11 @@ def main() -> None: print("\n\n") print(iterations) - iteration_name = iterations[0].name + # iteration_name = iterations[0].name # summary_access = SummaryAccess(access_token=access_token, case_uuid=sumo_case_id, iteration_name=iteration_name) # test_summary_access(summary_access) - surface_access = SurfaceAccess(access_token=access_token, case_uuid=sumo_case_id, iteration_name=iteration_name) - test_surface_access(surface_access) - # Running: # python -m src.services.sumo_access.dev_sumo_access_test_driver diff --git a/backend/src/services/sumo_access/generic_types.py b/backend/src/services/sumo_access/generic_types.py index 5109d651b..a0de8649a 100644 --- a/backend/src/services/sumo_access/generic_types.py +++ b/backend/src/services/sumo_access/generic_types.py @@ -65,7 +65,10 @@ class SumoContent(str, Enum): DEPTH = "depth" TIME = "time" PROPERTY = "property" - WELLPICKS = "wellpicks" + SEISMIC = "seismic" + THICKNESS = "thickness" + ISOCHORE = "isochore" + FLUID_CONTACT = "fluid_contact" @classmethod def values(cls) -> List[str]: diff --git a/backend/src/services/sumo_access/surface_access.py b/backend/src/services/sumo_access/surface_access.py index af0e44f26..561c33b2b 100644 --- a/backend/src/services/sumo_access/surface_access.py +++ b/backend/src/services/sumo_access/surface_access.py @@ -1,6 +1,6 @@ import logging from io import BytesIO -from typing import List, Optional, Tuple +from typing import List, Optional import xtgeo from fmu.sumo.explorer import TimeFilter, TimeType @@ -11,7 +11,7 @@ from src.services.utils.statistic_function import StatisticFunction from ._helpers import create_sumo_client_instance -from .surface_types import DynamicSurfaceDirectory, StaticSurfaceDirectory +from .surface_types import SurfaceMeta from .generic_types import SumoContent LOGGER = logging.getLogger(__name__) @@ -24,146 +24,73 @@ def __init__(self, access_token: str, case_uuid: str, iteration_name: str): self._iteration_name = iteration_name self._sumo_case_obj: Optional[Case] = None - def get_dynamic_surf_dir(self) -> DynamicSurfaceDirectory: - """ - Get a directory of surface names, attributes and date strings for simulated dynamic surfaces. - These are the non-observed surfaces that DO have time stamps or time intervals - """ - timer = PerfTimer() - - LOGGER.debug("Getting data for dynamic surface directory...") - + def get_surface_directory(self) -> List[SurfaceMeta]: case = self._get_my_sumo_case_obj() - - filter_with_timestamp_or_interval = TimeFilter(TimeType.ALL) surface_collection: SurfaceCollection = case.surfaces.filter( iteration=self._iteration_name, aggregation=False, - time=filter_with_timestamp_or_interval, - ) - - names = sorted(surface_collection.names) - attributes = sorted(surface_collection.tagnames) - timestamps: List[str] = surface_collection.timestamps - intervals: List[Tuple[str, str]] = surface_collection.intervals - available_contents = list(set(surf["data"]["content"] for surf in surface_collection)) - - LOGGER.debug(f"available surface contents: {available_contents}") - - # ISO 8601 recommends '/' as separator, alternatively '--' - # https://en.wikipedia.org/wiki/ISO_8601#Time_intervals - intervals_as_strings: List[str] = [f"{interval[0]}--{interval[1]}" for interval in intervals] - - date_strings: List[str] = [] - date_strings.extend(timestamps) - date_strings.extend(intervals_as_strings) - - surf_dir = DynamicSurfaceDirectory(names=names, attributes=attributes, date_strings=date_strings) - - LOGGER.debug(f"Downloaded and built dynamic surface directory in: {timer.elapsed_ms():}ms") - - return surf_dir - - def get_static_surf_dir(self, content_filter: Optional[List[SumoContent]] = None) -> StaticSurfaceDirectory: - """ - Get a directory of surface names and attributes for static surfaces. - These are the non-observed surfaces that do NOT have time stamps - """ - timer = PerfTimer() - - LOGGER.debug("Getting data for static surface directory...") - - case = self._get_my_sumo_case_obj() - - filter_no_time_data = TimeFilter(TimeType.NONE) - surface_collection: SurfaceCollection = case.surfaces.filter( - iteration=self._iteration_name, - aggregation=False, - time=filter_no_time_data, realization=0, ) - names = sorted(surface_collection.names) - attributes = sorted(surface_collection.tagnames) - available_contents = list(set(surf["data"]["content"] for surf in surface_collection)) - - LOGGER.debug(f"available surface contents: {available_contents}") - - if content_filter is not None: - if not any(SumoContent.has(content) for content in content_filter): - raise ValueError(f"Invalid content filter: {content_filter}") - surfaces_with_filtered_content = [ - surf for surf in surface_collection if surf["data"]["content"] in content_filter - ] - - names = sorted(list(set(surf.name for surf in surfaces_with_filtered_content))) - attributes = sorted(list(set(surf.tagname for surf in surfaces_with_filtered_content))) - - else: - names = sorted(surface_collection.names) - attributes = sorted(surface_collection.tagnames) - - LOGGER.debug( - f"Build valid name/attribute combinations for static surface directory " - f"(num names={len(names)}, num attributes={len(attributes)})..." - ) - - valid_attributes_for_name: List[List[int]] = [] - - for name in names: - filtered_coll = surface_collection.filter(name=name) - filtered_attributes = [tagname for tagname in filtered_coll.tagnames if tagname in attributes] - attribute_indices: List[int] = [] - for attr in filtered_attributes: - attr_idx = attributes.index(attr) - attribute_indices.append(attr_idx) - - valid_attributes_for_name.append(attribute_indices) - - surf_dir = StaticSurfaceDirectory( - names=names, - attributes=attributes, - valid_attributes_for_name=valid_attributes_for_name, - ) + surfs: List[SurfaceMeta] = [] + for surf in surface_collection: + iso_string_or_time_interval = None + + t_start = surf["data"].get("time", {}).get("t0", {}).get("value", None) + t_end = surf["data"].get("time", {}).get("t1", {}).get("value", None) + if t_start and not t_end: + iso_string_or_time_interval = t_start + if t_start and t_end: + iso_string_or_time_interval = f"{t_start}/{t_end}" + + surf_meta = SurfaceMeta( + name=surf["data"]["name"], + tagname=surf["data"].get("tagname", "Unknown"), + iso_date_or_interval=iso_string_or_time_interval, + content=surf["data"].get("content", SumoContent.DEPTH), + is_observation=surf["data"]["is_observation"], + is_stratigraphic=surf["data"]["stratigraphic"], + zmin=surf["data"]["bbox"]["zmin"], + zmax=surf["data"]["bbox"]["zmax"], + ) - LOGGER.debug(f"Downloaded and built static surface directory in: {timer.elapsed_ms():}ms") + surfs.append(surf_meta) - return surf_dir + return surfs - def get_dynamic_surf( - self, real_num: int, name: str, attribute: str, time_or_interval_str: str + def get_realization_surface_data( + self, real_num: int, name: str, attribute: str, time_or_interval_str: Optional[str] = None ) -> Optional[xtgeo.RegularSurface]: """ - Get actual surface data for a simulated dynamic surface + Get surface data for a realization surface """ timer = PerfTimer() addr_str = self._make_addr_str(real_num, name, attribute, time_or_interval_str) - # Must be either a string containing a time stamp or a time interval - if not time_or_interval_str or len(time_or_interval_str) < 1: - raise ValueError("time_or_interval_str must contain a non-empty string") + if time_or_interval_str is None: + time_filter = TimeFilter(TimeType.NONE) - timestamp_arr = time_or_interval_str.split("--", 1) - if len(timestamp_arr) == 0 or len(timestamp_arr) > 2: - raise ValueError("time_or_interval_str must contain a single timestamp or interval") + else: + timestamp_arr = time_or_interval_str.split("/", 1) + if len(timestamp_arr) == 0 or len(timestamp_arr) > 2: + raise ValueError("time_or_interval_str must contain a single timestamp or interval") + if len(timestamp_arr) == 1: + time_filter = TimeFilter( + TimeType.TIMESTAMP, + start=timestamp_arr[0], + end=timestamp_arr[0], + exact=True, + ) + else: + time_filter = TimeFilter( + TimeType.INTERVAL, + start=timestamp_arr[0], + end=timestamp_arr[1], + exact=True, + ) case = self._get_my_sumo_case_obj() - if len(timestamp_arr) == 1: - time_filter = TimeFilter( - TimeType.TIMESTAMP, - start=timestamp_arr[0], - end=timestamp_arr[0], - exact=True, - ) - else: - time_filter = TimeFilter( - TimeType.INTERVAL, - start=timestamp_arr[0], - end=timestamp_arr[1], - exact=True, - ) - surface_collection = case.surfaces.filter( iteration=self._iteration_name, aggregation=False, @@ -175,7 +102,7 @@ def get_dynamic_surf( surf_count = len(surface_collection) if surf_count == 0: - LOGGER.warning(f"No dynamic surface found in Sumo for {addr_str}") + LOGGER.warning(f"No realization surface found in Sumo for {addr_str}") return None if surf_count > 1: LOGGER.warning(f"Multiple ({surf_count}) surfaces found in Sumo for: {addr_str}. Returning first surface.") @@ -184,141 +111,59 @@ def get_dynamic_surf( byte_stream: BytesIO = sumo_surf.blob xtgeo_surf = xtgeo.surface_from_file(byte_stream) - LOGGER.debug(f"Got dynamic surface from Sumo in: {timer.elapsed_ms()}ms ({addr_str})") + LOGGER.debug(f"Got realization surface from Sumo in: {timer.elapsed_ms()}ms ({addr_str})") return xtgeo_surf - def get_statistical_dynamic_surf( + def get_statistical_surface_data( self, statistic_function: StatisticFunction, name: str, attribute: str, - time_or_interval_str: str, + time_or_interval_str: Optional[str] = None, ) -> Optional[xtgeo.RegularSurface]: """ - Compute statistic and return surface data for a dynamic surface + Compute statistic and return surface data """ timer = PerfTimer() addr_str = self._make_addr_str(-1, name, attribute, time_or_interval_str) - # Must be either a string containing a time stamp or a time interval - if not time_or_interval_str or len(time_or_interval_str) < 1: - raise ValueError("time_or_interval_str must contain a non-empty string") - - timestamp_arr = time_or_interval_str.split("--", 1) - if len(timestamp_arr) == 0 or len(timestamp_arr) > 2: - raise ValueError("time_or_interval_str must contain a single timestamp or interval") + if time_or_interval_str is None: + time_filter = TimeFilter(TimeType.NONE) - case = self._get_my_sumo_case_obj() - et_get_case_ms = timer.lap_ms() - - if len(timestamp_arr) == 1: - time_filter = TimeFilter( - TimeType.TIMESTAMP, - start=timestamp_arr[0], - end=timestamp_arr[0], - exact=True, - ) else: - time_filter = TimeFilter( - TimeType.INTERVAL, - start=timestamp_arr[0], - end=timestamp_arr[1], - exact=True, - ) - - surface_collection = case.surfaces.filter( - iteration=self._iteration_name, - aggregation=False, - name=name, - tagname=attribute, - time=time_filter, - ) - et_collect_surfaces_ms = timer.lap_ms() - - surf_count = len(surface_collection) - if surf_count == 0: - LOGGER.warning(f"No dynamic surfaces found in Sumo for {addr_str}") - return None - - realizations = surface_collection.realizations - - xtgeo_surf = _compute_statistical_surface(statistic_function, surface_collection) - et_calc_stat_ms = timer.lap_ms() - - if not xtgeo_surf: - LOGGER.warning(f"Could not calculate dynamic statistical surface using Sumo for {addr_str}") - return None - - LOGGER.debug( - f"Calculated dynamic statistical surface using Sumo in: {timer.elapsed_ms()}ms (" - f"get_case={et_get_case_ms}ms, " - f"collect_surfaces={et_collect_surfaces_ms}ms, " - f"calc_stat={et_calc_stat_ms}ms) " - f"({addr_str} {len(realizations)=} )" - ) - - return xtgeo_surf - - def get_static_surf(self, real_num: int, name: str, attribute: str) -> Optional[xtgeo.RegularSurface]: - """ - Get actual surface data for a static surface - """ - timer = PerfTimer() - addr_str = self._make_addr_str(real_num, name, attribute, None) - - case = self._get_my_sumo_case_obj() - - filter_no_time_data = TimeFilter(TimeType.NONE) - surface_collection = case.surfaces.filter( - iteration=self._iteration_name, - aggregation=False, - realization=real_num, - name=name, - tagname=attribute, - time=filter_no_time_data, - ) - - surf_count = len(surface_collection) - if surf_count == 0: - LOGGER.warning(f"No static surface found in Sumo for {addr_str}") - return None - if surf_count > 1: - LOGGER.warning(f"Multiple ({surf_count}) surfaces found in Sumo for: {addr_str}. Returning first surface.") - - sumo_surf = surface_collection[0] - byte_stream: BytesIO = sumo_surf.blob - xtgeo_surf = xtgeo.surface_from_file(byte_stream) - - LOGGER.debug(f"Got static surface from Sumo in: {timer.elapsed_ms()}ms ({addr_str})") - - return xtgeo_surf - - def get_statistical_static_surf( - self, statistic_function: StatisticFunction, name: str, attribute: str - ) -> Optional[xtgeo.RegularSurface]: - """ - Compute statistic and return surface data for a static surface - """ - timer = PerfTimer() - addr_str = self._make_addr_str(-1, name, attribute, None) - + timestamp_arr = time_or_interval_str.split("/", 1) + if len(timestamp_arr) == 0 or len(timestamp_arr) > 2: + raise ValueError("time_or_interval_str must contain a single timestamp or interval") + if len(timestamp_arr) == 1: + time_filter = TimeFilter( + TimeType.TIMESTAMP, + start=timestamp_arr[0], + end=timestamp_arr[0], + exact=True, + ) + else: + time_filter = TimeFilter( + TimeType.INTERVAL, + start=timestamp_arr[0], + end=timestamp_arr[1], + exact=True, + ) case = self._get_my_sumo_case_obj() et_get_case_ms = timer.lap_ms() - filter_no_time_data = TimeFilter(TimeType.NONE) surface_collection = case.surfaces.filter( iteration=self._iteration_name, aggregation=False, name=name, tagname=attribute, - time=filter_no_time_data, + time=time_filter, ) et_collect_surfaces_ms = timer.lap_ms() surf_count = len(surface_collection) if surf_count == 0: - LOGGER.warning(f"No static surfaces found in Sumo for {addr_str}") + LOGGER.warning(f"No statistical surfaces found in Sumo for {addr_str}") return None realizations = surface_collection.realizations @@ -327,11 +172,11 @@ def get_statistical_static_surf( et_calc_stat_ms = timer.lap_ms() if not xtgeo_surf: - LOGGER.warning(f"Could not calculate static statistical surface using Sumo for {addr_str}") + LOGGER.warning(f"Could not calculate statistical surface using Sumo for {addr_str}") return None LOGGER.debug( - f"Calculated static statistical surface using Sumo in: {timer.elapsed_ms()}ms (" + f"Calculated statistical surface using Sumo in: {timer.elapsed_ms()}ms (" f"get_case={et_get_case_ms}ms, " f"collect_surfaces={et_collect_surfaces_ms}ms, " f"calc_stat={et_calc_stat_ms}ms) " diff --git a/backend/src/services/sumo_access/surface_types.py b/backend/src/services/sumo_access/surface_types.py index 332da8064..a4b6867b4 100644 --- a/backend/src/services/sumo_access/surface_types.py +++ b/backend/src/services/sumo_access/surface_types.py @@ -1,25 +1,17 @@ from __future__ import annotations -from typing import List +from typing import Optional from pydantic import BaseModel - - -class DynamicSurfaceDirectory(BaseModel): - names: List[str] - attributes: List[str] - date_strings: List[str] - - @classmethod - def create_empty(cls) -> DynamicSurfaceDirectory: - return cls(attributes=[], names=[], date_strings=[]) - - -class StaticSurfaceDirectory(BaseModel): - names: List[str] - attributes: List[str] - valid_attributes_for_name: List[List[int]] - - @classmethod - def create_empty(cls) -> StaticSurfaceDirectory: - return cls(attributes=[], names=[], valid_attributes_for_name=[]) +from .generic_types import SumoContent + + +class SurfaceMeta(BaseModel): + name: str + tagname: str + iso_date_or_interval: Optional[str] + content: SumoContent + is_observation: bool + is_stratigraphic: bool + zmin: Optional[float] + zmax: Optional[float] diff --git a/backend/tests/unit/services/smda_access/test_stratigraphy_utils.py b/backend/tests/unit/services/smda_access/test_stratigraphy_utils.py new file mode 100644 index 000000000..13eab9066 --- /dev/null +++ b/backend/tests/unit/services/smda_access/test_stratigraphy_utils.py @@ -0,0 +1,59 @@ +from typing import List +import pytest + +from services.smda_access.stratigraphy_utils import ( + sort_stratigraphic_names_by_hierarchy, + sort_stratigraphic_units_by_hierarchy, +) +from services.smda_access.types import StratigraphicUnit, StratigraphicSurface, StratigraphicFeature +from services.smda_access.mocked_drogon_smda_access._mocked_stratigraphy_access import DROGON_STRAT_UNITS + + +@pytest.mark.parametrize( + "strat_units, expected_output", + [ + ( + DROGON_STRAT_UNITS, + [DROGON_STRAT_UNITS[3], DROGON_STRAT_UNITS[0], DROGON_STRAT_UNITS[1], DROGON_STRAT_UNITS[2]], + ), + ], +) +def test_sort_stratigraphic_units_by_hierarchy( + strat_units: List[StratigraphicUnit], expected_output: List[StratigraphicUnit] +) -> None: + sorted_units = sort_stratigraphic_units_by_hierarchy(strat_units) + assert sorted_units == expected_output + + +@pytest.mark.parametrize( + "strat_units, expected_output", + [ + ( + DROGON_STRAT_UNITS, + [ + StratigraphicSurface(name="VOLANTIS GP. Top", feature=StratigraphicFeature.HORIZON), + StratigraphicSurface(name="VOLANTIS GP.", feature=StratigraphicFeature.ZONE), + StratigraphicSurface(name="Valysar Fm. Top", feature=StratigraphicFeature.HORIZON), + StratigraphicSurface(name="Valysar Fm.", feature=StratigraphicFeature.ZONE), + StratigraphicSurface(name="Valysar Fm. Base", feature=StratigraphicFeature.HORIZON), + StratigraphicSurface(name="Therys Fm. Top", feature=StratigraphicFeature.HORIZON), + StratigraphicSurface(name="Therys Fm.", feature=StratigraphicFeature.ZONE), + StratigraphicSurface(name="Therys Fm. Base", feature=StratigraphicFeature.HORIZON), + StratigraphicSurface(name="Volon Fm. Top", feature=StratigraphicFeature.HORIZON), + StratigraphicSurface(name="Volon Fm.", feature=StratigraphicFeature.ZONE), + StratigraphicSurface(name="Volon Fm. Base", feature=StratigraphicFeature.HORIZON), + StratigraphicSurface(name="VOLANTIS GP. Base", feature=StratigraphicFeature.HORIZON), + ], + ), + ], +) +def test_sort_stratigraphic_names_by_hierarchy( + strat_units: List[StratigraphicUnit], expected_output: List[StratigraphicUnit] +) -> None: + sorted_surfaces = sort_stratigraphic_names_by_hierarchy(strat_units) + sorted_surface_names = [surf.name for surf in sorted_surfaces] + expected_surface_names = [surf.name for surf in expected_output] + assert sorted_surface_names == expected_surface_names + sorted_features = [surf.feature for surf in sorted_surfaces] + expected_features = [surf.feature for surf in expected_output] + assert sorted_features == expected_features diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 174ba475c..8cef83525 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -13,7 +13,6 @@ export type { B64EncodedNumpyArray as B64EncodedNumpyArray_api } from './models/ export type { Body_get_realizations_response as Body_get_realizations_response_api } from './models/Body_get_realizations_response'; export type { CaseInfo as CaseInfo_api } from './models/CaseInfo'; export type { Completions as Completions_api } from './models/Completions'; -export type { DynamicSurfaceDirectory as DynamicSurfaceDirectory_api } from './models/DynamicSurfaceDirectory'; export type { EnsembleDetails as EnsembleDetails_api } from './models/EnsembleDetails'; export type { EnsembleInfo as EnsembleInfo_api } from './models/EnsembleInfo'; export type { EnsembleParameter as EnsembleParameter_api } from './models/EnsembleParameter'; @@ -31,11 +30,12 @@ export type { InplaceVolumetricsTableMetaData as InplaceVolumetricsTableMetaData export type { PolygonData as PolygonData_api } from './models/PolygonData'; export type { PvtData as PvtData_api } from './models/PvtData'; export { SensitivityType as SensitivityType_api } from './models/SensitivityType'; -export type { StaticSurfaceDirectory as StaticSurfaceDirectory_api } from './models/StaticSurfaceDirectory'; export { StatisticFunction as StatisticFunction_api } from './models/StatisticFunction'; export type { StatisticValueObject as StatisticValueObject_api } from './models/StatisticValueObject'; -export { SumoContent as SumoContent_api } from './models/SumoContent'; +export { StratigraphicFeature as StratigraphicFeature_api } from './models/StratigraphicFeature'; +export { SurfaceAttributeType as SurfaceAttributeType_api } from './models/SurfaceAttributeType'; export type { SurfaceData as SurfaceData_api } from './models/SurfaceData'; +export type { SurfaceMeta as SurfaceMeta_api } from './models/SurfaceMeta'; export type { SurfacePolygonDirectory as SurfacePolygonDirectory_api } from './models/SurfacePolygonDirectory'; export { SurfaceStatisticFunction as SurfaceStatisticFunction_api } from './models/SurfaceStatisticFunction'; export type { UserInfo as UserInfo_api } from './models/UserInfo'; diff --git a/frontend/src/api/models/DynamicSurfaceDirectory.ts b/frontend/src/api/models/DynamicSurfaceDirectory.ts deleted file mode 100644 index 3bc6a5c35..000000000 --- a/frontend/src/api/models/DynamicSurfaceDirectory.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ - -export type DynamicSurfaceDirectory = { - names: Array; - attributes: Array; - time_or_interval_strings: Array; -}; - diff --git a/frontend/src/api/models/StaticSurfaceDirectory.ts b/frontend/src/api/models/StaticSurfaceDirectory.ts deleted file mode 100644 index e1f0fd4bf..000000000 --- a/frontend/src/api/models/StaticSurfaceDirectory.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ - -export type StaticSurfaceDirectory = { - names: Array; - attributes: Array; - valid_attributes_for_name: Array>; -}; - diff --git a/frontend/src/api/models/StratigraphicFeature.ts b/frontend/src/api/models/StratigraphicFeature.ts new file mode 100644 index 000000000..2c0090f6d --- /dev/null +++ b/frontend/src/api/models/StratigraphicFeature.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * The stratigraphic feature of a surface + */ +export enum StratigraphicFeature { + ZONE = 'zone', + HORIZON = 'horizon', +} diff --git a/frontend/src/api/models/SumoContent.ts b/frontend/src/api/models/SumoContent.ts deleted file mode 100644 index 28ef51411..000000000 --- a/frontend/src/api/models/SumoContent.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ - -/** - * Enum for the different values of the `content` metadata key in a Sumo object. - */ -export enum SumoContent { - DEPTH = 'depth', - TIME = 'time', - PROPERTY = 'property', - WELLPICKS = 'wellpicks', -} diff --git a/frontend/src/api/models/SurfaceAttributeType.ts b/frontend/src/api/models/SurfaceAttributeType.ts new file mode 100644 index 000000000..0e88abaea --- /dev/null +++ b/frontend/src/api/models/SurfaceAttributeType.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * A surface has a single array with values, e.g. depth, time, property, seismic, thickness. + * Only surfaces with depth and time have z-values that can be plotted in 3D. + * The other attributes are scalar values that can be plotted in 2D or used as colormapping for 3D surfaces. + * + * Ideally if the attribute is a scalar, there should be corresponding z-values, but this information is not + * available in the metadata. + * + * To be revisited later when the metadata is more mature. + */ +export enum SurfaceAttributeType { + DEPTH = 'depth', + TIME = 'time', + PROPERTY = 'property', + SEISMIC = 'seismic', + THICKNESS = 'thickness', + ISOCHORE = 'isochore', + FLUID_CONTACT = 'fluid_contact', +} diff --git a/frontend/src/api/models/SurfaceMeta.ts b/frontend/src/api/models/SurfaceMeta.ts new file mode 100644 index 000000000..998567c22 --- /dev/null +++ b/frontend/src/api/models/SurfaceMeta.ts @@ -0,0 +1,22 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { StratigraphicFeature } from './StratigraphicFeature'; +import type { SurfaceAttributeType } from './SurfaceAttributeType'; + +export type SurfaceMeta = { + name: string; + name_is_stratigraphic_offical: boolean; + stratigraphic_identifier: (string | null); + relative_stratigraphic_level: (number | null); + parent_stratigraphic_identifier: (string | null); + stratigraphic_feature: (StratigraphicFeature | null); + attribute_name: string; + attribute_type: SurfaceAttributeType; + iso_date_or_interval: (string | null); + is_observation: boolean; + value_min: (number | null); + value_max: (number | null); +}; + diff --git a/frontend/src/api/services/SurfaceService.ts b/frontend/src/api/services/SurfaceService.ts index d6c5aedc4..2eb47a6bc 100644 --- a/frontend/src/api/services/SurfaceService.ts +++ b/frontend/src/api/services/SurfaceService.ts @@ -1,10 +1,8 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -import type { DynamicSurfaceDirectory } from '../models/DynamicSurfaceDirectory'; -import type { StaticSurfaceDirectory } from '../models/StaticSurfaceDirectory'; -import type { SumoContent } from '../models/SumoContent'; import type { SurfaceData } from '../models/SurfaceData'; +import type { SurfaceMeta } from '../models/SurfaceMeta'; import type { SurfaceStatisticFunction } from '../models/SurfaceStatisticFunction'; import type { CancelablePromise } from '../core/CancelablePromise'; @@ -15,20 +13,20 @@ export class SurfaceService { constructor(public readonly httpRequest: BaseHttpRequest) {} /** - * Get Dynamic Surface Directory - * Get a directory of surface names, attributes and time/interval strings for simulated dynamic surfaces. + * Get Surface Directory + * Get a directory of surfaces in a Sumo ensemble * @param caseUuid Sumo case uuid * @param ensembleName Ensemble name - * @returns DynamicSurfaceDirectory Successful Response + * @returns SurfaceMeta Successful Response * @throws ApiError */ - public getDynamicSurfaceDirectory( + public getSurfaceDirectory( caseUuid: string, ensembleName: string, - ): CancelablePromise { + ): CancelablePromise> { return this.httpRequest.request({ method: 'GET', - url: '/surface/dynamic_surface_directory/', + url: '/surface/surface_directory/', query: { 'case_uuid': caseUuid, 'ensemble_name': ensembleName, @@ -40,27 +38,34 @@ export class SurfaceService { } /** - * Get Static Surface Directory - * Get a directory of surface names and attributes for static surfaces. - * These are the non-observed surfaces that do NOT have time stamps + * Get Realization Surface Data * @param caseUuid Sumo case uuid * @param ensembleName Ensemble name - * @param sumoContentFilter Optional filter by Sumo content type - * @returns StaticSurfaceDirectory Successful Response + * @param realizationNum Realization number + * @param name Surface name + * @param attribute Surface attribute + * @param timeOrInterval Time point or time interval string + * @returns SurfaceData Successful Response * @throws ApiError */ - public getStaticSurfaceDirectory( + public getRealizationSurfaceData( caseUuid: string, ensembleName: string, - sumoContentFilter?: Array, - ): CancelablePromise { + realizationNum: number, + name: string, + attribute: string, + timeOrInterval?: (string | null), + ): CancelablePromise { return this.httpRequest.request({ method: 'GET', - url: '/surface/static_surface_directory/', + url: '/surface/realization_surface_data/', query: { 'case_uuid': caseUuid, 'ensemble_name': ensembleName, - 'sumo_content_filter': sumoContentFilter, + 'realization_num': realizationNum, + 'name': name, + 'attribute': attribute, + 'time_or_interval': timeOrInterval, }, errors: { 422: `Validation Error`, @@ -69,31 +74,34 @@ export class SurfaceService { } /** - * Get Static Surface Data + * Get Statistical Surface Data * @param caseUuid Sumo case uuid * @param ensembleName Ensemble name - * @param realizationNum Realization number + * @param statisticFunction Statistics to calculate * @param name Surface name * @param attribute Surface attribute + * @param timeOrInterval Time point or time interval string * @returns SurfaceData Successful Response * @throws ApiError */ - public getStaticSurfaceData( + public getStatisticalSurfaceData( caseUuid: string, ensembleName: string, - realizationNum: number, + statisticFunction: SurfaceStatisticFunction, name: string, attribute: string, + timeOrInterval?: (string | null), ): CancelablePromise { return this.httpRequest.request({ method: 'GET', - url: '/surface/static_surface_data/', + url: '/surface/statistical_surface_data/', query: { 'case_uuid': caseUuid, 'ensemble_name': ensembleName, - 'realization_num': realizationNum, + 'statistic_function': statisticFunction, 'name': name, 'attribute': attribute, + 'time_or_interval': timeOrInterval, }, errors: { 422: `Validation Error`, @@ -111,6 +119,7 @@ export class SurfaceService { * @param realizationNumProperty Realization number * @param nameProperty Surface name * @param attributeProperty Surface attribute + * @param timeOrIntervalProperty Time point or time interval string * @returns SurfaceData Successful Response * @throws ApiError */ @@ -123,6 +132,7 @@ export class SurfaceService { realizationNumProperty: number, nameProperty: string, attributeProperty: string, + timeOrIntervalProperty?: (string | null), ): CancelablePromise { return this.httpRequest.request({ method: 'GET', @@ -136,6 +146,7 @@ export class SurfaceService { 'realization_num_property': realizationNumProperty, 'name_property': nameProperty, 'attribute_property': attributeProperty, + 'time_or_interval_property': timeOrIntervalProperty, }, errors: { 422: `Validation Error`, @@ -152,6 +163,7 @@ export class SurfaceService { * @param attributeMesh Surface attribute * @param nameProperty Surface name * @param attributeProperty Surface attribute + * @param timeOrIntervalProperty Time point or time interval string * @returns SurfaceData Successful Response * @throws ApiError */ @@ -163,6 +175,7 @@ export class SurfaceService { attributeMesh: string, nameProperty: string, attributeProperty: string, + timeOrIntervalProperty?: (string | null), ): CancelablePromise { return this.httpRequest.request({ method: 'GET', @@ -175,111 +188,7 @@ export class SurfaceService { 'attribute_mesh': attributeMesh, 'name_property': nameProperty, 'attribute_property': attributeProperty, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - - /** - * Get Dynamic Surface Data - * @param caseUuid Sumo case uuid - * @param ensembleName Ensemble name - * @param realizationNum Realization number - * @param name Surface name - * @param attribute Surface attribute - * @param timeOrInterval Timestamp or time interval string - * @returns SurfaceData Successful Response - * @throws ApiError - */ - public getDynamicSurfaceData( - caseUuid: string, - ensembleName: string, - realizationNum: number, - name: string, - attribute: string, - timeOrInterval: string, - ): CancelablePromise { - return this.httpRequest.request({ - method: 'GET', - url: '/surface/dynamic_surface_data/', - query: { - 'case_uuid': caseUuid, - 'ensemble_name': ensembleName, - 'realization_num': realizationNum, - 'name': name, - 'attribute': attribute, - 'time_or_interval': timeOrInterval, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - - /** - * Get Statistical Dynamic Surface Data - * @param caseUuid Sumo case uuid - * @param ensembleName Ensemble name - * @param statisticFunction Statistics to calculate - * @param name Surface name - * @param attribute Surface attribute - * @param timeOrInterval Timestamp or time interval string - * @returns SurfaceData Successful Response - * @throws ApiError - */ - public getStatisticalDynamicSurfaceData( - caseUuid: string, - ensembleName: string, - statisticFunction: SurfaceStatisticFunction, - name: string, - attribute: string, - timeOrInterval: string, - ): CancelablePromise { - return this.httpRequest.request({ - method: 'GET', - url: '/surface/statistical_dynamic_surface_data/', - query: { - 'case_uuid': caseUuid, - 'ensemble_name': ensembleName, - 'statistic_function': statisticFunction, - 'name': name, - 'attribute': attribute, - 'time_or_interval': timeOrInterval, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - - /** - * Get Statistical Static Surface Data - * @param caseUuid Sumo case uuid - * @param ensembleName Ensemble name - * @param statisticFunction Statistics to calculate - * @param name Surface name - * @param attribute Surface attribute - * @returns SurfaceData Successful Response - * @throws ApiError - */ - public getStatisticalStaticSurfaceData( - caseUuid: string, - ensembleName: string, - statisticFunction: SurfaceStatisticFunction, - name: string, - attribute: string, - ): CancelablePromise { - return this.httpRequest.request({ - method: 'GET', - url: '/surface/statistical_static_surface_data/', - query: { - 'case_uuid': caseUuid, - 'ensemble_name': ensembleName, - 'statistic_function': statisticFunction, - 'name': name, - 'attribute': attribute, + 'time_or_interval_property': timeOrIntervalProperty, }, errors: { 422: `Validation Error`, diff --git a/frontend/src/framework/utils/timestampUtils.ts b/frontend/src/framework/utils/timestampUtils.ts index 4aa143dfd..3bc5d7074 100644 --- a/frontend/src/framework/utils/timestampUtils.ts +++ b/frontend/src/framework/utils/timestampUtils.ts @@ -8,10 +8,17 @@ export function hasTime(isoDateTimeString: string): boolean { // Check if specified date-time string contains timezone information export function hasTimezone(isoDateTimeString: string): boolean { - const regex = /T.*(Z|[+-]\d{2})/ + const regex = /T.*(Z|[+-]\d{2})/; return regex.test(isoDateTimeString); } +export function isIsoStringInterval(input: string): boolean { + // Checks if the input string is a valid ISO 8601 interval string + //"2018-01-01T00:00:00/2018-07-01T00:00:00" + const regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/; + return regex.test(input); +} + // Convert ISO 8601 string to timestamp in milliseconds UTC // Note that for date-time strings that do not contain timezone information, this function assumes that the date-time string is in UTC export function isoStringToTimestampUtcMs(isoDateTimeString: string): number { @@ -42,7 +49,8 @@ export function timestampUtcMsToCompactIsoString(timestampUtcMs: number): string const fullIsoString = date.toISOString(); const hasMilliseconds = date.getUTCMilliseconds() !== 0; - const hasTime = date.getUTCHours() !== 0 || date.getUTCMinutes() !== 0 || date.getUTCSeconds() !== 0 || hasMilliseconds; + const hasTime = + date.getUTCHours() !== 0 || date.getUTCMinutes() !== 0 || date.getUTCSeconds() !== 0 || hasMilliseconds; if (!hasTime) { return fullIsoString.split("T")[0]; diff --git a/frontend/src/modules/Map/MapQueryHooks.ts b/frontend/src/modules/Map/MapQueryHooks.ts deleted file mode 100644 index 64851fb0d..000000000 --- a/frontend/src/modules/Map/MapQueryHooks.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { DynamicSurfaceDirectory_api, StaticSurfaceDirectory_api, SurfaceData_api } from "@api"; -import { apiService } from "@framework/ApiService"; -import { QueryFunction, QueryKey, UseQueryResult, useQuery } from "@tanstack/react-query"; - -import { SurfAddr } from "./SurfAddr"; - -const STALE_TIME = 60 * 1000; -const CACHE_TIME = 60 * 1000; - -export function useDynamicSurfaceDirectoryQuery( - caseUuid: string | undefined, - ensembleName: string | undefined, - allowEnable: boolean -): UseQueryResult { - return useQuery({ - queryKey: ["getDynamicSurfaceDirectory", caseUuid, ensembleName], - queryFn: () => apiService.surface.getDynamicSurfaceDirectory(caseUuid ?? "", ensembleName ?? ""), - staleTime: STALE_TIME, - cacheTime: STALE_TIME, - enabled: allowEnable && caseUuid && ensembleName ? true : false, - }); -} - -export function useStaticSurfaceDirectoryQuery( - caseUuid: string | undefined, - ensembleName: string | undefined, - allowEnable: boolean -): UseQueryResult { - return useQuery({ - queryKey: ["getStaticSurfaceDirectory", caseUuid, ensembleName], - queryFn: () => apiService.surface.getStaticSurfaceDirectory(caseUuid ?? "", ensembleName ?? ""), - staleTime: STALE_TIME, - cacheTime: STALE_TIME, - enabled: allowEnable && caseUuid && ensembleName ? true : false, - }); -} - -export function useSurfaceDataQueryByAddress(surfAddr: SurfAddr | null): UseQueryResult { - function dummyApiCall(): Promise { - return new Promise((_resolve, reject) => { - reject(null); - }); - } - - if (!surfAddr) { - return useQuery({ - queryKey: ["getSurfaceData_DUMMY_ALWAYS_DISABLED"], - queryFn: () => dummyApiCall, - enabled: false, - }); - } - - let queryFn: QueryFunction | null = null; - let queryKey: QueryKey | null = null; - - // Dynamic, per realization surface - if (surfAddr.addressType === "dynamic") { - queryKey = [ - "getDynamicSurfaceData", - surfAddr.caseUuid, - surfAddr.ensemble, - surfAddr.realizationNum, - surfAddr.name, - surfAddr.attribute, - surfAddr.timeOrInterval, - ]; - queryFn = () => - apiService.surface.getDynamicSurfaceData( - surfAddr.caseUuid, - surfAddr.ensemble, - surfAddr.realizationNum, - surfAddr.name, - surfAddr.attribute, - surfAddr.timeOrInterval - ); - } - - // Dynamic, statistical surface - else if (surfAddr.addressType === "statistical-dynamic") { - queryKey = [ - "getStatisticalDynamicSurfaceData", - surfAddr.caseUuid, - surfAddr.ensemble, - surfAddr.statisticFunction, - surfAddr.name, - surfAddr.attribute, - surfAddr.timeOrInterval, - ]; - queryFn = () => - apiService.surface.getStatisticalDynamicSurfaceData( - surfAddr.caseUuid, - surfAddr.ensemble, - surfAddr.statisticFunction, - surfAddr.name, - surfAddr.attribute, - surfAddr.timeOrInterval - ); - } - - // Static, per realization surface - else if (surfAddr.addressType === "static") { - queryKey = [ - "getStaticSurfaceData", - surfAddr.caseUuid, - surfAddr.ensemble, - surfAddr.realizationNum, - surfAddr.name, - surfAddr.attribute, - ]; - queryFn = () => - apiService.surface.getStaticSurfaceData( - surfAddr.caseUuid, - surfAddr.ensemble, - surfAddr.realizationNum, - surfAddr.name, - surfAddr.attribute - ); - } - - // Static, statistical surface - else if (surfAddr.addressType === "statistical-static") { - queryKey = [ - "getStatisticalStaticSurfaceData", - surfAddr.caseUuid, - surfAddr.ensemble, - surfAddr.statisticFunction, - surfAddr.name, - surfAddr.attribute, - ]; - queryFn = () => - apiService.surface.getStatisticalStaticSurfaceData( - surfAddr.caseUuid, - surfAddr.ensemble, - surfAddr.statisticFunction, - surfAddr.name, - surfAddr.attribute - ); - } else { - throw new Error("Invalid surface address type"); - } - - return useQuery({ - queryKey: queryKey, - queryFn: queryFn, - staleTime: STALE_TIME, - cacheTime: CACHE_TIME, - }); -} diff --git a/frontend/src/modules/Map/MapSettings.tsx b/frontend/src/modules/Map/MapSettings.tsx index c98be50a7..b3b64789b 100644 --- a/frontend/src/modules/Map/MapSettings.tsx +++ b/frontend/src/modules/Map/MapSettings.tsx @@ -1,6 +1,5 @@ import React from "react"; -import { DynamicSurfaceDirectory_api, StaticSurfaceDirectory_api } from "@api"; import { SurfaceStatisticFunction_api } from "@api"; import { EnsembleIdent } from "@framework/EnsembleIdent"; import { ModuleFCProps } from "@framework/Module"; @@ -13,13 +12,24 @@ import { Checkbox } from "@lib/components/Checkbox"; import { CircularProgress } from "@lib/components/CircularProgress"; import { Input } from "@lib/components/Input"; import { Label } from "@lib/components/Label"; +import { RadioGroup } from "@lib/components/RadioGroup"; import { Select, SelectOption } from "@lib/components/Select"; +import { + SurfaceAddress, + SurfaceAddressFactory, + SurfaceDirectory, + TimeType, + useSurfaceDirectoryQuery, +} from "@modules/_shared/Surface"; -import { useDynamicSurfaceDirectoryQuery, useStaticSurfaceDirectoryQuery } from "./MapQueryHooks"; import { MapState } from "./MapState"; -import { SurfAddr, SurfAddrFactory } from "./SurfAddr"; import { AggregationDropdown } from "./UiComponents"; +const TimeTypeEnumToStringMapping = { + [TimeType.None]: "Static", + [TimeType.TimePoint]: "Time point", + [TimeType.Interval]: "Time interval", +}; //----------------------------------------------------------------------------------------------------------- export function MapSettings(props: ModuleFCProps) { const myInstanceIdStr = props.moduleContext.getInstanceIdString(); @@ -27,14 +37,14 @@ export function MapSettings(props: ModuleFCProps) { const ensembleSet = useEnsembleSet(props.workbenchSession); const [selectedEnsembleIdent, setSelectedEnsembleIdent] = React.useState(null); + const [timeType, setTimeType] = React.useState(TimeType.None); - const [surfaceType, setSurfaceType] = React.useState<"static" | "dynamic">("dynamic"); const [selectedSurfaceName, setSelectedSurfaceName] = React.useState(null); const [selectedSurfaceAttribute, setSelectedSurfaceAttribute] = React.useState(null); const [realizationNum, setRealizationNum] = React.useState(0); const [selectedTimeOrInterval, setSelectedTimeOrInterval] = React.useState(null); const [aggregation, setAggregation] = React.useState(null); - + const [useObserved, toggleUseObserved] = React.useState(false); const syncedSettingKeys = props.moduleContext.useSyncedSettingKeys(); const syncHelper = new SyncSettingsHelper(syncedSettingKeys, props.workbenchServices); const syncedValueEnsembles = syncHelper.useValue(SyncSettingKey.ENSEMBLE, "global.syncValue.ensembles"); @@ -48,60 +58,33 @@ export function MapSettings(props: ModuleFCProps) { const candidateEnsembleIdent = maybeAssignFirstSyncedEnsemble(selectedEnsembleIdent, syncedValueEnsembles); const computedEnsembleIdent = fixupEnsembleIdent(candidateEnsembleIdent, ensembleSet); - - const dynamicSurfDirQuery = useDynamicSurfaceDirectoryQuery( - computedEnsembleIdent?.getCaseUuid(), - computedEnsembleIdent?.getEnsembleName(), - surfaceType === "dynamic" - ); - const staticSurfDirQuery = useStaticSurfaceDirectoryQuery( + const surfaceDirectoryQuery = useSurfaceDirectoryQuery( computedEnsembleIdent?.getCaseUuid(), - computedEnsembleIdent?.getEnsembleName(), - surfaceType === "static" + computedEnsembleIdent?.getEnsembleName() ); - let computedSurfaceName: string | null = null; - let computedSurfaceAttribute: string | null = null; - let computedTimeOrInterval: string | null = null; - if (surfaceType == "static" && staticSurfDirQuery.data) { - computedSurfaceName = fixupStringValueFromList(selectedSurfaceName, staticSurfDirQuery.data.names); - computedSurfaceAttribute = fixupStaticSurfAttribute( - computedSurfaceName, - selectedSurfaceAttribute, - staticSurfDirQuery.data - ); - computedTimeOrInterval = null; - - if (syncedValueSurface) { - if (isValidStaticSurf(syncedValueSurface.name, syncedValueSurface.attribute, staticSurfDirQuery.data)) { - computedSurfaceName = syncedValueSurface.name; - computedSurfaceAttribute = syncedValueSurface.attribute; - } - } - } - if (surfaceType == "dynamic" && dynamicSurfDirQuery.data) { - computedSurfaceName = fixupStringValueFromList(selectedSurfaceName, dynamicSurfDirQuery.data.names); - computedSurfaceAttribute = fixupStringValueFromList( - selectedSurfaceAttribute, - dynamicSurfDirQuery.data.attributes - ); - computedTimeOrInterval = fixupStringValueFromList( - selectedTimeOrInterval, - dynamicSurfDirQuery.data.time_or_interval_strings - ); + const surfaceDirectory = new SurfaceDirectory( + surfaceDirectoryQuery.data + ? { surfaceMetas: surfaceDirectoryQuery.data, timeType: timeType, useObservedSurfaces: useObserved } + : null + ); - if (syncedValueSurface) { - if (isValidDynamicSurf(syncedValueSurface.name, syncedValueSurface.attribute, dynamicSurfDirQuery.data)) { - computedSurfaceName = syncedValueSurface.name; - computedSurfaceAttribute = syncedValueSurface.attribute; - } - } - if (syncedValueDate) { - if (isValidDynamicSurfTimeOrInterval(syncedValueDate.timeOrInterval, dynamicSurfDirQuery.data)) { - computedTimeOrInterval = syncedValueDate.timeOrInterval; - } + const fixedSurfSpec = fixupSurface( + surfaceDirectory, + { + surfaceName: selectedSurfaceName, + surfaceAttribute: selectedSurfaceAttribute, + timeOrInterval: selectedTimeOrInterval, + }, + { + surfaceName: syncedValueSurface?.name || null, + surfaceAttribute: syncedValueSurface?.attribute || null, + timeOrInterval: syncedValueDate?.timeOrInterval || null, } - } + ); + const computedSurfaceName = fixedSurfSpec.surfaceName; + const computedSurfaceAttribute = fixedSurfSpec.surfaceAttribute; + const computedTimeOrInterval = fixedSurfSpec.timeOrInterval; if (computedEnsembleIdent && !computedEnsembleIdent.equals(selectedEnsembleIdent)) { setSelectedEnsembleIdent(computedEnsembleIdent); @@ -117,41 +100,24 @@ export function MapSettings(props: ModuleFCProps) { } React.useEffect(function propagateSurfaceSelectionToView() { - // console.debug("propagateSurfaceSelectionToView()"); - // console.debug(` caseUuid=${caseUuid}`); - // console.debug(` ensembleName=${ensembleName}`); - // console.debug(` surfaceName=${surfaceName}`); - // console.debug(` surfaceAttribute=${surfaceAttribute}`); - // console.debug(` surfaceType=${surfaceType}`); - // console.debug(` aggregation=${aggregation}`); - // console.debug(` realizationNum=${realizationNum}`); - // console.debug(` timeOrInterval=${timeOrInterval}`); - - let surfAddr: SurfAddr | null = null; + let surfaceAddress: SurfaceAddress | null = null; if (computedEnsembleIdent && computedSurfaceName && computedSurfaceAttribute) { - const addrFactory = new SurfAddrFactory( + const addrFactory = new SurfaceAddressFactory( computedEnsembleIdent.getCaseUuid(), computedEnsembleIdent.getEnsembleName(), computedSurfaceName, - computedSurfaceAttribute + computedSurfaceAttribute, + computedTimeOrInterval ); - if (surfaceType === "dynamic" && computedTimeOrInterval) { - if (aggregation === null) { - surfAddr = addrFactory.createDynamicAddr(realizationNum, computedTimeOrInterval); - } else { - surfAddr = addrFactory.createStatisticalDynamicAddr(aggregation, computedTimeOrInterval); - } - } else if (surfaceType === "static") { - if (aggregation === null) { - surfAddr = addrFactory.createStaticAddr(realizationNum); - } else { - surfAddr = addrFactory.createStatisticalStaticAddr(aggregation); - } + if (aggregation === null) { + surfaceAddress = addrFactory.createRealizationAddress(realizationNum); + } else { + surfaceAddress = addrFactory.createStatisticalAddress(aggregation); } } - console.debug(`propagateSurfaceSelectionToView() => ${surfAddr ? "valid surfAddr" : "NULL surfAddr"}`); - props.moduleContext.getStateStore().setValue("surfaceAddress", surfAddr); + console.debug(`propagateSurfaceSelectionToView() => ${surfaceAddress ? "valid surfAddr" : "NULL surfAddr"}`); + props.moduleContext.getStateStore().setValue("surfaceAddress", surfaceAddress); }); function handleEnsembleSelectionChange(newEnsembleIdent: EnsembleIdent | null) { @@ -162,11 +128,6 @@ export function MapSettings(props: ModuleFCProps) { } } - function handleStaticSurfacesCheckboxChanged(event: React.ChangeEvent, staticChecked: boolean) { - const newSurfType = staticChecked ? "static" : "dynamic"; - setSurfaceType(newSurfType); - } - function handleSurfNameSelectionChange(selectedSurfNames: string[]) { console.debug("handleSurfNameSelectionChange()"); const newName = selectedSurfNames[0] ?? null; @@ -191,9 +152,9 @@ export function MapSettings(props: ModuleFCProps) { } } - function handleTimeOrIntervalSelectionChange(selectedTimeOrIntervals: string[]) { + function handleTimeOrIntervalSelectionChange(selectedSurfTimeIntervals: string[]) { console.debug("handleTimeOrIntervalSelectionChange()"); - const newTimeOrInterval = selectedTimeOrIntervals[0] ?? null; + const newTimeOrInterval = selectedSurfTimeIntervals[0] ?? null; setSelectedTimeOrInterval(newTimeOrInterval); if (newTimeOrInterval) { syncHelper.publishValue(SyncSettingKey.DATE, "global.syncValue.date", { @@ -215,38 +176,33 @@ export function MapSettings(props: ModuleFCProps) { } } + function handleTimeModeChange(event: React.ChangeEvent) { + setTimeType(event.target.value as TimeType); + } + let surfNameOptions: SelectOption[] = []; let surfAttributeOptions: SelectOption[] = []; let timeOrIntervalOptions: SelectOption[] = []; - if (surfaceType == "static" && staticSurfDirQuery.data) { - const validAttrNames = getValidAttributesForSurfName(computedSurfaceName ?? "", staticSurfDirQuery.data); - surfNameOptions = staticSurfDirQuery.data.names.map((name) => ({ value: name, label: name })); - surfAttributeOptions = validAttrNames.map((attr) => ({ value: attr, label: attr })); - } else if (surfaceType == "dynamic" && dynamicSurfDirQuery.data) { - surfNameOptions = dynamicSurfDirQuery.data.names.map((name) => ({ value: name, label: name })); - surfAttributeOptions = dynamicSurfDirQuery.data.attributes.map((attr) => ({ value: attr, label: attr })); - timeOrIntervalOptions = dynamicSurfDirQuery.data.time_or_interval_strings.map((time) => ({ - value: time, - label: time, - })); - } - - let chooseTimeOrIntervalElement: JSX.Element | null = null; - if (surfaceType === "dynamic") { - chooseTimeOrIntervalElement = ( - + )} ) { // Helpers // ------------------------------------------------------------------------------------- -function getValidAttributesForSurfName(surfName: string, surfDir: StaticSurfaceDirectory_api): string[] { - const idxOfSurfName = surfDir.names.indexOf(surfName); - if (idxOfSurfName == -1) { - return []; - } - - const attrIndices = surfDir.valid_attributes_for_name[idxOfSurfName]; - const attrNames: string[] = []; - for (const idx of attrIndices) { - attrNames.push(surfDir.attributes[idx]); - } - - return attrNames; -} - -function fixupStringValueFromList(currValue: string | null, legalValues: string[] | null): string | null { - if (!legalValues || legalValues.length == 0) { - return null; +type PartialSurfSpec = { + surfaceName: string | null; + surfaceAttribute: string | null; + timeOrInterval: string | null; +}; + +function fixupSurface( + surfaceDirectory: SurfaceDirectory, + selectedSurface: PartialSurfSpec, + syncedSurface: PartialSurfSpec +): PartialSurfSpec { + const surfaceNames = surfaceDirectory.getSurfaceNames(null); + const finalSurfaceName = fixupSyncedOrSelectedOrFirstValue( + syncedSurface.surfaceName, + selectedSurface.surfaceName, + surfaceNames + ); + let finalSurfaceAttribute: string | null = null; + let finalTimeOrInterval: string | null = null; + if (finalSurfaceName) { + const surfaceAttributes = surfaceDirectory.getAttributeNames(finalSurfaceName); + finalSurfaceAttribute = fixupSyncedOrSelectedOrFirstValue( + syncedSurface.surfaceAttribute, + selectedSurface.surfaceAttribute, + surfaceAttributes + ); } - if (currValue && legalValues.includes(currValue)) { - return currValue; + if (finalSurfaceName && finalSurfaceAttribute) { + const selectedTimeOrIntervals = surfaceDirectory.getTimeOrIntervalStrings( + finalSurfaceName, + finalSurfaceAttribute + ); + finalTimeOrInterval = fixupSyncedOrSelectedOrFirstValue( + syncedSurface.timeOrInterval, + selectedSurface.timeOrInterval, + selectedTimeOrIntervals + ); } - - return legalValues[0]; + return { + surfaceName: finalSurfaceName, + surfaceAttribute: finalSurfaceAttribute, + timeOrInterval: finalTimeOrInterval, + }; } -function fixupStaticSurfAttribute( - surfName: string | null, - currAttribute: string | null, - surfDir: StaticSurfaceDirectory_api +function fixupSyncedOrSelectedOrFirstValue( + syncedValue: string | null, + selectedValue: string | null, + values: string[] ): string | null { - if (!surfName) { - return null; - } - const validAttrNames = getValidAttributesForSurfName(surfName, surfDir); - if (validAttrNames.length == 0) { - return null; - } - - if (currAttribute && validAttrNames.includes(currAttribute)) { - return currAttribute; + if (syncedValue && values.includes(syncedValue)) { + return syncedValue; } - - return validAttrNames[0]; -} - -function isValidStaticSurf( - surfName: string | null, - surfAttribute: string | null, - surfDir: StaticSurfaceDirectory_api -): boolean { - if (!surfName || !surfAttribute) { - return false; - } - - const validAttrNames = getValidAttributesForSurfName(surfName, surfDir); - if (validAttrNames.length == 0) { - return false; + if (selectedValue && values.includes(selectedValue)) { + return selectedValue; } - - if (!validAttrNames.includes(surfAttribute)) { - return false; + if (values.length) { + return values[0]; } - - return true; + return null; } -function isValidDynamicSurf( - surfName: string | null, - surfAttribute: string | null, - surfDir: DynamicSurfaceDirectory_api -): boolean { - if (!surfName || !surfAttribute) { - return false; - } - - if (!surfDir.names.includes(surfName)) { - return false; - } - if (!surfDir.attributes.includes(surfAttribute)) { - return false; - } - - return true; +function isoStringToDateLabel(input: string): string { + const date = input.split("T")[0]; + return `${date}`; } -function isValidDynamicSurfTimeOrInterval(timeOrInterval: string | null, surfDir: DynamicSurfaceDirectory_api): boolean { - if (!timeOrInterval || !surfDir) { - return false; - } - - if (!surfDir.time_or_interval_strings.includes(timeOrInterval)) { - return false; - } - - return true; +function isoIntervalStringToDateLabel(input: string): string { + const [start, end] = input.split("/"); + const startDate = start.split("T")[0]; + const endDate = end.split("T")[0]; + return `${startDate}/${endDate}`; } diff --git a/frontend/src/modules/Map/MapState.ts b/frontend/src/modules/Map/MapState.ts index cb7bd5b99..512686952 100644 --- a/frontend/src/modules/Map/MapState.ts +++ b/frontend/src/modules/Map/MapState.ts @@ -1,5 +1,5 @@ -import { SurfAddr } from "./SurfAddr"; +import { SurfaceAddress } from "@modules/_shared/Surface/surfaceAddress"; export interface MapState { - surfaceAddress: SurfAddr | null; + surfaceAddress: SurfaceAddress | null; } diff --git a/frontend/src/modules/Map/MapView.tsx b/frontend/src/modules/Map/MapView.tsx index 81f8d2774..37a5fcda7 100644 --- a/frontend/src/modules/Map/MapView.tsx +++ b/frontend/src/modules/Map/MapView.tsx @@ -1,24 +1,21 @@ import React from "react"; import { ModuleFCProps } from "@framework/Module"; +import { useSurfaceDataQueryByAddress } from "@modules/_shared/Surface"; import SubsurfaceViewer from "@webviz/subsurface-viewer"; -import { useSurfaceDataQueryByAddress } from "./MapQueryHooks"; import { MapState } from "./MapState"; -import { makeSurfAddrString } from "./SurfAddr"; //----------------------------------------------------------------------------------------------------------- export function MapView(props: ModuleFCProps) { - const surfAddr = props.moduleContext.useStoreValue("surfaceAddress"); + const surfaceAddress = props.moduleContext.useStoreValue("surfaceAddress"); const renderCount = React.useRef(0); React.useEffect(function incrementRenderCount() { renderCount.current = renderCount.current + 1; }); - console.debug(`render MapView, surfAddr=${surfAddr ? makeSurfAddrString(surfAddr) : "null"}`); - - const surfDataQuery = useSurfaceDataQueryByAddress(surfAddr); + const surfDataQuery = useSurfaceDataQueryByAddress(surfaceAddress); if (!surfDataQuery.data) { return
No data
; } @@ -49,7 +46,9 @@ export function MapView(props: ModuleFCProps) { }, ]} /> -
{props.moduleContext.getInstanceIdString()}
+
+ {props.moduleContext.getInstanceIdString()} +
); } diff --git a/frontend/src/modules/Map/SurfAddr.ts b/frontend/src/modules/Map/SurfAddr.ts deleted file mode 100644 index 53b58400b..000000000 --- a/frontend/src/modules/Map/SurfAddr.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { SurfaceStatisticFunction_api } from "@api"; - -export interface StaticSurfAddr { - addressType: "static"; - caseUuid: string; - ensemble: string; - name: string; - attribute: string; - realizationNum: number; -} - -export interface DynamicSurfAddr { - addressType: "dynamic"; - caseUuid: string; - ensemble: string; - name: string; - attribute: string; - realizationNum: number; - timeOrInterval: string; -} - -export interface StatisticalStaticSurfAddr { - addressType: "statistical-static"; - caseUuid: string; - ensemble: string; - name: string; - attribute: string; - statisticFunction: SurfaceStatisticFunction_api; -} - -export interface StatisticalDynamicSurfAddr { - addressType: "statistical-dynamic"; - caseUuid: string; - ensemble: string; - name: string; - attribute: string; - timeOrInterval: string; - statisticFunction: SurfaceStatisticFunction_api; -} - -export type SurfAddr = StaticSurfAddr | DynamicSurfAddr | StatisticalDynamicSurfAddr | StatisticalStaticSurfAddr; - -export function makeSurfAddrString(addr: SurfAddr): string { - const valueArr = Object.values(addr); - const str = valueArr.join("--"); - return str; -} - -export class SurfAddrFactory { - private _caseUuid: string; - private _ensemble: string; - private _name: string; - private _attribute: string; - - constructor(caseUuid: string, ensemble: string, name: string, attribute: string) { - this._caseUuid = caseUuid; - this._ensemble = ensemble; - this._name = name; - this._attribute = attribute; - } - - createDynamicAddr(realizationNum: number, timeOrInterval: string): DynamicSurfAddr { - return { - addressType: "dynamic", - caseUuid: this._caseUuid, - ensemble: this._ensemble, - name: this._name, - attribute: this._attribute, - realizationNum: realizationNum, - timeOrInterval: timeOrInterval, - }; - } - - createStaticAddr(realizationNum: number): StaticSurfAddr { - return { - addressType: "static", - caseUuid: this._caseUuid, - ensemble: this._ensemble, - name: this._name, - attribute: this._attribute, - realizationNum: realizationNum, - }; - } - - createStatisticalDynamicAddr( - statFunction: SurfaceStatisticFunction_api, - timeOrInterval: string - ): StatisticalDynamicSurfAddr { - return { - addressType: "statistical-dynamic", - caseUuid: this._caseUuid, - ensemble: this._ensemble, - name: this._name, - attribute: this._attribute, - timeOrInterval: timeOrInterval, - statisticFunction: statFunction, - }; - } - - createStatisticalStaticAddr(statFunction: SurfaceStatisticFunction_api): StatisticalStaticSurfAddr { - return { - addressType: "statistical-static", - caseUuid: this._caseUuid, - ensemble: this._ensemble, - name: this._name, - attribute: this._attribute, - statisticFunction: statFunction, - }; - } -} diff --git a/frontend/src/modules/SubsurfaceMap/queryHooks.tsx b/frontend/src/modules/SubsurfaceMap/queryHooks.tsx index fd58b5050..6b3f04968 100644 --- a/frontend/src/modules/SubsurfaceMap/queryHooks.tsx +++ b/frontend/src/modules/SubsurfaceMap/queryHooks.tsx @@ -1,35 +1,19 @@ import { PolygonData_api, - StaticSurfaceDirectory_api, - SumoContent_api, SurfaceData_api, SurfacePolygonDirectory_api, WellBoreHeader_api, WellBoreTrajectory_api, } from "@api"; import { apiService } from "@framework/ApiService"; +import { SurfaceAddress } from "@modules/_shared/Surface"; import { QueryFunction, QueryKey, UseQueryResult, useQuery } from "@tanstack/react-query"; -import { SurfAddr } from "./SurfaceAddress"; import { SurfacePolygonsAddress } from "./SurfacePolygonsAddress"; const STALE_TIME = 60 * 1000; const CACHE_TIME = 60 * 1000; -export function useSurfaceDirectoryQuery( - caseUuid: string | undefined, - ensembleName: string | undefined, - contentFilter?: SumoContent_api[] -): UseQueryResult { - return useQuery({ - queryKey: ["getStaticSurfaceDirectory", caseUuid, ensembleName, contentFilter], - queryFn: () => apiService.surface.getStaticSurfaceDirectory(caseUuid ?? "", ensembleName ?? "", contentFilter), - staleTime: STALE_TIME, - cacheTime: STALE_TIME, - enabled: caseUuid && ensembleName ? true : false, - }); -} - export function usePolygonDirectoryQuery( caseUuid: string | undefined, ensembleName: string | undefined @@ -72,80 +56,9 @@ export function useGetFieldWellsTrajectories(caseUuid: string | undefined): UseQ }); } -export function useSurfaceDataQueryByAddress( - surfAddr: SurfAddr | null, - enabled: boolean -): UseQueryResult { - function dummyApiCall(): Promise { - return new Promise((_resolve, reject) => { - reject(null); - }); - } - - if (!surfAddr) { - return useQuery({ - queryKey: ["getSurfaceData_DUMMY_ALWAYS_DISABLED"], - queryFn: () => dummyApiCall, - enabled: false, - }); - } - - let queryFn: QueryFunction | null = null; - let queryKey: QueryKey | null = null; - - // Static, per realization surface - if (surfAddr.addressType === "static") { - queryKey = [ - "getStaticSurfaceData", - surfAddr.caseUuid, - surfAddr.ensemble, - surfAddr.realizationNum, - surfAddr.name, - surfAddr.attribute, - ]; - queryFn = () => - apiService.surface.getStaticSurfaceData( - surfAddr.caseUuid, - surfAddr.ensemble, - surfAddr.realizationNum, - surfAddr.name, - surfAddr.attribute - ); - } - - // Static, statistical surface - else if (surfAddr.addressType === "statistical-static") { - queryKey = [ - "getStatisticalStaticSurfaceData", - surfAddr.caseUuid, - surfAddr.ensemble, - surfAddr.statisticFunction, - surfAddr.name, - surfAddr.attribute, - ]; - queryFn = () => - apiService.surface.getStatisticalStaticSurfaceData( - surfAddr.caseUuid, - surfAddr.ensemble, - surfAddr.statisticFunction, - surfAddr.name, - surfAddr.attribute - ); - } else { - throw new Error("Invalid surface address type"); - } - - return useQuery({ - queryKey: queryKey, - queryFn: queryFn, - staleTime: STALE_TIME, - cacheTime: CACHE_TIME, - enabled: enabled, - }); -} export function usePropertySurfaceDataByQueryAddress( - meshSurfAddr: SurfAddr | null, - propertySurfAddr: SurfAddr | null, + meshSurfAddr: SurfaceAddress | null, + propertySurfAddr: SurfaceAddress | null, enabled: boolean ): UseQueryResult { function dummyApiCall(): Promise { @@ -166,7 +79,7 @@ export function usePropertySurfaceDataByQueryAddress( let queryKey: QueryKey | null = null; // Static, per realization surface - if (meshSurfAddr.addressType === "static" && propertySurfAddr.addressType === "static") { + if (meshSurfAddr.addressType === "realization" && propertySurfAddr.addressType === "realization") { queryKey = [ "getPropertySurfaceResampledToStaticSurface", meshSurfAddr.caseUuid, @@ -177,6 +90,7 @@ export function usePropertySurfaceDataByQueryAddress( propertySurfAddr.realizationNum, propertySurfAddr.name, propertySurfAddr.attribute, + propertySurfAddr.isoDateOrInterval, ]; queryFn = () => apiService.surface.getPropertySurfaceResampledToStaticSurface( @@ -187,12 +101,10 @@ export function usePropertySurfaceDataByQueryAddress( meshSurfAddr.attribute, propertySurfAddr.realizationNum, propertySurfAddr.name, - propertySurfAddr.attribute + propertySurfAddr.attribute, + propertySurfAddr.isoDateOrInterval ); - } else if ( - meshSurfAddr.addressType === "statistical-static" && - propertySurfAddr.addressType === "statistical-static" - ) { + } else if (meshSurfAddr.addressType === "statistical" && propertySurfAddr.addressType === "statistical") { queryKey = [ "getPropertySurfaceResampledToStaticSurface", meshSurfAddr.caseUuid, @@ -203,6 +115,7 @@ export function usePropertySurfaceDataByQueryAddress( // propertySurfAddr.statisticFunction, propertySurfAddr.name, propertySurfAddr.attribute, + propertySurfAddr.isoDateOrInterval, ]; queryFn = () => apiService.surface.getPropertySurfaceResampledToStatisticalStaticSurface( @@ -213,7 +126,8 @@ export function usePropertySurfaceDataByQueryAddress( meshSurfAddr.attribute, // propertySurfAddr.statisticFunction, propertySurfAddr.name, - propertySurfAddr.attribute + propertySurfAddr.attribute, + propertySurfAddr.isoDateOrInterval ); } else { throw new Error("Invalid surface address type"); diff --git a/frontend/src/modules/SubsurfaceMap/settings.tsx b/frontend/src/modules/SubsurfaceMap/settings.tsx index 274d8a7f7..0b8047eb2 100644 --- a/frontend/src/modules/SubsurfaceMap/settings.tsx +++ b/frontend/src/modules/SubsurfaceMap/settings.tsx @@ -1,7 +1,6 @@ import React from "react"; -import { SurfaceStatisticFunction_api } from "@api"; -import { SumoContent_api } from "@api"; +import { SurfaceAttributeType_api, SurfaceStatisticFunction_api } from "@api"; import { EnsembleIdent } from "@framework/EnsembleIdent"; import { ModuleFCProps } from "@framework/Module"; import { SyncSettingKey, SyncSettingsHelper } from "@framework/SyncSettings"; @@ -15,15 +14,21 @@ import { CircularProgress } from "@lib/components/CircularProgress"; import { CollapsibleGroup } from "@lib/components/CollapsibleGroup"; import { Input } from "@lib/components/Input"; import { Label } from "@lib/components/Label"; +import { RadioGroup } from "@lib/components/RadioGroup"; import { Select, SelectOption } from "@lib/components/Select"; +import { + SurfaceAddress, + SurfaceAddressFactory, + SurfaceDirectory, + TimeType, + useSurfaceDirectoryQuery, +} from "@modules/_shared/Surface"; -import { SurfAddr, SurfAddrFactory } from "./SurfaceAddress"; import { SurfacePolygonsAddress } from "./SurfacePolygonsAddress"; import { AggregationSelector } from "./components/AggregationSelector"; import { PolygonDirectoryProvider } from "./polygonsDirectoryProvider"; -import { useGetWellHeaders, usePolygonDirectoryQuery, useSurfaceDirectoryQuery } from "./queryHooks"; +import { useGetWellHeaders, usePolygonDirectoryQuery } from "./queryHooks"; import { state } from "./state"; -import { SurfaceDirectoryProvider } from "./surfaceDirectoryProvider"; //----------------------------------------------------------------------------------------------------------- type LabelledCheckboxProps = { @@ -44,7 +49,11 @@ function LabelledCheckbox(props: LabelledCheckboxProps): JSX.Element { function Header(props: { text: string }): JSX.Element { return ; } - +const TimeTypeEnumToStringMapping = { + [TimeType.None]: "Static", + [TimeType.TimePoint]: "Time point", + [TimeType.Interval]: "Time interval", +}; export function settings({ moduleContext, workbenchSession, workbenchServices }: ModuleFCProps) { const myInstanceIdStr = moduleContext.getInstanceIdString(); console.debug(`${myInstanceIdStr} -- render TopographicMap settings`); @@ -56,6 +65,8 @@ export function settings({ moduleContext, workbenchSession, workbenchServices }: const [usePropertySurface, setUsePropertySurface] = React.useState(false); const [selectedPropertySurfaceName, setSelectedPropertySurfaceName] = React.useState(null); const [selectedPropertySurfaceAttribute, setSelectedPropertySurfaceAttribute] = React.useState(null); + const [selectedPropertyTimeOrInterval, setSelectedPropertyTimeOrInterval] = React.useState(null); + const [timeType, setTimeType] = React.useState(TimeType.None); const [selectedPolygonName, setSelectedPolygonName] = React.useState(null); const [selectedPolygonAttribute, setSelectedPolygonAttribute] = React.useState(null); const [linkPolygonNameToSurfaceName, setLinkPolygonNameToSurfaceName] = React.useState(true); @@ -74,7 +85,7 @@ export function settings({ moduleContext, workbenchSession, workbenchServices }: const syncedSettingKeys = moduleContext.useSyncedSettingKeys(); const syncHelper = new SyncSettingsHelper(syncedSettingKeys, workbenchServices); const syncedValueEnsembles = syncHelper.useValue(SyncSettingKey.ENSEMBLE, "global.syncValue.ensembles"); - + const syncedValueSurface = syncHelper.useValue(SyncSettingKey.SURFACE, "global.syncValue.surface"); const candidateEnsembleIdent = maybeAssignFirstSyncedEnsemble(selectedEnsembleIdent, syncedValueEnsembles); const computedEnsembleIdent = fixupEnsembleIdent(candidateEnsembleIdent, ensembleSet); if (computedEnsembleIdent && !computedEnsembleIdent.equals(selectedEnsembleIdent)) { @@ -83,16 +94,34 @@ export function settings({ moduleContext, workbenchSession, workbenchServices }: // Mesh surface const meshSurfDirQuery = useSurfaceDirectoryQuery( computedEnsembleIdent?.getCaseUuid(), - computedEnsembleIdent?.getEnsembleName(), - [SumoContent_api.DEPTH] + computedEnsembleIdent?.getEnsembleName() ); - const meshSurfDirProvider = new SurfaceDirectoryProvider(meshSurfDirQuery, "tops"); - const computedMeshSurfaceName = meshSurfDirProvider.validateOrResetSurfaceName(selectedMeshSurfaceName); - const computedMeshSurfaceAttribute = meshSurfDirProvider.validateOrResetSurfaceAttribute( - computedMeshSurfaceName, - selectedMeshSurfaceAttribute + const meshSurfaceDirectory = new SurfaceDirectory( + meshSurfDirQuery.data + ? { + surfaceMetas: meshSurfDirQuery.data, + timeType: TimeType.None, + includeAttributeTypes: [SurfaceAttributeType_api.DEPTH], + } + : null ); + const fixedMeshSurfSpec = fixupSurface( + meshSurfaceDirectory, + { + surfaceName: selectedMeshSurfaceName, + surfaceAttribute: selectedMeshSurfaceAttribute, + timeOrInterval: null, + }, + { + surfaceName: syncedValueSurface?.name || null, + surfaceAttribute: syncedValueSurface?.attribute || null, + timeOrInterval: null, + } + ); + const computedMeshSurfaceName = fixedMeshSurfSpec.surfaceName; + const computedMeshSurfaceAttribute = fixedMeshSurfSpec.surfaceAttribute; + if (computedMeshSurfaceName && computedMeshSurfaceName !== selectedMeshSurfaceName) { setSelectedMeshSurfaceName(computedMeshSurfaceName); } @@ -102,34 +131,75 @@ export function settings({ moduleContext, workbenchSession, workbenchServices }: let meshSurfNameOptions: SelectOption[] = []; let meshSurfAttributeOptions: SelectOption[] = []; - meshSurfNameOptions = meshSurfDirProvider.surfaceNames().map((name) => ({ value: name, label: name })); - meshSurfAttributeOptions = meshSurfDirProvider - .attributesForSurfaceName(computedMeshSurfaceName) + meshSurfNameOptions = meshSurfaceDirectory.getSurfaceNames(null).map((name) => ({ value: name, label: name })); + meshSurfAttributeOptions = meshSurfaceDirectory + .getAttributeNames(computedMeshSurfaceName) .map((attr) => ({ value: attr, label: attr })); // Property surface + // TODO add timestamp and time interval surfaces const propertySurfDirQuery = useSurfaceDirectoryQuery( computedEnsembleIdent?.getCaseUuid(), - computedEnsembleIdent?.getEnsembleName() // Should be SumoContent_api.PROPERTY + computedEnsembleIdent?.getEnsembleName() + ); + + const propertySurfaceDirectory = new SurfaceDirectory( + propertySurfDirQuery.data + ? { + surfaceMetas: propertySurfDirQuery.data, + timeType: timeType, + excludeAttributeTypes: [SurfaceAttributeType_api.DEPTH], + } + : null ); - const propertySurfDirProvider = new SurfaceDirectoryProvider(propertySurfDirQuery, "formations"); - const computedPropertySurfaceName = propertySurfDirProvider.validateOrResetSurfaceName(selectedPropertySurfaceName); - const computedPropertySurfaceAttribute = propertySurfDirProvider.validateOrResetSurfaceAttribute( - computedPropertySurfaceName, - selectedPropertySurfaceAttribute + + const fixedPropertySurfSpec = fixupSurface( + propertySurfaceDirectory, + { + surfaceName: selectedPropertySurfaceName, + surfaceAttribute: selectedPropertySurfaceAttribute, + timeOrInterval: selectedPropertyTimeOrInterval, + }, + { + surfaceName: null, + surfaceAttribute: null, + timeOrInterval: null, + } ); + const computedPropertySurfaceName = fixedPropertySurfSpec.surfaceName; + const computedPropertySurfaceAttribute = fixedPropertySurfSpec.surfaceAttribute; + const computedPropertyTimeOrInterval = fixedPropertySurfSpec.timeOrInterval; + if (computedPropertySurfaceName && computedPropertySurfaceName !== selectedPropertySurfaceName) { setSelectedPropertySurfaceName(computedPropertySurfaceName); } if (computedPropertySurfaceAttribute && computedPropertySurfaceAttribute !== selectedPropertySurfaceAttribute) { setSelectedPropertySurfaceAttribute(computedPropertySurfaceAttribute); } + if (computedPropertyTimeOrInterval && computedPropertyTimeOrInterval !== computedPropertyTimeOrInterval) { + setSelectedPropertyTimeOrInterval(computedPropertyTimeOrInterval); + } let propertySurfNameOptions: SelectOption[] = []; let propertySurfAttributeOptions: SelectOption[] = []; - propertySurfNameOptions = propertySurfDirProvider.surfaceNames().map((name) => ({ value: name, label: name })); - propertySurfAttributeOptions = propertySurfDirProvider - .attributesForSurfaceName(computedPropertySurfaceName) + let propertySurfTimeOrIntervalOptions: SelectOption[] = []; + + propertySurfNameOptions = propertySurfaceDirectory + .getSurfaceNames(null) + .map((name) => ({ value: name, label: name })); + propertySurfAttributeOptions = propertySurfaceDirectory + .getAttributeNames(computedPropertySurfaceName) .map((attr) => ({ value: attr, label: attr })); + if (timeType === TimeType.Interval || timeType === TimeType.TimePoint) { + propertySurfTimeOrIntervalOptions = propertySurfaceDirectory + .getTimeOrIntervalStrings(computedPropertySurfaceName, computedPropertySurfaceAttribute) + .map((interval) => ({ + value: interval, + label: + timeType === TimeType.TimePoint + ? isoStringToDateLabel(interval) + : isoIntervalStringToDateLabel(interval), + })); + } // Polygon const polygonDirQuery = usePolygonDirectoryQuery( @@ -161,20 +231,21 @@ export function settings({ moduleContext, workbenchSession, workbenchServices }: React.useEffect( function propagateMeshSurfaceSelectionToView() { - let surfAddr: SurfAddr | null = null; + let surfAddr: SurfaceAddress | null = null; if (computedEnsembleIdent && computedMeshSurfaceName && computedMeshSurfaceAttribute) { - const addrFactory = new SurfAddrFactory( + const addrFactory = new SurfaceAddressFactory( computedEnsembleIdent.getCaseUuid(), computedEnsembleIdent.getEnsembleName(), computedMeshSurfaceName, - computedMeshSurfaceAttribute + computedMeshSurfaceAttribute, + null ); if (aggregation === null) { - surfAddr = addrFactory.createStaticAddr(realizationNum); + surfAddr = addrFactory.createRealizationAddress(realizationNum); } else { - surfAddr = addrFactory.createStatisticalStaticAddr(aggregation); + surfAddr = addrFactory.createStatisticalAddress(aggregation); } } @@ -185,23 +256,24 @@ export function settings({ moduleContext, workbenchSession, workbenchServices }: ); React.useEffect( function propagatePropertySurfaceSelectionToView() { - let surfAddr: SurfAddr | null = null; + let surfAddr: SurfaceAddress | null = null; if (!usePropertySurface) { moduleContext.getStateStore().setValue("propertySurfaceAddress", surfAddr); return; } if (computedEnsembleIdent && computedPropertySurfaceName && computedPropertySurfaceAttribute) { - const addrFactory = new SurfAddrFactory( + const addrFactory = new SurfaceAddressFactory( computedEnsembleIdent.getCaseUuid(), computedEnsembleIdent.getEnsembleName(), computedPropertySurfaceName, - computedPropertySurfaceAttribute + computedPropertySurfaceAttribute, + computedPropertyTimeOrInterval ); if (aggregation === null) { - surfAddr = addrFactory.createStaticAddr(realizationNum); + surfAddr = addrFactory.createRealizationAddress(realizationNum); } else { - surfAddr = addrFactory.createStatisticalStaticAddr(aggregation); + surfAddr = addrFactory.createStatisticalAddress(aggregation); } } @@ -212,6 +284,7 @@ export function settings({ moduleContext, workbenchSession, workbenchServices }: selectedEnsembleIdent, selectedPropertySurfaceName, selectedPropertySurfaceAttribute, + selectedPropertyTimeOrInterval, aggregation, realizationNum, usePropertySurface, @@ -352,6 +425,19 @@ export function settings({ moduleContext, workbenchSession, workbenchServices }: setContourIncValue(contourInc); } } + function handleTimeOrIntervalSelectionChange(selectedSurfTimeIntervals: string[]) { + console.debug("handleTimeOrIntervalSelectionChange()"); + const newTimeOrInterval = selectedSurfTimeIntervals[0] ?? null; + setSelectedPropertyTimeOrInterval(newTimeOrInterval); + if (newTimeOrInterval) { + syncHelper.publishValue(SyncSettingKey.DATE, "global.syncValue.date", { + timeOrInterval: newTimeOrInterval, + }); + } + } + function handleTimeModeChange(event: React.ChangeEvent) { + setTimeType(event.target.value as TimeType); + } return (
@@ -422,6 +508,15 @@ export function settings({ moduleContext, workbenchSession, workbenchServices }: errorComponent={"Error loading surface directory"} loadingComponent={} > + {" "} + { + return { value: val, label: TimeTypeEnumToStringMapping[val] }; + })} + onChange={handleTimeModeChange} + /> + {timeType !== TimeType.None && ( +