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

Support sending of binary arrays using base64 encoding #371

Merged
merged 6 commits into from
Oct 2, 2023
Merged
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
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