Skip to content

Commit 69f3a6c

Browse files
Add seismic intersection module - seismic fence (#449)
Co-authored-by: Hans Kallekleiv <[email protected]>
1 parent 99647cf commit 69f3a6c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2467
-120
lines changed

.github/workflows/webviz.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ jobs:
7171
- name: 🕵️ Check auto-generated frontend code is in sync with backend
7272
run: |
7373
docker build -f backend.Dockerfile -t backend:latest .
74-
CONTAINER_ID=$(docker run --detach -p 5000:5000 --env UVICORN_PORT=5000 --env UVICORN_ENTRYPOINT=src.backend.primary.main:app --env WEBVIZ_CLIENT_SECRET=0 --env WEBVIZ_SMDA_SUBSCRIPTION_KEY=0 --env WEBVIZ_SMDA_RESOURCE_SCOPE=0 backend:latest)
74+
CONTAINER_ID=$(docker run --detach -p 5000:5000 --env UVICORN_PORT=5000 --env UVICORN_ENTRYPOINT=src.backend.primary.main:app --env WEBVIZ_CLIENT_SECRET=0 --env WEBVIZ_SMDA_SUBSCRIPTION_KEY=0 --env WEBVIZ_SMDA_RESOURCE_SCOPE=0 --env WEBVIZ_VDS_HOST_ADDRESS=0 backend:latest)
7575
sleep 5 # Ensure the backend server is up and running exposing /openapi.json
7676
npm run generate-api --prefix ./frontend
7777
docker stop $CONTAINER_ID
@@ -113,6 +113,7 @@ jobs:
113113
WEBVIZ_CLIENT_SECRET: 0
114114
WEBVIZ_SMDA_SUBSCRIPTION_KEY: 0
115115
WEBVIZ_SMDA_RESOURCE_SCOPE: 0
116+
WEBVIZ_VDS_HOST_ADDRESS: 0
116117
run: |
117118
pytest ./tests/unit
118119

backend/poetry.lock

+20-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ vtk = "^9.2.6"
2626
fmu-sumo = "1.0.3"
2727
sumo-wrapper-python = "1.0.6"
2828
azure-monitor-opentelemetry = "^1.1.0"
29+
requests-toolbelt = "^1.0.0"
2930

3031

3132
[tool.poetry.group.dev.dependencies]
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,100 @@
11
import logging
2-
from typing import List
2+
from typing import List, Optional
33

4-
from fastapi import APIRouter, Depends, HTTPException, Query
4+
from fastapi import APIRouter, Depends, HTTPException, Query, Body
55

6-
from src.services.sumo_access.seismic_access import SeismicAccess
6+
from src.services.sumo_access.seismic_access import SeismicAccess, VdsHandle
7+
from src.services.vds_access.vds_access import VdsAccess
78
from src.services.utils.authenticated_user import AuthenticatedUser
89
from src.backend.auth.auth_helper import AuthHelper
10+
from src.services.utils.b64 import b64_encode_float_array_as_float32
11+
from src.services.vds_access.response_types import VdsMetadata
12+
from src.services.vds_access.request_types import VdsCoordinateSystem, VdsCoordinates
913

1014
from . import schemas
1115

16+
1217
LOGGER = logging.getLogger(__name__)
1318

1419
router = APIRouter()
1520

1621

17-
@router.get("/seismic_directory/")
18-
async def get_seismic_directory(
22+
@router.get("/seismic_cube_meta_list/")
23+
async def get_seismic_cube_meta_list(
1924
authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user),
2025
case_uuid: str = Query(description="Sumo case uuid"),
2126
ensemble_name: str = Query(description="Ensemble name"),
2227
) -> List[schemas.SeismicCubeMeta]:
2328
"""
24-
Get a directory of seismic cubes.
29+
Get a list of seismic cube meta.
2530
"""
2631
access = await SeismicAccess.from_case_uuid(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name)
27-
seismic_cube_metas = await access.get_seismic_directory()
32+
seismic_cube_meta_list = await access.get_seismic_cube_meta_list_async()
2833
try:
29-
return [schemas.SeismicCubeMeta(**meta.__dict__) for meta in seismic_cube_metas]
34+
return [schemas.SeismicCubeMeta(**meta.__dict__) for meta in seismic_cube_meta_list]
3035
except ValueError as exc:
3136
raise HTTPException(status_code=400, detail=str(exc)) from exc
37+
38+
39+
@router.post("/get_seismic_fence/")
40+
async def post_get_seismic_fence(
41+
authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user),
42+
case_uuid: str = Query(description="Sumo case uuid"),
43+
ensemble_name: str = Query(description="Ensemble name"),
44+
realization_num: int = Query(description="Realization number"),
45+
seismic_attribute: str = Query(description="Seismic cube attribute"),
46+
time_or_interval_str: str = Query(description="Timestamp or timestep"),
47+
observed: bool = Query(description="Observed or simulated"),
48+
polyline: schemas.SeismicFencePolyline = Body(embed=True),
49+
) -> schemas.SeismicFenceData:
50+
"""Get a fence of seismic data from a polyline defined by a set of (x, y) coordinates in domain coordinate system.
51+
52+
The fence data contains a set of traces perpendicular to the polyline, with one trace per (x, y)-point in polyline.
53+
Each trace has equal number of samples, and is a set of sample values along the depth direction of the seismic cube.
54+
55+
Returns:
56+
A SeismicFenceData object with fence traces in encoded 1D array, metadata for trace array decoding and fence min/max depth.
57+
"""
58+
seismic_access = await SeismicAccess.from_case_uuid(
59+
authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name
60+
)
61+
62+
vds_handle: Optional[VdsHandle] = None
63+
try:
64+
vds_handle = await seismic_access.get_vds_handle_async(
65+
realization=realization_num,
66+
seismic_attribute=seismic_attribute,
67+
time_or_interval_str=time_or_interval_str,
68+
observed=observed,
69+
)
70+
except ValueError as err:
71+
raise HTTPException(status_code=404, detail=str(err)) from err
72+
73+
if vds_handle is None:
74+
raise HTTPException(status_code=404, detail="Vds handle not found")
75+
76+
vds_access = VdsAccess(sas_token=vds_handle.sas_token, vds_url=vds_handle.vds_url)
77+
78+
# Retrieve fence and post as seismic intersection using cdp coordinates for vds-slice
79+
# NOTE: Correct coordinate format and scaling - see VdsCoordinateSystem?
80+
[
81+
flattened_fence_traces_array,
82+
num_traces,
83+
num_samples_per_trace,
84+
] = await vds_access.get_flattened_fence_traces_array_and_metadata_async(
85+
coordinates=VdsCoordinates(polyline.x_points, polyline.y_points),
86+
coordinate_system=VdsCoordinateSystem.CDP,
87+
)
88+
89+
meta: VdsMetadata = await vds_access.get_metadata_async()
90+
if len(meta.axis) != 3:
91+
raise ValueError(f"Expected 3 axes, got {len(meta.axis)}")
92+
depth_axis_meta = meta.axis[2]
93+
94+
return schemas.SeismicFenceData(
95+
fence_traces_b64arr=b64_encode_float_array_as_float32(flattened_fence_traces_array),
96+
num_traces=num_traces,
97+
num_samples_per_trace=num_samples_per_trace,
98+
min_fence_depth=depth_axis_meta.min,
99+
max_fence_depth=depth_axis_meta.max,
100+
)
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
from typing import List
2+
13
from pydantic import BaseModel
24

5+
from src.services.utils.b64 import B64FloatArray
6+
37

48
class SeismicCubeMeta(BaseModel):
59
seismic_attribute: str
@@ -8,6 +12,50 @@ class SeismicCubeMeta(BaseModel):
812
is_depth: bool
913

1014

11-
class VdsHandle(BaseModel):
12-
sas_token: str
13-
vds_url: str
15+
class SeismicFencePolyline(BaseModel):
16+
"""
17+
(x, y) points defining a polyline in domain coordinate system, to retrieve fence of seismic data.
18+
19+
Expect equal number of x- and y-points.
20+
21+
Note: Coordinates are in domain coordinate system (UTM).
22+
23+
NOTE:
24+
- Verify coordinates are in domain coordinate system (UTM)?
25+
- Consider points_xy: List[float] - i.e. array with [x1, y1, x2, y2, ..., xn, yn] instead of x_points and y_points arrays?
26+
- Ensure equal length of x_points and y_points arrays?
27+
"""
28+
29+
x_points: List[float]
30+
y_points: List[float]
31+
32+
33+
class SeismicFenceData(BaseModel):
34+
"""
35+
Definition of a fence of seismic data from a set of (x, y) coordinates in domain coordinate system.
36+
Each (x, y) point provides a trace perpendicular to the x-y plane, with number of samples equal to the depth of the seismic cube.
37+
38+
Each trace is defined to be a set of depth value samples along the length direction of the fence.
39+
40+
`Properties:`
41+
- `fence_traces_b64arr`: The fence trace array is base64 encoded 1D float array - where data is stored trace by trace.
42+
- `num_traces`: The number of traces in the fence trace array. Equals the number of (x, y) coordinates in requested polyline.
43+
- `num_samples_per_trace`: The number of samples in each trace.
44+
- `min_fence_depth`: The minimum depth value of the fence.
45+
- `max_fence_depth`: The maximum depth value of the fence.
46+
47+
`Description - fence_traces_b64arr:`\n
48+
The encoded fence trace array is a flattened array of traces, where data is stored trace by trace.
49+
With `m = num_traces`, and `n = num_samples_per_trace`, the flattened array has length `mxn`.
50+
51+
Fence traces 1D array: [trace_1_sample_1, trace_1_sample_2, ..., trace_1_sample_n, ..., trace_m_sample_1, trace_m_sample_2, ..., trace_m_sample_n] \n
52+
53+
See:
54+
- VdsAxis: https://github.com/equinor/vds-slice/blob/ab6f39789bf3d3b59a8df14f1c4682d340dc0bf3/internal/core/core.go#L37-L55
55+
"""
56+
57+
fence_traces_b64arr: B64FloatArray
58+
num_traces: int
59+
num_samples_per_trace: int
60+
min_fence_depth: float
61+
max_fence_depth: float

backend/src/config.py

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
SMDA_RESOURCE_SCOPE = os.environ["WEBVIZ_SMDA_RESOURCE_SCOPE"]
1818
SUMO_ENV = os.getenv("WEBVIZ_SUMO_ENV", "prod")
1919
GRAPH_SCOPES = ["User.Read", "User.ReadBasic.All"]
20+
VDS_HOST_ADDRESS = os.environ["WEBVIZ_VDS_HOST_ADDRESS"]
2021

2122
RESOURCE_SCOPES_DICT = {
2223
"sumo": [f"api://{sumo_app_reg[SUMO_ENV]['RESOURCE_ID']}/access_as_user"],

backend/src/services/sumo_access/seismic_access.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212

1313

1414
class SeismicAccess(SumoEnsemble):
15-
async def get_seismic_directory(self) -> List[SeismicCubeMeta]:
15+
async def get_seismic_cube_meta_list_async(self) -> List[SeismicCubeMeta]:
1616
seismic_cube_collection: CubeCollection = self._case.cubes.filter(iteration=self._iteration_name, realization=0)
17-
seismic_cube_metas: List[SeismicCubeMeta] = []
17+
seismic_cube_meta_list: List[SeismicCubeMeta] = []
1818
async for cube in seismic_cube_collection:
1919
t_start = cube["data"].get("time", {}).get("t0", {}).get("value", None)
2020
t_end = cube["data"].get("time", {}).get("t1", {}).get("value", None)
@@ -34,10 +34,10 @@ async def get_seismic_directory(self) -> List[SeismicCubeMeta]:
3434
is_observation=cube["data"]["is_observation"],
3535
is_depth=cube["data"]["vertical_domain"] == "depth",
3636
)
37-
seismic_cube_metas.append(seismic_meta)
38-
return seismic_cube_metas
37+
seismic_cube_meta_list.append(seismic_meta)
38+
return seismic_cube_meta_list
3939

40-
async def get_vds_handle(
40+
async def get_vds_handle_async(
4141
self,
4242
seismic_attribute: str,
4343
realization: int,

backend/src/services/vds_access/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
from dataclasses import dataclass
2+
from enum import StrEnum
3+
from typing import List
4+
5+
######################################################################################################
6+
#
7+
# This file contains the request types for the vds-slice service found in the following file:
8+
#
9+
# https://github.com/equinor/vds-slice/blob/master/api/request.go
10+
#
11+
# Master commit hash: ab6f39789bf3d3b59a8df14f1c4682d340dc0bf3
12+
#
13+
# https://github.com/equinor/vds-slice/blob/ab6f39789bf3d3b59a8df14f1c4682d340dc0bf3/api/request.go
14+
#
15+
######################################################################################################
16+
17+
18+
class VdsInterpolation(StrEnum):
19+
"""
20+
Interpolation options for vds fence
21+
22+
Source: https://github.com/equinor/vds-slice/blob/ab6f39789bf3d3b59a8df14f1c4682d340dc0bf3/api/request.go#L98
23+
"""
24+
25+
NEAREST = "nearest"
26+
LINEAR = "linear"
27+
CUBIC = "cubic"
28+
ANGULAR = "angular"
29+
TRIANGULAR = "triangular"
30+
31+
32+
class VdsCoordinateSystem(StrEnum):
33+
"""
34+
Coordinate system options for vds fence
35+
36+
* ilxl: inline, crossline pairs
37+
* ij: Coordinates are given as in 0-indexed system, where the first
38+
line in each direction is 0 and the last is number-of-lines - 1.
39+
* cdp: Coordinates are given as cdpx/cdpy pairs. In the original SEGY
40+
this would correspond to the cdpx and cdpy fields in the
41+
trace-headers after applying the scaling factor.
42+
43+
Source: https://github.com/equinor/vds-slice/blob/ab6f39789bf3d3b59a8df14f1c4682d340dc0bf3/api/request.go#L86C3-L86C3
44+
"""
45+
46+
CDP = "cdp"
47+
IJ = "ij"
48+
ILXL = "ilxl"
49+
50+
51+
@dataclass
52+
class VdsCoordinates:
53+
"""
54+
A list of coordinates in the selected VdsCoordinateSystem, as (x, y) points.
55+
56+
Convert coordinates to format for query request parameter - [[x1,y1], [x2,y2], ..., [xn,yn]]
57+
58+
Source: https://github.com/equinor/vds-slice/blob/ab6f39789bf3d3b59a8df14f1c4682d340dc0bf3/api/request.go#L90
59+
"""
60+
61+
x_points: List[float]
62+
y_points: List[float]
63+
64+
def __init__(self, x_points: List[float], y_points: List[float]) -> None:
65+
if len(x_points) != len(y_points):
66+
raise ValueError("x_points and y_points must be of equal length")
67+
68+
self.x_points = x_points
69+
self.y_points = y_points
70+
71+
def to_list(self) -> List[List[float]]:
72+
return [[x, y] for x, y in zip(self.x_points, self.y_points)]
73+
74+
75+
@dataclass
76+
class VdsRequestedResource:
77+
"""
78+
Definition of requested vds resource for vds-slice
79+
This is a base class for request types for vds-slice requests
80+
81+
See: https://github.com/equinor/vds-slice/blob/ab6f39789bf3d3b59a8df14f1c4682d340dc0bf3/api/request.go#L13-L35
82+
"""
83+
84+
vds: str # blob url
85+
sas: str # sas-token
86+
87+
def request_parameters(self) -> dict:
88+
return {"vds": self.vds, "sas": self.sas}
89+
90+
91+
@dataclass
92+
class VdsMetadataRequest(VdsRequestedResource):
93+
"""
94+
Definition of metadata request for vds-slice
95+
96+
See: https://github.com/equinor/vds-slice/blob/ab6f39789bf3d3b59a8df14f1c4682d340dc0bf3/api/request.go#L62-L64
97+
"""
98+
99+
100+
@dataclass
101+
class VdsFenceRequest(VdsRequestedResource):
102+
"""
103+
Definition of a fence request struct for vds-slice
104+
105+
See: https://github.com/equinor/vds-slice/blob/ab6f39789bf3d3b59a8df14f1c4682d340dc0bf3/api/request.go#L76-L105
106+
"""
107+
108+
coordinate_system: VdsCoordinateSystem
109+
coordinates: VdsCoordinates
110+
interpolation: VdsInterpolation
111+
fill_value: float
112+
113+
def request_parameters(self) -> dict:
114+
return {
115+
"vds": self.vds,
116+
"sas": self.sas,
117+
"coordinateSystem": self.coordinate_system.value,
118+
"coordinates": self.coordinates.to_list(),
119+
"interpolation": self.interpolation.value,
120+
"fillValue": self.fill_value,
121+
}

0 commit comments

Comments
 (0)