diff --git a/backend_py/primary/primary/routers/timeseries/converters.py b/backend_py/primary/primary/routers/timeseries/converters.py index e449e6e8d..478fa5102 100644 --- a/backend_py/primary/primary/routers/timeseries/converters.py +++ b/backend_py/primary/primary/routers/timeseries/converters.py @@ -31,14 +31,7 @@ def to_api_vector_statistic_data( """ Create API VectorStatisticData from service layer VectorStatistics """ - value_objects: List[schemas.StatisticValueObject] = [] - for api_func_enum in schemas.StatisticFunction: - service_func_enum = StatisticFunction.from_string_value(api_func_enum.value) - if service_func_enum is not None: - value_arr = vector_statistics.values_dict.get(service_func_enum) - if value_arr is not None: - value_objects.append(schemas.StatisticValueObject(statistic_function=api_func_enum, values=value_arr)) - + value_objects = _create_statistic_value_object_list(vector_statistics) ret_data = schemas.VectorStatisticData( realizations=vector_statistics.realizations, timestamps_utc_ms=vector_statistics.timestamps_utc_ms, @@ -48,3 +41,36 @@ def to_api_vector_statistic_data( ) return ret_data + + +def to_api_delta_ensemble_vector_statistic_data( + vector_statistics: VectorStatistics, is_rate: bool, unit: str +) -> schemas.VectorStatisticData: + """ + Create API VectorStatisticData from service layer VectorStatistics + """ + value_objects = _create_statistic_value_object_list(vector_statistics) + ret_data = schemas.VectorStatisticData( + realizations=vector_statistics.realizations, + timestamps_utc_ms=vector_statistics.timestamps_utc_ms, + value_objects=value_objects, + unit=unit, + is_rate=is_rate, + ) + + return ret_data + + +def _create_statistic_value_object_list(vector_statistics: VectorStatistics) -> list[schemas.StatisticValueObject]: + """ + Create list of statistic value objects from vector statistics object + """ + value_objects: list[schemas.StatisticValueObject] = [] + for api_func_enum in schemas.StatisticFunction: + service_func_enum = StatisticFunction.from_string_value(api_func_enum.value) + if service_func_enum is not None: + value_arr = vector_statistics.values_dict.get(service_func_enum) + if value_arr is not None: + value_objects.append(schemas.StatisticValueObject(statistic_function=api_func_enum, values=value_arr)) + + return value_objects diff --git a/backend_py/primary/primary/routers/timeseries/router.py b/backend_py/primary/primary/routers/timeseries/router.py index 9f0a88708..8b4cc7485 100644 --- a/backend_py/primary/primary/routers/timeseries/router.py +++ b/backend_py/primary/primary/routers/timeseries/router.py @@ -12,8 +12,10 @@ from primary.services.sumo_access.parameter_access import ParameterAccess from primary.services.sumo_access.summary_access import Frequency, SummaryAccess from primary.services.utils.authenticated_user import AuthenticatedUser +from primary.services.summary_delta_vectors import create_delta_vector_table, create_realization_delta_vector_list from . import converters, schemas +import asyncio LOGGER = logging.getLogger(__name__) @@ -48,6 +50,56 @@ async def get_vector_list( return ret_arr +@router.get("/delta_ensemble_vector_list/") +async def get_delta_ensemble_vector_list( + response: Response, + authenticated_user: Annotated[AuthenticatedUser, Depends(AuthHelper.get_authenticated_user)], + compare_case_uuid: Annotated[str, Query(description="Sumo case uuid for compare ensemble")], + compare_ensemble_name: Annotated[str, Query(description="Compare ensemble name")], + reference_case_uuid: Annotated[str, Query(description="Sumo case uuid for reference ensemble")], + reference_ensemble_name: Annotated[str, Query(description="Reference ensemble name")], +) -> list[schemas.VectorDescription]: + """Get list of all vectors for a delta ensemble based on all vectors in a given Sumo ensemble, excluding any historical vectors + + Definition: + + delta_ensemble = compare_ensemble - reference_ensemble + """ + + perf_metrics = ResponsePerfMetrics(response) + + compare_ensemble_access = SummaryAccess.from_case_uuid( + authenticated_user.get_sumo_access_token(), compare_case_uuid, compare_ensemble_name + ) + reference_ensemble_access = SummaryAccess.from_case_uuid( + authenticated_user.get_sumo_access_token(), reference_case_uuid, reference_ensemble_name + ) + perf_metrics.record_lap("get-access") + + # Get vectors parallel + compare_vector_info_arr, reference_vector_info_arr = await asyncio.gather( + compare_ensemble_access.get_available_vectors_async(), + reference_ensemble_access.get_available_vectors_async(), + ) + perf_metrics.record_lap("get-available-vectors") + + # Create intersection of vector names + vector_names = {vi.name for vi in compare_vector_info_arr} + vector_names.intersection_update({vi.name for vi in reference_vector_info_arr}) + perf_metrics.record_lap("create-vectors-names-intersection") + + # Create vector descriptions, no historical vectors! + ret_arr: list[schemas.VectorDescription] = [ + schemas.VectorDescription(name=vi, descriptive_name=vi, has_historical=False) for vi in vector_names + ] + + perf_metrics.record_lap("convert-data-to-schema") + + LOGGER.info(f"Got delta ensemble vector list in: {perf_metrics.to_string()}") + + return ret_arr + + @router.get("/realizations_vector_data/") async def get_realizations_vector_data( # fmt:off @@ -91,6 +143,98 @@ async def get_realizations_vector_data( return ret_arr +@router.get("/delta_ensemble_realizations_vector_data/") +async def get_delta_ensemble_realizations_vector_data( + # fmt:off + response: Response, + authenticated_user: Annotated[AuthenticatedUser, Depends(AuthHelper.get_authenticated_user)], + compare_case_uuid: Annotated[str, Query(description="Sumo case uuid for compare ensemble")], + compare_ensemble_name: Annotated[str, Query(description="Compare ensemble name")], + reference_case_uuid: Annotated[str, Query(description="Sumo case uuid for reference ensemble")], + reference_ensemble_name: Annotated[str, Query(description="Reference ensemble name")], + vector_name: Annotated[str, Query(description="Name of the vector")], + resampling_frequency: Annotated[schemas.Frequency, Query(description="Resampling frequency")], + realizations: Annotated[list[int] | None, Query(description="Optional list of realizations to include. If not specified, all realizations will be returned.")] = None, + # relative_to_timestamp: Annotated[datetime.datetime | None, Query(description="Calculate relative to timestamp")] = None, + # fmt:on +) -> list[schemas.VectorRealizationData]: + """Get vector data per realization + + Definition: + + delta_ensemble = compare_ensemble - reference_ensemble + + """ + + perf_metrics = ResponsePerfMetrics(response) + + service_freq = Frequency.from_string_value(resampling_frequency.value) + if service_freq is None: + raise HTTPException( + status_code=400, detail="Resampling frequency must be specified to create delta ensemble vector" + ) + + compare_ensemble_access = SummaryAccess.from_case_uuid( + authenticated_user.get_sumo_access_token(), compare_case_uuid, compare_ensemble_name + ) + reference_ensemble_access = SummaryAccess.from_case_uuid( + authenticated_user.get_sumo_access_token(), reference_case_uuid, reference_ensemble_name + ) + + # Get tables parallel + # - Resampled data is assumed to be s.t. dates/timestamps are comparable between ensembles and cases, i.e. timestamps + # for a resampling of a daily vector in both ensembles should be the same + (compare_vector_table_pa, compare_metadata), (reference_vector_table_pa, reference_metadata) = await asyncio.gather( + compare_ensemble_access.get_vector_table_async( + vector_name=vector_name, + resampling_frequency=service_freq, + realizations=realizations, + ), + reference_ensemble_access.get_vector_table_async( + vector_name=vector_name, + resampling_frequency=service_freq, + realizations=realizations, + ), + ) + + # Check for mismatching metadata + if compare_metadata.is_rate != reference_metadata.is_rate: + raise HTTPException( + status_code=400, detail="Rate mismatch between ensembles for delta ensemble statistical vector data" + ) + if compare_metadata.unit != reference_metadata.unit: + raise HTTPException( + status_code=400, detail="Unit mismatch between ensembles for delta ensemble statistical vector data" + ) + + # Get metadata from reference ensemble + is_rate = reference_metadata.is_rate + unit = reference_metadata.unit + + # Create delta ensemble data + delta_vector_table = create_delta_vector_table(compare_vector_table_pa, reference_vector_table_pa, vector_name) + perf_metrics.record_lap("create-delta-vector-table") + + realization_delta_vector_list = create_realization_delta_vector_list(delta_vector_table, vector_name, is_rate, unit) + perf_metrics.record_lap("create-realization-delta-vector-list") + + ret_arr: list[schemas.VectorRealizationData] = [] + for vec in realization_delta_vector_list: + ret_arr.append( + schemas.VectorRealizationData( + realization=vec.realization, + timestamps_utc_ms=vec.timestamps_utc_ms, + values=vec.values, + unit=vec.unit, + is_rate=vec.is_rate, + ) + ) + + LOGGER.info(f"Loaded realization delta ensemble summary data in: {perf_metrics.to_string()}") + + return ret_arr + + @router.get("/timestamps_list/") async def get_timestamps_list( authenticated_user: Annotated[AuthenticatedUser, Depends(AuthHelper.get_authenticated_user)], @@ -181,6 +325,93 @@ async def get_statistical_vector_data( return ret_data +@router.get("/delta_ensemble_statistical_vector_data/") +async def get_delta_ensemble_statistical_vector_data( + # fmt:off + response: Response, + authenticated_user: Annotated[AuthenticatedUser, Depends(AuthHelper.get_authenticated_user)], + compare_case_uuid: Annotated[str, Query(description="Sumo case uuid for compare ensemble")], + compare_ensemble_name: Annotated[str, Query(description="Compare ensemble name")], + reference_case_uuid: Annotated[str, Query(description="Sumo case uuid for reference ensemble")], + reference_ensemble_name: Annotated[str, Query(description="Reference ensemble name")], + vector_name: Annotated[str, Query(description="Name of the vector")], + resampling_frequency: Annotated[schemas.Frequency, Query(description="Resampling frequency")], + statistic_functions: Annotated[list[schemas.StatisticFunction] | None, Query(description="Optional list of statistics to calculate. If not specified, all statistics will be calculated.")] = None, + realizations: Annotated[list[int] | None, Query(description="Optional list of realizations to include. If not specified, all realizations will be included.")] = None, + # relative_to_timestamp: Annotated[datetime.datetime | None, Query(description="Calculate relative to timestamp")] = None, + # fmt:on +) -> schemas.VectorStatisticData: + """Get statistical vector data for an ensemble + + Definition: + + delta_ensemble = compare_ensemble - reference_ensemble + + """ + + perf_metrics = ResponsePerfMetrics(response) + + service_freq = Frequency.from_string_value(resampling_frequency.value) + service_stat_funcs_to_compute = converters.to_service_statistic_functions(statistic_functions) + + if service_freq is None: + raise HTTPException( + status_code=400, detail="Resampling frequency must be specified to create delta ensemble vector" + ) + + compare_ensemble_access = SummaryAccess.from_case_uuid( + authenticated_user.get_sumo_access_token(), compare_case_uuid, compare_ensemble_name + ) + reference_ensemble_access = SummaryAccess.from_case_uuid( + authenticated_user.get_sumo_access_token(), reference_case_uuid, reference_ensemble_name + ) + + # Get tables parallel + # - Resampled data is assumed to be s.t. dates/timestamps are comparable between ensembles and cases, i.e. timestamps + # for a resampling of a daily vector in both ensembles should be the same + (compare_vector_table_pa, compare_metadata), (reference_vector_table_pa, reference_metadata) = await asyncio.gather( + compare_ensemble_access.get_vector_table_async( + vector_name=vector_name, + resampling_frequency=service_freq, + realizations=realizations, + ), + reference_ensemble_access.get_vector_table_async( + vector_name=vector_name, + resampling_frequency=service_freq, + realizations=realizations, + ), + ) + + # Check for mismatching metadata + if compare_metadata.is_rate != reference_metadata.is_rate: + raise HTTPException( + status_code=400, detail="Rate mismatch between ensembles for delta ensemble statistical vector data" + ) + if compare_metadata.unit != reference_metadata.unit: + raise HTTPException( + status_code=400, detail="Unit mismatch between ensembles for delta ensemble statistical vector data" + ) + + # Get metadata from reference ensemble + is_rate = reference_metadata.is_rate + unit = reference_metadata.unit + + # Create delta ensemble data and compute statistics + delta_vector_table = create_delta_vector_table(compare_vector_table_pa, reference_vector_table_pa, vector_name) + statistics = compute_vector_statistics(delta_vector_table, vector_name, service_stat_funcs_to_compute) + if not statistics: + raise HTTPException(status_code=404, detail="Could not compute statistics") + perf_metrics.record_lap("calc-delta-vector-stat") + + ret_data: schemas.VectorStatisticData = converters.to_api_delta_ensemble_vector_statistic_data( + statistics, is_rate, unit + ) + + LOGGER.info(f"Loaded and computed statistical delta ensemble summary data in: {perf_metrics.to_string()}") + + return ret_data + + @router.get("/statistical_vector_data_per_sensitivity/") async def get_statistical_vector_data_per_sensitivity( # fmt:off @@ -250,18 +481,3 @@ async def get_realization_vector_at_timestamp( vector_name=vector_name, timestamp_utc_ms=timestamp_utc_ms, realizations=None ) return ensemble_response - - -# @router.get("/realizations_calculated_vector_data/") -# def get_realizations_calculated_vector_data( -# authenticated_user: Annotated[AuthenticatedUser, Depends(AuthHelper.get_authenticated_user)], -# case_uuid: Annotated[str, Query(description="Sumo case uuid")], -# ensemble_name: Annotated[str, Query(description="Ensemble name")], -# expression: Annotated[schemas.VectorExpressionInfo, Depends()], -# resampling_frequency: Annotated[schemas.Frequency, Query(description="Resampling frequency")], -# relative_to_timestamp: Annotated[datetime.datetime | None, Query(description="Calculate relative to timestamp")] = None, -# ) -> str: -# """Get calculated vector data per realization""" -# print(expression) -# print(type(expression)) -# return "hei" diff --git a/backend_py/primary/primary/services/summary_delta_vectors.py b/backend_py/primary/primary/services/summary_delta_vectors.py new file mode 100644 index 000000000..e480160d8 --- /dev/null +++ b/backend_py/primary/primary/services/summary_delta_vectors.py @@ -0,0 +1,115 @@ +from dataclasses import dataclass + +import pyarrow as pa +import pyarrow.compute as pc +import numpy as np + +from primary.services.service_exceptions import InvalidDataError, Service + + +@dataclass +class RealizationDeltaVector: + realization: int + timestamps_utc_ms: list[int] + values: list[float] + is_rate: bool + unit: str + + +def _validate_summary_vector_table_pa( + vector_table: pa.Table, vector_name: str, service: Service = Service.GENERAL +) -> None: + """ + Check if the pyarrow vector table is valid. + + Expect the pyarrow single vector table to contain the following columns: DATE, REAL, vector_name. + + Raises InvalidDataError if the table does not contain the expected columns. + """ + expected_columns = {"DATE", "REAL", vector_name} + actual_columns = set(vector_table.column_names) + if actual_columns != expected_columns: + unexpected_columns = actual_columns - expected_columns + raise InvalidDataError(f"Unexpected columns in table {unexpected_columns}", service) + + # Validate table column types + if vector_table.field("DATE").type != pa.timestamp("ms"): + raise InvalidDataError( + f'DATE column must be of type timestamp(ms), but got {vector_table.field("DATE").type}', service + ) + if vector_table.field("REAL").type != pa.int16(): + raise InvalidDataError("REAL column must be of type int16", service) + if vector_table.field(vector_name).type != pa.float32(): + raise InvalidDataError(f"{vector_name} column must be of type float32", service) + + +def create_delta_vector_table( + compare_vector_table: pa.Table, reference_vector_table: pa.Table, vector_name: str +) -> pa.Table: + """ + Create a table with delta values of the requested vector name between the two input tables. + + Definition: + + delta_vector = compare_vector - reference_vector + + Performs "inner join". Only obtain matching index ["DATE", "REAL"] - i.e "DATE"-"REAL" combination + present in only one vector is neglected. + + Returns: A table with columns ["DATE", "REAL", vector_name] where vector_name contains the delta values. + + `Note`: Pre-processing of DATE-columns, e.g. resampling, should be done before calling this function. + """ + _validate_summary_vector_table_pa(compare_vector_table, vector_name) + _validate_summary_vector_table_pa(reference_vector_table, vector_name) + + joined_vector_table = compare_vector_table.join( + reference_vector_table, keys=["DATE", "REAL"], join_type="inner", right_suffix="_reference" + ) + delta_vector = pc.subtract( + joined_vector_table.column(vector_name), joined_vector_table.column(f"{vector_name}_reference") + ) + + delta_table = pa.table( + { + "DATE": joined_vector_table.column("DATE"), + "REAL": joined_vector_table.column("REAL"), + vector_name: delta_vector, + } + ) + + return delta_table + + +def create_realization_delta_vector_list( + delta_vector_table: pa.Table, vector_name: str, is_rate: bool, unit: str +) -> list[RealizationDeltaVector]: + """ + Create a list of RealizationDeltaVector from the delta vector table. + """ + _validate_summary_vector_table_pa(delta_vector_table, vector_name) + + real_arr_np = delta_vector_table.column("REAL").to_numpy() + unique_reals, first_occurrence_idx, real_counts = np.unique(real_arr_np, return_index=True, return_counts=True) + + whole_date_np_arr = delta_vector_table.column("DATE").to_numpy() + whole_value_np_arr = delta_vector_table.column(vector_name).to_numpy() + + ret_arr: list[RealizationDeltaVector] = [] + for i, real in enumerate(unique_reals): + start_row_idx = first_occurrence_idx[i] + row_count = real_counts[i] + date_np_arr = whole_date_np_arr[start_row_idx : start_row_idx + row_count] + value_np_arr = whole_value_np_arr[start_row_idx : start_row_idx + row_count] + + ret_arr.append( + RealizationDeltaVector( + realization=real, + timestamps_utc_ms=date_np_arr.astype(int).tolist(), + values=value_np_arr.tolist(), + is_rate=is_rate, + unit=unit, + ) + ) + + return ret_arr diff --git a/backend_py/primary/primary/services/sumo_access/summary_access.py b/backend_py/primary/primary/services/sumo_access/summary_access.py index e125ec7f6..bb4a7acc7 100644 --- a/backend_py/primary/primary/services/sumo_access/summary_access.py +++ b/backend_py/primary/primary/services/sumo_access/summary_access.py @@ -11,8 +11,11 @@ from fmu.sumo.explorer.objects import TableCollection, Table from webviz_pkg.core_utils.perf_timer import PerfTimer -from primary.services.utils.arrow_helpers import sort_table_on_real_then_date, is_date_column_monotonically_increasing -from primary.services.utils.arrow_helpers import find_first_non_increasing_date_pair +from primary.services.utils.arrow_helpers import ( + find_first_non_increasing_date_pair, + sort_table_on_real_then_date, + is_date_column_monotonically_increasing, +) from primary.services.service_exceptions import ( Service, NoDataError, diff --git a/backend_py/primary/tests/unit/services/test_summary_delta_vectors.py b/backend_py/primary/tests/unit/services/test_summary_delta_vectors.py new file mode 100644 index 000000000..050f99922 --- /dev/null +++ b/backend_py/primary/tests/unit/services/test_summary_delta_vectors.py @@ -0,0 +1,192 @@ +import pytest +import pyarrow as pa + +from primary.services.service_exceptions import InvalidDataError, Service +from primary.services.summary_delta_vectors import ( + create_delta_vector_table, + create_realization_delta_vector_list, + RealizationDeltaVector, + _validate_summary_vector_table_pa, +) + + +VECTOR_TABLE_SCHEMA = pa.schema([("DATE", pa.timestamp("ms")), ("REAL", pa.int16()), ("vector", pa.float32())]) + + +def test_create_delta_vector_table() -> None: + # Create sample data for compare_vector_table + compare_data = {"DATE": [1, 2, 3, 4], "REAL": [1, 1, 2, 2], "vector": [10.0, 20.0, 30.0, 40.0]} + compare_vector_table = pa.table(compare_data, schema=VECTOR_TABLE_SCHEMA) + + # Create sample data for reference_vector_table + reference_data = {"DATE": [1, 2, 3, 4], "REAL": [1, 1, 2, 2], "vector": [5.0, 15.0, 25.0, 35.0]} + reference_vector_table = pa.table(reference_data, schema=VECTOR_TABLE_SCHEMA) + + # Expected delta values + expected_delta_data = {"DATE": [1, 2, 3, 4], "REAL": [1, 1, 2, 2], "vector": [5.0, 5.0, 5.0, 5.0]} + expected_delta_table = pa.table(expected_delta_data, schema=VECTOR_TABLE_SCHEMA) + + # Call the function + result_table = create_delta_vector_table(compare_vector_table, reference_vector_table, "vector") + + # Validate the result + assert result_table.equals(expected_delta_table) + + +def test_create_delta_vector_table_with_missing_dates() -> None: + # Create sample data for compare_vector_table + compare_data = {"DATE": [1, 2, 4], "REAL": [1, 1, 2], "vector": [10.0, 20.0, 40.0]} + compare_vector_table = pa.table(compare_data, schema=VECTOR_TABLE_SCHEMA) + + # Create sample data for reference_vector_table + reference_data = {"DATE": [1, 2, 3], "REAL": [1, 1, 2], "vector": [5.0, 15.0, 25.0]} + reference_vector_table = pa.table(reference_data, schema=VECTOR_TABLE_SCHEMA) + + # Expected delta values + expected_delta_data = {"DATE": [1, 2], "REAL": [1, 1], "vector": [5.0, 5.0]} + expected_delta_table = pa.table(expected_delta_data, schema=VECTOR_TABLE_SCHEMA) + + # Call the function + result_table = create_delta_vector_table(compare_vector_table, reference_vector_table, "vector") + + # Validate the result + assert result_table.equals(expected_delta_table) + + +def test_create_delta_vector_table_with_different_reals() -> None: + # Create sample data for compare_vector_table + compare_data = {"DATE": [1, 2, 3, 4], "REAL": [1, 1, 2, 3], "vector": [10.0, 20.0, 30.0, 40.0]} + compare_vector_table = pa.table(compare_data, schema=VECTOR_TABLE_SCHEMA) + + # Create sample data for reference_vector_table + reference_data = {"DATE": [1, 2, 3, 4], "REAL": [1, 1, 2, 2], "vector": [5.0, 15.0, 25.0, 35.0]} + reference_vector_table = pa.table(reference_data, schema=VECTOR_TABLE_SCHEMA) + + # Expected delta values + expected_delta_data = {"DATE": [1, 2, 3], "REAL": [1, 1, 2], "vector": [5.0, 5.0, 5.0]} + expected_delta_table = pa.table(expected_delta_data, schema=VECTOR_TABLE_SCHEMA) + + # Call the function + result_table = create_delta_vector_table(compare_vector_table, reference_vector_table, "vector") + + # Validate the result + assert result_table.equals(expected_delta_table) + + +def test_create_realization_delta_vector_list() -> None: + # Create sample data for delta_vector_table + delta_data = {"DATE": [1, 2, 3, 4], "REAL": [1, 1, 2, 2], "vector": [5.0, 10.0, 15.0, 20.0]} + delta_vector_table = pa.table(delta_data, schema=VECTOR_TABLE_SCHEMA) + + # Expected result + expected_result = [ + RealizationDeltaVector(realization=1, timestamps_utc_ms=[1, 2], values=[5.0, 10.0], is_rate=True, unit="unit"), + RealizationDeltaVector(realization=2, timestamps_utc_ms=[3, 4], values=[15.0, 20.0], is_rate=True, unit="unit"), + ] + + # Call the function + result = create_realization_delta_vector_list(delta_vector_table, "vector", is_rate=True, unit="unit") + + # Validate the result + assert result == expected_result + + +def test_create_realization_delta_vector_list_with_single_real() -> None: + # Create sample data for delta_vector_table + delta_data = {"DATE": [1, 2, 3, 4], "REAL": [1, 1, 1, 1], "vector": [5.0, 10.0, 15.0, 20.0]} + delta_vector_table = pa.table(delta_data, schema=VECTOR_TABLE_SCHEMA) + + # Expected result + expected_result = [ + RealizationDeltaVector( + realization=1, timestamps_utc_ms=[1, 2, 3, 4], values=[5.0, 10.0, 15.0, 20.0], is_rate=False, unit="unit" + ) + ] + + # Call the function + result = create_realization_delta_vector_list(delta_vector_table, "vector", is_rate=False, unit="unit") + + # Validate the result + assert result == expected_result + + +def test_create_realization_delta_vector_list_with_empty_table() -> None: + # Create an empty delta_vector_table + delta_vector_table = pa.table({"DATE": [], "REAL": [], "vector": []}, schema=VECTOR_TABLE_SCHEMA) + + # Expected result + expected_result: list[RealizationDeltaVector] = [] + + # Call the function + result = create_realization_delta_vector_list(delta_vector_table, "vector", is_rate=True, unit="unit") + + # Validate the result + assert result == expected_result + + +def test_validate_summary_vector_table_pa_valid() -> None: + vector_name = "VECTOR" + data = {"DATE": [1, 2, 3], "REAL": [4, 5, 6], vector_name: [7.0, 8.0, 9.0]} + schema = pa.schema([("DATE", pa.timestamp("ms")), ("REAL", pa.int16()), (vector_name, pa.float32())]) + table = pa.Table.from_pydict(data, schema=schema) + try: + _validate_summary_vector_table_pa(table, vector_name) + except InvalidDataError: + pytest.fail("validate_summary_vector_table_pa raised InvalidDataError unexpectedly!") + + +def test_validate_summary_vector_table_pa_missing_column() -> None: + vector_name = "VECTOR" + data = {"DATE": [1, 2, 3], "REAL": [4, 5, 6]} + schema = pa.schema([("DATE", pa.timestamp("ms")), ("REAL", pa.int16())]) + table = pa.Table.from_pydict(data, schema=schema) + with pytest.raises(InvalidDataError): + _validate_summary_vector_table_pa(table, vector_name) + + +def test_validate_summary_vector_table_pa_unexpected_column() -> None: + vector_name = "VECTOR" + data = {"DATE": [1, 2, 3], "REAL": [4, 5, 6], vector_name: [7.0, 8.0, 9.0], "EXTRA": [10.0, 11.0, 12.0]} + schema = pa.schema( + [("DATE", pa.timestamp("ms")), ("REAL", pa.int16()), (vector_name, pa.float32()), ("EXTRA", pa.float32())] + ) + table = pa.Table.from_pydict(data, schema=schema) + with pytest.raises(InvalidDataError): + _validate_summary_vector_table_pa(table, vector_name) + + +def test_validate_summary_vector_table_pa_invalid_date_type() -> None: + vector_name = "VECTOR" + data = {"DATE": [1, 2, 3], "REAL": [4, 5, 6], vector_name: [7.0, 8.0, 9.0]} + schema = pa.schema([("DATE", pa.int32()), ("REAL", pa.int16()), (vector_name, pa.float32())]) + table = pa.Table.from_pydict(data, schema=schema) + with pytest.raises(InvalidDataError): + _validate_summary_vector_table_pa(table, vector_name) + + +def test_validate_summary_vector_table_pa_invalid_real_type() -> None: + vector_name = "VECTOR" + data = {"DATE": [1, 2, 3], "REAL": [4.0, 5.0, 6.0], vector_name: [7.0, 8.0, 9.0]} + schema = pa.schema([("DATE", pa.timestamp("ms")), ("REAL", pa.float32()), (vector_name, pa.float32())]) + table = pa.Table.from_pydict(data, schema=schema) + with pytest.raises(InvalidDataError): + _validate_summary_vector_table_pa(table, vector_name) + + +def test_validate_summary_vector_table_pa_invalid_vector_type() -> None: + vector_name = "VECTOR" + data = {"DATE": [1, 2, 3], "REAL": [4, 5, 6], vector_name: [7, 8, 9]} + schema = pa.schema([("DATE", pa.timestamp("ms")), ("REAL", pa.int16()), (vector_name, pa.int32())]) + table = pa.Table.from_pydict(data, schema=schema) + with pytest.raises(InvalidDataError): + _validate_summary_vector_table_pa(table, vector_name) + + +def test_validate_summary_vector_table_pa_sumo_service() -> None: + vector_name = "VECTOR" + data = {"DATE": [1, 2, 3], "REAL": [4, 5, 6]} + schema = pa.schema([("DATE", pa.timestamp("ms")), ("REAL", pa.int16())]) + table = pa.Table.from_pydict(data, schema=schema) + with pytest.raises(InvalidDataError) as excinfo: + _validate_summary_vector_table_pa(table, vector_name, Service.SUMO) + assert excinfo.value.service == Service.SUMO diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1d03a1c5b..99930f0c7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -85,11 +85,18 @@ function App() { setIsMounted(true); const storedEnsembleIdents = workbench.maybeLoadEnsembleSettingsFromLocalStorage(); - if (storedEnsembleIdents) { + const storedDeltaEnsembles = workbench.maybeLoadDeltaEnsembleSettingsFromLocalStorage(); + if (storedEnsembleIdents || storedDeltaEnsembles) { setInitAppState(InitAppState.LoadingEnsembles); - workbench.loadAndSetupEnsembleSetInSession(queryClient, storedEnsembleIdents).finally(() => { - initApp(); - }); + workbench + .loadAndSetupEnsembleSetInSession( + queryClient, + storedEnsembleIdents ?? [], + storedDeltaEnsembles ?? [] + ) + .finally(() => { + initApp(); + }); } else { initApp(); } diff --git a/frontend/src/api/services/TimeseriesService.ts b/frontend/src/api/services/TimeseriesService.ts index b872c2ed4..967dbc1d4 100644 --- a/frontend/src/api/services/TimeseriesService.ts +++ b/frontend/src/api/services/TimeseriesService.ts @@ -38,6 +38,40 @@ export class TimeseriesService { }, }); } + /** + * Get Delta Ensemble Vector List + * Get list of all vectors for a delta ensemble based on all vectors in a given Sumo ensemble, excluding any historical vectors + * + * Definition: + * + * delta_ensemble = compare_ensemble - reference_ensemble + * @param compareCaseUuid Sumo case uuid for compare ensemble + * @param compareEnsembleName Compare ensemble name + * @param referenceCaseUuid Sumo case uuid for reference ensemble + * @param referenceEnsembleName Reference ensemble name + * @returns VectorDescription Successful Response + * @throws ApiError + */ + public getDeltaEnsembleVectorList( + compareCaseUuid: string, + compareEnsembleName: string, + referenceCaseUuid: string, + referenceEnsembleName: string, + ): CancelablePromise> { + return this.httpRequest.request({ + method: 'GET', + url: '/timeseries/delta_ensemble_vector_list/', + query: { + 'compare_case_uuid': compareCaseUuid, + 'compare_ensemble_name': compareEnsembleName, + 'reference_case_uuid': referenceCaseUuid, + 'reference_ensemble_name': referenceEnsembleName, + }, + errors: { + 422: `Validation Error`, + }, + }); + } /** * Get Realizations Vector Data * Get vector data per realization @@ -71,6 +105,49 @@ export class TimeseriesService { }, }); } + /** + * Get Delta Ensemble Realizations Vector Data + * Get vector data per realization + * + * Definition: + * + * delta_ensemble = compare_ensemble - reference_ensemble + * @param compareCaseUuid Sumo case uuid for compare ensemble + * @param compareEnsembleName Compare ensemble name + * @param referenceCaseUuid Sumo case uuid for reference ensemble + * @param referenceEnsembleName Reference ensemble name + * @param vectorName Name of the vector + * @param resamplingFrequency Resampling frequency + * @param realizations Optional list of realizations to include. If not specified, all realizations will be returned. + * @returns VectorRealizationData Successful Response + * @throws ApiError + */ + public getDeltaEnsembleRealizationsVectorData( + compareCaseUuid: string, + compareEnsembleName: string, + referenceCaseUuid: string, + referenceEnsembleName: string, + vectorName: string, + resamplingFrequency: Frequency, + realizations?: (Array | null), + ): CancelablePromise> { + return this.httpRequest.request({ + method: 'GET', + url: '/timeseries/delta_ensemble_realizations_vector_data/', + query: { + 'compare_case_uuid': compareCaseUuid, + 'compare_ensemble_name': compareEnsembleName, + 'reference_case_uuid': referenceCaseUuid, + 'reference_ensemble_name': referenceEnsembleName, + 'vector_name': vectorName, + 'resampling_frequency': resamplingFrequency, + 'realizations': realizations, + }, + errors: { + 422: `Validation Error`, + }, + }); + } /** * Get Timestamps List * Get the intersection of available timestamps. @@ -168,6 +245,52 @@ export class TimeseriesService { }, }); } + /** + * Get Delta Ensemble Statistical Vector Data + * Get statistical vector data for an ensemble + * + * Definition: + * + * delta_ensemble = compare_ensemble - reference_ensemble + * @param compareCaseUuid Sumo case uuid for compare ensemble + * @param compareEnsembleName Compare ensemble name + * @param referenceCaseUuid Sumo case uuid for reference ensemble + * @param referenceEnsembleName Reference ensemble name + * @param vectorName Name of the vector + * @param resamplingFrequency Resampling frequency + * @param statisticFunctions Optional list of statistics to calculate. If not specified, all statistics will be calculated. + * @param realizations Optional list of realizations to include. If not specified, all realizations will be included. + * @returns VectorStatisticData Successful Response + * @throws ApiError + */ + public getDeltaEnsembleStatisticalVectorData( + compareCaseUuid: string, + compareEnsembleName: string, + referenceCaseUuid: string, + referenceEnsembleName: string, + vectorName: string, + resamplingFrequency: Frequency, + statisticFunctions?: (Array | null), + realizations?: (Array | null), + ): CancelablePromise { + return this.httpRequest.request({ + method: 'GET', + url: '/timeseries/delta_ensemble_statistical_vector_data/', + query: { + 'compare_case_uuid': compareCaseUuid, + 'compare_ensemble_name': compareEnsembleName, + 'reference_case_uuid': referenceCaseUuid, + 'reference_ensemble_name': referenceEnsembleName, + 'vector_name': vectorName, + 'resampling_frequency': resamplingFrequency, + 'statistic_functions': statisticFunctions, + 'realizations': realizations, + }, + errors: { + 422: `Validation Error`, + }, + }); + } /** * Get Statistical Vector Data Per Sensitivity * Get statistical vector data for an ensemble per sensitivity diff --git a/frontend/src/framework/DeltaEnsemble.ts b/frontend/src/framework/DeltaEnsemble.ts new file mode 100644 index 000000000..2b727d431 --- /dev/null +++ b/frontend/src/framework/DeltaEnsemble.ts @@ -0,0 +1,105 @@ +import { DeltaEnsembleIdent } from "./DeltaEnsembleIdent"; +import { EnsembleParameters } from "./EnsembleParameters"; +import { EnsembleSensitivities } from "./EnsembleSensitivities"; +import { RegularEnsemble } from "./RegularEnsemble"; +import { RegularEnsembleIdent } from "./RegularEnsembleIdent"; + +/** + * Delta ensemble class. + * + * Delta ensemble is a user created ensemble defined as the difference between two ensembles. + * + * Definition: + * + * DeltaEnsemble = CompareEnsemble - ReferenceEnsemble + * + */ +export class DeltaEnsemble { + private _deltaEnsembleIdent: DeltaEnsembleIdent; + private _compareEnsemble: RegularEnsemble; + private _referenceEnsemble: RegularEnsemble; + private _color: string; + private _customName: string | null; + + private _realizationsArray: readonly number[]; + private _parameters: EnsembleParameters; + private _sensitivities: EnsembleSensitivities | null; + + constructor( + compareEnsemble: RegularEnsemble, + referenceEnsemble: RegularEnsemble, + color: string, + customName: string | null = null + ) { + this._deltaEnsembleIdent = new DeltaEnsembleIdent(compareEnsemble.getIdent(), referenceEnsemble.getIdent()); + + this._compareEnsemble = compareEnsemble; + this._referenceEnsemble = referenceEnsemble; + this._color = color; + this._customName = customName; + + // Intersection of realizations + const realizationIntersection = this._compareEnsemble + .getRealizations() + .filter((realization) => this._referenceEnsemble.getRealizations().includes(realization)); + this._realizationsArray = Array.from(realizationIntersection).sort((a, b) => a - b); + + // For future implementation: + // - Decide how to handle parameters and sensitivities. + // - Note: Intersection or union? How to handle parameter values? + this._parameters = new EnsembleParameters([]); + this._sensitivities = null; + } + + getIdent(): DeltaEnsembleIdent { + return this._deltaEnsembleIdent; + } + + getDisplayName(): string { + if (this._customName) { + return this._customName; + } + + return `(${this._compareEnsemble.getDisplayName()}) - (${this._referenceEnsemble.getDisplayName()})`; + } + + getEnsembleName(): string { + return this._deltaEnsembleIdent.getEnsembleName(); + } + + getRealizations(): readonly number[] { + return this._realizationsArray; + } + + getRealizationCount(): number { + return this._realizationsArray.length; + } + + getMaxRealizationNumber(): number | undefined { + return this._realizationsArray[this._realizationsArray.length - 1]; + } + + getColor(): string { + return this._color; + } + + getCustomName(): string | null { + return this._customName; + } + + getParameters(): EnsembleParameters { + return this._parameters; + } + + getSensitivities(): EnsembleSensitivities | null { + return this._sensitivities; + } + + getCompareEnsembleIdent(): RegularEnsembleIdent { + return this._compareEnsemble.getIdent(); + } + + getReferenceEnsembleIdent(): RegularEnsembleIdent { + return this._referenceEnsemble.getIdent(); + } +} diff --git a/frontend/src/framework/DeltaEnsembleIdent.ts b/frontend/src/framework/DeltaEnsembleIdent.ts new file mode 100644 index 000000000..fe71cc7e5 --- /dev/null +++ b/frontend/src/framework/DeltaEnsembleIdent.ts @@ -0,0 +1,109 @@ +import { RegularEnsembleIdent } from "./RegularEnsembleIdent"; +import { ensembleIdentUuidRegexString, isEnsembleIdentOfType } from "./utils/ensembleIdentUtils"; + +/** + * Delta ensemble ident class. + * + * DeltaEnsembleIdent is the ensemble ident for a delta ensemble. + * + * The class holds the ensemble idents of the two ensembles used to define the delta ensemble, + * i.e. compareEnsembleIdent and referenceEnsembleIdent, for easy usage in the framework. + * + * Definition: + * + * DeltaEnsemble = CompareEnsemble - ReferenceEnsemble + * + */ +export class DeltaEnsembleIdent { + private _ensembleName: string; + private _compareEnsembleIdent: RegularEnsembleIdent; + private _referenceEnsembleIdent: RegularEnsembleIdent; + + constructor(compareEnsembleIdent: RegularEnsembleIdent, referenceEnsembleIdent: RegularEnsembleIdent) { + this._ensembleName = `(${compareEnsembleIdent.getEnsembleName()}) - (${referenceEnsembleIdent.getEnsembleName()})`; + this._compareEnsembleIdent = compareEnsembleIdent; + this._referenceEnsembleIdent = referenceEnsembleIdent; + } + + static compareEnsembleIdentAndReferenceEnsembleIdentToString( + compareEnsembleIdent: RegularEnsembleIdent, + referenceEnsembleIdent: RegularEnsembleIdent + ): string { + return `~@@~${compareEnsembleIdent.toString()}~@@~${referenceEnsembleIdent.toString()}~@@~`; + } + + private static getEnsembleIdentRegex(): RegExp { + const compareEnsembleIdentRegexString = `(?${ensembleIdentUuidRegexString()})::(?.*)`; + const referenceEnsembleIdentRegexString = `(?${ensembleIdentUuidRegexString()})::(?.*)`; + return new RegExp(`^~@@~${compareEnsembleIdentRegexString}~@@~${referenceEnsembleIdentRegexString}~@@~$`); + } + + static isValidEnsembleIdentString(ensembleIdentString: string): boolean { + const regex = DeltaEnsembleIdent.getEnsembleIdentRegex(); + const result = regex.exec(ensembleIdentString); + return ( + !!result && + !!result.groups && + !!result.groups.compareCaseUuid && + !!result.groups.compareEnsembleName && + !!result.groups.referenceCaseUuid && + !!result.groups.referenceEnsembleName + ); + } + + static fromString(ensembleIdentString: string): DeltaEnsembleIdent { + const regex = DeltaEnsembleIdent.getEnsembleIdentRegex(); + const result = regex.exec(ensembleIdentString); + if ( + !result || + !result.groups || + !result.groups.compareCaseUuid || + !result.groups.compareEnsembleName || + !result.groups.referenceCaseUuid || + !result.groups.referenceEnsembleName + ) { + throw new Error(`Invalid ensemble ident: ${ensembleIdentString}`); + } + + const { compareCaseUuid, compareEnsembleName, referenceCaseUuid, referenceEnsembleName } = result.groups; + + return new DeltaEnsembleIdent( + new RegularEnsembleIdent(compareCaseUuid, compareEnsembleName), + new RegularEnsembleIdent(referenceCaseUuid, referenceEnsembleName) + ); + } + + getEnsembleName(): string { + return this._ensembleName; + } + + getCompareEnsembleIdent(): RegularEnsembleIdent { + return this._compareEnsembleIdent; + } + + getReferenceEnsembleIdent(): RegularEnsembleIdent { + return this._referenceEnsembleIdent; + } + + toString(): string { + return DeltaEnsembleIdent.compareEnsembleIdentAndReferenceEnsembleIdentToString( + this._compareEnsembleIdent, + this._referenceEnsembleIdent + ); + } + + equals(otherIdent: any | null): boolean { + if (!otherIdent || !isEnsembleIdentOfType(otherIdent, DeltaEnsembleIdent)) { + return false; + } + if (otherIdent === this) { + return true; + } + + return ( + this._ensembleName === otherIdent._ensembleName && + this._compareEnsembleIdent.equals(otherIdent._compareEnsembleIdent) && + this._referenceEnsembleIdent.equals(otherIdent._referenceEnsembleIdent) + ); + } +} diff --git a/frontend/src/framework/EnsembleIdent.ts b/frontend/src/framework/EnsembleIdent.ts deleted file mode 100644 index 28ec10983..000000000 --- a/frontend/src/framework/EnsembleIdent.ts +++ /dev/null @@ -1,53 +0,0 @@ -export class EnsembleIdent { - private _caseUuid: string; - private _ensembleName: string; - - constructor(caseUuid: string, ensembleName: string) { - this._caseUuid = caseUuid; - this._ensembleName = ensembleName; - } - - static fromCaseUuidAndEnsembleName(caseUuid: string, ensembleName: string): EnsembleIdent { - return new EnsembleIdent(caseUuid, ensembleName); - } - - static caseUuidAndEnsembleNameToString(caseUuid: string, ensembleName: string): string { - return `${caseUuid}::${ensembleName}`; - } - - static fromString(ensembleIdentString: string): EnsembleIdent { - const regex = - /^(?[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12})::(?.*)$/; - const result = regex.exec(ensembleIdentString); - if (!result || !result.groups || !result.groups.caseUuid || !result.groups.ensembleName) { - throw new Error(`Invalid ensemble ident: ${ensembleIdentString}`); - } - - const { caseUuid, ensembleName } = result.groups; - - return new EnsembleIdent(caseUuid, ensembleName); - } - - getCaseUuid(): string { - return this._caseUuid; - } - - getEnsembleName(): string { - return this._ensembleName; - } - - toString(): string { - return EnsembleIdent.caseUuidAndEnsembleNameToString(this._caseUuid, this._ensembleName); - } - - equals(otherIdent: EnsembleIdent | null): boolean { - if (!otherIdent) { - return false; - } - if (otherIdent === this) { - return true; - } - - return this._caseUuid === otherIdent._caseUuid && this._ensembleName === otherIdent._ensembleName; - } -} diff --git a/frontend/src/framework/EnsembleSet.ts b/frontend/src/framework/EnsembleSet.ts index f1198869d..63e534ed9 100644 --- a/frontend/src/framework/EnsembleSet.ts +++ b/frontend/src/framework/EnsembleSet.ts @@ -1,44 +1,127 @@ -import { Ensemble } from "./Ensemble"; -import { EnsembleIdent } from "./EnsembleIdent"; +import { DeltaEnsemble } from "./DeltaEnsemble"; +import { DeltaEnsembleIdent } from "./DeltaEnsembleIdent"; +import { RegularEnsemble } from "./RegularEnsemble"; +import { RegularEnsembleIdent } from "./RegularEnsembleIdent"; +import { isEnsembleIdentOfType } from "./utils/ensembleIdentUtils"; export class EnsembleSet { - private _ensembleArr: Ensemble[]; + private _regularEnsembleArray: RegularEnsemble[]; + private _deltaEnsembleArray: DeltaEnsemble[]; - constructor(ensembles: Ensemble[]) { - this._ensembleArr = ensembles; + constructor(ensembles: RegularEnsemble[], deltaEnsembles: DeltaEnsemble[] = []) { + this._regularEnsembleArray = ensembles; + this._deltaEnsembleArray = deltaEnsembles; } /** - * Returns true if there is at least one ensemble in the set. + * Returns true if there are any regular ensembles in the set. + * @returns True if there are any regular ensembles in the set. + */ + hasAnyRegularEnsembles(): boolean { + return this._regularEnsembleArray.length > 0; + } + + /** + * Returns true if there are any delta ensembles in the set. + * @returns True if there are any delta ensembles in the set. + */ + hasAnyDeltaEnsembles(): boolean { + return this._deltaEnsembleArray.length > 0; + } + + /** + * Returns true if there are any regular or delta ensembles in the set. + * @returns True if there are any regular or delta ensembles in the set. */ hasAnyEnsembles(): boolean { - return this._ensembleArr.length > 0; + return this.hasAnyRegularEnsembles() || this.hasAnyDeltaEnsembles(); } - hasEnsemble(ensembleIdent: EnsembleIdent): boolean { - return this.findEnsemble(ensembleIdent) !== null; + /** + * Get an array of all regular ensembles in the set. + * @returns An array of all regular ensembles in the set. + */ + getRegularEnsembleArray(): readonly RegularEnsemble[] { + return this._regularEnsembleArray; } - findEnsemble(ensembleIdent: EnsembleIdent): Ensemble | null { - return this._ensembleArr.find((ens) => ens.getIdent().equals(ensembleIdent)) ?? null; + /** + * Get an array of all delta ensembles in the set. + * @returns An array of all delta ensembles in the set. + */ + getDeltaEnsembleArray(): readonly DeltaEnsemble[] { + return this._deltaEnsembleArray; } - findEnsembleByIdentString(ensembleIdentString: string): Ensemble | null { - try { - const ensembleIdent = EnsembleIdent.fromString(ensembleIdentString); - return this.findEnsemble(ensembleIdent); - } catch { - return null; + /** + * Get an array of all ensembles in the set. + * @returns An array of all ensembles in the set. + */ + getEnsembleArray(): readonly (RegularEnsemble | DeltaEnsemble)[] { + return [...this._regularEnsembleArray, ...this._deltaEnsembleArray]; + } + + /** + * Returns true if the ensemble set has the given ensemble ident. + * + * @param ensembleIdent - The ensemble ident to check for, can be either a regular or delta ensemble ident. + * @returns True if the ensemble set has the given ensemble ident. + */ + hasEnsemble(ensembleIdent: RegularEnsembleIdent | DeltaEnsembleIdent): boolean { + return this.findEnsemble(ensembleIdent) !== null; + } + + /** + * Get an ensemble in the set by its ensemble ident + * + * @param ensembleIdent - The ensemble ident to search for. + * @returns The ensemble if found. Throws an error if the ensemble is not found. + */ + getEnsemble(ensembleIdent: RegularEnsembleIdent): RegularEnsemble; + getEnsemble(ensembleIdent: DeltaEnsembleIdent): DeltaEnsemble; + getEnsemble(ensembleIdent: RegularEnsembleIdent | DeltaEnsembleIdent): RegularEnsemble | DeltaEnsemble; + getEnsemble(ensembleIdent: RegularEnsembleIdent | DeltaEnsembleIdent): RegularEnsemble | DeltaEnsemble { + const ensemble = this.findEnsemble(ensembleIdent); + if (!ensemble) { + throw new Error(`Ensemble not found in EnsembleSet: ${ensembleIdent.toString()}`); } + return ensemble; } - getEnsembleArr(): readonly Ensemble[] { - return this._ensembleArr; + /** + * Find an ensemble in the set by its ensemble ident. + * + * @param ensembleIdent - The ensemble ident to search for. + * @returns The ensemble if found, otherwise null. + */ + findEnsemble(ensembleIdent: RegularEnsembleIdent): RegularEnsemble | null; + findEnsemble(ensembleIdent: DeltaEnsembleIdent): DeltaEnsemble | null; + findEnsemble(ensembleIdent: RegularEnsembleIdent | DeltaEnsembleIdent): RegularEnsemble | DeltaEnsemble | null; + findEnsemble(ensembleIdent: RegularEnsembleIdent | DeltaEnsembleIdent): RegularEnsemble | DeltaEnsemble | null { + if (isEnsembleIdentOfType(ensembleIdent, RegularEnsembleIdent)) { + return this._regularEnsembleArray.find((ens) => ens.getIdent().equals(ensembleIdent)) ?? null; + } + if (isEnsembleIdentOfType(ensembleIdent, DeltaEnsembleIdent)) { + return this._deltaEnsembleArray.find((ens) => ens.getIdent().equals(ensembleIdent)) ?? null; + } + return null; } - // Temporary helper method - findCaseName(ensembleIdent: EnsembleIdent): string { - const foundEnsemble = this.findEnsemble(ensembleIdent); - return foundEnsemble?.getCaseName() ?? ""; + /** + * Find an ensemble in the set by its ensemble ident string. + * + * @param ensembleIdentString - The ensemble ident string to search for. + * @returns The ensemble if found, otherwise null. + */ + findEnsembleByIdentString(ensembleIdentString: string): RegularEnsemble | DeltaEnsemble | null { + if (RegularEnsembleIdent.isValidEnsembleIdentString(ensembleIdentString)) { + const ensembleIdent = RegularEnsembleIdent.fromString(ensembleIdentString); + return this.findEnsemble(ensembleIdent); + } + if (DeltaEnsembleIdent.isValidEnsembleIdentString(ensembleIdentString)) { + const deltaEnsembleIdent = DeltaEnsembleIdent.fromString(ensembleIdentString); + return this.findEnsemble(deltaEnsembleIdent); + } + return null; } } diff --git a/frontend/src/framework/GlobalAtoms.ts b/frontend/src/framework/GlobalAtoms.ts index 443e02abc..c0695ca14 100644 --- a/frontend/src/framework/GlobalAtoms.ts +++ b/frontend/src/framework/GlobalAtoms.ts @@ -3,8 +3,9 @@ import { EnsembleSet } from "@framework/EnsembleSet"; import { atom } from "jotai"; import { isEqual } from "lodash"; -import { EnsembleIdent } from "./EnsembleIdent"; +import { DeltaEnsembleIdent } from "./DeltaEnsembleIdent"; import { RealizationFilterSet } from "./RealizationFilterSet"; +import { RegularEnsembleIdent } from "./RegularEnsembleIdent"; import { EnsembleRealizationFilterFunction } from "./WorkbenchSession"; import { atomWithCompare } from "./utils/atomUtils"; @@ -26,7 +27,7 @@ export const EnsembleRealizationFilterFunctionAtom = atom + return (ensembleIdent: RegularEnsembleIdent | DeltaEnsembleIdent) => realizationFilterSet.getRealizationFilterForEnsembleIdent(ensembleIdent).getFilteredRealizations(); }); @@ -40,7 +41,7 @@ export const ValidEnsembleRealizationsFunctionAtom = atom((get) => { let validEnsembleRealizationsFunction = get(EnsembleRealizationFilterFunctionAtom); if (validEnsembleRealizationsFunction === null) { - validEnsembleRealizationsFunction = (ensembleIdent: EnsembleIdent) => { + validEnsembleRealizationsFunction = (ensembleIdent: RegularEnsembleIdent | DeltaEnsembleIdent) => { return ensembleSet.findEnsemble(ensembleIdent)?.getRealizations() ?? []; }; } diff --git a/frontend/src/framework/RealizationFilter.ts b/frontend/src/framework/RealizationFilter.ts index 63c29e9a3..cc66910bd 100644 --- a/frontend/src/framework/RealizationFilter.ts +++ b/frontend/src/framework/RealizationFilter.ts @@ -1,7 +1,7 @@ import { isEqual } from "lodash"; -import { Ensemble } from "./Ensemble"; -import { EnsembleIdent } from "./EnsembleIdent"; +import { DeltaEnsemble } from "./DeltaEnsemble"; +import { DeltaEnsembleIdent } from "./DeltaEnsembleIdent"; import { ContinuousParameter, DiscreteParameter, @@ -10,6 +10,8 @@ import { ParameterIdent, ParameterType, } from "./EnsembleParameters"; +import { RegularEnsemble } from "./RegularEnsemble"; +import { RegularEnsembleIdent } from "./RegularEnsembleIdent"; import { DiscreteParameterValueSelection, IncludeExcludeFilter, @@ -28,16 +30,16 @@ import { /** * Class for filtering realizations based on realization number or parameter values. * - * The class is designed to be used in conjunction with the Ensemble class. + * The class is designed to be used in conjunction with the RegularEnsemble or DeltaEnsemble class. * * The class is designed to keep track of the filtering state and provide the filtered realizations * for an ensemble. * - * Should not provide interface to get the Ensemble object itself, but can provide access to information about the ensemble, - * such as the ensemble ident and realization numbers. + * Should not provide interface to get the RegularEnsemble/DeltaEnsemble object itself, but can provide + * access to information about the ensemble - such as the ensemble ident and realization numbers. */ export class RealizationFilter { - private _assignedEnsemble: Ensemble; + private _assignedEnsemble: RegularEnsemble | DeltaEnsemble; private _includeExcludeFilter: IncludeExcludeFilter; private _filterType: RealizationFilterType; @@ -52,7 +54,7 @@ export class RealizationFilter { private _filteredRealizations: readonly number[]; constructor( - assignedEnsemble: Ensemble, + assignedEnsemble: RegularEnsemble | DeltaEnsemble, initialIncludeExcludeFilter = IncludeExcludeFilter.INCLUDE_FILTER, initialFilterType = RealizationFilterType.BY_REALIZATION_NUMBER ) { @@ -65,7 +67,7 @@ export class RealizationFilter { this._parameterIdentStringToValueSelectionMap = null; } - getAssignedEnsembleIdent(): EnsembleIdent { + getAssignedEnsembleIdent(): RegularEnsembleIdent | DeltaEnsembleIdent { return this._assignedEnsemble.getIdent(); } diff --git a/frontend/src/framework/RealizationFilterSet.ts b/frontend/src/framework/RealizationFilterSet.ts index bd1f29fb6..e8adb2539 100644 --- a/frontend/src/framework/RealizationFilterSet.ts +++ b/frontend/src/framework/RealizationFilterSet.ts @@ -1,8 +1,7 @@ -import { isEqual } from "lodash"; - -import { EnsembleIdent } from "./EnsembleIdent"; +import { DeltaEnsembleIdent } from "./DeltaEnsembleIdent"; import { EnsembleSet } from "./EnsembleSet"; import { RealizationFilter } from "./RealizationFilter"; +import { RegularEnsembleIdent } from "./RegularEnsembleIdent"; export class RealizationFilterSet { // Map of ensembleIdent string to RealizationFilter @@ -17,14 +16,23 @@ export class RealizationFilterSet { synchronizeWithEnsembleSet(ensembleSet: EnsembleSet): void { // Remove filters for ensembles that are no longer in the ensemble set for (const ensembleIdentString of this._ensembleIdentStringRealizationFilterMap.keys()) { - const ensembleIdent = EnsembleIdent.fromString(ensembleIdentString); + let ensembleIdent = null; + if (RegularEnsembleIdent.isValidEnsembleIdentString(ensembleIdentString)) { + ensembleIdent = RegularEnsembleIdent.fromString(ensembleIdentString); + } else if (DeltaEnsembleIdent.isValidEnsembleIdentString(ensembleIdentString)) { + ensembleIdent = DeltaEnsembleIdent.fromString(ensembleIdentString); + } + if (!ensembleIdent) { + throw new Error(`Invalid ensemble ident string: ${ensembleIdentString}`); + } + if (!ensembleSet.hasEnsemble(ensembleIdent)) { this._ensembleIdentStringRealizationFilterMap.delete(ensembleIdentString); } } // Add filters for ensembles that are new to the ensemble set - for (const ensemble of ensembleSet.getEnsembleArr()) { + for (const ensemble of ensembleSet.getEnsembleArray()) { const ensembleIdentString = ensemble.getIdent().toString(); const isEnsembleInMap = this._ensembleIdentStringRealizationFilterMap.has(ensembleIdentString); if (!isEnsembleInMap) { @@ -36,7 +44,7 @@ export class RealizationFilterSet { /** * Get filter for ensembleIdent */ - getRealizationFilterForEnsembleIdent(ensembleIdent: EnsembleIdent): RealizationFilter { + getRealizationFilterForEnsembleIdent(ensembleIdent: RegularEnsembleIdent | DeltaEnsembleIdent): RealizationFilter { const filter = this._ensembleIdentStringRealizationFilterMap.get(ensembleIdent.toString()); if (filter === undefined) { throw new Error( @@ -46,21 +54,4 @@ export class RealizationFilterSet { return filter; } - - isEqual(other: RealizationFilterSet): boolean { - if ( - this._ensembleIdentStringRealizationFilterMap.size !== other._ensembleIdentStringRealizationFilterMap.size - ) { - return false; - } - - for (const [ensembleIdentString, realizationFilter] of this._ensembleIdentStringRealizationFilterMap) { - const otherRealizationFilter = other._ensembleIdentStringRealizationFilterMap.get(ensembleIdentString); - if (!otherRealizationFilter || isEqual(realizationFilter, otherRealizationFilter)) { - return false; - } - } - - return true; - } } diff --git a/frontend/src/framework/Ensemble.ts b/frontend/src/framework/RegularEnsemble.ts similarity index 64% rename from frontend/src/framework/Ensemble.ts rename to frontend/src/framework/RegularEnsemble.ts index cd028fd1c..add818619 100644 --- a/frontend/src/framework/Ensemble.ts +++ b/frontend/src/framework/RegularEnsemble.ts @@ -1,13 +1,13 @@ -import { EnsembleIdent } from "./EnsembleIdent"; import { EnsembleParameters, Parameter } from "./EnsembleParameters"; import { EnsembleSensitivities, Sensitivity } from "./EnsembleSensitivities"; +import { RegularEnsembleIdent } from "./RegularEnsembleIdent"; -export class Ensemble { - private _ensembleIdent: EnsembleIdent; +export class RegularEnsemble { + private _ensembleIdent: RegularEnsembleIdent; private _fieldIdentifier: string; private _caseName: string; - private _stratigraphic_column_identifier: string; - private _realizationsArr: number[]; + private _stratigraphicColumnIdentifier: string; + private _realizationsArray: number[]; private _parameters: EnsembleParameters; private _sensitivities: EnsembleSensitivities | null; private _color: string; @@ -18,29 +18,29 @@ export class Ensemble { caseUuid: string, caseName: string, ensembleName: string, - stratigraphic_column_identifier: string, - realizationsArr: number[], - parameterArr: Parameter[], - sensitivityArr: Sensitivity[] | null, + stratigraphicColumnIdentifier: string, + realizationsArray: number[], + parameterArray: Parameter[], + sensitivityArray: Sensitivity[] | null, color: string, customName: string | null = null ) { - this._ensembleIdent = new EnsembleIdent(caseUuid, ensembleName); + this._ensembleIdent = new RegularEnsembleIdent(caseUuid, ensembleName); this._fieldIdentifier = fieldIdentifier; this._caseName = caseName; - this._stratigraphic_column_identifier = stratigraphic_column_identifier; - this._realizationsArr = Array.from(realizationsArr).sort((a, b) => a - b); - this._parameters = new EnsembleParameters(parameterArr); + this._stratigraphicColumnIdentifier = stratigraphicColumnIdentifier; + this._realizationsArray = Array.from(realizationsArray).sort((a, b) => a - b); + this._parameters = new EnsembleParameters(parameterArray); this._color = color; this._customName = customName; this._sensitivities = null; - if (sensitivityArr && sensitivityArr.length > 0) { - this._sensitivities = new EnsembleSensitivities(sensitivityArr); + if (sensitivityArray && sensitivityArray.length > 0) { + this._sensitivities = new EnsembleSensitivities(sensitivityArray); } } - getIdent(): EnsembleIdent { + getIdent(): RegularEnsembleIdent { return this._ensembleIdent; } @@ -49,7 +49,7 @@ export class Ensemble { } getStratigraphicColumnIdentifier(): string { - return this._stratigraphic_column_identifier; + return this._stratigraphicColumnIdentifier; } getDisplayName(): string { @@ -72,19 +72,19 @@ export class Ensemble { } getRealizations(): readonly number[] { - return this._realizationsArr; + return this._realizationsArray; } getRealizationCount(): number { - return this._realizationsArr.length; + return this._realizationsArray.length; } getMaxRealizationNumber(): number | undefined { - if (this._realizationsArr.length == 0) { + if (this._realizationsArray.length == 0) { return undefined; } - return this._realizationsArr[this._realizationsArr.length - 1]; + return this._realizationsArray[this._realizationsArray.length - 1]; } getParameters(): EnsembleParameters { diff --git a/frontend/src/framework/RegularEnsembleIdent.ts b/frontend/src/framework/RegularEnsembleIdent.ts new file mode 100644 index 000000000..e18a0b93a --- /dev/null +++ b/frontend/src/framework/RegularEnsembleIdent.ts @@ -0,0 +1,65 @@ +import { ensembleIdentUuidRegexString, isEnsembleIdentOfType } from "./utils/ensembleIdentUtils"; + +export class RegularEnsembleIdent { + private _caseUuid: string; + private _ensembleName: string; + + constructor(caseUuid: string, ensembleName: string) { + const uuidRegex = new RegExp(ensembleIdentUuidRegexString()); + if (!uuidRegex.exec(caseUuid)) { + throw new Error(`Invalid caseUuid: ${caseUuid}`); + } + + this._caseUuid = caseUuid; + this._ensembleName = ensembleName; + } + + static caseUuidAndEnsembleNameToString(caseUuid: string, ensembleName: string): string { + return `${caseUuid}::${ensembleName}`; + } + + static getEnsembleIdentRegex(): RegExp { + return new RegExp(`^(?${ensembleIdentUuidRegexString()})::(?.*)$`); + } + + static isValidEnsembleIdentString(ensembleIdentString: string): boolean { + const regex = RegularEnsembleIdent.getEnsembleIdentRegex(); + const result = regex.exec(ensembleIdentString); + return !!result && !!result.groups && !!result.groups.caseUuid && !!result.groups.ensembleName; + } + + static fromString(ensembleIdentString: string): RegularEnsembleIdent { + const regex = RegularEnsembleIdent.getEnsembleIdentRegex(); + const result = regex.exec(ensembleIdentString); + if (!result || !result.groups || !result.groups.caseUuid || !result.groups.ensembleName) { + throw new Error(`Invalid ensemble ident: ${ensembleIdentString}`); + } + + const { caseUuid, ensembleName } = result.groups; + + return new RegularEnsembleIdent(caseUuid, ensembleName); + } + + getCaseUuid(): string { + return this._caseUuid; + } + + getEnsembleName(): string { + return this._ensembleName; + } + + toString(): string { + return RegularEnsembleIdent.caseUuidAndEnsembleNameToString(this._caseUuid, this._ensembleName); + } + + equals(otherIdent: any | null): boolean { + if (!otherIdent || !isEnsembleIdentOfType(otherIdent, RegularEnsembleIdent)) { + return false; + } + if (otherIdent === this) { + return true; + } + + return this._caseUuid === otherIdent._caseUuid && this._ensembleName === otherIdent._ensembleName; + } +} diff --git a/frontend/src/framework/Workbench.ts b/frontend/src/framework/Workbench.ts index 424813de4..09c4c2066 100644 --- a/frontend/src/framework/Workbench.ts +++ b/frontend/src/framework/Workbench.ts @@ -1,15 +1,15 @@ import { QueryClient } from "@tanstack/react-query"; import { AtomStoreMaster } from "./AtomStoreMaster"; -import { EnsembleIdent } from "./EnsembleIdent"; import { GuiMessageBroker, GuiState } from "./GuiMessageBroker"; import { InitialSettings } from "./InitialSettings"; import { ImportState } from "./Module"; import { ModuleInstance } from "./ModuleInstance"; import { ModuleRegistry } from "./ModuleRegistry"; +import { RegularEnsembleIdent } from "./RegularEnsembleIdent"; import { Template } from "./TemplateRegistry"; import { WorkbenchServices } from "./WorkbenchServices"; -import { loadEnsembleSetMetadataFromBackend } from "./internal/EnsembleSetLoader"; +import { loadMetadataFromBackendAndCreateEnsembleSet } from "./internal/EnsembleSetLoader"; import { PrivateWorkbenchServices } from "./internal/PrivateWorkbenchServices"; import { PrivateWorkbenchSettings } from "./internal/PrivateWorkbenchSettings"; import { WorkbenchSessionPrivate } from "./internal/WorkbenchSessionPrivate"; @@ -28,14 +28,28 @@ export type LayoutElement = { }; export type UserEnsembleSetting = { - ensembleIdent: EnsembleIdent; + ensembleIdent: RegularEnsembleIdent; + customName: string | null; + color: string; +}; + +export type UserDeltaEnsembleSetting = { + compareEnsembleIdent: RegularEnsembleIdent; + referenceEnsembleIdent: RegularEnsembleIdent; customName: string | null; color: string; }; export type StoredUserEnsembleSetting = { ensembleIdent: string; - customName: string; + customName: string | null; + color: string; +}; + +export type StoredUserDeltaEnsembleSetting = { + compareEnsembleIdent: string; + referenceEnsembleIdent: string; + customName: string | null; color: string; }; @@ -228,38 +242,69 @@ export class Workbench { async loadAndSetupEnsembleSetInSession( queryClient: QueryClient, - userEnsembleSettings: UserEnsembleSetting[] + userEnsembleSettings: UserEnsembleSetting[], + userDeltaEnsembleSettings: UserDeltaEnsembleSetting[] ): Promise { this.storeEnsembleSetInLocalStorage(userEnsembleSettings); + this.storeDeltaEnsembleSetInLocalStorage(userDeltaEnsembleSettings); console.debug("loadAndSetupEnsembleSetInSession - starting load"); this._workbenchSession.setEnsembleSetLoadingState(true); - const newEnsembleSet = await loadEnsembleSetMetadataFromBackend(queryClient, userEnsembleSettings); + const newEnsembleSet = await loadMetadataFromBackendAndCreateEnsembleSet( + queryClient, + userEnsembleSettings, + userDeltaEnsembleSettings + ); console.debug("loadAndSetupEnsembleSetInSession - loading done"); console.debug("loadAndSetupEnsembleSetInSession - publishing"); this._workbenchSession.setEnsembleSetLoadingState(false); return this._workbenchSession.setEnsembleSet(newEnsembleSet); } - private storeEnsembleSetInLocalStorage(ensemblesToStore: UserEnsembleSetting[]): void { - const ensembleIdentsToStore = ensemblesToStore.map((el) => ({ + private storeEnsembleSetInLocalStorage(ensembleSettingsToStore: UserEnsembleSetting[]): void { + const ensembleSettingsArrayToStore: StoredUserEnsembleSetting[] = ensembleSettingsToStore.map((el) => ({ ...el, ensembleIdent: el.ensembleIdent.toString(), })); - localStorage.setItem("userEnsembleSettings", JSON.stringify(ensembleIdentsToStore)); + localStorage.setItem("userEnsembleSettings", JSON.stringify(ensembleSettingsArrayToStore)); + } + + private storeDeltaEnsembleSetInLocalStorage(deltaEnsembleSettingsToStore: UserDeltaEnsembleSetting[]): void { + const deltaEnsembleSettingsArrayToStore: StoredUserDeltaEnsembleSetting[] = deltaEnsembleSettingsToStore.map( + (el) => ({ + ...el, + compareEnsembleIdent: el.compareEnsembleIdent.toString(), + referenceEnsembleIdent: el.referenceEnsembleIdent.toString(), + }) + ); + localStorage.setItem("userDeltaEnsembleSettings", JSON.stringify(deltaEnsembleSettingsArrayToStore)); } maybeLoadEnsembleSettingsFromLocalStorage(): UserEnsembleSetting[] | null { const ensembleSettingsString = localStorage.getItem("userEnsembleSettings"); if (!ensembleSettingsString) return null; - const ensembleIdents = JSON.parse(ensembleSettingsString) as StoredUserEnsembleSetting[]; - const ensembleIdentsParsed = ensembleIdents.map((el) => ({ + const ensembleSettingsArray = JSON.parse(ensembleSettingsString) as StoredUserEnsembleSetting[]; + const parsedEnsembleSettingsArray: UserEnsembleSetting[] = ensembleSettingsArray.map((el) => ({ + ...el, + ensembleIdent: RegularEnsembleIdent.fromString(el.ensembleIdent), + })); + + return parsedEnsembleSettingsArray; + } + + maybeLoadDeltaEnsembleSettingsFromLocalStorage(): UserDeltaEnsembleSetting[] | null { + const deltaEnsembleSettingsString = localStorage.getItem("userDeltaEnsembleSettings"); + if (!deltaEnsembleSettingsString) return null; + + const deltaEnsembleSettingsArray = JSON.parse(deltaEnsembleSettingsString) as StoredUserDeltaEnsembleSetting[]; + const parsedDeltaEnsembleSettingsArray: UserDeltaEnsembleSetting[] = deltaEnsembleSettingsArray.map((el) => ({ ...el, - ensembleIdent: EnsembleIdent.fromString(el.ensembleIdent), + compareEnsembleIdent: RegularEnsembleIdent.fromString(el.compareEnsembleIdent), + referenceEnsembleIdent: RegularEnsembleIdent.fromString(el.referenceEnsembleIdent), })); - return ensembleIdentsParsed; + return parsedDeltaEnsembleSettingsArray; } applyTemplate(template: Template): void { diff --git a/frontend/src/framework/WorkbenchServices.ts b/frontend/src/framework/WorkbenchServices.ts index 2757fc3c2..83af3c61c 100644 --- a/frontend/src/framework/WorkbenchServices.ts +++ b/frontend/src/framework/WorkbenchServices.ts @@ -4,7 +4,7 @@ import { Point2D, Point3D } from "@webviz/subsurface-viewer"; import { isEqual } from "lodash"; -import { EnsembleIdent } from "./EnsembleIdent"; +import { RegularEnsembleIdent } from "./RegularEnsembleIdent"; import { Workbench } from "./Workbench"; import { InplaceVolumetricsFilter } from "./types/inplaceVolumetricsFilter"; import { Intersection } from "./types/intersection"; @@ -24,7 +24,7 @@ export type GlobalTopicDefinitions = { "global.hoverRegion": { regionName: string } | null; "global.hoverFacies": { faciesName: string } | null; - "global.syncValue.ensembles": EnsembleIdent[]; + "global.syncValue.ensembles": RegularEnsembleIdent[]; "global.syncValue.date": { timeOrInterval: string }; "global.syncValue.timeSeries": { vectorName: string }; "global.syncValue.surface": { name: string; attribute: string }; diff --git a/frontend/src/framework/WorkbenchSession.ts b/frontend/src/framework/WorkbenchSession.ts index 54fa1c5ad..b1774c595 100644 --- a/frontend/src/framework/WorkbenchSession.ts +++ b/frontend/src/framework/WorkbenchSession.ts @@ -1,13 +1,15 @@ import React from "react"; import { AtomStoreMaster } from "./AtomStoreMaster"; -import { Ensemble } from "./Ensemble"; -import { EnsembleIdent } from "./EnsembleIdent"; +import { DeltaEnsembleIdent } from "./DeltaEnsembleIdent"; import { EnsembleSet } from "./EnsembleSet"; import { RealizationFilterSet } from "./RealizationFilterSet"; +import { RegularEnsembleIdent } from "./RegularEnsembleIdent"; import { UserCreatedItems } from "./UserCreatedItems"; -export type EnsembleRealizationFilterFunction = (ensembleIdent: EnsembleIdent) => readonly number[]; +export type EnsembleRealizationFilterFunction = ( + ensembleIdent: RegularEnsembleIdent | DeltaEnsembleIdent +) => readonly number[]; export enum WorkbenchSessionEvent { EnsembleSetChanged = "EnsembleSetChanged", @@ -78,7 +80,9 @@ export class WorkbenchSession { } export function createEnsembleRealizationFilterFuncForWorkbenchSession(workbenchSession: WorkbenchSession) { - return function ensembleRealizationFilterFunc(ensembleIdent: EnsembleIdent): readonly number[] { + return function ensembleRealizationFilterFunc( + ensembleIdent: RegularEnsembleIdent | DeltaEnsembleIdent + ): readonly number[] { const realizationFilterSet = workbenchSession.getRealizationFilterSet(); const realizationFilter = realizationFilterSet.getRealizationFilterForEnsembleIdent(ensembleIdent); @@ -137,15 +141,6 @@ export function useEnsembleSet(workbenchSession: WorkbenchSession): EnsembleSet return storedEnsembleSet; } -export function useFirstEnsembleInEnsembleSet(workbenchSession: WorkbenchSession): Ensemble | null { - const ensembleSet = useEnsembleSet(workbenchSession); - if (!ensembleSet.hasAnyEnsembles()) { - return null; - } - - return ensembleSet.getEnsembleArr()[0]; -} - export function useIsEnsembleSetLoading(workbenchSession: WorkbenchSession): boolean { const [isLoading, setIsLoading] = React.useState(false); diff --git a/frontend/src/framework/components/EnsembleDropdown/ensembleDropdown.tsx b/frontend/src/framework/components/EnsembleDropdown/ensembleDropdown.tsx index f8bfd6a8c..86ffd582e 100644 --- a/frontend/src/framework/components/EnsembleDropdown/ensembleDropdown.tsx +++ b/frontend/src/framework/components/EnsembleDropdown/ensembleDropdown.tsx @@ -1,32 +1,56 @@ import React from "react"; -import { Ensemble } from "@framework/Ensemble"; -import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { DeltaEnsemble } from "@framework/DeltaEnsemble"; +import { DeltaEnsembleIdent } from "@framework/DeltaEnsembleIdent"; +import { RegularEnsemble } from "@framework/RegularEnsemble"; +import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; import { ColorTile } from "@lib/components/ColorTile"; import { Dropdown, DropdownOption, DropdownProps } from "@lib/components/Dropdown"; -type EnsembleDropdownProps = { - ensembles: readonly Ensemble[]; - value: EnsembleIdent | null; - onChange: (ensembleIdent: EnsembleIdent | null) => void; +// Overload for EnsembleDropdown with DeltaEnsembleIdent +export type EnsembleDropdownWithDeltaEnsemblesProps = { + ensembles: readonly (RegularEnsemble | DeltaEnsemble)[]; + allowDeltaEnsembles: true; + value: RegularEnsembleIdent | DeltaEnsembleIdent | null; + onChange: (ensembleIdent: RegularEnsembleIdent | DeltaEnsembleIdent | null) => void; } & Omit, "options" | "value" | "onChange">; -export function EnsembleDropdown(props: EnsembleDropdownProps): JSX.Element { - const { onChange, value, ...rest } = props; +// Overload for EnsembleDropdown without DeltaEnsembleIdent +export type EnsembleDropdownWithoutDeltaEnsemblesProps = { + ensembles: readonly RegularEnsemble[]; + allowDeltaEnsembles?: false | undefined; + value: RegularEnsembleIdent | null; + onChange: (ensembleIdent: RegularEnsembleIdent | null) => void; +} & Omit, "options" | "value" | "onChange">; + +export function EnsembleDropdown(props: EnsembleDropdownWithDeltaEnsemblesProps): JSX.Element; +export function EnsembleDropdown(props: EnsembleDropdownWithoutDeltaEnsemblesProps): JSX.Element; +export function EnsembleDropdown( + props: EnsembleDropdownWithDeltaEnsemblesProps | EnsembleDropdownWithoutDeltaEnsemblesProps +): JSX.Element { + const { onChange, ensembles, allowDeltaEnsembles, value, ...rest } = props; const handleSelectionChange = React.useCallback( function handleSelectionChange(selectedEnsembleIdentStr: string) { - const foundEnsemble = props.ensembles.find( + const foundEnsemble = ensembles.find( (ensemble) => ensemble.getIdent().toString() === selectedEnsembleIdentStr ); - onChange(foundEnsemble ? foundEnsemble.getIdent() : null); + if (foundEnsemble && allowDeltaEnsembles) { + onChange(foundEnsemble.getIdent()); + return; + } + if (!foundEnsemble || foundEnsemble instanceof DeltaEnsemble) { + onChange(null); + return; + } + onChange(foundEnsemble.getIdent()); }, - [props.ensembles, onChange] + [allowDeltaEnsembles, ensembles, onChange] ); - const optionsArr: DropdownOption[] = []; - for (const ens of props.ensembles) { - optionsArr.push({ + const optionsArray: DropdownOption[] = []; + for (const ens of ensembles) { + optionsArray.push({ value: ens.getIdent().toString(), label: ens.getDisplayName(), adornment: ( @@ -37,5 +61,5 @@ export function EnsembleDropdown(props: EnsembleDropdownProps): JSX.Element { }); } - return ; + return ; } diff --git a/frontend/src/framework/components/EnsembleSelect/ensembleSelect.tsx b/frontend/src/framework/components/EnsembleSelect/ensembleSelect.tsx index 6fae717a6..b40b64d68 100644 --- a/frontend/src/framework/components/EnsembleSelect/ensembleSelect.tsx +++ b/frontend/src/framework/components/EnsembleSelect/ensembleSelect.tsx @@ -1,37 +1,64 @@ import React from "react"; -import { Ensemble } from "@framework/Ensemble"; -import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { DeltaEnsemble } from "@framework/DeltaEnsemble"; +import { DeltaEnsembleIdent } from "@framework/DeltaEnsembleIdent"; +import { RegularEnsemble } from "@framework/RegularEnsemble"; +import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; +import { isEnsembleIdentOfType } from "@framework/utils/ensembleIdentUtils"; import { ColorTile } from "@lib/components/ColorTile"; import { Select, SelectOption, SelectProps } from "@lib/components/Select"; -type EnsembleSelectProps = { - ensembles: readonly Ensemble[]; - value: EnsembleIdent[]; - onChange: (ensembleIdentArr: EnsembleIdent[]) => void; +// Overload for EnsembleSelect with DeltaEnsembleIdent +export type EnsembleSelectWithDeltaEnsemblesProps = { + ensembles: readonly (RegularEnsemble | DeltaEnsemble)[]; + multiple?: boolean; + allowDeltaEnsembles: true; + value: (RegularEnsembleIdent | DeltaEnsembleIdent)[]; + onChange: (ensembleIdentArray: (RegularEnsembleIdent | DeltaEnsembleIdent)[]) => void; } & Omit, "options" | "value" | "onChange">; -export function EnsembleSelect(props: EnsembleSelectProps): React.ReactNode { - const { ensembles, value, onChange, multiple, ...rest } = props; +// Overload for EnsembleSelect without DeltaEnsembleIdent +export type EnsembleSelectWithoutDeltaEnsemblesProps = { + ensembles: readonly RegularEnsemble[]; + multiple?: boolean; + allowDeltaEnsembles?: false | undefined; + value: RegularEnsembleIdent[]; + onChange: (ensembleIdentArray: RegularEnsembleIdent[]) => void; +} & Omit, "options" | "value" | "onChange">; + +export function EnsembleSelect(props: EnsembleSelectWithDeltaEnsemblesProps): JSX.Element; +export function EnsembleSelect(props: EnsembleSelectWithoutDeltaEnsemblesProps): JSX.Element; +export function EnsembleSelect( + props: EnsembleSelectWithDeltaEnsemblesProps | EnsembleSelectWithoutDeltaEnsemblesProps +): JSX.Element { + const { onChange, ensembles, value, allowDeltaEnsembles, multiple, ...rest } = props; const handleSelectionChange = React.useCallback( - function handleSelectionChanged(selectedEnsembleIdentStrArr: string[]) { - const identArr: EnsembleIdent[] = []; - for (const identStr of selectedEnsembleIdentStrArr) { + function handleSelectionChanged(selectedEnsembleIdentStringArray: string[]) { + const identArray: (RegularEnsembleIdent | DeltaEnsembleIdent)[] = []; + for (const identStr of selectedEnsembleIdentStringArray) { const foundEnsemble = ensembles.find((ens) => ens.getIdent().toString() === identStr); - if (foundEnsemble) { - identArr.push(foundEnsemble.getIdent()); + if (foundEnsemble && (allowDeltaEnsembles || foundEnsemble instanceof RegularEnsemble)) { + identArray.push(foundEnsemble.getIdent()); } } - onChange(identArr); + // Filter to match the correct return type before calling onChange + if (!allowDeltaEnsembles) { + const validIdentArray = identArray.filter((ident) => + isEnsembleIdentOfType(ident, RegularEnsembleIdent) + ) as RegularEnsembleIdent[]; + onChange(validIdentArray); + return; + } + onChange(identArray); }, - [ensembles, onChange] + [allowDeltaEnsembles, ensembles, onChange] ); - const optionsArr: SelectOption[] = []; + const optionsArray: SelectOption[] = []; for (const ens of ensembles) { - optionsArr.push({ + optionsArray.push({ value: ens.getIdent().toString(), label: ens.getDisplayName(), adornment: ( @@ -42,17 +69,17 @@ export function EnsembleSelect(props: EnsembleSelectProps): React.ReactNode { }); } - const selectedArr: string[] = []; + const selectedArray: string[] = []; for (const ident of value) { - selectedArr.push(ident.toString()); + selectedArray.push(ident.toString()); } const isMultiple = multiple ?? true; return ( ) => - handleEnsembleCustomNameChange( - item.caseUuid, - item.ensembleName, - e.target.value - ) - } - /> - - -
- {item.caseName} -
- - -
- {item.ensembleName} -
- - - - handleRemoveEnsemble(item.caseUuid, item.ensembleName) - } - color="danger" - title="Remove ensemble from selection" - > - - {" "} - +
+
+
Selected Ensembles
+
+ + + + + + + + - ))} - -
ColorCustom nameCaseEnsembleActions
- - {newlySelectedEnsembles.length === 0 && ( -
No ensembles selected.
- )} + + + {newlySelectedEnsembles.map((item) => ( + + + + handleColorChange(item.caseUuid, item.ensembleName, value) + } + /> + + + ) => + handleEnsembleCustomNameChange( + item.caseUuid, + item.ensembleName, + e.target.value + ) + } + /> + + +
+ {item.caseName} +
+ + +
+ {item.ensembleName} +
+ + + + handleRemoveEnsemble(item.caseUuid, item.ensembleName) + } + color="danger" + title="Remove ensemble from selection" + > + + {" "} + + + ))} + + +
+ {newlySelectedEnsembles.length === 0 && ( +
No ensembles selected.
+ )} +
+
+
+ +
Delta Ensembles
+ + + +
+
+ + + + + + + + + + + + {deltaEnsembles.map((elm) => { + const isDeltaEnsembleValid = + elm.compareEnsemble !== null && elm.referenceEnsemble !== null; + const isDuplicateDeltaEnsemble = + deltaEnsembles.filter( + (e) => + e.compareEnsemble?.caseUuid === elm.compareEnsemble?.caseUuid && + e.compareEnsemble?.ensembleName === + elm.compareEnsemble?.ensembleName && + e.referenceEnsemble?.caseUuid === + elm.referenceEnsemble?.caseUuid && + e.referenceEnsemble?.ensembleName === + elm.referenceEnsemble?.ensembleName + ).length > 1; + return ( + + + + + + + + ); + })} + +
ColorCustom nameCompare EnsembleReference EnsembleActions
+ + handleDeltaEnsembleColorChange(elm.uuid, value) + } + /> + + ) => + handleDeltaEnsembleCustomNameChange( + elm.uuid, + e.target.value + ) + } + /> + + { + return { + value: createCaseUuidAndEnsembleNameString( + elm.caseUuid, + elm.ensembleName + ), + label: + elm.customName ?? + `${elm.ensembleName} (${elm.caseName})`, + }; + })} + value={ + elm.compareEnsemble + ? createCaseUuidAndEnsembleNameString( + elm.compareEnsemble.caseUuid, + elm.compareEnsemble.ensembleName + ) + : undefined + } + onChange={(newCaseUuidAndEnsembleNameString) => { + handleDeltaEnsembleCompareEnsembleChange( + elm.uuid, + newCaseUuidAndEnsembleNameString + ); + }} + /> + + { + return { + value: createCaseUuidAndEnsembleNameString( + elm.caseUuid, + elm.ensembleName + ), + label: + elm.customName ?? + `${elm.ensembleName} (${elm.caseName})`, + }; + })} + value={ + elm.referenceEnsemble + ? createCaseUuidAndEnsembleNameString( + elm.referenceEnsemble.caseUuid, + elm.referenceEnsemble.ensembleName + ) + : undefined + } + onChange={(value) => { + handleDeltaEnsembleReferenceEnsembleChange( + elm.uuid, + value + ); + }} + /> + + handleRemoveDeltaEnsemble(elm.uuid)} + color="danger" + title="Remove delta ensemble from selection" + > + + {" "} +
+
+ {deltaEnsembles.length === 0 && ( +
No delta ensembles created.
+ )} +
{isLoadingEnsembles && } - { - setConfirmCancel(false)} - title="Unsaved changes" - modal - actions={ -
- - -
- } - > - You have unsaved changes which will be lost. Are you sure you want to cancel? -
- } + setConfirmCancel(false)} + title="Unsaved changes" + modal + actions={ +
+ + +
+ } + > + You have unsaved changes which will be lost. Are you sure you want to cancel? +
); }; + +function createCaseUuidAndEnsembleNameString(caseUuid: string, ensembleName: string): string { + return `${caseUuid}${CASE_UUID_ENSEMBLE_NAME_SEPARATOR}${ensembleName}`; +} + +function createCaseUuidAndEnsembleNameFromString(caseUuidAndEnsembleNameString: string): { + caseUuid: string; + ensembleName: string; +} { + const [caseUuid, ensembleName] = caseUuidAndEnsembleNameString.split(CASE_UUID_ENSEMBLE_NAME_SEPARATOR); + if (!caseUuid || !ensembleName) { + throw new Error("Invalid caseUuidAndEnsembleNameString"); + } + + return { caseUuid, ensembleName }; +} diff --git a/frontend/src/framework/types/inplaceVolumetricsFilter.ts b/frontend/src/framework/types/inplaceVolumetricsFilter.ts index bcb18b1f7..7a5c462c8 100644 --- a/frontend/src/framework/types/inplaceVolumetricsFilter.ts +++ b/frontend/src/framework/types/inplaceVolumetricsFilter.ts @@ -1,8 +1,8 @@ import { FluidZone_api, InplaceVolumetricsIdentifierWithValues_api } from "@api"; -import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; export type InplaceVolumetricsFilter = { - ensembleIdents: EnsembleIdent[]; + ensembleIdents: RegularEnsembleIdent[]; tableNames: string[]; fluidZones: FluidZone_api[]; identifiersValues: InplaceVolumetricsIdentifierWithValues_api[]; diff --git a/frontend/src/framework/utils/ensembleIdentUtils.ts b/frontend/src/framework/utils/ensembleIdentUtils.ts new file mode 100644 index 000000000..abb6963e4 --- /dev/null +++ b/frontend/src/framework/utils/ensembleIdentUtils.ts @@ -0,0 +1,68 @@ +import { DeltaEnsembleIdent } from "@framework/DeltaEnsembleIdent"; +import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; + +/** + * Check if two ensemble idents are equal. + */ +export function areEnsembleIdentsEqual( + a: RegularEnsembleIdent | DeltaEnsembleIdent | null, + b: RegularEnsembleIdent | DeltaEnsembleIdent | null +): boolean { + if (a === null) { + return b === null; + } + return a.equals(b); +} + +/** + * Check if two lists of ensemble idents are equal. + */ +export function areEnsembleIdentListsEqual( + a: (RegularEnsembleIdent | DeltaEnsembleIdent)[], + b: (RegularEnsembleIdent | DeltaEnsembleIdent)[] +): boolean { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (!a[i].equals(b[i])) { + return false; + } + } + return true; +} + +/** + * Check if provided EnsembleIdentInterface implementation is of specified type + */ +export function isEnsembleIdentOfType( + ensembleIdent: RegularEnsembleIdent | DeltaEnsembleIdent, + type: new (...args: any[]) => T +): ensembleIdent is T { + return ensembleIdent instanceof type; +} + +/** + * Creates a new array of ensemble idents only containing the specified type. + * + * A list of classes implementing EnsembleIdentInterface, and a type are passed as arguments. + * + * @param ensembleIdents - List of implemented classes of EnsembleIdentInterface + * @param type - The type of the ensemble idents to filter + * @returns A new array of ensemble idents that are of the specified type + */ +export function filterEnsembleIdentsByType( + ensembleIdents: (RegularEnsembleIdent | DeltaEnsembleIdent)[], + type: new (...args: any[]) => T +): T[] { + return ensembleIdents.filter((ensembleIdent) => isEnsembleIdentOfType(ensembleIdent, type)) as T[]; +} + +/** + * Generates a regex pattern for a UUID. + * + * @returns A string that represents a regex pattern for a UUID + */ +export function ensembleIdentUuidRegexString(): string { + return "[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}"; +} diff --git a/frontend/src/framework/utils/ensembleUiHelpers.ts b/frontend/src/framework/utils/ensembleUiHelpers.ts index 850e2d477..3fe6247b4 100644 --- a/frontend/src/framework/utils/ensembleUiHelpers.ts +++ b/frontend/src/framework/utils/ensembleUiHelpers.ts @@ -1,10 +1,11 @@ -import { EnsembleIdent } from "../EnsembleIdent"; +import { DeltaEnsembleIdent } from "../DeltaEnsembleIdent"; import { EnsembleSet } from "../EnsembleSet"; +import { RegularEnsembleIdent } from "../RegularEnsembleIdent"; export function maybeAssignFirstSyncedEnsemble( - currIdent: EnsembleIdent | null, - syncedEnsembleValues: EnsembleIdent[] | null -): EnsembleIdent | null { + currIdent: RegularEnsembleIdent | null, + syncedEnsembleValues: RegularEnsembleIdent[] | null +): RegularEnsembleIdent | null { if (!syncedEnsembleValues || syncedEnsembleValues.length < 1) { return currIdent; } @@ -18,52 +19,101 @@ export function maybeAssignFirstSyncedEnsemble( } /** - * Validates the the EnsembleIdent specified in currIdent against the contents of the - * EnsembleSet and fixes the value if it isn't valid. + * Validates the the RegularEnsembleIdent or DeltaEnsembleIdent specified in currIdent against the + * contents of the EnsembleSet and fixes the value if it isn't valid. * * Returns null if an empty EnsembleSet is specified. * - * Note that if the specified EnsembleIdent is valid, this function will always return - * a reference to the exact same object that was passed in currIdent. This means that - * you can compare the references (fixedIdent !== currIdent) to detect any changes. + * Note that if the specified RegularEnsembleIdents and DeltaEnsembleIdents are valid, this function + * will always return a reference to the exact same object that was passed in currIdent. This + * means that you can compare the references (fixedIdent !== currIdent) to detect any changes. */ export function fixupEnsembleIdent( - currIdent: EnsembleIdent | null, + currIdent: RegularEnsembleIdent | DeltaEnsembleIdent | null, ensembleSet: EnsembleSet | null -): EnsembleIdent | null { +): RegularEnsembleIdent | DeltaEnsembleIdent | null { if (!ensembleSet?.hasAnyEnsembles()) { return null; } - if (currIdent) { - if (ensembleSet.hasEnsemble(currIdent)) { - return currIdent; - } + if (currIdent && ensembleSet.hasEnsemble(currIdent)) { + return currIdent; } - return ensembleSet.getEnsembleArr()[0].getIdent(); + return ensembleSet.getEnsembleArray()[0].getIdent(); } /** - * Validates the the EnsembleIdents specified in currIdents against the contents of the + * Validates the the RegularEnsembleIdent specified in currIdent against the contents of the * EnsembleSet and fixes the value if it isn't valid. * + * Returns null if specified EnsembleSet does not contain any regular ensembles. + * + * Note that if the specified RegularEnsembleIdents is valid, this function will always return + * a reference to the exact same object that was passed in currIdent. This means that you can + * compare the references (fixedIdent !== currIdent) to detect any changes. + */ +export function fixupRegularEnsembleIdent( + currIdent: RegularEnsembleIdent | null, + ensembleSet: EnsembleSet | null +): RegularEnsembleIdent | null { + if (!ensembleSet?.hasAnyRegularEnsembles()) { + return null; + } + + if (currIdent && ensembleSet.hasEnsemble(currIdent)) { + return currIdent; + } + + return ensembleSet.getRegularEnsembleArray()[0].getIdent(); +} + +/** + * Validates the the RegularEnsembleIdents or DeltaEnsembleIdents specified in currIdents + * against the contents of the EnsembleSet and fixes the value if it isn't valid. + * * Returns null if an empty EnsembleSet is specified. * - * Note that if the specified EnsembleIdents are valid, this function will always return - * a reference to the exact same object that was passed in currIdent. This means that - * you can compare the references (fixedIdent !== currIdent) to detect any changes. + * Note that if the specified RegularEnsembleIdents and DeltaEnsembleIdents are valid, this + * function will always return a reference to the exact same object that was passed in + * currIdent. This means that you can compare the references (fixedIdent !== currIdent) to + * detect any changes. */ export function fixupEnsembleIdents( - currIdents: EnsembleIdent[] | null, + currIdents: (RegularEnsembleIdent | DeltaEnsembleIdent)[] | null, ensembleSet: EnsembleSet | null -): EnsembleIdent[] | null { +): (RegularEnsembleIdent | DeltaEnsembleIdent)[] | null { if (!ensembleSet?.hasAnyEnsembles()) { return null; } if (currIdents === null || currIdents.length === 0) { - return [ensembleSet.getEnsembleArr()[0].getIdent()]; + return [ensembleSet.getEnsembleArray()[0].getIdent()]; + } + + return currIdents.filter((currIdent) => ensembleSet.hasEnsemble(currIdent)); +} + +/** + * Validates the the RegularEnsembleIdents specified in currIdents against the contents of the + * EnsembleSet and fixes the value if it isn't valid. + * + * Returns null if an empty EnsembleSet is specified. + * + * Note that if the specified RegularEnsembleIdents are valid, this function will always return + * a reference to the exact same object that was passed in currIdent. This means that you can + * compare the references (fixedIdent !== currIdent) to detect any changes. + */ +export function fixupRegularEnsembleIdents( + currIdents: RegularEnsembleIdent[] | null, + ensembleSet: EnsembleSet | null +): RegularEnsembleIdent[] | null { + if (!ensembleSet?.hasAnyRegularEnsembles()) { + return null; + } + + if (currIdents === null || currIdents.length === 0) { + return [ensembleSet.getRegularEnsembleArray()[0].getIdent()]; } return currIdents.filter((currIdent) => ensembleSet.hasEnsemble(currIdent)); diff --git a/frontend/src/modules/2DViewer/layers/framework/LayerManager/LayerManager.ts b/frontend/src/modules/2DViewer/layers/framework/LayerManager/LayerManager.ts index 98208330a..e4aaed2a9 100644 --- a/frontend/src/modules/2DViewer/layers/framework/LayerManager/LayerManager.ts +++ b/frontend/src/modules/2DViewer/layers/framework/LayerManager/LayerManager.ts @@ -1,4 +1,4 @@ -import { Ensemble } from "@framework/Ensemble"; +import { RegularEnsemble } from "@framework/RegularEnsemble"; import { EnsembleRealizationFilterFunction, WorkbenchSession, @@ -36,7 +36,7 @@ export type LayerManagerTopicPayload = { export type GlobalSettings = { fieldId: string | null; - ensembles: readonly Ensemble[]; + ensembles: readonly RegularEnsemble[]; realizationFilterFunction: EnsembleRealizationFilterFunction; }; @@ -194,7 +194,7 @@ export class LayerManager implements Group, PublishSubscribe ensemble.getFieldIdentifier() === fieldIdentifier) .map((ensemble) => ensemble.getIdent()); diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/ObservedSurfaceLayer/types.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/ObservedSurfaceLayer/types.ts index a4bc063f3..d89905be3 100644 --- a/frontend/src/modules/2DViewer/layers/layers/implementations/ObservedSurfaceLayer/types.ts +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/ObservedSurfaceLayer/types.ts @@ -1,8 +1,8 @@ -import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; export type ObservedSurfaceSettings = { - [SettingType.ENSEMBLE]: EnsembleIdent | null; + [SettingType.ENSEMBLE]: RegularEnsembleIdent | null; [SettingType.SURFACE_ATTRIBUTE]: string | null; [SettingType.SURFACE_NAME]: string | null; [SettingType.TIME_OR_INTERVAL]: string | null; diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationGridLayer/types.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationGridLayer/types.ts index 2ac9df1c2..a746e527c 100644 --- a/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationGridLayer/types.ts +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationGridLayer/types.ts @@ -1,8 +1,8 @@ -import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; export type RealizationGridSettings = { - [SettingType.ENSEMBLE]: EnsembleIdent | null; + [SettingType.ENSEMBLE]: RegularEnsembleIdent | null; [SettingType.REALIZATION]: number | null; [SettingType.GRID_ATTRIBUTE]: string | null; [SettingType.GRID_NAME]: string | null; diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationPolygonsLayer/types.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationPolygonsLayer/types.ts index 1b79a705b..5535d6772 100644 --- a/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationPolygonsLayer/types.ts +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationPolygonsLayer/types.ts @@ -1,9 +1,9 @@ -import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; import { SettingType } from "../../../settings/settingsTypes"; export type RealizationPolygonsSettings = { - [SettingType.ENSEMBLE]: EnsembleIdent | null; + [SettingType.ENSEMBLE]: RegularEnsembleIdent | null; [SettingType.REALIZATION]: number | null; [SettingType.POLYGONS_ATTRIBUTE]: string | null; [SettingType.POLYGONS_NAME]: string | null; diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationSurfaceLayer/types.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationSurfaceLayer/types.ts index 6477cdcc6..d1d1bfed8 100644 --- a/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationSurfaceLayer/types.ts +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/RealizationSurfaceLayer/types.ts @@ -1,9 +1,9 @@ -import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; import { SettingType } from "../../../settings/settingsTypes"; export type RealizationSurfaceSettings = { - [SettingType.ENSEMBLE]: EnsembleIdent | null; + [SettingType.ENSEMBLE]: RegularEnsembleIdent | null; [SettingType.REALIZATION]: number | null; [SettingType.SURFACE_ATTRIBUTE]: string | null; [SettingType.SURFACE_NAME]: string | null; diff --git a/frontend/src/modules/2DViewer/layers/layers/implementations/StatisticalSurfaceLayer/types.ts b/frontend/src/modules/2DViewer/layers/layers/implementations/StatisticalSurfaceLayer/types.ts index 639fc8253..adb0a2aa7 100644 --- a/frontend/src/modules/2DViewer/layers/layers/implementations/StatisticalSurfaceLayer/types.ts +++ b/frontend/src/modules/2DViewer/layers/layers/implementations/StatisticalSurfaceLayer/types.ts @@ -1,11 +1,11 @@ import { SurfaceStatisticFunction_api } from "@api"; -import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; import { SettingType } from "@modules/2DViewer/layers/settings/settingsTypes"; import { SensitivityNameCasePair } from "../../../settings/implementations/SensitivitySetting"; export type StatisticalSurfaceSettings = { - [SettingType.ENSEMBLE]: EnsembleIdent | null; + [SettingType.ENSEMBLE]: RegularEnsembleIdent | null; [SettingType.STATISTIC_FUNCTION]: SurfaceStatisticFunction_api; [SettingType.SENSITIVITY]: SensitivityNameCasePair | null; [SettingType.SURFACE_ATTRIBUTE]: string | null; diff --git a/frontend/src/modules/2DViewer/layers/settings/implementations/EnsembleSetting.tsx b/frontend/src/modules/2DViewer/layers/settings/implementations/EnsembleSetting.tsx index de7eeb630..49172a50a 100644 --- a/frontend/src/modules/2DViewer/layers/settings/implementations/EnsembleSetting.tsx +++ b/frontend/src/modules/2DViewer/layers/settings/implementations/EnsembleSetting.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; import { EnsembleDropdown } from "@framework/components/EnsembleDropdown"; import { SettingDelegate } from "../../delegates/SettingDelegate"; @@ -8,11 +8,11 @@ import { Setting, SettingComponentProps, ValueToStringArgs } from "../../interfa import { SettingRegistry } from "../SettingRegistry"; import { SettingType } from "../settingsTypes"; -export class EnsembleSetting implements Setting { - private _delegate: SettingDelegate; +export class EnsembleSetting implements Setting { + private _delegate: SettingDelegate; constructor() { - this._delegate = new SettingDelegate(null, this); + this._delegate = new SettingDelegate(null, this); } getType(): SettingType { @@ -23,20 +23,20 @@ export class EnsembleSetting implements Setting { return "Ensemble"; } - getDelegate(): SettingDelegate { + getDelegate(): SettingDelegate { return this._delegate; } - serializeValue(value: EnsembleIdent | null): string { + serializeValue(value: RegularEnsembleIdent | null): string { return value?.toString() ?? ""; } - deserializeValue(serializedValue: string): EnsembleIdent | null { - return serializedValue !== "" ? EnsembleIdent.fromString(serializedValue) : null; + deserializeValue(serializedValue: string): RegularEnsembleIdent | null { + return serializedValue !== "" ? RegularEnsembleIdent.fromString(serializedValue) : null; } - makeComponent(): (props: SettingComponentProps) => React.ReactNode { - return function Ensemble(props: SettingComponentProps) { + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function Ensemble(props: SettingComponentProps) { const ensembles = props.globalSettings.ensembles.filter((ensemble) => props.availableValues.includes(ensemble.getIdent()) ); @@ -53,7 +53,7 @@ export class EnsembleSetting implements Setting { }; } - valueToString(args: ValueToStringArgs): string { + valueToString(args: ValueToStringArgs): string { const { value, workbenchSession } = args; if (value === null) { return "-"; diff --git a/frontend/src/modules/2DViewer/settings/atoms/derivedAtoms.ts b/frontend/src/modules/2DViewer/settings/atoms/derivedAtoms.ts index 5fc4447f4..ae64f8ba7 100644 --- a/frontend/src/modules/2DViewer/settings/atoms/derivedAtoms.ts +++ b/frontend/src/modules/2DViewer/settings/atoms/derivedAtoms.ts @@ -10,9 +10,9 @@ export const selectedFieldIdentifierAtom = atom((get) => { if ( !userSelectedField || - !ensembleSet.getEnsembleArr().some((ens) => ens.getFieldIdentifier() === userSelectedField) + !ensembleSet.getRegularEnsembleArray().some((ens) => ens.getFieldIdentifier() === userSelectedField) ) { - return ensembleSet.getEnsembleArr().at(0)?.getFieldIdentifier() ?? null; + return ensembleSet.getRegularEnsembleArray().at(0)?.getFieldIdentifier() ?? null; } return userSelectedField; diff --git a/frontend/src/modules/3DViewer/interfaces.ts b/frontend/src/modules/3DViewer/interfaces.ts index b74c6fb9f..b4b3352ea 100644 --- a/frontend/src/modules/3DViewer/interfaces.ts +++ b/frontend/src/modules/3DViewer/interfaces.ts @@ -1,5 +1,5 @@ import { BoundingBox3d_api } from "@api"; -import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; import { InterfaceInitialization } from "@framework/UniDirectionalModuleComponentsInterface"; import { IntersectionType } from "@framework/types/intersection"; import { ColorScale } from "@lib/utils/ColorScale"; @@ -34,7 +34,7 @@ import { } from "./view/atoms/baseAtoms"; export type SettingsToViewInterface = { - ensembleIdent: EnsembleIdent | null; + ensembleIdent: RegularEnsembleIdent | null; highlightedWellboreUuid: string | null; customIntersectionPolylineId: string | null; intersectionType: IntersectionType; diff --git a/frontend/src/modules/3DViewer/settings/atoms/baseAtoms.ts b/frontend/src/modules/3DViewer/settings/atoms/baseAtoms.ts index 72c62537b..d64800670 100644 --- a/frontend/src/modules/3DViewer/settings/atoms/baseAtoms.ts +++ b/frontend/src/modules/3DViewer/settings/atoms/baseAtoms.ts @@ -1,4 +1,4 @@ -import { EnsembleIdent } from "@framework/EnsembleIdent"; +import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; import { IntersectionType } from "@framework/types/intersection"; import { ColorScale } from "@lib/utils/ColorScale"; import { GridCellIndexRanges } from "@modules/3DViewer/typesAndEnums"; @@ -16,7 +16,7 @@ export const addCustomIntersectionPolylineEditModeActiveAtom = atom(fal export const editCustomIntersectionPolylineEditModeActiveAtom = atom(false); export const currentCustomIntersectionPolylineAtom = atom([]); -export const userSelectedEnsembleIdentAtom = atom(null); +export const userSelectedEnsembleIdentAtom = atom(null); export const userSelectedRealizationAtom = atom(null); export const userSelectedGridModelNameAtom = atom(null); export const userSelectedGridModelParameterNameAtom = atom(null); diff --git a/frontend/src/modules/3DViewer/settings/atoms/derivedAtoms.ts b/frontend/src/modules/3DViewer/settings/atoms/derivedAtoms.ts index a646a9785..d7a221cd1 100644 --- a/frontend/src/modules/3DViewer/settings/atoms/derivedAtoms.ts +++ b/frontend/src/modules/3DViewer/settings/atoms/derivedAtoms.ts @@ -1,6 +1,6 @@ import { Grid3dDimensions_api } from "@api"; -import { EnsembleIdent } from "@framework/EnsembleIdent"; -import { EnsembleRealizationFilterFunctionAtom, EnsembleSetAtom } from "@framework/GlobalAtoms"; +import { EnsembleSetAtom, ValidEnsembleRealizationsFunctionAtom } from "@framework/GlobalAtoms"; +import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; import { IntersectionPolylinesAtom } from "@framework/userCreatedItems/IntersectionPolylines"; import { GridCellIndexRanges } from "@modules/3DViewer/typesAndEnums"; @@ -19,12 +19,12 @@ import { } from "./baseAtoms"; import { drilledWellboreHeadersQueryAtom, gridModelInfosQueryAtom } from "./queryAtoms"; -export const selectedEnsembleIdentAtom = atom((get) => { +export const selectedEnsembleIdentAtom = atom((get) => { const ensembleSet = get(EnsembleSetAtom); const userSelectedEnsembleIdent = get(userSelectedEnsembleIdentAtom); if (userSelectedEnsembleIdent === null || !ensembleSet.hasEnsemble(userSelectedEnsembleIdent)) { - return ensembleSet.getEnsembleArr()[0]?.getIdent() || null; + return ensembleSet.getRegularEnsembleArray()[0]?.getIdent() || null; } return userSelectedEnsembleIdent; @@ -67,22 +67,14 @@ export const selectedCustomIntersectionPolylineIdAtom = atom((get) => { }); export const availableRealizationsAtom = atom((get) => { - const ensembleSet = get(EnsembleSetAtom); const selectedEnsembleIdent = get(selectedEnsembleIdentAtom); if (selectedEnsembleIdent === null) { return []; } - let ensembleRealizationFilterFunction = get(EnsembleRealizationFilterFunctionAtom); - - if (ensembleRealizationFilterFunction === null) { - ensembleRealizationFilterFunction = (ensembleIdent: EnsembleIdent) => { - return ensembleSet.findEnsemble(ensembleIdent)?.getRealizations() ?? []; - }; - } - - return ensembleRealizationFilterFunction(selectedEnsembleIdent); + const validEnsembleRealizationsFunction = get(ValidEnsembleRealizationsFunctionAtom); + return validEnsembleRealizationsFunction(selectedEnsembleIdent); }); export const selectedRealizationAtom = atom((get) => { diff --git a/frontend/src/modules/3DViewer/settings/settings.tsx b/frontend/src/modules/3DViewer/settings/settings.tsx index 893c80e01..a880310bb 100644 --- a/frontend/src/modules/3DViewer/settings/settings.tsx +++ b/frontend/src/modules/3DViewer/settings/settings.tsx @@ -1,8 +1,8 @@ import React from "react"; import { Grid3dInfo_api, WellboreHeader_api } from "@api"; -import { EnsembleIdent } from "@framework/EnsembleIdent"; import { ModuleSettingsProps } from "@framework/Module"; +import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; import { useSettingsStatusWriter } from "@framework/StatusWriter"; import { SyncSettingKey, SyncSettingsHelper } from "@framework/SyncSettings"; import { useIntersectionPolylines } from "@framework/UserCreatedItems"; @@ -77,7 +77,7 @@ export function Settings(props: ModuleSettingsProps): JSX.Element { const setPolylineEditModeActive = useSetAtom(editCustomIntersectionPolylineEditModeActiveAtom); const [prevSyncedIntersection, setPrevSyncedIntersection] = React.useState(null); - const [prevSyncedEnsembles, setPrevSyncedEnsembles] = React.useState(null); + const [prevSyncedEnsembles, setPrevSyncedEnsembles] = React.useState(null); const [pickSingleGridCellIndexI, setPickSingleGridCellIndexI] = React.useState(false); const [pickSingleGridCellIndexJ, setPickSingleGridCellIndexJ] = React.useState(false); const [pickSingleGridCellIndexK, setPickSingleGridCellIndexK] = React.useState(false); @@ -153,7 +153,7 @@ export function Settings(props: ModuleSettingsProps): JSX.Element { const gridModelErrorMessage = usePropagateApiErrorToStatusWriter(gridModelInfos, statusWriter) ?? ""; const wellHeadersErrorMessage = usePropagateApiErrorToStatusWriter(wellHeaders, statusWriter) ?? ""; - function handleEnsembleSelectionChange(ensembleIdent: EnsembleIdent | null) { + function handleEnsembleSelectionChange(ensembleIdent: RegularEnsembleIdent | null) { setSelectedEnsembleIdent(ensembleIdent); syncHelper.publishValue( SyncSettingKey.ENSEMBLE, @@ -256,7 +256,7 @@ export function Settings(props: ModuleSettingsProps): JSX.Element {