Skip to content

Commit

Permalink
Support sending of binary arrays using base64 encoding (equinor#371)
Browse files Browse the repository at this point in the history
  • Loading branch information
sigurdp authored Oct 2, 2023
1 parent 05ce9de commit e50e4b9
Show file tree
Hide file tree
Showing 21 changed files with 333 additions and 98 deletions.
6 changes: 4 additions & 2 deletions backend/src/backend/primary/routers/grid/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

from pydantic import BaseModel

from src.services.utils.b64 import B64FloatArray, B64UintArray


class GridSurface(BaseModel):
polys: dict
points: dict
polys_b64arr: B64UintArray
points_b64arr: B64FloatArray
xmin: float
xmax: float
ymin: float
Expand Down
16 changes: 11 additions & 5 deletions backend/src/backend/primary/routers/surface/converters.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from typing import List

import orjson
import numpy as np
import xtgeo
from src.services.sumo_access.surface_types import SurfaceMeta as SumoSurfaceMeta
from numpy.typing import NDArray

from src.services.smda_access.types import StratigraphicSurface
from src.services.utils.surface_to_float32 import surface_to_float32_array
from src.services.sumo_access.surface_types import SurfaceMeta as SumoSurfaceMeta
from src.services.utils.b64 import b64_encode_float_array_as_float32
from src.services.utils.surface_to_float32 import surface_to_float32_numpy_array

from . import schemas


Expand All @@ -25,7 +29,9 @@ def to_api_surface_data(xtgeo_surf: xtgeo.RegularSurface) -> schemas.SurfaceData
"""
Create API SurfaceData from xtgeo regular surface
"""
float32values = surface_to_float32_array(xtgeo_surf)

float32_np_arr: NDArray[np.float32] = surface_to_float32_numpy_array(xtgeo_surf)
values_b64arr = b64_encode_float_array_as_float32(float32_np_arr)

return schemas.SurfaceData(
x_ori=xtgeo_surf.xori,
Expand All @@ -41,7 +47,7 @@ def to_api_surface_data(xtgeo_surf: xtgeo.RegularSurface) -> schemas.SurfaceData
val_min=xtgeo_surf.values.min(),
val_max=xtgeo_surf.values.max(),
rot_deg=xtgeo_surf.rotation,
mesh_data=orjson.dumps(float32values).decode(), # pylint: disable=maybe-no-member
values_b64arr=values_b64arr,
)


Expand Down
39 changes: 26 additions & 13 deletions backend/src/backend/primary/routers/surface/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,19 @@ def get_realization_surface_data(
xtgeo_surf = access.get_realization_surface_data(
real_num=realization_num, name=name, attribute=attribute, time_or_interval_str=time_or_interval
)
et_get_surf_ms = timer.lap_ms()

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

surf_data_response = converters.to_api_surface_data(xtgeo_surf)
et_convert_ms = timer.lap_ms()

LOGGER.debug(f"Loaded static surface and created image, total time: {timer.elapsed_ms()}ms")
LOGGER.debug(
f"Loaded realization surface in: {timer.elapsed_ms()}ms ("
f"get_surf={et_get_surf_ms}ms, "
f"convert={et_convert_ms}ms)"
)

return surf_data_response

Expand All @@ -89,23 +95,30 @@ def get_statistical_surface_data(
) -> 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_surface_data(
statistic_function=service_stat_func_to_compute,
name=name,
attribute=attribute,
time_or_interval_str=time_or_interval,
)
if service_stat_func_to_compute is None:
raise HTTPException(status_code=404, detail="Invalid statistic requested")

access = SurfaceAccess(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name)
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,
)
et_calc_ms = timer.lap_ms()

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)
surf_data_response: schemas.SurfaceData = converters.to_api_surface_data(xtgeo_surf)
et_convert_ms = timer.lap_ms()

LOGGER.debug(f"Calculated statistical dynamic surface and created image, total time: {timer.elapsed_ms()}ms")
LOGGER.debug(
f"Calculated statistical surface in: {timer.elapsed_ms()}ms ("
f"calc={et_calc_ms}ms, "
f"convert={et_convert_ms}ms)"
)

return surf_data_response

Expand Down Expand Up @@ -142,7 +155,7 @@ def get_property_surface_resampled_to_static_surface(

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)
surf_data_response: schemas.SurfaceData = converters.to_api_surface_data(resampled_surface)

LOGGER.debug(f"Loaded property surface and created image, total time: {timer.elapsed_ms()}ms")

Expand Down
3 changes: 2 additions & 1 deletion backend/src/backend/primary/routers/surface/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pydantic import BaseModel

from src.services.smda_access.types import StratigraphicFeature
from src.services.utils.b64 import B64FloatArray


class SurfaceStatisticFunction(str, Enum):
Expand Down Expand Up @@ -66,4 +67,4 @@ class SurfaceData(BaseModel):
val_min: float
val_max: float
rot_deg: float
mesh_data: str
values_b64arr: B64FloatArray
6 changes: 3 additions & 3 deletions backend/src/backend/user_session/routers/grid/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
GridIntersection,
)
from src.services.sumo_access.grid_access import GridAccess
from src.services.utils.b64 import b64_encode_numpy
from src.services.utils.b64 import b64_encode_float_array_as_float32, b64_encode_uint_array_as_smallest_size
from src.services.utils.vtk_utils import (
VtkGridSurface,
get_scalar_values,
Expand Down Expand Up @@ -72,8 +72,8 @@ async def grid_surface(
points_np = np.around(points_np, decimals=2)

grid_surface_payload = GridSurface(
points=b64_encode_numpy(points_np),
polys=b64_encode_numpy(polys_np),
points_b64arr=b64_encode_float_array_as_float32(points_np),
polys_b64arr=b64_encode_uint_array_as_smallest_size(polys_np),
**grid_geometrics,
)
return ORJSONResponse(grid_surface_payload.dict())
Expand Down
128 changes: 89 additions & 39 deletions backend/src/services/utils/b64.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,94 @@
import base64
from typing import Any, Optional
from typing import Literal
from pydantic import BaseModel

import numpy as np
from numpy.typing import NDArray


def b64_encode_numpy(obj: Any) -> dict:
# Convert 1D numpy arrays with numeric types to memoryviews with
# datatype and shape metadata.
if len(obj) == 0:
return obj.tolist()

buffer: Optional[str] = None
dtype = obj.dtype
if dtype.kind in ["u", "i", "f"] and str(dtype) != "int64" and str(dtype) != "uint64":
# We have a numpy array that is compatible with JavaScript typed
# arrays
buffer = base64.b64encode(memoryview(obj.ravel(order="C"))).decode("utf-8")
return {"bvals": buffer, "dtype": str(dtype), "shape": obj.shape}

dtype_str: Optional[str] = None
# Try to see if we can downsize the array
max_value = np.amax(obj)
min_value = np.amin(obj)
signed = min_value < 0
test_value = max(max_value, -min_value)
if test_value < np.iinfo(np.int16).max and signed:
dtype_str = "int16"
buffer = base64.b64encode(memoryview(obj.astype(np.int16).ravel(order="C"))).decode("utf-8")
elif test_value < np.iinfo(np.int32).max and signed:
dtype_str = "int32"
buffer = base64.b64encode(memoryview(obj.astype(np.int32).ravel(order="C"))).decode("utf-8")
elif test_value < np.iinfo(np.uint16).max and not signed:
dtype_str = "uint16"
buffer = base64.b64encode(memoryview(obj.astype(np.uint16).ravel(order="C"))).decode("utf-8")
elif test_value < np.iinfo(np.uint32).max and not signed:
dtype_str = "uint32"
buffer = base64.b64encode(memoryview(obj.astype(np.uint32).ravel(order="C"))).decode("utf-8")

if dtype:
return {"bvals": buffer, "dtype": dtype_str, "shape": obj.shape}

# Convert all other numpy arrays to lists
return obj.tolist()
class B64FloatArray(BaseModel):
element_type: Literal["float32", "float64"]
data_b64str: str


class B64UintArray(BaseModel):
element_type: Literal["uint16", "uint32", "uint64"]
data_b64str: str


class B64IntArray(BaseModel):
element_type: Literal["int16", "int32"]
data_b64str: str


# class B64TypedArray(BaseModel):
# element_type: Literal["float32", "float64", "uint16", "uint32", "uint64", "int16", "int32"]
# data_b64str: str


def b64_encode_float_array_as_float32(input_arr: NDArray[np.floating] | list[float]) -> B64FloatArray:
"""
Base64 encodes an array of floating point numbers using 32bit float element size.
"""
np_arr: NDArray[np.float32] = np.asarray(input_arr, dtype=np.float32)
base64_str = _base64_encode_numpy_arr_to_str(np_arr)
return B64FloatArray(element_type="float32", data_b64str=base64_str)


def b64_encode_float_array_as_float64(input_arr: NDArray[np.floating] | list[float]) -> B64FloatArray:
"""
Base64 encodes array of floating point numbers using 64bit float element size.
"""
np_arr: NDArray[np.float64] = np.asarray(input_arr, dtype=np.float64)
base64_str = _base64_encode_numpy_arr_to_str(np_arr)
return B64FloatArray(element_type="float64", data_b64str=base64_str)


def b64_encode_int_array_as_int32(input_arr: NDArray[np.integer] | list[int]) -> B64IntArray:
"""
Base64 encodes an array of signed integers as using 32bit int element size.
"""
np_arr: NDArray[np.int32] = np.asarray(input_arr, dtype=np.int32)
base64_str = _base64_encode_numpy_arr_to_str(np_arr)
return B64IntArray(element_type="int32", data_b64str=base64_str)


def b64_encode_uint_array_as_uint32(input_arr: NDArray[np.unsignedinteger] | list[int]) -> B64UintArray:
"""
Base64 encodes an array of unsigned integers using 32bit uint element size.
"""
np_arr: NDArray[np.uint32] = np.asarray(input_arr, dtype=np.uint32)
base64_str = _base64_encode_numpy_arr_to_str(np_arr)
return B64UintArray(element_type="uint32", data_b64str=base64_str)


def b64_encode_uint_array_as_smallest_size(
input_arr: NDArray[np.unsignedinteger] | list[int], max_value: int | None = None
) -> B64UintArray:
"""
Base64 encodes an array of unsigned integers using the smallest possible element size.
If the maximum value in the array is known, it can be specified in the max_value parameter.
"""
if max_value is None:
max_value = np.amax(input_arr)

element_type: Literal["uint16", "uint32", "uint64"]

if max_value <= np.iinfo(np.uint16).max:
np_arr = np.asarray(input_arr, dtype=np.uint16)
element_type = "uint16"
elif max_value <= np.iinfo(np.uint32).max:
np_arr = np.asarray(input_arr, dtype=np.uint32)
element_type = "uint32"
else:
np_arr = np.asarray(input_arr, dtype=np.uint64)
element_type = "uint64"

base64_str = _base64_encode_numpy_arr_to_str(np_arr)

return B64UintArray(element_type=element_type, data_b64str=base64_str)


def _base64_encode_numpy_arr_to_str(np_arr: NDArray) -> str:
base64_bytes: bytes = base64.b64encode(np_arr.ravel(order="C").data)
return base64_bytes.decode("ascii")
12 changes: 5 additions & 7 deletions backend/src/services/utils/surface_to_float32.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
from typing import List

import numpy as np
from numpy.typing import NDArray
import xtgeo


def surface_to_float32_array(surface: xtgeo.RegularSurface) -> List[float]:
values = surface.values.astype(np.float32)
values.fill_value = np.nan
values = np.ma.filled(values)
def surface_to_float32_numpy_array(surface: xtgeo.RegularSurface) -> NDArray[np.float32]:
masked_values = surface.values.astype(np.float32)
values = np.ma.filled(masked_values, fill_value=np.nan)

# Rotate 90 deg left.
# This will cause the width of to run along the X axis
# and height of along Y axis (starting from bottom.)
values = np.rot90(values)

return values.flatten().tolist()
return values.flatten()
2 changes: 2 additions & 0 deletions frontend/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export { CancelablePromise, CancelError } from './core/CancelablePromise';
export { OpenAPI } from './core/OpenAPI';
export type { OpenAPIConfig } from './core/OpenAPI';

export { B64FloatArray as B64FloatArray_api } from './models/B64FloatArray';
export { B64UintArray as B64UintArray_api } from './models/B64UintArray';
export type { Body_get_realizations_response as Body_get_realizations_response_api } from './models/Body_get_realizations_response';
export type { CaseInfo as CaseInfo_api } from './models/CaseInfo';
export type { Completions as Completions_api } from './models/Completions';
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/api/models/B64FloatArray.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */

export type B64FloatArray = {
element_type: B64FloatArray.element_type;
data_b64str: string;
};

export namespace B64FloatArray {

export enum element_type {
FLOAT32 = 'float32',
FLOAT64 = 'float64',
}


}

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

export type B64UintArray = {
element_type: B64UintArray.element_type;
data_b64str: string;
};

export namespace B64UintArray {

export enum element_type {
UINT16 = 'uint16',
UINT32 = 'uint32',
UINT64 = 'uint64',
}


}

7 changes: 5 additions & 2 deletions frontend/src/api/models/GridSurface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
/* tslint:disable */
/* eslint-disable */

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

export type GridSurface = {
polys: Record<string, any>;
points: Record<string, any>;
polys_b64arr: B64UintArray;
points_b64arr: B64FloatArray;
xmin: number;
xmax: number;
ymin: number;
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/api/models/SurfaceData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
/* tslint:disable */
/* eslint-disable */

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

export type SurfaceData = {
x_ori: number;
y_ori: number;
Expand All @@ -16,6 +18,6 @@ export type SurfaceData = {
val_min: number;
val_max: number;
rot_deg: number;
mesh_data: string;
values_b64arr: B64FloatArray;
};

Loading

0 comments on commit e50e4b9

Please sign in to comment.