Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
HansKallekleiv committed Aug 18, 2023
1 parent e7f5d6c commit dd1f2e0
Show file tree
Hide file tree
Showing 27 changed files with 1,337 additions and 227 deletions.
2 changes: 2 additions & 0 deletions backend/src/backend/primary/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .routers.grid.router import router as grid_router
from .routers.pvt.router import router as pvt_router
from .routers.well.router import router as well_router
from .routers.surface_polygons.router import router as surface_polygons_router

logging.basicConfig(
level=logging.WARNING,
Expand Down Expand Up @@ -52,6 +53,7 @@ def custom_generate_unique_id(route: APIRoute) -> str:
app.include_router(grid_router, prefix="/grid", tags=["grid"])
app.include_router(pvt_router, prefix="/pvt", tags=["pvt"])
app.include_router(well_router, prefix="/well", tags=["well"])
app.include_router(surface_polygons_router, prefix="/surface_polygons", tags=["surface_polygons"])

authHelper = AuthHelper()
app.include_router(authHelper.router)
Expand Down
13 changes: 13 additions & 0 deletions backend/src/backend/primary/routers/surface/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@
from . import schemas


def resample_property_surface_to_mesh_surface(
mesh_surface: xtgeo.RegularSurface, property_surface: xtgeo.RegularSurface
) -> xtgeo.RegularSurface:
"""
Regrid property surface to mesh surface if topology is different
"""
if mesh_surface.compare_topology(property_surface):
return property_surface

mesh_surface.resample(property_surface)
return mesh_surface


def to_api_surface_data(xtgeo_surf: xtgeo.RegularSurface) -> schemas.SurfaceData:
"""
Create API SurfaceData from xtgeo regular surface
Expand Down
32 changes: 32 additions & 0 deletions backend/src/backend/primary/routers/surface/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,38 @@ def get_static_surface_data(
return surf_data_response


@router.get("/property_surface_resampled_to_static_surface/")
def get_property_surface_resampled_to_static_surface(
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_mesh: int = Query(description="Realization number"),
name_mesh: str = Query(description="Surface name"),
attribute_mesh: str = Query(description="Surface attribute"),
realization_num_property: int = Query(description="Realization number"),
name_property: str = Query(description="Surface name"),
attribute_property: str = Query(description="Surface attribute"),
) -> 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
)

if not xtgeo_surf_mesh or not xtgeo_surf_property:
raise HTTPException(status_code=404, detail="Surface not found")

resampled_surface = converters.resample_property_surface_to_mesh_surface(xtgeo_surf_mesh, xtgeo_surf_property)

surf_data_response = converters.to_api_surface_data(resampled_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),
Expand Down
Empty file.
23 changes: 23 additions & 0 deletions backend/src/backend/primary/routers/surface_polygons/converters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import List
import orjson
import xtgeo

from src.services.utils.surface_to_float32 import surface_to_float32_array
from . import schemas


def to_api_polygons_data(xtgeo_poly: xtgeo.Polygons) -> List[schemas.PolygonData]:
"""
Create API SurfaceData from xtgeo regular surface
"""
polydata: List[schemas.PolygonData] = []
for poly_id, polygon in xtgeo_poly.dataframe.groupby("POLY_ID"):
polydata.append(
schemas.PolygonData(
x_arr=list(polygon.X_UTME),
y_arr=list(polygon.Y_UTMN),
z_arr=list(polygon.Z_TVDSS),
poly_id=poly_id,
)
)
return polydata
57 changes: 57 additions & 0 deletions backend/src/backend/primary/routers/surface_polygons/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import logging
from typing import List

from fastapi import APIRouter, Depends, HTTPException, Query

from src.services.sumo_access.surface_polygon_access import SurfacePolygonsAccess
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 schemas
from . import converters

LOGGER = logging.getLogger(__name__)

router = APIRouter()


@router.get("/surface_polygons_directory/")
def get_surface_polygons_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.SurfacePolygonDirectory:
"""
Get a directory of surface polygon names and attributes
"""
access = SurfacePolygonsAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name)
polygons_dir = access.get_surface_polygons_dir()

return polygons_dir


@router.get("/surface_polygons_data/")
def get_surface_polygons_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"),
) -> List[schemas.PolygonData]:
timer = PerfTimer()

access = SurfacePolygonsAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name)
xtgeo_poly = access.get_surface_polygons(real_num=realization_num, name=name, attribute=attribute)

if not xtgeo_poly:
raise HTTPException(status_code=404, detail="Surface not found")

poly_data_response = converters.to_api_polygons_data(xtgeo_poly)

LOGGER.debug(f"Loaded static surface and created image, total time: {timer.elapsed_ms()}ms")
return poly_data_response
17 changes: 17 additions & 0 deletions backend/src/backend/primary/routers/surface_polygons/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from enum import Enum
from typing import List

from pydantic import BaseModel


class SurfacePolygonDirectory(BaseModel):
names: List[str]
attributes: List[str]
valid_attributes_for_name: List[List[int]]


class PolygonData(BaseModel):
x_arr: List[float]
y_arr: List[float]
z_arr: List[float]
poly_id: int | str
153 changes: 153 additions & 0 deletions backend/src/services/sumo_access/surface_polygon_access.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import logging
from io import BytesIO
from typing import List, Optional, Tuple

import pandas as pd
import xtgeo
from fmu.sumo.explorer import TimeFilter, TimeType
from fmu.sumo.explorer.objects import Case, CaseCollection, SurfaceCollection, PolygonsCollection
from sumo.wrapper import SumoClient

from src.services.utils.perf_timer import PerfTimer
from src.services.utils.statistic_function import StatisticFunction

from ._helpers import create_sumo_client_instance
from .surface_polygon_types import SurfacePolygonsDirectory
from .generic_types import SumoContent

LOGGER = logging.getLogger(__name__)


class SurfacePolygonsAccess:
def __init__(self, access_token: str, case_uuid: str, iteration_name: str):
self._sumo_client: SumoClient = create_sumo_client_instance(access_token)
self._case_uuid = case_uuid
self._iteration_name = iteration_name
self._sumo_case_obj: Optional[Case] = None

def get_surface_polygons_dir(self, content_filter: Optional[List[SumoContent]] = None) -> SurfacePolygonsDirectory:
"""
Get a directory of surface polygon names and attributes.
"""
timer = PerfTimer()

LOGGER.debug("Getting data for surface polygon directory...")

case = self._get_my_sumo_case_obj()

polygons_collection: PolygonsCollection = case.polygons.filter(
iteration=self._iteration_name,
realization=0,
)

names = sorted(polygons_collection.names)
attributes = sorted(polygons_collection.tagnames)

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}")
polygons_with_filtered_content = [
surf for surf in polygons_collection if surf["data"]["content"] in content_filter
]
for surf in polygons_collection:
if surf["data"]["content"] in content_filter:
print(surf["data"]["content"])
names = sorted(list(set([surf.name for surf in polygons_with_filtered_content])))
attributes = sorted(list(set([surf.tagname for surf in polygons_with_filtered_content])))

else:
names = sorted(polygons_collection.names)
attributes = sorted(polygons_collection.tagnames)

LOGGER.debug(
f"Build valid name/attribute combinations for surface polygons directory "
f"(num names={len(names)}, num attributes={len(attributes)})..."
)

valid_attributes_for_name: List[List[int]] = []

for name in names:
filtered_coll = polygons_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)

polygon_dir = SurfacePolygonsDirectory(
names=names,
attributes=attributes,
valid_attributes_for_name=valid_attributes_for_name,
)

LOGGER.debug(f"Downloaded and built surface polygon directory in: {timer.elapsed_ms():}ms")

return polygon_dir

def get_surface_polygons(self, real_num: int, name: str, attribute: str) -> Optional[xtgeo.Polygons]:
"""
Get polygons data
"""
timer = PerfTimer()
addr_str = self._make_addr_str(real_num, name, attribute, None)

case = self._get_my_sumo_case_obj()

polygons_collection: PolygonsCollection = case.polygons.filter(
iteration=self._iteration_name,
realization=real_num,
name=name,
tagname=attribute,
)

surface_polygons_count = len(polygons_collection)
if surface_polygons_count == 0:
LOGGER.warning(f"No surface polygons found in Sumo for {addr_str}")
return None

is_valid = False
if surface_polygons_count > 1:
LOGGER.warning(
f"Multiple ({surface_polygons_count}) polygons set found in Sumo for: {addr_str}. Returning first polygons set."
)
# HACK HACK HACK Some fields has multiple polygons sets for some reason... Unknown which one works
for poly in polygons_collection:
byte_stream: BytesIO = poly.blob
poly_df = pd.read_csv(byte_stream)
is_valid = set(["X_UTME", "Y_UTMN", "Z_TVDSS", "POLY_ID"]) == set(poly_df.columns)
if is_valid:
break
else:
sumo_polys = polygons_collection[0]
byte_stream: BytesIO = sumo_polys.blob
poly_df = pd.read_csv(byte_stream)
is_valid = set(["X_UTME", "Y_UTMN", "Z_TVDSS", "POLY_ID"]) == set(poly_df.columns)
if not is_valid:
LOGGER.warning(f"Invalid surface polygons found in Sumo for {addr_str}")
return None
xtgeo_polygons = xtgeo.Polygons(poly_df)

LOGGER.debug(f"Got surface polygons from Sumo in: {timer.elapsed_ms()}ms ({addr_str})")

return xtgeo_polygons

def _get_my_sumo_case_obj(self) -> Case:
"""
Get the Sumo case that we should be working on.
Raises exception if case isn't found
"""
if self._sumo_case_obj is None:
case_collection = CaseCollection(self._sumo_client).filter(uuid=self._case_uuid)
if len(case_collection) != 1:
raise ValueError(f"None or multiple sumo cases found {self._case_uuid=}")

self._sumo_case_obj = case_collection[0]

return self._sumo_case_obj

def _make_addr_str(self, real_num: int, name: str, attribute: str, date_str: Optional[str]) -> str:
addr_str = f"R:{real_num}__N:{name}__A:{attribute}__D:{date_str}__I:{self._iteration_name}__C:{self._case_uuid}"
return addr_str
16 changes: 16 additions & 0 deletions backend/src/services/sumo_access/surface_polygon_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from __future__ import annotations

from typing import List
from enum import Enum

from pydantic import BaseModel


class SurfacePolygonsDirectory(BaseModel):
names: List[str]
attributes: List[str]
valid_attributes_for_name: List[List[int]]

@classmethod
def create_empty(cls) -> SurfacePolygonsDirectory:
return cls(attributes=[], names=[])
3 changes: 3 additions & 0 deletions frontend/src/api/ApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { InplaceVolumetricsService } from './services/InplaceVolumetricsService'
import { ParametersService } from './services/ParametersService';
import { PvtService } from './services/PvtService';
import { SurfaceService } from './services/SurfaceService';
import { SurfacePolygonsService } from './services/SurfacePolygonsService';
import { TimeseriesService } from './services/TimeseriesService';
import { WellService } from './services/WellService';

Expand All @@ -28,6 +29,7 @@ export class ApiService {
public readonly parameters: ParametersService;
public readonly pvt: PvtService;
public readonly surface: SurfaceService;
public readonly surfacePolygons: SurfacePolygonsService;
public readonly timeseries: TimeseriesService;
public readonly well: WellService;

Expand All @@ -54,6 +56,7 @@ export class ApiService {
this.parameters = new ParametersService(this.request);
this.pvt = new PvtService(this.request);
this.surface = new SurfaceService(this.request);
this.surfacePolygons = new SurfacePolygonsService(this.request);
this.timeseries = new TimeseriesService(this.request);
this.well = new WellService(this.request);
}
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ export type { GridSurface as GridSurface_api } from './models/GridSurface';
export type { HTTPValidationError as HTTPValidationError_api } from './models/HTTPValidationError';
export type { InplaceVolumetricsCategoricalMetaData as InplaceVolumetricsCategoricalMetaData_api } from './models/InplaceVolumetricsCategoricalMetaData';
export type { InplaceVolumetricsTableMetaData as InplaceVolumetricsTableMetaData_api } from './models/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 type { SurfaceData as SurfaceData_api } from './models/SurfaceData';
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';
export type { ValidationError as ValidationError_api } from './models/ValidationError';
Expand All @@ -55,5 +57,6 @@ export { InplaceVolumetricsService } from './services/InplaceVolumetricsService'
export { ParametersService } from './services/ParametersService';
export { PvtService } from './services/PvtService';
export { SurfaceService } from './services/SurfaceService';
export { SurfacePolygonsService } from './services/SurfacePolygonsService';
export { TimeseriesService } from './services/TimeseriesService';
export { WellService } from './services/WellService';
Loading

0 comments on commit dd1f2e0

Please sign in to comment.