Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add VDS Access #319

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion backend/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ psutil = "^5.9.5"
vtk = "^9.2.6"
fmu-sumo = "^0.3.10"
sumo-wrapper-python = "^0.3.4"
requests-toolbelt = "^1.0.0"


[tool.poetry.group.dev.dependencies]
Expand Down
14 changes: 14 additions & 0 deletions backend/src/backend/primary/routers/seismic/converters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from numpy.typing import NDArray

from src.services.utils.b64 import b64_encode_float_array_as_float32
from src.services.vds_access.types import VdsAxis

from .schemas import SeismicIntersectionData


def to_api_seismic_fence_data(vds_fence_data: NDArray, z_axis: VdsAxis) -> SeismicIntersectionData:
"""
Convert VDS fence data to API fence data
"""
values_b64arr = b64_encode_float_array_as_float32(vds_fence_data)
return SeismicIntersectionData(values_base64arr=values_b64arr, z_axis=z_axis)
57 changes: 56 additions & 1 deletion backend/src/backend/primary/routers/seismic/router.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import logging
from typing import List

from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi import APIRouter, Depends, HTTPException, Query # , Body

from src.services.sumo_access.seismic_access import SeismicAccess
from src.services.vds_access.vds_access import VdsAccess
from src.services.utils.authenticated_user import AuthenticatedUser
from src.backend.auth.auth_helper import AuthHelper

from . import schemas
from .converters import to_api_seismic_fence_data

LOGGER = logging.getLogger(__name__)

Expand All @@ -29,3 +31,56 @@ def get_seismic_directory(
return [schemas.SeismicCubeMeta(**meta.__dict__) for meta in seismic_cube_metas]
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc


@router.post("/fence/")
async def get_fence(
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"),
seismic_attribute: str = Query(description="Seismic cube attribute"),
time_or_interval_str: str = Query(description="Timestamp or timestep"),
observed: bool = Query(description="Observed or simulated"),
# cutting_plane: schemas.CuttingPlane = Body(alias="cuttingPlane", embed=True),
) -> schemas.SeismicIntersectionData:
"""Get a fence of seismic data from a set of coordinates."""
seismic_access = SeismicAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name)

try:
vds_handle = seismic_access.get_vds_handle(
realization=realization_num,
seismic_attribute=seismic_attribute,
time_or_interval_str=time_or_interval_str,
observed=observed,
)
except ValueError as err:
raise HTTPException(status_code=404, detail=str(err)) from err

vdsaccess = VdsAccess(vds_handle)

vals = await vdsaccess.get_fence(
coordinate_system="cdp",
coordinates=[
[x, y]
for x, y in zip(
[
463156.911,
463564.402,
463637.925,
463690.658,
463910.452,
],
[
5929542.294,
5931057.803,
5931184.235,
5931278.837,
5931688.122,
],
)
],
)
meta = await vdsaccess.get_metadata()
z_axis_meta = meta.axis[2]
return to_api_seismic_fence_data(vals, z_axis_meta)
8 changes: 8 additions & 0 deletions backend/src/backend/primary/routers/seismic/schemas.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from pydantic import BaseModel

from src.services.vds_access.types import VdsAxis
from src.services.utils.b64 import B64FloatArray


class SeismicCubeMeta(BaseModel):
seismic_attribute: str
Expand All @@ -11,3 +14,8 @@ class SeismicCubeMeta(BaseModel):
class VdsHandle(BaseModel):
sas_token: str
vds_url: str


class SeismicIntersectionData(BaseModel):
values_base64arr: B64FloatArray
z_axis: VdsAxis
Empty file.
23 changes: 23 additions & 0 deletions backend/src/services/vds_access/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import List

from pydantic import BaseModel


class VdsAxis(BaseModel):
annotation: str
max: float
min: float
samples: int
unit: str


class VdsBoundingBox(BaseModel):
cdp: List[List[float]]
ij: List[List[float]]
ilxl: List[List[float]]


class VdsMetaData(BaseModel):
axis: List[VdsAxis]
boundingBox: VdsBoundingBox
crs: str
113 changes: 113 additions & 0 deletions backend/src/services/vds_access/vds_access.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import logging
import os
from typing import List
import json

import numpy as np
from numpy.typing import NDArray
from requests_toolbelt.multipart.decoder import MultipartDecoder
import httpx

from ..sumo_access.seismic_types import VdsHandle
from .types import VdsMetaData

VDS_HOST_ADDRESS = os.getenv("WEBVIZ_VDS_HOST_ADDRESS")

LOGGER = logging.getLogger(__name__)


class VdsAccess:
"""Access to the service hosting vds-slice.
https://github.com/equinor/vds-slice

This access class is used to query the service for slices and fences of seismic data stored in Sumo in vds format.
Note that we are not providing the service with the actual vds file, but rather a SAS token and an URL to the vds file.
"""

def __init__(self, sumo_seismic_vds_handle: VdsHandle) -> None:
self.sas: str = sumo_seismic_vds_handle.sas_token
self.vds_url: str = sumo_seismic_vds_handle.vds_url

async def _query(self, endpoint: str, params: dict) -> httpx.Response:
"""Query the service"""
params.update({"url": self.vds_url, "sas": self.sas})

async with httpx.AsyncClient() as client:
response = await client.post(
f"{VDS_HOST_ADDRESS}/{endpoint}",
headers={"Content-Type": "application/json"},
content=json.dumps(params),
timeout=60,
)

if response.is_error:
raise RuntimeError(f"({str(response.status_code)})-{response.reason_phrase}-{response.text}")

return response

async def get_slice(self, direction: str, lineno: int) -> NDArray[np.float32]:
"""Gets a slice in i,j,k direction from the VDS service"""

endpoint = "slice"
params = {
"direction": direction,
"lineno": lineno,
"vds": self.vds_url,
"sas": self.sas,
}
response = await self._query(endpoint, params)

# Use MultipartDecoder with httpx's Response content and headers
decoder = MultipartDecoder(content=response.content, content_type=response.headers["Content-Type"])
parts = decoder.parts

metadata = json.loads(parts[0].content)
shape = (metadata["y"]["samples"], metadata["x"]["samples"])
if metadata["format"] != "<f4":
raise ValueError(f"Expected float32, got {metadata['format']}")
byte_array = parts[1].content
values_np = bytes_to_ndarray_float22(byte_array, list(shape))
return values_np

async def get_fence(self, coordinates: List[List[float]], coordinate_system: str = "cdp") -> NDArray[np.float32]:
"""Gets traces along an arbitrary path of x,y coordinates."""
endpoint = "fence"

params = {
"coordinateSystem": coordinate_system,
"coordinates": coordinates,
"vds": self.vds_url,
"sas": self.sas,
"interpolation": "linear",
"fillValue": -999,
}

response = await self._query(endpoint, params)

# Use MultipartDecoder with httpx's Response content and headers
decoder = MultipartDecoder(content=response.content, content_type=response.headers["Content-Type"])
parts = decoder.parts

metadata = json.loads(parts[0].content)
byte_array = parts[1].content
if metadata["format"] != "<f4":
raise ValueError(f"Expected float32, got {metadata['format']}")
values_np = bytes_to_ndarray_float22(byte_array, list(metadata["shape"]))
return values_np

async def get_metadata(self) -> VdsMetaData:
"""Gets metadata from the cube"""
endpoint = "metadata"

params = {
"vds": self.vds_url,
"sas": self.sas,
}
response = await self._query(endpoint, params)
metadata = response.json()
return VdsMetaData(**metadata)


def bytes_to_ndarray_float22(bytes_data: bytes, shape: List[int]) -> NDArray[np.float32]:
"""Convert bytes to numpy ndarray"""
return np.ndarray(shape, "<f4", bytes_data)
1 change: 1 addition & 0 deletions docker-compose-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ services:
- WEBVIZ_SMDA_RESOURCE_SCOPE
- WEBVIZ_SMDA_SUBSCRIPTION_KEY
- WEBVIZ_SUMO_ENV
- WEBVIZ_VDS_HOST_ADDRESS

backend-user-session:
build:
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ services:
- WEBVIZ_SMDA_RESOURCE_SCOPE
- WEBVIZ_SMDA_SUBSCRIPTION_KEY
- WEBVIZ_SUMO_ENV
- WEBVIZ_VDS_HOST_ADDRESS
- CODESPACE_NAME # Automatically set env. variable by GitHub codespace
volumes:
- ./backend/src:/home/appuser/backend/src
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type { InplaceVolumetricsTableMetaData as InplaceVolumetricsTableMetaData
export type { PolygonData as PolygonData_api } from './models/PolygonData';
export type { PvtData as PvtData_api } from './models/PvtData';
export type { SeismicCubeMeta as SeismicCubeMeta_api } from './models/SeismicCubeMeta';
export type { SeismicIntersectionData as SeismicIntersectionData_api } from './models/SeismicIntersectionData';
export { SensitivityType as SensitivityType_api } from './models/SensitivityType';
export { StatisticFunction as StatisticFunction_api } from './models/StatisticFunction';
export type { StatisticValueObject as StatisticValueObject_api } from './models/StatisticValueObject';
Expand All @@ -42,6 +43,7 @@ export type { SurfacePolygonDirectory as SurfacePolygonDirectory_api } from './m
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';
export type { VdsAxis as VdsAxis_api } from './models/VdsAxis';
export type { VectorDescription as VectorDescription_api } from './models/VectorDescription';
export type { VectorHistoricalData as VectorHistoricalData_api } from './models/VectorHistoricalData';
export type { VectorRealizationData as VectorRealizationData_api } from './models/VectorRealizationData';
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/api/models/SeismicIntersectionData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */

import type { B64FloatArray } from './B64FloatArray';
import type { VdsAxis } from './VdsAxis';

export type SeismicIntersectionData = {
values_base64arr: B64FloatArray;
z_axis: VdsAxis;
};

12 changes: 12 additions & 0 deletions frontend/src/api/models/VdsAxis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */

export type VdsAxis = {
annotation: string;
max: number;
min: number;
samples: number;
unit: string;
};

38 changes: 38 additions & 0 deletions frontend/src/api/services/SeismicService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/* tslint:disable */
/* eslint-disable */
import type { SeismicCubeMeta } from '../models/SeismicCubeMeta';
import type { SeismicIntersectionData } from '../models/SeismicIntersectionData';

import type { CancelablePromise } from '../core/CancelablePromise';
import type { BaseHttpRequest } from '../core/BaseHttpRequest';
Expand Down Expand Up @@ -35,4 +36,41 @@ export class SeismicService {
});
}

/**
* Get Fence
* Get a fence of seismic data from a set of coordinates.
* @param caseUuid Sumo case uuid
* @param ensembleName Ensemble name
* @param realizationNum Realization number
* @param seismicAttribute Seismic cube attribute
* @param timeOrIntervalStr Timestamp or timestep
* @param observed Observed or simulated
* @returns SeismicIntersectionData Successful Response
* @throws ApiError
*/
public getFence(
caseUuid: string,
ensembleName: string,
realizationNum: number,
seismicAttribute: string,
timeOrIntervalStr: string,
observed: boolean,
): CancelablePromise<SeismicIntersectionData> {
return this.httpRequest.request({
method: 'POST',
url: '/seismic/fence/',
query: {
'case_uuid': caseUuid,
'ensemble_name': ensembleName,
'realization_num': realizationNum,
'seismic_attribute': seismicAttribute,
'time_or_interval_str': timeOrIntervalStr,
'observed': observed,
},
errors: {
422: `Validation Error`,
},
});
}

}
2 changes: 2 additions & 0 deletions radixconfig.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ spec:
envVar: WEBVIZ_SMDA_RESOURCE_SCOPE
- name: WEBVIZ-SMDA-SUBSCRIPTION-KEY
envVar: WEBVIZ_SMDA_SUBSCRIPTION_KEY
- name: WEBVIZ-VDS-HOST-ADDRESS
envVar: WEBVIZ_VDS_HOST_ADDRESS
variables:
UVICORN_PORT: 5000
UVICORN_ENTRYPOINT: src.backend.primary.main:app
Expand Down