diff --git a/backend_py/primary/primary/main.py b/backend_py/primary/primary/main.py index d2e9fa22e..eb90df0bf 100644 --- a/backend_py/primary/primary/main.py +++ b/backend_py/primary/primary/main.py @@ -17,7 +17,7 @@ from primary.routers.general import router as general_router from primary.routers.graph.router import router as graph_router from primary.routers.grid3d.router import router as grid3d_router -from primary.routers.group_tree.router import router as group_tree_router +from primary.routers.flow_network.router import router as flow_network_router from primary.routers.inplace_volumetrics.router import router as inplace_volumetrics_router from primary.routers.observations.router import router as observations_router from primary.routers.parameters.router import router as parameters_router @@ -77,7 +77,7 @@ def custom_generate_unique_id(route: APIRoute) -> str: app.include_router(surface_router, prefix="/surface", tags=["surface"]) app.include_router(parameters_router, prefix="/parameters", tags=["parameters"]) app.include_router(grid3d_router, prefix="/grid3d", tags=["grid3d"]) -app.include_router(group_tree_router, prefix="/group_tree", tags=["group_tree"]) +app.include_router(flow_network_router, prefix="/flow_network", tags=["flow_network"]) app.include_router(pvt_router, prefix="/pvt", tags=["pvt"]) app.include_router(well_completions_router, prefix="/well_completions", tags=["well_completions"]) app.include_router(well_router, prefix="/well", tags=["well"]) diff --git a/backend_py/primary/primary/routers/flow_network/__init__.py b/backend_py/primary/primary/routers/flow_network/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend_py/primary/primary/routers/group_tree/router.py b/backend_py/primary/primary/routers/flow_network/router.py similarity index 72% rename from backend_py/primary/primary/routers/group_tree/router.py rename to backend_py/primary/primary/routers/flow_network/router.py index 41fef2f7e..e6fe2be19 100644 --- a/backend_py/primary/primary/routers/group_tree/router.py +++ b/backend_py/primary/primary/routers/flow_network/router.py @@ -1,23 +1,23 @@ +import logging from fastapi import APIRouter, Depends, Query +from webviz_pkg.core_utils.perf_timer import PerfTimer from primary.auth.auth_helper import AuthHelper -from primary.services.group_tree_assembler.group_tree_assembler import GroupTreeAssembler + +from primary.services.flow_network_assembler.flow_network_assembler import FlowNetworkAssembler +from primary.services.flow_network_assembler.flow_network_types import NetworkModeOptions, NodeType from primary.services.sumo_access.group_tree_access import GroupTreeAccess -from primary.services.sumo_access.group_tree_types import TreeModeOptions, NodeType from primary.services.sumo_access.summary_access import Frequency, SummaryAccess from primary.services.utils.authenticated_user import AuthenticatedUser from . import schemas -from webviz_pkg.core_utils.perf_timer import PerfTimer -import logging - LOGGER = logging.getLogger(__name__) router = APIRouter() -@router.get("/realization_group_tree_data/") -async def get_realization_group_tree_data( +@router.get("/realization_flow_network/") +async def get_realization_flow_network( # fmt:off authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), case_uuid: str = Query(description="Sumo case uuid"), @@ -26,7 +26,7 @@ async def get_realization_group_tree_data( resampling_frequency: schemas.Frequency = Query(description="Resampling frequency"), node_type_set: set[schemas.NodeType] = Query(description="Node types"), # fmt:on -) -> schemas.GroupTreeData: +) -> schemas.FlowNetworkData: timer = PerfTimer() group_tree_access = await GroupTreeAccess.from_case_uuid_async( @@ -38,26 +38,26 @@ async def get_realization_group_tree_data( summary_frequency = Frequency.YEARLY # Convert to NodeType enum in group_tree_types - unique_node_types = set([NodeType(elm.value) for elm in node_type_set]) + unique_node_types = {NodeType(elm.value) for elm in node_type_set} - group_tree_data = GroupTreeAssembler( + network_assembler = FlowNetworkAssembler( group_tree_access=group_tree_access, summary_access=summary_access, realization=realization, summary_frequency=summary_frequency, node_types=unique_node_types, - group_tree_mode=TreeModeOptions.SINGLE_REAL, + flow_network_mode=NetworkModeOptions.SINGLE_REAL, ) timer.lap_ms() - await group_tree_data.fetch_and_initialize_async() + await network_assembler.fetch_and_initialize_async() initialize_time_ms = timer.lap_ms() ( - dated_trees, + dated_networks, edge_metadata, node_metadata, - ) = await group_tree_data.create_dated_trees_and_metadata_lists() + ) = await network_assembler.create_dated_networks_and_metadata_lists() create_data_time_ms = timer.lap_ms() LOGGER.info( @@ -65,6 +65,6 @@ async def get_realization_group_tree_data( f"(initialize={initialize_time_ms}ms, create group tree={create_data_time_ms}ms)" ) - return schemas.GroupTreeData( - edge_metadata_list=edge_metadata, node_metadata_list=node_metadata, dated_trees=dated_trees + return schemas.FlowNetworkData( + edgeMetadataList=edge_metadata, nodeMetadataList=node_metadata, datedNetworks=dated_networks ) diff --git a/backend_py/primary/primary/routers/flow_network/schemas.py b/backend_py/primary/primary/routers/flow_network/schemas.py new file mode 100644 index 000000000..86a829019 --- /dev/null +++ b/backend_py/primary/primary/routers/flow_network/schemas.py @@ -0,0 +1,36 @@ +from enum import Enum, StrEnum + +from pydantic import BaseModel, ConfigDict +from primary.services.flow_network_assembler.flow_network_types import DatedFlowNetwork, FlowNetworkMetadata + + +class Frequency(str, Enum): + DAILY = "DAILY" + WEEKLY = "WEEKLY" + MONTHLY = "MONTHLY" + QUARTERLY = "QUARTERLY" + YEARLY = "YEARLY" + + +class StatOption(str, Enum): + MEAN = "MEAN" + P10 = "P10" + P90 = "P90" + P50 = "P50" + MIN = "MIN" + MAX = "MAX" + + +# ! Copy of the flow network service NodeType enum +class NodeType(StrEnum): + PROD = "prod" + INJ = "inj" + OTHER = "other" + + +class FlowNetworkData(BaseModel): + model_config = ConfigDict(revalidate_instances="always") + + edgeMetadataList: list[FlowNetworkMetadata] + nodeMetadataList: list[FlowNetworkMetadata] + datedNetworks: list[DatedFlowNetwork] diff --git a/backend_py/primary/primary/routers/group_tree/schemas.py b/backend_py/primary/primary/routers/group_tree/schemas.py deleted file mode 100644 index d1372d1c3..000000000 --- a/backend_py/primary/primary/routers/group_tree/schemas.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import List -from enum import Enum -from primary.services.sumo_access.group_tree_types import DatedTree, GroupTreeMetadata - -from pydantic import BaseModel - - -class Frequency(str, Enum): - DAILY = "DAILY" - WEEKLY = "WEEKLY" - MONTHLY = "MONTHLY" - QUARTERLY = "QUARTERLY" - YEARLY = "YEARLY" - - -class StatOption(str, Enum): - MEAN = "MEAN" - P10 = "P10" - P90 = "P90" - P50 = "P50" - MIN = "MIN" - MAX = "MAX" - - -class NodeType(str, Enum): - PROD = "prod" - INJ = "inj" - OTHER = "other" - - -class GroupTreeData(BaseModel): - edge_metadata_list: List[GroupTreeMetadata] - node_metadata_list: List[GroupTreeMetadata] - dated_trees: List[DatedTree] diff --git a/backend_py/primary/primary/services/flow_network_assembler/__init__.py b/backend_py/primary/primary/services/flow_network_assembler/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend_py/primary/primary/services/group_tree_assembler/_group_tree_dataframe_model.py b/backend_py/primary/primary/services/flow_network_assembler/_group_tree_dataframe_model.py similarity index 63% rename from backend_py/primary/primary/services/group_tree_assembler/_group_tree_dataframe_model.py rename to backend_py/primary/primary/services/flow_network_assembler/_group_tree_dataframe_model.py index 4ea7987fc..969579f84 100644 --- a/backend_py/primary/primary/services/group_tree_assembler/_group_tree_dataframe_model.py +++ b/backend_py/primary/primary/services/flow_network_assembler/_group_tree_dataframe_model.py @@ -2,54 +2,7 @@ import pandas as pd -from primary.services.sumo_access.group_tree_types import DataType, TreeType - -GROUP_TREE_FIELD_DATATYPE_TO_VECTOR_MAP = { - DataType.OILRATE: "FOPR", - DataType.GASRATE: "FGPR", - DataType.WATERRATE: "FWPR", - DataType.WATERINJRATE: "FWIR", - DataType.GASINJRATE: "FGIR", - DataType.PRESSURE: "GPR", -} - -TREE_TYPE_DATATYPE_TO_GROUP_VECTOR_DATATYPE_MAP = { - "GRUPTREE": { - DataType.OILRATE: "GOPR", - DataType.GASRATE: "GGPR", - DataType.WATERRATE: "GWPR", - DataType.WATERINJRATE: "GWIR", - DataType.GASINJRATE: "GGIR", - DataType.PRESSURE: "GPR", - }, - # BRANPROP can not be used for injection, but the nodes - # might also be GNETINJE and could therefore have injection. - "BRANPROP": { - DataType.OILRATE: "GOPRNB", - DataType.GASRATE: "GGPRNB", - DataType.WATERRATE: "GWPRNB", - DataType.WATERINJRATE: "GWIR", - DataType.GASINJRATE: "GGIR", - DataType.PRESSURE: "GPR", - }, -} - -GROUPTREE_DATATYPE_TO_WELL_VECTOR_DATATYPE_MAP = { - DataType.OILRATE: "WOPR", - DataType.GASRATE: "WGPR", - DataType.WATERRATE: "WWPR", - DataType.WATERINJRATE: "WWIR", - DataType.GASINJRATE: "WGIR", - DataType.PRESSURE: "WTHP", - DataType.BHP: "WBHP", - DataType.WMCTL: "WMCTL", -} - -FIELD_VECTORS_OF_INTEREST: List[str] = list(GROUP_TREE_FIELD_DATATYPE_TO_VECTOR_MAP.values()) -WELLS_VECTOR_DATATYPES_OF_INTEREST: List[str] = list(GROUPTREE_DATATYPE_TO_WELL_VECTOR_DATATYPE_MAP.values()) -GROUP_VECTOR_DATATYPES_OF_INTEREST = [ - v for kw in ["GRUPTREE", "BRANPROP"] for v in TREE_TYPE_DATATYPE_TO_GROUP_VECTOR_DATATYPE_MAP[kw].values() -] +from primary.services.sumo_access.group_tree_types import TreeType class GroupTreeDataframeModel: @@ -77,7 +30,6 @@ class GroupTreeDataframeModel: _grouptree_wells: List[str] = [] _grouptree_groups: List[str] = [] - _grouptree_wstat_vectors: List[str] = [] def __init__( self, @@ -132,7 +84,6 @@ def __init__( self._grouptree_wells = list(group_tree_wells) self._grouptree_groups = list(group_tree_groups) - self._grouptree_wstat_vectors = [f"WSTAT:{well}" for well in self._grouptree_wells] @property def dataframe(self) -> pd.DataFrame: @@ -157,15 +108,6 @@ def group_tree_wells(self) -> List[str]: def group_tree_groups(self) -> List[str]: return self._grouptree_groups - @property - def wstat_vectors(self) -> List[str]: - """ - Returns the well state indicator vectors for all wells in the group tree - - The vectors are of the form "WSTAT:{well_name}" - """ - return self._grouptree_wstat_vectors - def create_filtered_dataframe( self, terminal_node: Optional[str] = None, @@ -214,40 +156,6 @@ def filter_wells(dframe: pd.DataFrame, well_name_criteria: Callable) -> pd.DataF return df.copy() - def create_vector_of_interest_list(self) -> List[str]: - """ - Create a list of vectors based on the possible combinations of vector datatypes and vector nodes - for a group tree - - This implies vectors for field, group and well. - - Only returns the candidates which exist among the valid vectors - """ - - # Find all summary vectors with group tree wells - group_tree_well_vector_candidates = _create_vector_candidates( - WELLS_VECTOR_DATATYPES_OF_INTEREST, self._grouptree_wells - ) - - # Find all summary vectors with group tree groups - group_tree_group_vector_candidates = _create_vector_candidates( - GROUP_VECTOR_DATATYPES_OF_INTEREST, self._grouptree_groups - ) - - # Find all summary vectors with field vectors - group_tree_field_vectors_candidates = FIELD_VECTORS_OF_INTEREST - - all_vectors_of_interest = ( - group_tree_well_vector_candidates - + group_tree_group_vector_candidates - + group_tree_field_vectors_candidates - + self._grouptree_wstat_vectors - ) - - # Ensure non duplicated vectors - unique_vectors_of_interst = list(set(all_vectors_of_interest)) - return unique_vectors_of_interst - def _create_branch_node_list(self, terminal_node: str) -> List[str]: """ This function lists all nodes in a branch of the group tree starting from the terminal node. @@ -263,7 +171,7 @@ def _create_branch_node_list(self, terminal_node: str) -> List[str]: current_parents = [terminal_node] while len(current_parents) > 0: # Find all indexes matching the current parents - children_indices = set([i for i, x in enumerate(parents_array) if x in current_parents]) + children_indices = {i for i, x in enumerate(parents_array) if x in current_parents} # Find all children of the current parents children = nodes_array[list(children_indices)] @@ -271,17 +179,3 @@ def _create_branch_node_list(self, terminal_node: str) -> List[str]: current_parents = children return list(branch_node_set) - - -def _create_vector_candidates(vector_datatype_candidates: List[str], vector_node_candidates: List[str]) -> List[str]: - """Create a list of vectors based on the list of vector datatype candidates and vector node candidates - - A vector is then given by "{vector_datatype}:{vector_node}" - - E.g. "WOPT:WELL1" - """ - result: List[str] = [] - for datatype in vector_datatype_candidates: - for node in vector_node_candidates: - result.append(f"{datatype}:{node}") - return result diff --git a/backend_py/primary/primary/services/flow_network_assembler/_utils.py b/backend_py/primary/primary/services/flow_network_assembler/_utils.py new file mode 100644 index 000000000..0538b11b5 --- /dev/null +++ b/backend_py/primary/primary/services/flow_network_assembler/_utils.py @@ -0,0 +1,257 @@ +import logging + +from primary.services.sumo_access.group_tree_types import TreeType +from .flow_network_types import ( + CategorizedNodeSummaryVectors, + NodeClassification, + SummaryVectorInfo, + NetworkClassification, + NodeSummaryVectorsInfo, + EdgeOrNode, + DataType, +) + + +LOGGER = logging.getLogger(__name__) + + +FIELD_DATATYPE_VECTOR_MAP = { + DataType.OILRATE: "FOPR", + DataType.GASRATE: "FGPR", + DataType.WATERRATE: "FWPR", + DataType.WATERINJRATE: "FWIR", + DataType.GASINJRATE: "FGIR", + DataType.PRESSURE: "GPR", +} +GROUPTYPE_DATATYPE_VECTORS_MAP = { + TreeType.GRUPTREE: { + DataType.OILRATE: "GOPR", + DataType.GASRATE: "GGPR", + DataType.WATERRATE: "GWPR", + DataType.WATERINJRATE: "GWIR", + DataType.GASINJRATE: "GGIR", + DataType.PRESSURE: "GPR", + }, + # BRANPROP can not be used for injection, but the nodes + # might also be GNETINJE and could therefore have injection. + TreeType.BRANPROP: { + DataType.OILRATE: "GOPRNB", + DataType.GASRATE: "GGPRNB", + DataType.WATERRATE: "GWPRNB", + DataType.WATERINJRATE: "GWIR", + DataType.GASINJRATE: "GGIR", + DataType.PRESSURE: "GPR", + }, +} +WELL_DATATYPE_VECTOR_MAP = { + DataType.WELL_STATUS: "WSTAT", + DataType.OILRATE: "WOPR", + DataType.GASRATE: "WGPR", + DataType.WATERRATE: "WWPR", + DataType.WATERINJRATE: "WWIR", + DataType.GASINJRATE: "WGIR", + DataType.PRESSURE: "WTHP", + DataType.BHP: "WBHP", + DataType.WMCTL: "WMCTL", +} + +DATATYPE_LABEL_MAP = { + DataType.OILRATE: "Oil Rate", + DataType.GASRATE: "Gas Rate", + DataType.WATERRATE: "Water Rate", + DataType.WATERINJRATE: "Water Inj Rate", + DataType.GASINJRATE: "Gas Inj Rate", + DataType.PRESSURE: "Pressure", + DataType.BHP: "BHP", + DataType.WMCTL: "WMCTL", +} + + +def compute_tree_well_vectors(group_tree_wells: list[str], data_type: DataType) -> set[str]: + """Given a vector type (WSTAT, WOPT, etc), returns a list of full summary vector names for each well in a group tree model; e.g. "WSTAT:A1", "WSTAT:A2", etc. Returns an empty array (and logs a warning) if the datatype has no vector""" + + vector_name = WELL_DATATYPE_VECTOR_MAP.get(data_type) + + if vector_name is None: + LOGGER.warning("No recognized well vector for type %s", data_type) + return set() + + return {f"{vector_name}:{well}" for well in group_tree_wells} + + +def compute_tree_group_vectors(group_tree_groups: list[str], data_type: DataType) -> set[str]: + """Given a vector type (GOPR, GGIR, etc), returns a list of full summary vector names for each group in a group tree model. Returns an empty array (and logs a warning) if the datatype has no vector""" + grup_tree_vectors = GROUPTYPE_DATATYPE_VECTORS_MAP[TreeType.GRUPTREE] + bran_prop_vectors = GROUPTYPE_DATATYPE_VECTORS_MAP[TreeType.BRANPROP] + + v_name_grup = grup_tree_vectors.get(data_type) + v_name_bran = bran_prop_vectors.get(data_type) + v_names = [v for v in (v_name_grup, v_name_bran) if v is not None] + + if len(v_names) == 0: + LOGGER.warning("No recognized group vectors for type %s", data_type) + return set() + + # Nested loop to create all possible combinations + return {f"{v_name}:{group}" for v_name in v_names for group in group_tree_groups} + + +def compute_all_well_vectors(group_tree_wells: list[str]) -> set[str]: + data_types = WELL_DATATYPE_VECTOR_MAP.keys() + + res = set() + for data_type in data_types: + res |= compute_tree_well_vectors(group_tree_wells, data_type) + + return res + + +def compute_all_group_vectors(group_tree_groups: list[str]) -> set[str]: + grup_data_types = GROUPTYPE_DATATYPE_VECTORS_MAP[TreeType.GRUPTREE].keys() + bran_data_types = GROUPTYPE_DATATYPE_VECTORS_MAP[TreeType.BRANPROP].keys() + all_data_types = set(grup_data_types) | set(bran_data_types) + + res = set() + for data_type in all_data_types: + res |= compute_tree_group_vectors(group_tree_groups, data_type) + + return res + + +def get_all_vectors_of_interest_for_tree(group_tree_wells: list[str], group_tree_groups: list[str]) -> set[str]: + """ + Create a list of vectors based on the possible combinations of vector datatypes and vector nodes + for a group tree + + This implies vectors for field, group and well. + + Only returns the candidates which exist among the valid vectors + """ + + # Find all summary vectors with field vectors + field_vectors = set(FIELD_DATATYPE_VECTOR_MAP.values()) + # Find all summary vectors with group tree wells + well_vectors = compute_all_well_vectors(group_tree_wells) + # Find all summary vectors with group tree groups + group_vectors = compute_all_group_vectors(group_tree_groups) + + all_vectors = field_vectors | well_vectors | group_vectors + + return all_vectors + + +def create_sumvec_from_datatype_node_name_and_keyword( + datatype: DataType, + node_name: str, + keyword: str, +) -> str: + """Returns the correct summary vector for a given + * datatype: oilrate, gasrate etc + * node_name: FIELD, well name or group name in Eclipse network + * keyword: GRUPTREE, BRANPROP or WELSPECS + """ + + if node_name == "FIELD": + datatype_ecl = FIELD_DATATYPE_VECTOR_MAP[datatype] + if datatype == "pressure": + return f"{datatype_ecl}:{node_name}" + return datatype_ecl + try: + if keyword == "WELSPECS": + datatype_ecl = WELL_DATATYPE_VECTOR_MAP[datatype] + elif keyword in [t.value for t in TreeType]: + datatype_ecl = GROUPTYPE_DATATYPE_VECTORS_MAP[TreeType[keyword]][datatype] + except KeyError as exc: + error = ( + f"Summary vector not found for eclipse keyword: {keyword}, " + f"data type: {datatype.value} and node name: {node_name}. " + ) + raise KeyError(error) from exc + return f"{datatype_ecl}:{node_name}" + + +def get_tree_element_for_data_type(data_type: DataType) -> EdgeOrNode: + """Returns if a given datatype is a tree edge (typically rates) or a node (f.ex pressures)""" + if data_type in [ + DataType.OILRATE, + DataType.GASRATE, + DataType.WATERRATE, + DataType.WATERINJRATE, + DataType.GASINJRATE, + ]: + return EdgeOrNode.EDGE + if data_type in [DataType.PRESSURE, DataType.BHP, DataType.WMCTL]: + return EdgeOrNode.NODE + raise ValueError(f"Data type {data_type.value} not implemented.") + + +def get_label_for_datatype(datatype: DataType) -> str: + """Returns a more readable label for the summary datatypes""" + label = DATATYPE_LABEL_MAP.get(datatype) + if label is None: + raise ValueError(f"Label for datatype {datatype.value} not implemented.") + return label + + +def get_node_vectors_info_and_categorized_node_summary_vectors_from_name_and_keyword( + node_name: str, + node_keyword: str, + node_classifications: dict[str, NodeClassification], + tree_classification: NetworkClassification, +) -> tuple[NodeSummaryVectorsInfo, CategorizedNodeSummaryVectors]: + if not isinstance(node_name, str) or not isinstance(node_keyword, str): + raise ValueError("Nodename and keyword must be strings") + + node_classification = node_classifications[node_name] + + datatypes = _compute_node_datatypes_for_name_and_keyword( + node_name, node_keyword, node_classification, tree_classification + ) + + node_vectors_info = NodeSummaryVectorsInfo(SMRY_INFO={}) + all_sumvecs = set() + edge_sumvecs = set() + + for datatype in datatypes: + sumvec_name = create_sumvec_from_datatype_node_name_and_keyword(datatype, node_name, node_keyword) + edge_or_node = get_tree_element_for_data_type(datatype) + + all_sumvecs.add(sumvec_name) + + if edge_or_node == EdgeOrNode.EDGE: + edge_sumvecs.add(sumvec_name) + + node_vectors_info.SMRY_INFO[sumvec_name] = SummaryVectorInfo(DATATYPE=datatype, EDGE_NODE=edge_or_node) + + return ( + node_vectors_info, + CategorizedNodeSummaryVectors(all_summary_vectors=all_sumvecs, edge_summary_vectors=edge_sumvecs), + ) + + +def _compute_node_datatypes_for_name_and_keyword( + node_name: str, + node_keyword: str, + node_classification: NodeClassification, + tree_classification: NetworkClassification, +) -> list[DataType]: + + is_prod = node_classification.IS_PROD + is_inj = node_classification.IS_INJ + is_terminal = node_name == tree_classification.TERMINAL_NODE + is_wellspec = node_keyword == "WELSPECS" + + datatypes = [DataType.PRESSURE] + + if is_wellspec: + datatypes += [DataType.BHP, DataType.WMCTL] + + if not is_terminal: + if is_prod: + datatypes += [DataType.OILRATE, DataType.GASRATE, DataType.WATERRATE] + if is_inj and tree_classification.HAS_WATER_INJ: + datatypes.append(DataType.WATERINJRATE) + if is_inj and tree_classification.HAS_GAS_INJ: + datatypes.append(DataType.GASINJRATE) + + return datatypes diff --git a/backend_py/primary/primary/services/flow_network_assembler/flow_network_assembler.py b/backend_py/primary/primary/services/flow_network_assembler/flow_network_assembler.py new file mode 100644 index 000000000..550207cd9 --- /dev/null +++ b/backend_py/primary/primary/services/flow_network_assembler/flow_network_assembler.py @@ -0,0 +1,993 @@ +import logging +import asyncio +from typing import Optional, Tuple, Literal +from dataclasses import dataclass + +import numpy as np +import pandas as pd +import pyarrow as pa +import pyarrow.compute as pc + +from fastapi import HTTPException +from webviz_pkg.core_utils.perf_timer import PerfTimer + + +from primary.services.sumo_access.summary_access import Frequency, SummaryAccess +from primary.services.sumo_access.group_tree_access import GroupTreeAccess +from primary.services.sumo_access.group_tree_types import TreeType + +from . import _utils +from ._group_tree_dataframe_model import ( + GroupTreeDataframeModel, +) + +from .flow_network_types import ( + DataType, + DatedFlowNetwork, + EdgeOrNode, + FlowNetworkMetadata, + FlowNetworkSummaryVectorsInfo, + NetworkModeOptions, + NetworkNode, + NodeClassification, + NodeSummaryVectorsInfo, + NodeType, + StaticNodeWorkingData, + NetworkClassification, +) + + +LOGGER = logging.getLogger(__name__) + + +@dataclass +# Dataclass needs to save a bunch of timestamps. Many attributes is okay here, as splitting it would be more cumbersome +# pylint: disable-next=too-many-instance-attributes +class PerformanceTimes: + """Simple utility class to store performance timer results for different internal method calls""" + + init_sumo_data: int = 0 + init_summary_vector_list: int = 0 + fetch_grouptree_df: int = 0 + init_grouptree_df_model: int = 0 + create_filtered_dataframe: int = 0 + init_summary_vector_data_table: int = 0 + create_node_classifications: int = 0 + create_network_summary_vectors_info: int = 0 + + # Unused for logging for now, but available if needed + build_and_verify_vectors_of_interest: int = 0 + create_well_node_classifications: int = 0 + + def log_sumo_download_times(self) -> None: + # Log download from Sumo times + LOGGER.info( + f"Total time to fetch data from Sumo: {self.init_sumo_data + self.init_summary_vector_data_table}ms, " + f"Get summary vector list in: {self.init_summary_vector_list}ms, " + f"Get group tree table in: {self.fetch_grouptree_df}ms, " + f"Get summary vectors in: {self.init_summary_vector_data_table}ms" + ) + + def log_structure_init_times(self) -> None: + # Log initialization of data structures times + LOGGER.info( + f"Initialize GroupTreeModel in: {self.init_grouptree_df_model}ms, " + f"Create filtered dataframe in: {self.create_filtered_dataframe}ms, " + f"Create node classifications in: {self.create_node_classifications}ms, " + f"Create group tree summary vectors info in: {self.create_network_summary_vectors_info}ms" + ) + + +@dataclass +class FlatNetworkNodeData: + """ + Utility class when assembling trees. A "flat" network node contains name of its parent node, and + its node data which is a NetworkNode with a empty children array. + """ + + parent_name: str + node_without_children: NetworkNode # Should have an empty children array + + +# Should probably aim to reduce this, but that's out of scope for my current task, so leaving it as is. It was like this when I got here >:I +# pylint: disable-next=too-many-instance-attributes +class FlowNetworkAssembler: + """ + Class to manage the fetching of data from sumo trees (GRUPTREE or BRANPROP) and vector summary + tables, and assembling them together to create a collection of dated flow networks trees + + **Note: Currently, only the single realization (SINGLE_REAL) mode is supported** + """ + + # As before, fixing the arguments would be breaking, and out of scope for now. Leaving it as I found it + # pylint: disable-next=too-many-arguments + def __init__( + self, + group_tree_access: GroupTreeAccess, + summary_access: SummaryAccess, + realization: int, + summary_frequency: Frequency, + node_types: set[NodeType], + flow_network_mode: NetworkModeOptions, + terminal_node: str = "FIELD", + tree_type: TreeType = TreeType.GRUPTREE, + excl_well_startswith: Optional[list[str]] = None, + excl_well_endswith: Optional[list[str]] = None, + ): + # NOTE: Temporary only supporting single real + if flow_network_mode != NetworkModeOptions.SINGLE_REAL: + raise ValueError("Only SINGLE_REAL mode is supported at the moment.") + + self._network_mode = flow_network_mode + self._realization = realization + self._group_tree_access = group_tree_access + self._summary_access = summary_access + self._tree_type = tree_type + self._excl_well_startswith = excl_well_startswith + self._excl_well_endswith = excl_well_endswith + self._summary_resampling_frequency = summary_frequency + self._node_types = node_types + + self._group_tree_df_model: Optional[GroupTreeDataframeModel] = None + self._filtered_group_tree_df: Optional[pd.DataFrame] = None + self._all_vectors: Optional[set[str]] = None + self._smry_table_sorted_by_date: pa.Table | None = None + + self._node_static_working_data: dict[str, StaticNodeWorkingData] | None = None + + self._performance_times = PerformanceTimes() + + # Store network details in data class to make it easier to feed it to the various helpers + self._network_classification = NetworkClassification( + HAS_GAS_INJ=False, HAS_WATER_INJ=False, TERMINAL_NODE=terminal_node + ) + + def _verify_that_sumvecs_exists(self, check_sumvecs: set[str]) -> None: + """ + Takes in a list of summary vectors and checks if they are present among the assemblers available vectors. + If any are missing, a ValueError is raised with the list of all missing summary vectors. + """ + # Find vectors that are missing in the valid sumvecs + missing_sumvecs = check_sumvecs - self._all_vectors_safe + if len(missing_sumvecs) > 0: + str_missing_sumvecs = ", ".join(missing_sumvecs) + raise ValueError("Missing summary vectors for the FlowNetwork plugin: ", f"{str_missing_sumvecs}.") + + async def _initialize_group_tree_df_async(self) -> None: + """Get group tree data from Sumo, and store it in a helper model class""" + timer = PerfTimer() + + group_tree_table_df = await self._group_tree_access.get_group_tree_table(realization=self._realization) + + # Store performance time for later logging + self._performance_times.fetch_grouptree_df = timer.lap_ms() + + if group_tree_table_df is None: + raise HTTPException(status_code=404, detail="Group tree data not found") + + self._group_tree_df_model = GroupTreeDataframeModel(group_tree_table_df, self._tree_type) + + # Store performance time for later logging + self._performance_times.init_grouptree_df_model = timer.lap_ms() + + # Create filtered group tree df from model + self._filtered_group_tree_df = self._group_tree_df_model.create_filtered_dataframe( + terminal_node=self._network_classification.TERMINAL_NODE, + excl_well_startswith=self._excl_well_startswith, + excl_well_endswith=self._excl_well_endswith, + ) + + # Store performance time for later logging + self._performance_times.create_filtered_dataframe = timer.lap_ms() + + async def _initialize_all_vectors_list_async(self) -> None: + timer = PerfTimer() + + vector_info_arr = await self._summary_access.get_available_vectors_async() + self._all_vectors = {vec.name for vec in vector_info_arr} + + # Store performance time for later logging + self._performance_times.init_summary_vector_list = timer.elapsed_ms() + + def _validate_assembler_config(self) -> None: + if self._network_mode != NetworkModeOptions.SINGLE_REAL: + raise ValueError("Network mode must be SINGLE_REAL to initialize single realization data") + if self._realization is None: + raise ValueError("FlowNetworkAssembler missing realization") + + @property + def _group_tree_df_model_safe(self) -> GroupTreeDataframeModel: + if self._group_tree_df_model is None: + raise ValueError("Grouptree dataframe model has not been initialized") + return self._group_tree_df_model + + @property + def _all_vectors_safe(self) -> set[str]: + if self._all_vectors is None: + raise ValueError("List of summary vectors has not been initialized") + return self._all_vectors + + @property + def _filtered_group_tree_df_safe(self) -> pd.DataFrame: + if self._filtered_group_tree_df is None: + raise ValueError("Filtered group-tree dataframe has not been initialized") + return self._filtered_group_tree_df + + async def fetch_and_initialize_async(self) -> None: + """ + Fetches the group tree and summary data from Sumo, and initializes the data structures needed to build a single realization + flow network. + """ + timer = PerfTimer() + self._performance_times = PerformanceTimes() + self._validate_assembler_config() + + # Run data fetch + init concurrently + await asyncio.gather(self._initialize_all_vectors_list_async(), self._initialize_group_tree_df_async()) + self._performance_times.init_sumo_data = timer.lap_ms() + + available_vectors = self._all_vectors_safe + group_tree_df_model = self._group_tree_df_model_safe + + # Compute the well status vectors ("WSTAT") that we expect to be available + tree_wstat_vectors = _utils.compute_tree_well_vectors( + group_tree_df_model.group_tree_wells, DataType.WELL_STATUS + ) + self._verify_that_sumvecs_exists(tree_wstat_vectors) + + vectors_of_interest = _utils.get_all_vectors_of_interest_for_tree( + group_tree_df_model.group_tree_wells, group_tree_df_model.group_tree_groups + ) + vectors_of_interest = vectors_of_interest & available_vectors + + self._verify_neccessary_injection_vectors(vectors_of_interest) + self._performance_times.build_and_verify_vectors_of_interest = timer.lap_ms() + + # Get summary vectors for all data simultaneously to obtain one request from Sumo + # Many summary vectors might not be needed, but will be filtered out later on. This is the most efficient way to get the data + # NOTE: "WSTAT" vectors are enumerated well state indicator, thus interpolated values might create issues (should be resolved by resampling-code) + single_realization_vectors_table, _ = await self._summary_access.get_single_real_vectors_table_async( + vector_names=list(vectors_of_interest), + resampling_frequency=self._summary_resampling_frequency, + realization=self._realization, + ) + self._performance_times.init_summary_vector_data_table = timer.lap_ms() + + # Create list of column names in the table once (for performance) + vectors_table_column_names = single_realization_vectors_table.column_names + node_classifications = self._create_node_classifications(tree_wstat_vectors, single_realization_vectors_table) + + # Initialize injection states based on group tree data + self._init_injection_states(node_classifications, single_realization_vectors_table, vectors_table_column_names) + + # Get nodes with summary vectors and their metadata, and all summary vectors, and edge summary vectors + network_summary_vectors_info = self._create_and_verify_network_summary_info( + node_classifications, vectors_table_column_names + ) + + self._init_node_static_working_data( + node_classifications, network_summary_vectors_info.node_summary_vectors_info_dict + ) + self._init_sorted_summary_table( + single_realization_vectors_table, + vectors_table_column_names, + network_summary_vectors_info.all_summary_vectors, + ) + + self._performance_times.log_sumo_download_times() + self._performance_times.log_structure_init_times() + + async def create_dated_networks_and_metadata_lists( + self, + ) -> Tuple[list[DatedFlowNetwork], list[FlowNetworkMetadata], list[FlowNetworkMetadata]]: + """ + This method creates date flow networks and metadata lists for a single realization dataset. + + It does not create new data structures, but access the already fetched and initialized data for the realization. + Data structures are chosen and tested for optimized access and performance. + """ + if self._network_mode != NetworkModeOptions.SINGLE_REAL: + raise ValueError("Network mode must be SINGLE_REAL to create a single realization dataset") + + if self._smry_table_sorted_by_date is None: + raise ValueError("Summary dataframe sorted by date has not been initialized") + + if self._node_static_working_data is None: + raise ValueError("Static working data for nodes has not been initialized") + + dated_network_list = _create_dated_networks( + self._smry_table_sorted_by_date, + self._filtered_group_tree_df_safe, + self._node_static_working_data, + self._node_types, + self._network_classification.TERMINAL_NODE, + ) + + return ( + dated_network_list, + self._get_edge_options(self._node_types), + [ + FlowNetworkMetadata(key=datatype.value, label=_utils.get_label_for_datatype(datatype)) + for datatype in [DataType.PRESSURE, DataType.BHP, DataType.WMCTL] + ], + ) + + def _get_edge_options(self, node_types: set[NodeType]) -> list[FlowNetworkMetadata]: + """Returns a list with edge node options for the dropdown + menu in the Flow Network module. + + The output list has the format: + [ + {"name": DataType.OILRATE.value, "label": "Oil Rate"}, + {"name": DataType.GASRATE.value, "label": "Gas Rate"}, + ] + """ + options: list[FlowNetworkMetadata] = [] + if NodeType.PROD in node_types: + for rate in [DataType.OILRATE, DataType.GASRATE, DataType.WATERRATE]: + options.append(FlowNetworkMetadata(key=rate.value, label=_utils.get_label_for_datatype(rate))) + if NodeType.INJ in node_types and self._network_classification.HAS_WATER_INJ: + options.append( + FlowNetworkMetadata( + key=DataType.WATERINJRATE.value, label=_utils.get_label_for_datatype(DataType.WATERINJRATE) + ) + ) + if NodeType.INJ in node_types and self._network_classification.HAS_GAS_INJ: + options.append( + FlowNetworkMetadata( + key=DataType.GASINJRATE.value, label=_utils.get_label_for_datatype(DataType.GASINJRATE) + ) + ) + if options: + return options + return [FlowNetworkMetadata(key=DataType.OILRATE.value, label=_utils.get_label_for_datatype(DataType.OILRATE))] + + def _verify_neccessary_injection_vectors(self, vectors_of_interest: set[str]) -> None: + # Has any water injection or gas injection vectors among vectors of interest + has_wi_vectors = False + has_gi_vectors = False + for vec in vectors_of_interest: + if has_wi_vectors and has_gi_vectors: + break + if vec.startswith("WWIR") or vec.startswith("GWIR"): + has_wi_vectors = True + if vec.startswith("WGIR") or vec.startswith("GGIR"): + has_gi_vectors = True + + # If any water or gas injection vectors exist, require field injection vectors exist + if has_wi_vectors and "FWIR" not in vectors_of_interest: + raise ValueError("Water injection vectors (WWIR/GWIR) found, but missing expected: FWIR") + if has_gi_vectors and "FGIR" not in vectors_of_interest: + raise ValueError("Gas injection vectors (WGIR/GGIR) found, but missing expected: FGIR") + + def _create_node_classifications( + self, wstat_vectors: set[str], vector_data_table: pa.Table + ) -> dict[str, NodeClassification]: + timer = PerfTimer() + + # Create well node classifications based on "WSTAT" vectors + well_node_classifications: dict[str, NodeClassification] = {} + for wstat_vector in wstat_vectors: + well = wstat_vector.split(":")[1] + well_states = set(vector_data_table[wstat_vector].to_pylist()) + well_node_classifications[well] = NodeClassification( + IS_PROD=1.0 in well_states, + IS_INJ=2.0 in well_states, + IS_OTHER=(1.0 not in well_states) and (2.0 not in well_states), + ) + + self._performance_times.create_well_node_classifications = timer.elapsed_ms() + + # Create node classifications based on leaf node classifications + node_classifications = _create_node_classification_dict( + self._filtered_group_tree_df_safe, well_node_classifications, vector_data_table + ) + self._performance_times.create_node_classifications = timer.lap_ms() + + return node_classifications + + def _init_injection_states( + self, + node_classifications: dict[str, NodeClassification], + vector_table: pa.Table, + vector_column_names: list[str], + ) -> None: + terminal_node_name = self._network_classification.TERMINAL_NODE + + if terminal_node_name in node_classifications: + is_inj_in_grouptree = node_classifications[terminal_node_name].IS_INJ + if is_inj_in_grouptree and "FWIR" in vector_column_names: + self._network_classification.HAS_WATER_INJ = pc.sum(vector_table["FWIR"]).as_py() > 0 + + if is_inj_in_grouptree and "FGIR" in vector_column_names: + self._network_classification.HAS_GAS_INJ = pc.sum(vector_table["FGIR"]).as_py() > 0 + + def _create_and_verify_network_summary_info( + self, + node_classifications: dict[str, NodeClassification], + vector_column_names: list[str], + ) -> FlowNetworkSummaryVectorsInfo: + timer = PerfTimer() + # Get nodes with summary vectors and their metadata, and all summary vectors, and edge summary vectors + # _node_sumvec_info_dict, all_sumvecs, edge_sumvecs = + network_summary_vectors_info = self._create_flow_network_summary_vectors_info(node_classifications) + + # Check if all edges is subset of initialized single realization vectors column names + if not network_summary_vectors_info.edge_summary_vectors.issubset(vector_column_names): + missing_sumvecs = network_summary_vectors_info.edge_summary_vectors - set(vector_column_names) + raise ValueError(f"Missing summary vectors for edges in the flow network: {', '.join(missing_sumvecs)}.") + + # Expect all dictionaries to have the same keys + if set(network_summary_vectors_info.node_summary_vectors_info_dict.keys()) != set(node_classifications.keys()): + raise ValueError("Node classifications and summary vector info must have the same keys.") + + self._performance_times.create_network_summary_vectors_info = timer.elapsed_ms() + + return network_summary_vectors_info + + def _init_node_static_working_data( + self, + node_classifications: dict[str, NodeClassification], + node_summary_vectors_info_dict: dict[str, NodeSummaryVectorsInfo], + ) -> None: + # Create static working data for each node + filtered_group_tree_df = self._filtered_group_tree_df_safe + node_static_working_data: dict[str, StaticNodeWorkingData] = {} + + for node_name, node_classification in node_classifications.items(): + node_summary_vectors_info = node_summary_vectors_info_dict[node_name].SMRY_INFO + node_static_working_data[node_name] = StaticNodeWorkingData( + node_name=node_name, + node_classification=node_classification, + node_summary_vectors_info=node_summary_vectors_info, + ) + + # Expect each node to have working data + node_names_set = set(filtered_group_tree_df["CHILD"].unique().tolist()) + if set(node_static_working_data.keys()) != node_names_set: + missing_node_working_data = node_names_set - set(node_static_working_data.keys()) + raise ValueError(f"Missing static working data for nodes: {missing_node_working_data}") + + self._node_static_working_data = node_static_working_data + + def _init_sorted_summary_table( + self, + vector_data_table: pa.Table, + vector_column_names: list[str], + all_summary_vectors: set[str], + ) -> None: + # Find group tree vectors existing in summary data + valid_summary_vectors = [vec for vec in all_summary_vectors if vec in vector_column_names] + + columns_of_interest = list(valid_summary_vectors) + ["DATE"] + self._smry_table_sorted_by_date = vector_data_table.select(columns_of_interest).sort_by("DATE") + + def _create_flow_network_summary_vectors_info( + self, node_classification_dict: dict[str, NodeClassification] + ) -> FlowNetworkSummaryVectorsInfo: + """ + Extract summary vector info from the provided group tree dataframe and node classifications. + + The group tree dataframe must have columns ["CHILD", "KEYWORD"] + + Returns a dataclass which holds summary vectors info for the flow network. A dictionary with node name as key, + and all its summary vectors info as value. Also returns a set with all summary vectors present in the network, + and a set with summary vectors used for edges in the network. + + Rates are not required for the terminal node since they will not be used. + + `Arguments`: + group_tree_df: pd.DataFrame - Group tree dataframe. Expected columns are: ["CHILD", "KEYWORD"] + node_classification_dict: dict[str, NodeClassification] - Dictionary with node name as key, and classification as value + terminal_node: str - Name of the terminal node in the group tree + has_waterinj: bool - True if water injection is present in the group tree + has_gasinj: bool - True if gas injection is present in the group tree + + `Returns`: + FlowNetworkSummaryVectorsInfo + """ + node_sumvecs_info_dict: dict[str, NodeSummaryVectorsInfo] = {} + all_sumvecs: set[str] = set() + edge_sumvecs: set[str] = set() + + group_tree_df_unique = self._filtered_group_tree_df_safe.drop_duplicates(subset=["CHILD", "KEYWORD"]) + + node_names = group_tree_df_unique["CHILD"].to_numpy() + node_keywords = group_tree_df_unique["KEYWORD"].to_numpy() + + for name, keyword in zip(node_names, node_keywords): + ( + node_vectors_info, + categorized_node_summary_vectors, + ) = _utils.get_node_vectors_info_and_categorized_node_summary_vectors_from_name_and_keyword( + name, keyword, node_classification_dict, self._network_classification + ) + + node_sumvecs_info_dict[name] = node_vectors_info + all_sumvecs |= categorized_node_summary_vectors.all_summary_vectors + edge_sumvecs |= categorized_node_summary_vectors.edge_summary_vectors + + return FlowNetworkSummaryVectorsInfo( + node_summary_vectors_info_dict=node_sumvecs_info_dict, + all_summary_vectors=all_sumvecs, + edge_summary_vectors=edge_sumvecs, + ) + + +def _create_node_classification_dict( + group_tree_df: pd.DataFrame, + well_node_classifications: dict[str, NodeClassification], + summary_vectors_table: pa.Table, +) -> dict[str, NodeClassification]: + """ + Create dictionary with node name as key, and corresponding classification as value. + + The nodes are classified without considering the dates of the flow networks. Thereby the classification + is given across all dates. + + The states are found for the leaf nodes, and then the parent nodes are classified based on the leaf nodes. "Bottom-up" approach. + + Well leaf nodes are classified from the well_node_classifications dictionary. A group leaf node is defined by summary vectors + for the node. + + `Arguments`: + `group_tree_df: pd.DataFrame - Group tree df to modify. Expected columns: ["PARENT", "CHILD", "KEYWORD", "DATE"] + `well_node_classifications: dict[str, NodeClassification] - Dictionary with well node as key, and classification as value + `summary_vectors_table: pa.Table - Summary table with all summary vectors. Needed to retrieve the classification for leaf nodes of type "GRUPTREE" or "BRANPROP" + """ + + # Get unique nodes, neglect dates + nodes_df = group_tree_df.drop_duplicates(subset=["CHILD"], keep="first").copy() + + timer = PerfTimer() + + # Prepare arrays for node names, parent nodes and keywords + node_parent_ndarray = nodes_df["PARENT"].to_numpy() + node_name_ndarray = nodes_df["CHILD"].to_numpy() + node_keyword_ndarray = nodes_df["KEYWORD"].to_numpy() + + # ? This check is uneccessary, no? + if len(node_parent_ndarray) != len(node_name_ndarray) or len(node_name_ndarray) != len(node_keyword_ndarray): + raise ValueError("Length of node names, parent names and keywords must be equal.") + + # Build lists of leaf node, their keyword and parent node. + leaf_node_list: list[str] = [] + leaf_node_keyword_list: list[str] = [] + leaf_node_parent_list: list[str] = [] + + for parent, node_name, keyword in zip(node_parent_ndarray, node_name_ndarray, node_keyword_ndarray): + if not np.any(node_parent_ndarray == node_name): + leaf_node_list.append(node_name) + leaf_node_keyword_list.append(keyword) + leaf_node_parent_list.append(parent) + + is_leafnode_time_ms = timer.lap_ms() + + # Classify leaf nodes as producer, injector or other + leaf_node_classification_map = _create_leaf_node_classification_map( + leaf_node_list, leaf_node_keyword_list, well_node_classifications, summary_vectors_table + ) + + classifying_leafnodes_time_ms = timer.lap_ms() + + node_classifications = _build_node_classifications_upwards( + leaf_node_classification_map, leaf_node_parent_list, node_parent_ndarray, node_name_ndarray + ) + + classify_remaining_nodes_time_ms = timer.lap_ms() + + LOGGER.info( + f"Leaf node classification took: {is_leafnode_time_ms}ms, " + f"Classifying leaf nodes took: {classifying_leafnodes_time_ms}ms, " + f"Classify remaining nodes took: {classify_remaining_nodes_time_ms}ms " + f"Total time add node type columns: {timer.elapsed_ms()}ms" + ) + + return node_classifications + + +def _build_node_classifications_upwards( + leaf_node_classification_map: dict[str, NodeClassification], + leaf_node_parent_list: list[str], + node_parent_ndarray: np.ndarray, + node_name_ndarray: np.ndarray, +) -> dict[str, NodeClassification]: + # Initial node classifications are leaf nodes + node_classifications: dict[str, NodeClassification] = leaf_node_classification_map + + # Build network node classifications bottom up + current_parent_nodes = set(leaf_node_parent_list) + node_name_list: list[str] = node_name_ndarray.tolist() + while len(current_parent_nodes) > 0: + grandparent_nodes = set() + + # For each parent node, handle its children + for parent_node in current_parent_nodes: + if parent_node is None: + continue + + children_indices = [index for index, value in enumerate(node_parent_ndarray) if value == parent_node] + children_node_names = node_name_ndarray[children_indices] + + parent_node_classification = NodeClassification(IS_PROD=False, IS_INJ=False, IS_OTHER=False) + children_classifications = [ + node_classifications[child] for child in children_node_names if child in node_classifications + ] + for child_classification in children_classifications: + # Update parent node classification (or-logic) + if child_classification.IS_PROD: + parent_node_classification.IS_PROD = True + if child_classification.IS_INJ: + parent_node_classification.IS_INJ = True + if child_classification.IS_OTHER: + parent_node_classification.IS_OTHER = True + + if ( + parent_node_classification.IS_PROD + and parent_node_classification.IS_INJ + and parent_node_classification.IS_OTHER + ): + break + + # Add parent node classification to the dict + node_classifications[parent_node] = parent_node_classification + + # Add grandparent node to the set + grandparent_node_index = node_name_list.index(parent_node) + grandparent_node = node_parent_ndarray[grandparent_node_index] + grandparent_nodes.add(grandparent_node) + + current_parent_nodes = grandparent_nodes + + # Expect the length of node classifications to be the same as the number of nodes + if set(node_classifications.keys()) != set(node_name_list): + missing_node_classifications = set(node_name_list) - set(node_classifications.keys()) + raise ValueError(f"Node classifications missing for nodes: {missing_node_classifications}") + + return node_classifications + + +def _create_leaf_node_classification_map( + leaf_nodes: list[str], + leaf_node_keywords: list[str], + well_node_classifications: dict[str, NodeClassification], + summary_vectors_table: pa.Table, +) -> dict[str, NodeClassification]: + """Creates a dictionary with node names as keys and NodeClassification as values. + + The leaf nodes and keywords must be sorted and have the same length. I.e. pairwise by index. + + Well leaf nodes are classified from the well_node_classifications dictionary. A group leaf node is defined by summary vectors + for the node. + + `Arguments`: + - `leaf_nodes`: list[str] - List of leaf node names + - `leaf_node_keywords`: list[str] - List of keywords for the leaf nodes + - `well_node_classifications`: dict[str, NodeClassification] - Dictionary with well node as key, and classification as value + - `summary_vectors_table`: pa.Table - Summary table with all summary vectors. Needed to retrieve the classification for leaf nodes of type "GRUPTREE" or "BRANPROP" + + `Return`: + dict of leaf node name as key, and NodeClassification as value + """ + if len(leaf_nodes) != len(leaf_node_keywords): + raise ValueError("Length of node names and keywords must be equal.") + + summary_columns = summary_vectors_table.column_names + + leaf_node_classifications: dict[str, NodeClassification] = {} + for i, node in enumerate(leaf_nodes): + well_node_classification = well_node_classifications.get(node) + if leaf_node_keywords[i] == "WELSPECS" and well_node_classification is not None: + leaf_node_classifications[node] = well_node_classification + else: + # For groups, classify based on summary vectors + prod_sumvecs = [ + _utils.create_sumvec_from_datatype_node_name_and_keyword(datatype, node, leaf_node_keywords[i]) + for datatype in [DataType.OILRATE, DataType.GASRATE, DataType.WATERRATE] + ] + inj_sumvecs = ( + [ + _utils.create_sumvec_from_datatype_node_name_and_keyword(datatype, node, leaf_node_keywords[i]) + for datatype in [DataType.WATERINJRATE, DataType.GASINJRATE] + ] + if leaf_node_keywords[i] != "BRANPROP" + else [] + ) + + prod_sum = sum( + pc.sum(summary_vectors_table[sumvec]).as_py() for sumvec in prod_sumvecs if sumvec in summary_columns + ) + inj_sums = sum( + pc.sum(summary_vectors_table[sumvec]).as_py() for sumvec in inj_sumvecs if sumvec in summary_columns + ) + is_prod = prod_sum > 0 + is_inj = inj_sums > 0 + + leaf_node_classifications[node] = NodeClassification( + IS_PROD=is_prod, IS_INJ=is_inj, IS_OTHER=not is_prod and not is_inj + ) + + return leaf_node_classifications + + +# Many of the variables are just taken by performance timer laps as we compute, so the count is hard to reduce +# pylint: disable-next=too-many-locals +def _create_dated_networks( + smry_sorted_by_date: pa.Table, + group_tree_df: pd.DataFrame, + node_static_working_data_dict: dict[str, StaticNodeWorkingData], + valid_node_types: set[NodeType], + terminal_node: str, +) -> list[DatedFlowNetwork]: + """ + Create a list of static flow networks with summary data, based on the group trees and resampled summary data. + + The summary data should be valid for the time span of the network's summary data. + + The node structure for a dated network in the list is static. The summary data for each node in the dated network is given by + the time span where the associated network is valid (from date of the network to the next network). + + `Arguments`: + - `smry_sorted_by_date`. pa.Table - Summary data table sorted by date. Expected columns: [DATE, summary_vector_1, ... , summary_vector_n] + - `group_tree_df`: Dataframe with group tree for dates - expected columns: [KEYWORD, CHILD, PARENT], optional column: [VFP_TABLE] + - `node_static_working_data_dict`: Dictionary with node name as key and its static work data for building flow networks + - `valid_node_types`: Set of node types to include from the group tree + - `terminal_node`: Name of the terminal node in the group tree + + `Returns`: + A list of dated networks with recursive node structure and summary data for each node in the tree. + """ + dated_networks: list[DatedFlowNetwork] = [] + + timer = PerfTimer() + + # Group the group tree data by date + grouptree_per_date = group_tree_df.groupby("DATE") + grouptree_dates = group_tree_df["DATE"].unique() + + timer.lap_ms() # initial_grouping_and_dates_extract_time_ms + + # NOTE: What if resampling freq of gruptree data is higher than summary data? + # A lot of "No summary data found for gruptree between {date} and {next_date}" is printed + # Pick the latest group tree state or? Can a node change states prod/inj in between and details are + total_create_dated_networks_time_ms = 0 + total_smry_table_filtering_ms = 0 + total_find_next_date_time_ms = 0 + + total_loop_time_ms_start = timer.elapsed_ms() + + for date, grouptree_at_date in grouptree_per_date: + timer.lap_ms() + next_date = grouptree_dates[grouptree_dates > date].min() + if pd.isna(next_date): + # Pick last smry date from sorted date column + next_date = smry_sorted_by_date["DATE"][-1] + total_find_next_date_time_ms += timer.lap_ms() + + timer.lap_ms() + # Filter summary data for the time span defined by the group tree date and the next group tree date + greater_equal_expr = pc.greater_equal(pc.field("DATE"), date) + less_expr = pc.less(pc.field("DATE"), next_date) + datespan_mask_expr = pc.and_kleene(greater_equal_expr, less_expr) + + smry_in_datespan_sorted_by_date: pa.Table = smry_sorted_by_date.filter(datespan_mask_expr) + total_smry_table_filtering_ms += timer.lap_ms() + + if smry_in_datespan_sorted_by_date.num_rows > 0: + dates = smry_in_datespan_sorted_by_date["DATE"] + + timer.lap_ms() + + formatted_dates = [date.as_py().strftime("%Y-%m-%d") for date in dates] + network = _create_dated_network( + grouptree_at_date, + date, + smry_in_datespan_sorted_by_date, + len(dates), + node_static_working_data_dict, + valid_node_types, + terminal_node, + ) + + dated_networks.append(DatedFlowNetwork(dates=formatted_dates, network=network)) + total_create_dated_networks_time_ms += timer.lap_ms() + else: + LOGGER.info(f"""No summary data found for gruptree between {date} and {next_date}""") + + total_loop_time_ms = timer.elapsed_ms() - total_loop_time_ms_start + + LOGGER.info( + f"Total time create_dated_networks func: {timer.elapsed_ms()}ms, " + f"Total loop time for grouptree_per_date: {total_loop_time_ms}ms, " + f"Total filter smry table: {total_smry_table_filtering_ms}ms " + f"Total create dated network: {total_create_dated_networks_time_ms}ms " + ) + + return dated_networks + + +def _create_dated_network( + grouptree_at_date: pd.DataFrame, + grouptree_date: pd.Timestamp, + smry_for_grouptree_sorted_by_date: pa.Table, + number_of_dates_in_smry: int, + node_static_working_data_dict: dict[str, StaticNodeWorkingData], + valid_node_types: set[NodeType], + terminal_node: str, +) -> NetworkNode: + """ + Create a static flowm network with summary data for a set of dates. + + The node structure is static, but the summary data for each node is given for a set of dates. + + `Arguments`: + - `grouptree_at_date`: Dataframe with group tree for one date - expected columns: [KEYWORD, CHILD, PARENT, EDGE_LABEL] + - `grouptree_date`: Timestamp - Date of the group tree + - smry_for_grouptree_sorted_by_date: Summary data for time span defined from the group tree at date to the next group tree date. The summary data is + sorted by date, which implies unique dates, ordered by date. Thereby each node or edge is a column in the summary dataframe. + - number_of_dates_in_smry: Number of unique dates in the summary data df. To be used for filling missing data - i.e. num rows of smry_sorted_by_date + - node_static_working_data_dict: Dictionary with node name as key and its static work data for building networks + - valid_node_types: Set of valid node types for the group tree + - terminal_node: Name of the terminal node in the group tree + + `Returns`: + A dated flow network with a recursive node structure, with summary data for the each date added to each node. + """ + # Dictionary of node name, with info about parent nodename RecursiveTreeNode with empty child array + # I.e. iterate over rows in df (better than recursive search) + nodes_dict = _create_flat_network_nodes_map( + grouptree_at_date, + node_static_working_data_dict, + valid_node_types, + smry_for_grouptree_sorted_by_date, + number_of_dates_in_smry, + ) + + terminal_node_elm = nodes_dict.get(terminal_node) + + if terminal_node_elm is None: + date_str = grouptree_date.strftime("%Y-%m-%d") + raise ValueError(f"No terminal node {terminal_node} found in group tree at date {date_str}") + + # Iterate over the nodes dict and add children to the nodes by looking at the parent name + # Operates by reference, so each node is updated in the dict + for _, flat_node_data in nodes_dict.items(): + parent_name = flat_node_data.parent_name + if parent_name in nodes_dict: + nodes_dict[parent_name].node_without_children.children.append(flat_node_data.node_without_children) + + # The terminal node is the final network + result = nodes_dict[terminal_node].node_without_children + + return result + + +def _create_flat_network_nodes_map( + grouptree_at_date: pd.DataFrame, + node_static_working_data_dict: dict[str, StaticNodeWorkingData], + valid_node_types: set[NodeType], + smry_for_grouptree_sorted_by_date: pa.Table, + number_of_dates_in_smry: int, +) -> dict[str, FlatNetworkNodeData]: + """ + Creates a map with node names and their respective flat network node data. + + The network nodes are created flat, i.e. non-recursively. This implies nodes without children. + Thereby the flat network node data contains info of parent node to assemble recursive structure + after data fetching + """ + nodes_dict: dict[str, FlatNetworkNodeData] = {} + + # Extract columns as numpy arrays for index access resulting in faster processing + # NOTE: Expect all columns to be 1D arrays and present in the dataframe + node_names = grouptree_at_date["CHILD"].to_numpy() + parent_names = grouptree_at_date["PARENT"].to_numpy() + keywords = grouptree_at_date["KEYWORD"].to_numpy() + + # Extract the names of all summary columns once + smry_columns_set = set(smry_for_grouptree_sorted_by_date.column_names) + + # Create edge label for nodes + edge_labels = [""] * len(node_names) + if "VFP_TABLE" in grouptree_at_date.columns: + edge_labels = _create_edge_label_list_from_vfp_table_column(grouptree_at_date["VFP_TABLE"]) + + # Iterate over every row in the grouptree dataframe to create the network nodes + for node_name, parent_name, node_keyword, edge_label in zip(node_names, parent_names, keywords, edge_labels): + if node_name in nodes_dict: + continue + + node_static_working_data = node_static_working_data_dict.get(node_name) + if node_static_working_data is None: + raise ValueError(f"No summary vector info found for node {node_name}") + + if not _is_valid_node_type(node_static_working_data.node_classification, valid_node_types): + continue + + network_node = _create_network_node( + node_name, + node_keyword, + edge_label, + node_static_working_data, + smry_columns_set, + smry_for_grouptree_sorted_by_date, + number_of_dates_in_smry, + ) + + nodes_dict[node_name] = FlatNetworkNodeData(parent_name=parent_name, node_without_children=network_node) + + return nodes_dict + + +def _is_valid_node_type(node_classification: NodeClassification, valid_node_types: set[NodeType]) -> bool: + """Returns True if the node classification is a valid node type""" + if node_classification.IS_PROD and NodeType.PROD in valid_node_types: + return True + if node_classification.IS_INJ and NodeType.INJ in valid_node_types: + return True + if node_classification.IS_OTHER and NodeType.OTHER in valid_node_types: + return True + return False + + +def _create_edge_label_list_from_vfp_table_column(vfp_table_column: pd.Series) -> list[str]: + """ + Creates an edge label list based on the column named "VFP_TABLE". + + If the VFP_TABLE column is not present, the function will raise a ValueError. + """ + if vfp_table_column.empty: + raise ValueError("VFP_TABLE column is empty.") + + edge_labels: list[str] = [] + for vfp_nb in vfp_table_column: + if vfp_nb in [None, 9999] or np.isnan(vfp_nb): + edge_labels.append("") + else: + edge_labels.append(f"VFP {int(vfp_nb)}") + + return edge_labels + + +def _create_network_node( + node_name: str, + keyword: str, + edge_label: str, + working_data: StaticNodeWorkingData, + smry_columns_set: set, + smry_for_grouptree_sorted_by_date: pa.Table, + number_of_dates_in_smry: int, +) -> NetworkNode: + # Find working data for the node + + node_type: Literal["Well", "Group"] = "Well" if keyword == "WELSPECS" else "Group" + edge_data: dict[str, list[float]] = {} + node_data: dict[str, list[float]] = {} + + # Array for vectors not existing in summary table + nan_array = np.array([np.nan] * number_of_dates_in_smry) + + # Each row in summary data is a unique date + summary_vector_info = working_data.node_summary_vectors_info + for sumvec, info in summary_vector_info.items(): + datatype = info.DATATYPE + + if sumvec in smry_columns_set: + data = smry_for_grouptree_sorted_by_date[sumvec].to_numpy().round(2) + else: + data = nan_array + + if info.EDGE_NODE == EdgeOrNode.EDGE: + edge_data[datatype] = data + else: + node_data[datatype] = data + + # children = [], and are added below after each node is created, to prevent recursive search + return NetworkNode( + node_label=node_name, + node_type=node_type, + edge_label=edge_label, + edge_data=edge_data, + node_data=node_data, + children=[], + ) diff --git a/backend_py/primary/primary/services/flow_network_assembler/flow_network_types.py b/backend_py/primary/primary/services/flow_network_assembler/flow_network_types.py new file mode 100644 index 000000000..a4001a558 --- /dev/null +++ b/backend_py/primary/primary/services/flow_network_assembler/flow_network_types.py @@ -0,0 +1,142 @@ +from dataclasses import dataclass +from enum import StrEnum +from typing import Literal +from pydantic import BaseModel + + +class NodeType(StrEnum): + PROD = "prod" + INJ = "inj" + OTHER = "other" + + +class EdgeOrNode(StrEnum): + EDGE = "edge" + NODE = "node" + + +class NetworkModeOptions(StrEnum): + STATISTICS = "statistics" + SINGLE_REAL = "single_real" + + +@dataclass +class NodeClassification: + """ + Classification of a node in the flow network. + Can be producer, injector and other over the time period of network. + """ + + # pylint: disable=invalid-name + IS_PROD: bool + IS_INJ: bool + IS_OTHER: bool + + +@dataclass +class NetworkClassification: + """ + Classification of a flow network. + Contains the name of the terminal/top node, and whether what type of injectors are present + """ + + # pylint: disable=invalid-name + TERMINAL_NODE: str + HAS_GAS_INJ: bool + HAS_WATER_INJ: bool + + +class DataType(StrEnum): + WELL_STATUS = "well_status" + OILRATE = "oilrate" + GASRATE = "gasrate" + WATERRATE = "waterrate" + WATERINJRATE = "waterinjrate" + GASINJRATE = "gasinjrate" + PRESSURE = "pressure" + BHP = "bhp" + WMCTL = "wmctl" + + +@dataclass +class SummaryVectorInfo: + """ + Info/metadata for a summary vector for a node in the network + """ + + # pylint: disable=invalid-name + DATATYPE: DataType + EDGE_NODE: EdgeOrNode + + +# For each node, the summary vectors needed to create the flow network dataset +@dataclass +class NodeSummaryVectorsInfo: + # Dict with summary vector name as key, and its metadata as values + # E.g.: {sumvec_1: SummaryVectorInfo, sumvec_2: SummaryVectorInfo, ...} + # pylint: disable=invalid-name + SMRY_INFO: dict[str, SummaryVectorInfo] + + +@dataclass +class CategorizedNodeSummaryVectors: + """ + Categorized summary vectors for a node in the network. + + - all_summary_vectors - All summary vectors present in the node + - edge_summary_vectors - Summary vectors used for edges in the node + """ + + all_summary_vectors: set[str] # All summary vectors for node + edge_summary_vectors: set[str] # All summary vectors for node used for edges + + +@dataclass +class FlowNetworkSummaryVectorsInfo: + """ + Dataclass to hold summary vectors info for the flow network. + + - node_summary_vectors_info_dict - Dict with node name and all its summary vectors info as value + - all_summary_vectors - List of all summary vectors present in the flow network + - edge_summary_vectors - List of summary vectors used for edges in the flow network + """ + + # Dict with node as key, and all the summary vectors w/ metadata for the node as value + node_summary_vectors_info_dict: dict[str, NodeSummaryVectorsInfo] + all_summary_vectors: set[str] # All summary vectors present in the group tree + edge_summary_vectors: set[str] # All summary vectors used for edges in the group tree + + +@dataclass +class StaticNodeWorkingData: + """ + Static working data for a node in the network. + + Data independent of dates, used for building the flow network. + """ + + node_name: str # Redundant, but kept for debugging purposes + node_classification: NodeClassification + node_summary_vectors_info: dict[str, SummaryVectorInfo] + + +# ! Explicitly using a pydantict model to avoid unnecessary re-computations when converting from service results to API schema-payload +class NetworkNode(BaseModel): + node_type: Literal["Group", "Well"] + node_label: str + edge_label: str + node_data: dict[str, list[float]] + edge_data: dict[str, list[float]] + children: list["NetworkNode"] + + +@dataclass +class DatedFlowNetwork: + dates: list[str] + network: NetworkNode + + +@dataclass +class FlowNetworkMetadata: + key: str + label: str diff --git a/backend_py/primary/primary/services/group_tree_assembler/group_tree_assembler.py b/backend_py/primary/primary/services/group_tree_assembler/group_tree_assembler.py deleted file mode 100644 index 7d6c0082a..000000000 --- a/backend_py/primary/primary/services/group_tree_assembler/group_tree_assembler.py +++ /dev/null @@ -1,951 +0,0 @@ -import logging -from typing import Dict, List, Literal, Optional, Sequence, Tuple -from dataclasses import dataclass - -import numpy as np -import pandas as pd -import pyarrow as pa -import pyarrow.compute as pc - -from fastapi import HTTPException -from primary.services.sumo_access.group_tree_access import GroupTreeAccess -from primary.services.sumo_access.group_tree_types import ( - DataType, - DataTypeToStringLabelMap, - DatedTree, - EdgeOrNode, - GroupTreeMetadata, - NodeType, - TreeNode, - TreeModeOptions, - TreeType, -) -from primary.services.sumo_access.summary_access import Frequency, SummaryAccess - -from ._group_tree_dataframe_model import ( - GroupTreeDataframeModel, - GROUP_TREE_FIELD_DATATYPE_TO_VECTOR_MAP, - TREE_TYPE_DATATYPE_TO_GROUP_VECTOR_DATATYPE_MAP, - GROUPTREE_DATATYPE_TO_WELL_VECTOR_DATATYPE_MAP, -) - -from webviz_pkg.core_utils.perf_timer import PerfTimer - -LOGGER = logging.getLogger(__name__) - - -@dataclass -class NodeClassification: - """ - Classification of a node in the group tree. - Can be producer, injector and other over the time period of the group tree. - """ - - IS_PROD: bool - IS_INJ: bool - IS_OTHER: bool - - -@dataclass -class SummaryVectorInfo: - """ - Info/metadata for a summary vector for a node in the group tree - """ - - DATATYPE: DataType - EDGE_NODE: EdgeOrNode - - -# For each node, the summary vectors needed to create the group tree dataset -@dataclass -class NodeSummaryVectorsInfo: - # Dict with summary vector name as key, and its metadata as values - # E.g.: {sumvec_1: SummaryVectorInfo, sumvec_2: SummaryVectorInfo, ...} - SMRY_INFO: Dict[str, SummaryVectorInfo] - - -@dataclass -class GroupTreeSummaryVectorsInfo: - """ - Dataclass to hold summary vectors info for the group tree. - - - node_summary_vectors_info_dict - Dict with node name and all its summary vectors info as value - - all_summary_vectors - List of all summary vectors present in the group tree - - edge_summary_vectors - List of summary vectors used for edges in the group tree - """ - - # Dict with node as key, and all the summary vectors w/ metadata for the node as value - node_summary_vectors_info_dict: Dict[str, NodeSummaryVectorsInfo] - all_summary_vectors: set[str] # All summary vectors present in the group tree - edge_summary_vectors: set[str] # All summary vectors used for edges in the group tree - - -@dataclass -class StaticNodeWorkingData: - """ - Static working data for a node in the group tree. - - Data independent of dates, used for building the group tree. - """ - - node_name: str # Redundant, but kept for debugging purposes - node_classification: NodeClassification - node_summary_vectors_info: Dict[str, SummaryVectorInfo] - - -class GroupTreeAssembler: - """ - Class to fetch group tree table data and summary data from access layers, and assemble - the data into a format for the router layer. - - """ - - def __init__( - self, - group_tree_access: GroupTreeAccess, - summary_access: SummaryAccess, - realization: int, - summary_frequency: Frequency, - node_types: set[NodeType], - group_tree_mode: TreeModeOptions, - terminal_node: str = "FIELD", - tree_type: TreeType = TreeType.GRUPTREE, - excl_well_startswith: Optional[List[str]] = None, - excl_well_endswith: Optional[List[str]] = None, - ): - self._tree_mode = group_tree_mode - - # NOTE: Temporary only supporting single real - if self._tree_mode != TreeModeOptions.SINGLE_REAL: - raise ValueError("Only SINGLE_REAL mode is supported at the moment.") - - self._realization = realization - self._group_tree_access = group_tree_access - self._summary_access = summary_access - self._terminal_node = terminal_node - self._tree_type = tree_type - self._excl_well_startswith = excl_well_startswith - self._excl_well_endswith = excl_well_endswith - self._summary_resampling_frequency = summary_frequency - self._node_types = node_types - - self._has_waterinj = False - self._has_gasinj = False - - self._group_tree_df: pd.DataFrame | None = None - self._all_vectors: List[str] | None = None - self._smry_table_sorted_by_date: pa.Table | None = None - - self._node_static_working_data_dict: Dict[str, StaticNodeWorkingData] | None = None - - async def _initialize_all_vectors_list_async(self) -> None: - vector_info_arr = await self._summary_access.get_available_vectors_async() - self._all_vectors = [vec.name for vec in vector_info_arr] - - async def fetch_and_initialize_async(self) -> None: - """ - Fetch group tree and summary data from Sumo, and initialize the data structures needed to build the single realization - group tree. - - This method initialize and create data structures for optimized access and performance for the single realization group tree - with summary data. - """ - if self._tree_mode != TreeModeOptions.SINGLE_REAL: - raise ValueError("Tree mode must be SINGLE_REAL to initialize single realization data") - if self._realization is None: - raise ValueError("GroupTreeAssembler missing realization") - - timer = PerfTimer() - - await self._initialize_all_vectors_list_async() - get_summary_vector_list_time_ms = timer.lap_ms() - if self._all_vectors is None: - raise ValueError("List of summary vectors has not been initialized") - - # Get group tree data from Sumo - group_tree_table_df = await self._group_tree_access.get_group_tree_table(realization=self._realization) - get_group_tree_table_time_ms = timer.lap_ms() - if group_tree_table_df is None: - raise HTTPException(status_code=404, detail="Group tree data not found") - - timer.lap_ms() - - # Initialize dataframe model - group_tree_df_model = GroupTreeDataframeModel(group_tree_table_df, self._tree_type) - initialize_grouptree_model_time_ms = timer.lap_ms() - - # Ensure "WSTAT" vectors expected for group tree exist among summary vectors - _verify_that_sumvecs_exists(group_tree_df_model.wstat_vectors, self._all_vectors) - - # Get all vectors of interest existing in the summary data - vectors_of_interest = group_tree_df_model.create_vector_of_interest_list() - vectors_of_interest = [vec for vec in vectors_of_interest if vec in self._all_vectors] - - # Has any water injection or gas injection vectors among vectors of interest - has_wi_vectors = False - has_gi_vectors = False - for vec in vectors_of_interest: - if has_wi_vectors and has_gi_vectors: - break - if vec.startswith("WWIR") or vec.startswith("GWIR"): - has_wi_vectors = True - if vec.startswith("WGIR") or vec.startswith("GGIR"): - has_gi_vectors = True - - # If any water or gas injection vectors exist, require field injection vectors exist - if has_wi_vectors and "FWIR" not in vectors_of_interest: - raise ValueError("Water injection vectors (WWIR/GWIR) found, but missing expected: FWIR") - if has_gi_vectors and "FGIR" not in vectors_of_interest: - raise ValueError("Gas injection vectors (WGIR/GGIR) found, but missing expected: FGIR") - - # Get summary vectors for all data simultaneously to obtain one request from Sumo - # Many summary vectors might not be needed, but will be filtered out later on. This is the most efficient way to get the data - # NOTE: "WSTAT" vectors are enumerated well state indicator, thus interpolated values might create issues (should be resolved by resampling-code) - timer.lap_ms() - single_realization_vectors_table, _ = await self._summary_access.get_single_real_vectors_table_async( - vector_names=vectors_of_interest, - resampling_frequency=self._summary_resampling_frequency, - realization=self._realization, - ) - get_summary_vectors_time_ms = timer.lap_ms() - - # Create list of column names in the table once (for performance) - vectors_table_column_names = single_realization_vectors_table.column_names - - # Create well node classifications based on "WSTAT" vectors - well_node_classifications: Dict[str, NodeClassification] = {} - for wstat_vector in group_tree_df_model.wstat_vectors: - well = wstat_vector.split(":")[1] - well_states = set(single_realization_vectors_table[wstat_vector].to_pylist()) - well_node_classifications[well] = NodeClassification( - IS_PROD=1.0 in well_states, - IS_INJ=2.0 in well_states, - IS_OTHER=(1.0 not in well_states) and (2.0 not in well_states), - ) - - # Create filtered group tree df from model - timer.lap_ms() - group_tree_df = group_tree_df_model.create_filtered_dataframe( - terminal_node=self._terminal_node, - excl_well_startswith=self._excl_well_startswith, - excl_well_endswith=self._excl_well_endswith, - ) - create_filtered_dataframe_time_ms = timer.lap_ms() - - # Create node classifications based on leaf node classifications - node_classification_dict = _create_node_classification_dict( - group_tree_df, well_node_classifications, single_realization_vectors_table - ) - create_node_classifications_time_ms = timer.lap_ms() - - # Initialize injection states based on group tree data - if self._terminal_node in node_classification_dict: - is_inj_in_grouptree = node_classification_dict[self._terminal_node].IS_INJ - if is_inj_in_grouptree and "FWIR" in vectors_table_column_names: - self._has_waterinj = pc.sum(single_realization_vectors_table["FWIR"]).as_py() > 0 - if is_inj_in_grouptree and "FGIR" in vectors_table_column_names: - self._has_gasinj = pc.sum(single_realization_vectors_table["FGIR"]).as_py() > 0 - - # Get nodes with summary vectors and their metadata, and all summary vectors, and edge summary vectors - # _node_sumvec_info_dict, all_sumvecs, edge_sumvecs = - _group_tree_summary_vectors_info = _create_group_tree_summary_vectors_info( - group_tree_df, node_classification_dict, self._terminal_node, self._has_waterinj, self._has_gasinj - ) - create_group_tree_summary_vectors_info_tims_ms = timer.lap_ms() - - # Check if all edges is subset of initialized single realization vectors column names - if not _group_tree_summary_vectors_info.edge_summary_vectors.issubset(vectors_table_column_names): - missing_sumvecs = _group_tree_summary_vectors_info.edge_summary_vectors - set(vectors_table_column_names) - raise ValueError(f"Missing summary vectors for edges in the GroupTree: {', '.join(missing_sumvecs)}.") - - # Expect all dictionaries to have the same keys - if set(_group_tree_summary_vectors_info.node_summary_vectors_info_dict.keys()) != set( - node_classification_dict.keys() - ): - raise ValueError("Node classifications and summary vector info must have the same keys.") - - # Create static working data for each node - node_static_working_data_dict: Dict[str, StaticNodeWorkingData] = {} - for node_name, node_classification in node_classification_dict.items(): - node_summary_vectors_info = _group_tree_summary_vectors_info.node_summary_vectors_info_dict[ - node_name - ].SMRY_INFO - node_static_working_data_dict[node_name] = StaticNodeWorkingData( - node_name=node_name, - node_classification=node_classification, - node_summary_vectors_info=node_summary_vectors_info, - ) - self._node_static_working_data_dict = node_static_working_data_dict - - # Expect each node to have working data - node_names_set = set(group_tree_df["CHILD"].unique().tolist()) - if set(self._node_static_working_data_dict.keys()) != node_names_set: - missing_node_working_data = node_names_set - set(self._node_static_working_data_dict.keys()) - raise ValueError(f"Missing static working data for nodes: {missing_node_working_data}") - - # Find group tree vectors existing in summary data - valid_summary_vectors = [ - vec for vec in _group_tree_summary_vectors_info.all_summary_vectors if vec in vectors_table_column_names - ] - columns_of_interest = list(valid_summary_vectors) + ["DATE"] - self._smry_table_sorted_by_date = single_realization_vectors_table.select(columns_of_interest).sort_by("DATE") - - # Assign group tree dataframe - self._group_tree_df = group_tree_df - - # Log download from Sumo times - LOGGER.info( - f"Total time to fetch data from Sumo: {get_summary_vector_list_time_ms+get_summary_vectors_time_ms+get_group_tree_table_time_ms}ms, " - f"Get summary vector list in: {get_summary_vector_list_time_ms}ms, " - f"Get group tree table in: {get_group_tree_table_time_ms}ms, " - f"Get summary vectors in: {get_summary_vectors_time_ms}ms" - ) - - # Log initialization of data structures times - LOGGER.info( - f"Initialize GroupTreeModel in: {initialize_grouptree_model_time_ms}ms, " - f"Create filtered dataframe in: {create_filtered_dataframe_time_ms}ms, " - f"Create node classifications in: {create_node_classifications_time_ms}ms, " - f"Create group tree summary vectors info in: {create_group_tree_summary_vectors_info_tims_ms}ms" - ) - - async def create_dated_trees_and_metadata_lists( - self, - ) -> Tuple[List[DatedTree], List[GroupTreeMetadata], List[GroupTreeMetadata]]: - """ - This method creates the dated trees and metadata lists for the single realization dataset. - - It does not create new data structures, but access the already fetched and initialized data for the single realization. - Data structures are chosen and tested for optimized access and performance. - """ - if self._tree_mode != TreeModeOptions.SINGLE_REAL: - raise ValueError("Tree mode must be SINGLE_REAL to create a single realization dataset") - - if self._smry_table_sorted_by_date is None: - raise ValueError("Summary dataframe sorted by date has not been initialized") - - if self._node_static_working_data_dict is None: - raise ValueError("Static working data for nodes has not been initialized") - - dated_tree_list = _create_dated_trees( - self._smry_table_sorted_by_date, - self._group_tree_df, - self._node_static_working_data_dict, - self._node_types, - self._terminal_node, - ) - - return ( - dated_tree_list, - self._get_edge_options(self._node_types), - [ - GroupTreeMetadata(key=datatype.value, label=_get_label(datatype)) - for datatype in [DataType.PRESSURE, DataType.BHP, DataType.WMCTL] - ], - ) - - def _get_edge_options(self, node_types: set[NodeType]) -> List[GroupTreeMetadata]: - """Returns a list with edge node options for the dropdown - menu in the GroupTree component. - - The output list has the format: - [ - {"name": DataType.OILRATE.value, "label": "Oil Rate"}, - {"name": DataType.GASRATE.value, "label": "Gas Rate"}, - ] - """ - options: List[GroupTreeMetadata] = [] - if NodeType.PROD in node_types: - for rate in [DataType.OILRATE, DataType.GASRATE, DataType.WATERRATE]: - options.append(GroupTreeMetadata(key=rate.value, label=_get_label(rate))) - if NodeType.INJ in node_types and self._has_waterinj: - options.append(GroupTreeMetadata(key=DataType.WATERINJRATE.value, label=_get_label(DataType.WATERINJRATE))) - if NodeType.INJ in node_types and self._has_gasinj: - options.append(GroupTreeMetadata(key=DataType.GASINJRATE.value, label=_get_label(DataType.GASINJRATE))) - if options: - return options - return [GroupTreeMetadata(key=DataType.OILRATE.value, label=_get_label(DataType.OILRATE))] - - -def _create_group_tree_summary_vectors_info( - group_tree_df: pd.DataFrame, - node_classification_dict: Dict[str, NodeClassification], - terminal_node: str, - has_waterinj: bool, - has_gasinj: bool, -) -> GroupTreeSummaryVectorsInfo: - """ - Extract summary vector info from the provided group tree dataframe and node classifications. - - The group tree dataframe must have columns ["CHILD", "KEYWORD"] - - Returns a dataclass which holds summary vectors info for the group tree. A dictionary with node name as key, - and all its summary vectors info as value. Also returns a set with all summary vectors present in the group tree, - and a set with summary vectors used for edges in the group tree. - - Rates are not required for the terminal node since they will not be used. - - `Arguments`: - group_tree_df: pd.DataFrame - Group tree dataframe. Expected columns are: ["CHILD", "KEYWORD"] - node_classification_dict: Dict[str, NodeClassification] - Dictionary with node name as key, and classification as value - terminal_node: str - Name of the terminal node in the group tree - has_waterinj: bool - True if water injection is present in the group tree - has_gasinj: bool - True if gas injection is present in the group tree - - `Returns`: - GroupTreeSummaryVectorsInfo - """ - node_sumvecs_info_dict: Dict[str, NodeSummaryVectorsInfo] = {} - all_sumvecs: set[str] = set() - edge_sumvecs: set[str] = set() - - unique_nodes = group_tree_df.drop_duplicates(subset=["CHILD", "KEYWORD"]) - - node_names = unique_nodes["CHILD"].to_numpy() - node_keyword = unique_nodes["KEYWORD"].to_numpy() - - if len(node_names) != len(node_keyword): - raise ValueError("Length of node names and keywords must be equal.") - - if set(node_names) != set(node_classification_dict.keys()): - missing_node_names = set(node_names) - set(node_classification_dict.keys()) - raise ValueError(f"Node names missing in node classification dict: {missing_node_names}") - - num_nodes = len(node_names) - for i in range(num_nodes): - nodename = node_names[i] - keyword = node_keyword[i] - node_classification = node_classification_dict[nodename] - is_prod = node_classification.IS_PROD - is_inj = node_classification.IS_INJ - - if not isinstance(nodename, str) or not isinstance(keyword, str): - raise ValueError("Nodename and keyword must be strings") - - datatypes = [DataType.PRESSURE] - if is_prod and nodename != terminal_node: - datatypes += [DataType.OILRATE, DataType.GASRATE, DataType.WATERRATE] - if is_inj and has_waterinj and nodename != terminal_node: - datatypes.append(DataType.WATERINJRATE) - if is_inj and has_gasinj and nodename != terminal_node: - datatypes.append(DataType.GASINJRATE) - if keyword == "WELSPECS": - datatypes += [DataType.BHP, DataType.WMCTL] - - if len(datatypes) > 0: - node_sumvecs_info_dict[nodename] = NodeSummaryVectorsInfo(SMRY_INFO={}) - - for datatype in datatypes: - sumvec_name = _create_sumvec_from_datatype_nodename_and_keyword(datatype, nodename, keyword) - edge_or_node = _get_edge_node(datatype) - all_sumvecs.add(sumvec_name) - if edge_or_node == EdgeOrNode.EDGE: - edge_sumvecs.add(sumvec_name) - node_sumvecs_info_dict[nodename].SMRY_INFO[sumvec_name] = SummaryVectorInfo( - DATATYPE=datatype, EDGE_NODE=edge_or_node - ) - - return GroupTreeSummaryVectorsInfo( - node_summary_vectors_info_dict=node_sumvecs_info_dict, - all_summary_vectors=all_sumvecs, - edge_summary_vectors=edge_sumvecs, - ) - - -def _verify_that_sumvecs_exists(check_sumvecs: Sequence[str], valid_sumvecs: Sequence[str]) -> None: - """ - Takes in a list of summary vectors and checks if they are present among the valid summary vectors. - If any are missing, a ValueError is raised with the list of all missing summary vectors. - """ - - # Find vectors that are missing in the valid sumvecs - missing_sumvecs = set(check_sumvecs) - set(valid_sumvecs) - if len(missing_sumvecs) > 0: - str_missing_sumvecs = ", ".join(missing_sumvecs) - raise ValueError("Missing summary vectors for the GroupTree plugin: " f"{str_missing_sumvecs}.") - - -def _create_node_classification_dict( - group_tree_df: pd.DataFrame, - well_node_classifications: Dict[str, NodeClassification], - summary_vectors_table: pa.Table, -) -> Dict[str, NodeClassification]: - """ - Create dictionary with node name as key, and corresponding classification as value. - - The nodes are classified without considering the dates of the group trees. Thereby the classification - is given across all dates. - - The states are found for the leaf nodes, and then the parent nodes are classified based on the leaf nodes. "Bottom-up" approach. - - Well leaf nodes are classified from the well_node_classifications dictionary. A group leaf node is defined by summary vectors - for the node. - - `Arguments`: - `group_tree_df: pd.DataFrame - Group tree df to modify. Expected columns: ["PARENT", "CHILD", "KEYWORD", "DATE"] - `well_node_classifications: Dict[str, NodeClassification] - Dictionary with well node as key, and classification as value - `summary_vectors_table: pa.Table - Summary table with all summary vectors. Needed to retrieve the classification for leaf nodes of type "GRUPTREE" or "BRANPROP" - """ - - # Get unique nodes, neglect dates - nodes_df = group_tree_df.drop_duplicates(subset=["CHILD"], keep="first").copy() - - timer = PerfTimer() - - # Prepare arrays for node names, parent nodes and keywords - node_parent_ndarray = nodes_df["PARENT"].to_numpy() - node_name_ndarray = nodes_df["CHILD"].to_numpy() - node_keyword_ndarray = nodes_df["KEYWORD"].to_numpy() - - if len(node_parent_ndarray) != len(node_name_ndarray) or len(node_name_ndarray) != len(node_keyword_ndarray): - raise ValueError("Length of node names, parent names and keywords must be equal.") - - num_nodes = len(node_name_ndarray) - - # Build lists of leaf node, their keyword and parent node. - leaf_node_list: List[str] = [] - leaf_node_keyword_list: List[str] = [] - leaf_node_parent_list: List[str] = [] - for i in range(num_nodes): - node_name = node_name_ndarray[i] - is_leaf_node = np.count_nonzero(node_parent_ndarray == node_name) == 0 - if is_leaf_node: - leaf_node_list.append(node_name) - leaf_node_keyword_list.append(node_keyword_ndarray[i]) - leaf_node_parent_list.append(node_parent_ndarray[i]) - - if len(leaf_node_list) != len(leaf_node_keyword_list) or len(leaf_node_list) != len(leaf_node_parent_list): - raise ValueError("Length of leaf node names, keyword and parent names must be equal.") - - is_leafnode_time_ms = timer.lap_ms() - - # Classify leaf nodes as producer, injector or other - leaf_node_classification_map = _create_leaf_node_classification_map( - leaf_node_list, leaf_node_keyword_list, well_node_classifications, summary_vectors_table - ) - - classifying_leafnodes_time_ms = timer.lap_ms() - - # Initial node classifications are leaf nodes - node_classifications: Dict[str, NodeClassification] = leaf_node_classification_map - - # Build tree node classifications bottom up - current_parent_nodes = set(leaf_node_parent_list) - node_name_list: List[str] = node_name_ndarray.tolist() - while len(current_parent_nodes) > 0: - grandparent_nodes = set() - - # For each parent node, handle its children - for parent_node in current_parent_nodes: - if parent_node is None: - continue - - children_indices = [index for index, value in enumerate(node_parent_ndarray) if value == parent_node] - children_node_names = node_name_ndarray[children_indices] - - parent_node_classification = NodeClassification(IS_PROD=False, IS_INJ=False, IS_OTHER=False) - children_classifications = [ - node_classifications[child] for child in children_node_names if child in node_classifications - ] - for child_classification in children_classifications: - # Update parent node classification (or-logic) - if child_classification.IS_PROD: - parent_node_classification.IS_PROD = True - if child_classification.IS_INJ: - parent_node_classification.IS_INJ = True - if child_classification.IS_OTHER: - parent_node_classification.IS_OTHER = True - - if ( - parent_node_classification.IS_PROD - and parent_node_classification.IS_INJ - and parent_node_classification.IS_OTHER - ): - break - - # Add parent node classification to the dict - node_classifications[parent_node] = parent_node_classification - - # Add grandparent node to the set - grandparent_node_index = node_name_list.index(parent_node) - grandparent_node = node_parent_ndarray[grandparent_node_index] - grandparent_nodes.add(grandparent_node) - - current_parent_nodes = grandparent_nodes - - # Expect the length of node classifications to be the same as the number of nodes - if set(node_classifications.keys()) != set(node_name_list): - missing_node_classifications = set(node_name_list) - set(node_classifications.keys()) - raise ValueError(f"Node classifications missing for nodes: {missing_node_classifications}") - - classify_remaining_nodes_time_ms = timer.lap_ms() - - LOGGER.info( - f"Leaf node classification took: {is_leafnode_time_ms}ms, " - f"Classifying leaf nodes took: {classifying_leafnodes_time_ms}ms, " - f"Classify remaining nodes took: {classify_remaining_nodes_time_ms}ms " - f"Total time add node type columns: {timer.elapsed_ms()}ms" - ) - - return node_classifications - - -def _create_leaf_node_classification_map( - leaf_nodes: List[str], - leaf_node_keywords: List[str], - well_node_classifications: Dict[str, NodeClassification], - summary_vectors_table: pa.Table, -) -> Dict[str, NodeClassification]: - """Creates a dictionary with node names as keys and NodeClassification as values. - - The leaf nodes and keywords must be sorted and have the same length. I.e. pairwise by index. - - Well leaf nodes are classified from the well_node_classifications dictionary. A group leaf node is defined by summary vectors - for the node. - - `Arguments`: - - `leaf_nodes`: List[str] - List of leaf node names - - `leaf_node_keywords`: List[str] - List of keywords for the leaf nodes - - `well_node_classifications`: Dict[str, NodeClassification] - Dictionary with well node as key, and classification as value - - `summary_vectors_table`: pa.Table - Summary table with all summary vectors. Needed to retrieve the classification for leaf nodes of type "GRUPTREE" or "BRANPROP" - - `Return`: - Dict of leaf node name as key, and NodeClassification as value - """ - if len(leaf_nodes) != len(leaf_node_keywords): - raise ValueError("Length of node names and keywords must be equal.") - - summary_columns = summary_vectors_table.column_names - - leaf_node_classifications: Dict[str, NodeClassification] = {} - for i, node in enumerate(leaf_nodes): - well_node_classification = well_node_classifications.get(node) - if leaf_node_keywords[i] == "WELSPECS" and well_node_classification is not None: - leaf_node_classifications[node] = well_node_classification - else: - # For groups, classify based on summary vectors - prod_sumvecs = [ - _create_sumvec_from_datatype_nodename_and_keyword(datatype, node, leaf_node_keywords[i]) - for datatype in [DataType.OILRATE, DataType.GASRATE, DataType.WATERRATE] - ] - inj_sumvecs = ( - [ - _create_sumvec_from_datatype_nodename_and_keyword(datatype, node, leaf_node_keywords[i]) - for datatype in [DataType.WATERINJRATE, DataType.GASINJRATE] - ] - if leaf_node_keywords[i] != "BRANPROP" - else [] - ) - - prod_sum = sum( - pc.sum(summary_vectors_table[sumvec]).as_py() for sumvec in prod_sumvecs if sumvec in summary_columns - ) - inj_sums = sum( - pc.sum(summary_vectors_table[sumvec]).as_py() for sumvec in inj_sumvecs if sumvec in summary_columns - ) - is_prod = prod_sum > 0 - is_inj = inj_sums > 0 - - leaf_node_classifications[node] = NodeClassification( - IS_PROD=is_prod, IS_INJ=is_inj, IS_OTHER=not is_prod and not is_inj - ) - - return leaf_node_classifications - - -def _get_label(datatype: DataType) -> str: - """Returns a more readable label for the summary datatypes""" - label = DataTypeToStringLabelMap.get(datatype) - if label is None: - raise ValueError(f"Label for datatype {datatype.value} not implemented.") - return label - - -def _get_edge_node(datatype: DataType) -> EdgeOrNode: - """Returns if a given datatype is edge (typically rates) or node (f.ex pressures)""" - if datatype in [ - DataType.OILRATE, - DataType.GASRATE, - DataType.WATERRATE, - DataType.WATERINJRATE, - DataType.GASINJRATE, - ]: - return EdgeOrNode.EDGE - if datatype in [DataType.PRESSURE, DataType.BHP, DataType.WMCTL]: - return EdgeOrNode.NODE - raise ValueError(f"Data type {datatype.value} not implemented.") - - -def _create_sumvec_from_datatype_nodename_and_keyword( - datatype: DataType, - nodename: str, - keyword: str, -) -> str: - """Returns the correct summary vector for a given - * datatype: oilrate, gasrate etc - * nodename: FIELD, well name or group name in Eclipse network - * keyword: GRUPTREE, BRANPROP or WELSPECS - """ - - if nodename == "FIELD": - datatype_ecl = GROUP_TREE_FIELD_DATATYPE_TO_VECTOR_MAP[datatype] - if datatype == "pressure": - return f"{datatype_ecl}:{nodename}" - return datatype_ecl - try: - if keyword == "WELSPECS": - datatype_ecl = GROUPTREE_DATATYPE_TO_WELL_VECTOR_DATATYPE_MAP[datatype] - else: - datatype_ecl = TREE_TYPE_DATATYPE_TO_GROUP_VECTOR_DATATYPE_MAP[keyword][datatype] - except KeyError as exc: - error = ( - f"Summary vector not found for eclipse keyword: {keyword}, " - f"data type: {datatype.value} and node name: {nodename}. " - ) - raise KeyError(error) from exc - return f"{datatype_ecl}:{nodename}" - - -def _create_dated_trees( - smry_sorted_by_date: pa.Table, - group_tree_df: pd.DataFrame, - node_static_working_data_dict: Dict[str, StaticNodeWorkingData], - valid_node_types: set[NodeType], - terminal_node: str, -) -> List[DatedTree]: - """ - Create a list of static group trees with summary data, based on the group trees and resampled summary data. - - The summary data should be valid for the time span of the group tree. - - The node structure for a dated tree in the list is static. The summary data for each node in the dated tree is given by - by the time span where the tree is valid (from date of the tree to the next tree). - - `Arguments`: - - `smry_sorted_by_date`. pa.Table - Summary data table sorted by date. Expected columns: [DATE, summary_vector_1, ... , summary_vector_n] - - `group_tree_df`: Dataframe with group tree for dates - expected columns: [KEYWORD, CHILD, PARENT], optional column: [VFP_TABLE] - - `node_static_working_data_dict`: Dictionary with node name as key and its static work data for building group trees - - `valid_node_types`: Set of valid node types for the group tree - - `terminal_node`: Name of the terminal node in the group tree - - `Returns`: - A list of dated trees with recursive node structure and summary data for each node in the tree. - """ - dated_trees: List[DatedTree] = [] - - # loop trees - timer = PerfTimer() - - # Group the group tree data by date - grouptree_per_date = group_tree_df.groupby("DATE") - grouptree_dates = group_tree_df["DATE"].unique() - - initial_grouping_and_dates_extract_time_ms = timer.lap_ms() - - # NOTE: What if resampling freq of gruptree data is higher than summary data? - # A lot of "No summary data found for gruptree between {date} and {next_date}" is printed - # Pick the latest group tree state or? Can a node change states prod/inj in between and details are - timer.lap_ms() - total_create_dated_trees_time_ms = 0 - total_smry_table_filtering_ms = 0 - total_find_next_date_time_ms = 0 - - total_loop_time_ms_start = timer.elapsed_ms() - for date, grouptree_at_date in grouptree_per_date: - timer.lap_ms() - next_date = grouptree_dates[grouptree_dates > date].min() - if pd.isna(next_date): - # Pick last smry date from sorted date column - next_date = smry_sorted_by_date["DATE"][-1] - total_find_next_date_time_ms += timer.lap_ms() - - timer.lap_ms() - # Filter summary data for the time span defined by the group tree date and the next group tree date - greater_equal_expr = pc.greater_equal(pc.field("DATE"), date) - less_expr = pc.less(pc.field("DATE"), next_date) - datespan_mask_expr = pc.and_kleene(greater_equal_expr, less_expr) - - smry_in_datespan_sorted_by_date: pa.Table = smry_sorted_by_date.filter(datespan_mask_expr) - total_smry_table_filtering_ms += timer.lap_ms() - - if smry_in_datespan_sorted_by_date.num_rows > 0: - dates = smry_in_datespan_sorted_by_date["DATE"] - - timer.lap_ms() - dated_trees.append( - DatedTree( - dates=[date.as_py().strftime("%Y-%m-%d") for date in dates], - tree=_create_dated_tree( - grouptree_at_date, - date, - smry_in_datespan_sorted_by_date, - len(dates), - node_static_working_data_dict, - valid_node_types, - terminal_node, - ), - ) - ) - total_create_dated_trees_time_ms += timer.lap_ms() - else: - LOGGER.info(f"""No summary data found for gruptree between {date} and {next_date}""") - - total_loop_time_ms = timer.elapsed_ms() - total_loop_time_ms_start - - LOGGER.info( - f"Total time create_dated_trees func: {timer.elapsed_ms()}ms, " - f"Total loop time for grouptree_per_date: {total_loop_time_ms}ms, " - f"Total filter smry table: {total_smry_table_filtering_ms}ms " - f"Total create dated tree: {total_create_dated_trees_time_ms}ms " - ) - - return dated_trees - - -def _create_dated_tree( - grouptree_at_date: pd.DataFrame, - grouptree_date: pd.Timestamp, - smry_for_grouptree_sorted_by_date: pa.Table, - number_of_dates_in_smry: int, - node_static_working_data_dict: Dict[str, StaticNodeWorkingData], - valid_node_types: set[NodeType], - terminal_node: str, -) -> TreeNode: - """ - Create a static group tree with summary data for a set of dates. - - The node structure is static, but the summary data for each node is given for a set of dates. - - `Arguments`: - - `grouptree_at_date`: Dataframe with group tree for one date - expected columns: [KEYWORD, CHILD, PARENT, EDGE_LABEL] - - `grouptree_date`: Timestamp - Date of the group tree - - smry_for_grouptree_sorted_by_date: Summary data for time span defined from the group tree at date to the next group tree date. The summary data is - sorted by date, which implies unique dates, ordered by date. Thereby each node or edge is a column in the summary dataframe. - - number_of_dates_in_smry: Number of unique dates in the summary data df. To be used for filling missing data - i.e. num rows of smry_sorted_by_date - - node_static_working_data_dict: Dictionary with node name as key and its static work data for building group tree - - valid_node_types: Set of valid node types for the group tree - - terminal_node: Name of the terminal node in the group tree - - `Returns`: - A dated tree with recursive node structure and summary data for each node for the set of dates. - """ - # Dictionary of node name, with info about parent nodename RecursiveTreeNode with empty child array - # I.e. iterate over rows in df (better than recursive search) - nodes_dict: Dict[str, Tuple[str, TreeNode]] = {} - - # Extract columns as numpy arrays for index access resulting in faster processing - # NOTE: Expect all columns to be 1D arrays and present in the dataframe - node_names = grouptree_at_date["CHILD"].to_numpy() - parent_names = grouptree_at_date["PARENT"].to_numpy() - keywords = grouptree_at_date["KEYWORD"].to_numpy() - - if len(node_names) != len(parent_names) or len(node_names) != len(keywords): - raise ValueError("Length of node_names, parent_names and keywords must be the same") - - # Create edge label for nodes - edge_labels = [""] * len(node_names) - if "VFP_TABLE" in grouptree_at_date.columns: - edge_labels = _create_edge_label_list_from_vfp_table_column(grouptree_at_date["VFP_TABLE"]) - - # Extract names once - smry_columns_set = set(smry_for_grouptree_sorted_by_date.column_names) - - num_rows = len(node_names) - # Iterate over every row in the grouptree dataframe to create the tree nodes - for i in range(num_rows): - node_name = node_names[i] - parent_name = parent_names[i] - if node_name not in nodes_dict: - # Find working data for the node - node_static_working_data = node_static_working_data_dict.get(node_name) - if node_static_working_data is None: - raise ValueError(f"No summary vector info found for node {node_name}") - - if not _is_valid_node_type(node_static_working_data.node_classification, valid_node_types): - continue - - edge_data: Dict[str, List[float]] = {} - node_data: Dict[str, List[float]] = {} - - # Each row in summary data is a unique date - summary_vector_info = node_static_working_data.node_summary_vectors_info - for sumvec, info in summary_vector_info.items(): - datatype = info.DATATYPE - sumvec_name = sumvec - if info.EDGE_NODE == EdgeOrNode.EDGE: - if sumvec_name in smry_columns_set: - edge_data[datatype] = smry_for_grouptree_sorted_by_date[sumvec_name].to_numpy().round(2) - continue - else: - edge_data[datatype] = list([np.nan] * number_of_dates_in_smry) - continue - else: - if sumvec_name in smry_columns_set: - node_data[datatype] = smry_for_grouptree_sorted_by_date[sumvec_name].to_numpy().round(2) - continue - else: - node_data[datatype] = list([np.nan] * number_of_dates_in_smry) - continue - - node_type: Literal["Well", "Group"] = "Well" if keywords[i] == "WELSPECS" else "Group" - edge_label = edge_labels[i] - - # children = [], and are added below after each node is created, to prevent recursive search - nodes_dict[node_name] = ( - parent_name, - TreeNode( - node_label=node_name, - node_type=node_type, - edge_label=edge_label, - edge_data=edge_data, - node_data=node_data, - children=[], - ), - ) - - # Add children to the nodes, start with terminal node - terminal_node_elm = nodes_dict.get(terminal_node) - if terminal_node_elm is None: - date_str = grouptree_date.strftime("%Y-%m-%d") - raise ValueError(f"No terminal node {terminal_node} found in group tree at date {date_str}") - - # Iterate over the nodes dict and add children to the nodes by looking at the parent name - # Operates by reference, so each node is updated in the dict - for node_name, (parent_name, node) in nodes_dict.items(): - if parent_name in nodes_dict: - nodes_dict[parent_name][1].children.append(node) - - # Terminal node is the dated tree - result = nodes_dict[terminal_node][1] - - return result - - -def _is_valid_node_type(node_classification: NodeClassification, valid_node_types: set[NodeType]) -> bool: - """Returns True if the node classification is a valid node type""" - if node_classification.IS_PROD and NodeType.PROD in valid_node_types: - return True - if node_classification.IS_INJ and NodeType.INJ in valid_node_types: - return True - if node_classification.IS_OTHER and NodeType.OTHER in valid_node_types: - return True - return False - - -def _create_edge_label_list_from_vfp_table_column(vfp_table_column: pd.Series) -> list[str]: - """ - Creates an edge label list based on the column named "VFP_TABLE". - - If the VFP_TABLE column is not present, the function will raise a ValueError. - """ - if vfp_table_column.empty: - raise ValueError("VFP_TABLE column is empty.") - - edge_labels: list[str] = [] - for vfp_nb in vfp_table_column: - if vfp_nb in [None, 9999] or np.isnan(vfp_nb): - edge_labels.append("") - else: - edge_labels.append(f"VFP {int(vfp_nb)}") - - return edge_labels diff --git a/backend_py/primary/primary/services/sumo_access/group_tree_types.py b/backend_py/primary/primary/services/sumo_access/group_tree_types.py index df8e396fd..fc9c0daf2 100644 --- a/backend_py/primary/primary/services/sumo_access/group_tree_types.py +++ b/backend_py/primary/primary/services/sumo_access/group_tree_types.py @@ -1,26 +1,4 @@ from enum import StrEnum -from typing import Dict, List, Literal - -from pydantic import BaseModel - - -class GroupTreeMetadata(BaseModel): - key: str - label: str - - -class TreeNode(BaseModel): - node_type: Literal["Group", "Well"] - node_label: str - edge_label: str - node_data: Dict[str, List[float]] - edge_data: Dict[str, List[float]] - children: List["TreeNode"] - - -class DatedTree(BaseModel): - dates: List[str] - tree: TreeNode class TreeType(StrEnum): @@ -28,11 +6,6 @@ class TreeType(StrEnum): BRANPROP = "BRANPROP" -class TreeModeOptions(StrEnum): - STATISTICS = "statistics" - SINGLE_REAL = "single_real" - - class StatOptions(StrEnum): MEAN = "mean" P10 = "p10" @@ -40,37 +13,3 @@ class StatOptions(StrEnum): P90 = "p90" MAX = "max" MIN = "min" - - -class NodeType(StrEnum): - PROD = "prod" - INJ = "inj" - OTHER = "other" - - -class DataType(StrEnum): - OILRATE = "oilrate" - GASRATE = "gasrate" - WATERRATE = "waterrate" - WATERINJRATE = "waterinjrate" - GASINJRATE = "gasinjrate" - PRESSURE = "pressure" - BHP = "bhp" - WMCTL = "wmctl" - - -class EdgeOrNode(StrEnum): - EDGE = "edge" - NODE = "node" - - -DataTypeToStringLabelMap = { - DataType.OILRATE: "Oil Rate", - DataType.GASRATE: "Gas Rate", - DataType.WATERRATE: "Water Rate", - DataType.WATERINJRATE: "Water Inj Rate", - DataType.GASINJRATE: "Gas Inj Rate", - DataType.PRESSURE: "Pressure", - DataType.BHP: "BHP", - DataType.WMCTL: "WMCTL", -} diff --git a/frontend/src/api/ApiService.ts b/frontend/src/api/ApiService.ts index bfce0c929..7fcc209a2 100644 --- a/frontend/src/api/ApiService.ts +++ b/frontend/src/api/ApiService.ts @@ -7,9 +7,9 @@ import type { OpenAPIConfig } from './core/OpenAPI'; import { AxiosHttpRequest } from './core/AxiosHttpRequest'; import { DefaultService } from './services/DefaultService'; import { ExploreService } from './services/ExploreService'; +import { FlowNetworkService } from './services/FlowNetworkService'; import { GraphService } from './services/GraphService'; import { Grid3DService } from './services/Grid3DService'; -import { GroupTreeService } from './services/GroupTreeService'; import { InplaceVolumetricsService } from './services/InplaceVolumetricsService'; import { ObservationsService } from './services/ObservationsService'; import { ParametersService } from './services/ParametersService'; @@ -26,9 +26,9 @@ type HttpRequestConstructor = new (config: OpenAPIConfig) => BaseHttpRequest; export class ApiService { public readonly default: DefaultService; public readonly explore: ExploreService; + public readonly flowNetwork: FlowNetworkService; public readonly graph: GraphService; public readonly grid3D: Grid3DService; - public readonly groupTree: GroupTreeService; public readonly inplaceVolumetrics: InplaceVolumetricsService; public readonly observations: ObservationsService; public readonly parameters: ParametersService; @@ -56,9 +56,9 @@ export class ApiService { }); this.default = new DefaultService(this.request); this.explore = new ExploreService(this.request); + this.flowNetwork = new FlowNetworkService(this.request); this.graph = new GraphService(this.request); this.grid3D = new Grid3DService(this.request); - this.groupTree = new GroupTreeService(this.request); this.inplaceVolumetrics = new InplaceVolumetricsService(this.request); this.observations = new ObservationsService(this.request); this.parameters = new ParametersService(this.request); diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index fd7cf0531..cf27bc404 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -23,7 +23,7 @@ export type { BoundingBox2d as BoundingBox2d_api } from './models/BoundingBox2d' export type { BoundingBox3d as BoundingBox3d_api } from './models/BoundingBox3d'; export type { CaseInfo as CaseInfo_api } from './models/CaseInfo'; export type { Completions as Completions_api } from './models/Completions'; -export type { DatedTree as DatedTree_api } from './models/DatedTree'; +export type { DatedFlowNetwork as DatedFlowNetwork_api } from './models/DatedFlowNetwork'; export type { EnsembleDetails as EnsembleDetails_api } from './models/EnsembleDetails'; export type { EnsembleInfo as EnsembleInfo_api } from './models/EnsembleInfo'; export type { EnsembleParameter as EnsembleParameter_api } from './models/EnsembleParameter'; @@ -33,6 +33,8 @@ export type { EnsembleSensitivity as EnsembleSensitivity_api } from './models/En export type { EnsembleSensitivityCase as EnsembleSensitivityCase_api } from './models/EnsembleSensitivityCase'; export type { FenceMeshSection as FenceMeshSection_api } from './models/FenceMeshSection'; export type { FieldInfo as FieldInfo_api } from './models/FieldInfo'; +export type { FlowNetworkData as FlowNetworkData_api } from './models/FlowNetworkData'; +export type { FlowNetworkMetadata as FlowNetworkMetadata_api } from './models/FlowNetworkMetadata'; export { FlowRateType as FlowRateType_api } from './models/FlowRateType'; export { FluidZone as FluidZone_api } from './models/FluidZone'; export { Frequency as Frequency_api } from './models/Frequency'; @@ -45,8 +47,6 @@ export type { Grid3dMappedProperty as Grid3dMappedProperty_api } from './models/ export type { Grid3dPropertyInfo as Grid3dPropertyInfo_api } from './models/Grid3dPropertyInfo'; export type { Grid3dZone as Grid3dZone_api } from './models/Grid3dZone'; export type { GridDimensions as GridDimensions_api } from './models/GridDimensions'; -export type { GroupTreeData as GroupTreeData_api } from './models/GroupTreeData'; -export type { GroupTreeMetadata as GroupTreeMetadata_api } from './models/GroupTreeMetadata'; export type { HTTPValidationError as HTTPValidationError_api } from './models/HTTPValidationError'; export type { InplaceStatisticalVolumetricTableData as InplaceStatisticalVolumetricTableData_api } from './models/InplaceStatisticalVolumetricTableData'; export type { InplaceStatisticalVolumetricTableDataPerFluidSelection as InplaceStatisticalVolumetricTableDataPerFluidSelection_api } from './models/InplaceStatisticalVolumetricTableDataPerFluidSelection'; @@ -57,6 +57,7 @@ export type { InplaceVolumetricsTableDefinition as InplaceVolumetricsTableDefini export { InplaceVolumetricStatistic as InplaceVolumetricStatistic_api } from './models/InplaceVolumetricStatistic'; export type { InplaceVolumetricTableData as InplaceVolumetricTableData_api } from './models/InplaceVolumetricTableData'; export type { InplaceVolumetricTableDataPerFluidSelection as InplaceVolumetricTableDataPerFluidSelection_api } from './models/InplaceVolumetricTableDataPerFluidSelection'; +export { NetworkNode as NetworkNode_api } from './models/NetworkNode'; export { NodeType as NodeType_api } from './models/NodeType'; export type { Observations as Observations_api } from './models/Observations'; export type { PointSetXY as PointSetXY_api } from './models/PointSetXY'; @@ -95,7 +96,6 @@ export type { TableColumnData as TableColumnData_api } from './models/TableColum export type { TableColumnStatisticalData as TableColumnStatisticalData_api } from './models/TableColumnStatisticalData'; export { TabType as TabType_api } from './models/TabType'; export type { THP as THP_api } from './models/THP'; -export { TreeNode as TreeNode_api } from './models/TreeNode'; export { UnitType as UnitType_api } from './models/UnitType'; export type { UserInfo as UserInfo_api } from './models/UserInfo'; export type { ValidationError as ValidationError_api } from './models/ValidationError'; @@ -123,9 +123,9 @@ export { WFR as WFR_api } from './models/WFR'; export { DefaultService } from './services/DefaultService'; export { ExploreService } from './services/ExploreService'; +export { FlowNetworkService } from './services/FlowNetworkService'; export { GraphService } from './services/GraphService'; export { Grid3DService } from './services/Grid3DService'; -export { GroupTreeService } from './services/GroupTreeService'; export { InplaceVolumetricsService } from './services/InplaceVolumetricsService'; export { ObservationsService } from './services/ObservationsService'; export { ParametersService } from './services/ParametersService'; diff --git a/frontend/src/api/models/DatedTree.ts b/frontend/src/api/models/DatedFlowNetwork.ts similarity index 59% rename from frontend/src/api/models/DatedTree.ts rename to frontend/src/api/models/DatedFlowNetwork.ts index f7ca2b7d6..a47ce08d4 100644 --- a/frontend/src/api/models/DatedTree.ts +++ b/frontend/src/api/models/DatedFlowNetwork.ts @@ -2,9 +2,9 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -import type { TreeNode } from './TreeNode'; -export type DatedTree = { +import type { NetworkNode } from './NetworkNode'; +export type DatedFlowNetwork = { dates: Array; - tree: TreeNode; + network: NetworkNode; }; diff --git a/frontend/src/api/models/FlowNetworkData.ts b/frontend/src/api/models/FlowNetworkData.ts new file mode 100644 index 000000000..857935a92 --- /dev/null +++ b/frontend/src/api/models/FlowNetworkData.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { DatedFlowNetwork } from './DatedFlowNetwork'; +import type { FlowNetworkMetadata } from './FlowNetworkMetadata'; +export type FlowNetworkData = { + edgeMetadataList: Array; + nodeMetadataList: Array; + datedNetworks: Array; +}; + diff --git a/frontend/src/api/models/GroupTreeMetadata.ts b/frontend/src/api/models/FlowNetworkMetadata.ts similarity index 82% rename from frontend/src/api/models/GroupTreeMetadata.ts rename to frontend/src/api/models/FlowNetworkMetadata.ts index 449561747..b92c0effb 100644 --- a/frontend/src/api/models/GroupTreeMetadata.ts +++ b/frontend/src/api/models/FlowNetworkMetadata.ts @@ -2,7 +2,7 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -export type GroupTreeMetadata = { +export type FlowNetworkMetadata = { key: string; label: string; }; diff --git a/frontend/src/api/models/GroupTreeData.ts b/frontend/src/api/models/GroupTreeData.ts deleted file mode 100644 index ba8941905..000000000 --- a/frontend/src/api/models/GroupTreeData.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* generated using openapi-typescript-codegen -- do no edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { DatedTree } from './DatedTree'; -import type { GroupTreeMetadata } from './GroupTreeMetadata'; -export type GroupTreeData = { - edge_metadata_list: Array; - node_metadata_list: Array; - dated_trees: Array; -}; - diff --git a/frontend/src/api/models/TreeNode.ts b/frontend/src/api/models/NetworkNode.ts similarity index 73% rename from frontend/src/api/models/TreeNode.ts rename to frontend/src/api/models/NetworkNode.ts index c0b5bbb02..e745398d5 100644 --- a/frontend/src/api/models/TreeNode.ts +++ b/frontend/src/api/models/NetworkNode.ts @@ -2,15 +2,15 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -export type TreeNode = { - node_type: TreeNode.node_type; +export type NetworkNode = { + node_type: NetworkNode.node_type; node_label: string; edge_label: string; node_data: Record>; edge_data: Record>; - children: Array; + children: Array; }; -export namespace TreeNode { +export namespace NetworkNode { export enum node_type { GROUP = 'Group', WELL = 'Well', diff --git a/frontend/src/api/services/GroupTreeService.ts b/frontend/src/api/services/FlowNetworkService.ts similarity index 79% rename from frontend/src/api/services/GroupTreeService.ts rename to frontend/src/api/services/FlowNetworkService.ts index a5f08cc67..a508f715e 100644 --- a/frontend/src/api/services/GroupTreeService.ts +++ b/frontend/src/api/services/FlowNetworkService.ts @@ -2,33 +2,33 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { FlowNetworkData } from '../models/FlowNetworkData'; import type { Frequency } from '../models/Frequency'; -import type { GroupTreeData } from '../models/GroupTreeData'; import type { NodeType } from '../models/NodeType'; import type { CancelablePromise } from '../core/CancelablePromise'; import type { BaseHttpRequest } from '../core/BaseHttpRequest'; -export class GroupTreeService { +export class FlowNetworkService { constructor(public readonly httpRequest: BaseHttpRequest) {} /** - * Get Realization Group Tree Data + * Get Realization Flow Network * @param caseUuid Sumo case uuid * @param ensembleName Ensemble name * @param realization Realization * @param resamplingFrequency Resampling frequency * @param nodeTypeSet Node types - * @returns GroupTreeData Successful Response + * @returns FlowNetworkData Successful Response * @throws ApiError */ - public getRealizationGroupTreeData( + public getRealizationFlowNetwork( caseUuid: string, ensembleName: string, realization: number, resamplingFrequency: Frequency, nodeTypeSet: Array, - ): CancelablePromise { + ): CancelablePromise { return this.httpRequest.request({ method: 'GET', - url: '/group_tree/realization_group_tree_data/', + url: '/flow_network/realization_flow_network/', query: { 'case_uuid': caseUuid, 'ensemble_name': ensembleName, diff --git a/frontend/src/modules/FlowNetwork/interfaces.ts b/frontend/src/modules/FlowNetwork/interfaces.ts index cf0193239..685e93e53 100644 --- a/frontend/src/modules/FlowNetwork/interfaces.ts +++ b/frontend/src/modules/FlowNetwork/interfaces.ts @@ -1,8 +1,8 @@ +import { DatedFlowNetwork_api, FlowNetworkMetadata_api } from "@api"; import { InterfaceInitialization } from "@framework/UniDirectionalModuleComponentsInterface"; -import { DatedTree, EdgeMetadata, NodeMetadata } from "@webviz/group-tree-plot"; import { - datedTreesAtom, + datedNetworksAtom, edgeMetadataListAtom, nodeMetadataListAtom, queryStatusAtom, @@ -13,9 +13,9 @@ import { import { QueryStatus } from "./types"; type SettingsToViewInterface = { - edgeMetadataList: EdgeMetadata[]; - nodeMetadataList: NodeMetadata[]; - datedTrees: DatedTree[]; + edgeMetadataList: FlowNetworkMetadata_api[]; + nodeMetadataList: FlowNetworkMetadata_api[]; + datedNetworks: DatedFlowNetwork_api[]; selectedEdgeKey: string; // | null; selectedNodeKey: string; // | null; selectedDateTime: string; // | null; @@ -33,8 +33,8 @@ export const settingsToViewInterfaceInitialization: InterfaceInitialization { return get(nodeMetadataListAtom); }, - datedTrees: (get) => { - return get(datedTreesAtom); + datedNetworks: (get) => { + return get(datedNetworksAtom); }, selectedEdgeKey: (get) => { return get(selectedEdgeKeyAtom) ?? ""; diff --git a/frontend/src/modules/FlowNetwork/registerModule.ts b/frontend/src/modules/FlowNetwork/registerModule.ts index 05eabcffc..d0d5b6340 100644 --- a/frontend/src/modules/FlowNetwork/registerModule.ts +++ b/frontend/src/modules/FlowNetwork/registerModule.ts @@ -7,7 +7,7 @@ import { preview } from "./preview"; export const MODULE_NAME = "FlowNetwork"; -const description = "Visualizes dated group trees over time."; +const description = "Visualizes dated flow networks over time."; ModuleRegistry.registerModule({ moduleName: MODULE_NAME, diff --git a/frontend/src/modules/FlowNetwork/settings/atoms/derivedAtoms.ts b/frontend/src/modules/FlowNetwork/settings/atoms/derivedAtoms.ts index 166298583..3af7858bb 100644 --- a/frontend/src/modules/FlowNetwork/settings/atoms/derivedAtoms.ts +++ b/frontend/src/modules/FlowNetwork/settings/atoms/derivedAtoms.ts @@ -1,7 +1,7 @@ +import { DatedFlowNetwork_api, FlowNetworkMetadata_api } from "@api"; import { EnsembleIdent } from "@framework/EnsembleIdent"; import { EnsembleSetAtom } from "@framework/GlobalAtoms"; import { fixupEnsembleIdent } from "@framework/utils/ensembleUiHelpers"; -import { DatedTree, EdgeMetadata, NodeMetadata } from "@webviz/group-tree-plot"; import { atom } from "jotai"; @@ -13,12 +13,12 @@ import { userSelectedRealizationNumberAtom, validRealizationNumbersAtom, } from "./baseAtoms"; -import { realizationGroupTreeQueryAtom } from "./queryAtoms"; +import { realizationFlowNetworkQueryAtom } from "./queryAtoms"; import { QueryStatus } from "../../types"; -export const groupTreeQueryResultAtom = atom((get) => { - return get(realizationGroupTreeQueryAtom); +export const flowNetworkQueryResultAtom = atom((get) => { + return get(realizationFlowNetworkQueryAtom); }); export const selectedEnsembleIdentAtom = atom((get) => { @@ -49,25 +49,25 @@ export const selectedRealizationNumberAtom = atom((get) => { }); export const queryStatusAtom = atom((get) => { - const groupTreeQueryResult = get(groupTreeQueryResultAtom); + const flowNetworkQueryResult = get(flowNetworkQueryResultAtom); - if (groupTreeQueryResult.isFetching) { + if (flowNetworkQueryResult.isFetching) { return QueryStatus.Loading; } - if (groupTreeQueryResult.isError) { + if (flowNetworkQueryResult.isError) { return QueryStatus.Error; } return QueryStatus.Idle; }); export const availableDateTimesAtom = atom((get) => { - const groupTreeQueryResult = get(groupTreeQueryResultAtom); + const flowNetworkQueryResult = get(flowNetworkQueryResultAtom); - if (!groupTreeQueryResult.data) return []; + if (!flowNetworkQueryResult.data) return []; const dateTimes = new Set(); - groupTreeQueryResult.data.dated_trees.forEach((datedTree) => { - datedTree.dates.forEach((date) => { + flowNetworkQueryResult.data.datedNetworks.forEach((datedNetwork) => { + datedNetwork.dates.forEach((date) => { dateTimes.add(date); }); }); @@ -89,15 +89,15 @@ export const selectedDateTimeAtom = atom((get) => { return userSelectedDateTime; }); -export const edgeMetadataListAtom = atom((get) => { - const groupTreeQueryResult = get(groupTreeQueryResultAtom); +export const edgeMetadataListAtom = atom((get) => { + const flowNetworkQueryResult = get(flowNetworkQueryResultAtom); - const data = groupTreeQueryResult.data; + const data = flowNetworkQueryResult.data; if (!data) { return []; } - return data.edge_metadata_list.map((elm) => ({ key: elm.key, label: elm.label })); + return data.edgeMetadataList; }); export const selectedEdgeKeyAtom = atom((get) => { @@ -115,15 +115,15 @@ export const selectedEdgeKeyAtom = atom((get) => { return userSelectedEdgeKey; }); -export const nodeMetadataListAtom = atom((get) => { - const groupTreeQueryResult = get(groupTreeQueryResultAtom); +export const nodeMetadataListAtom = atom((get) => { + const flowNetworkQueryResult = get(flowNetworkQueryResultAtom); - const data = groupTreeQueryResult.data; + const data = flowNetworkQueryResult.data; if (!data) { return []; } - return data.node_metadata_list.map((elm) => ({ key: elm.key, label: elm.label })); + return data.nodeMetadataList; }); export const selectedNodeKeyAtom = atom((get) => { @@ -141,13 +141,13 @@ export const selectedNodeKeyAtom = atom((get) => { return userSelectedNodeKey; }); -export const datedTreesAtom = atom((get) => { - const groupTreeQueryResult = get(groupTreeQueryResultAtom); +export const datedNetworksAtom = atom((get) => { + const flowNetworkQueryResult = get(flowNetworkQueryResultAtom); - const data = groupTreeQueryResult.data; + const data = flowNetworkQueryResult.data; if (!data) { return []; } - return data.dated_trees; + return data.datedNetworks; }); diff --git a/frontend/src/modules/FlowNetwork/settings/atoms/queryAtoms.ts b/frontend/src/modules/FlowNetwork/settings/atoms/queryAtoms.ts index b23a22f06..6cee09b22 100644 --- a/frontend/src/modules/FlowNetwork/settings/atoms/queryAtoms.ts +++ b/frontend/src/modules/FlowNetwork/settings/atoms/queryAtoms.ts @@ -8,7 +8,7 @@ import { selectedEnsembleIdentAtom, selectedRealizationNumberAtom } from "./deri const STALE_TIME = 60 * 1000; const CACHE_TIME = 60 * 1000; -export const realizationGroupTreeQueryAtom = atomWithQuery((get) => { +export const realizationFlowNetworkQueryAtom = atomWithQuery((get) => { const selectedEnsembleIdent = get(selectedEnsembleIdentAtom); const selectedRealizationNumber = get(selectedRealizationNumberAtom); const selectedResamplingFrequency = get(selectedResamplingFrequencyAtom); @@ -16,7 +16,7 @@ export const realizationGroupTreeQueryAtom = atomWithQuery((get) => { const query = { queryKey: [ - "getGroupTreeData", + "getRealizationFlowNetwork", selectedEnsembleIdent?.getCaseUuid(), selectedEnsembleIdent?.getEnsembleName(), selectedRealizationNumber, @@ -24,7 +24,7 @@ export const realizationGroupTreeQueryAtom = atomWithQuery((get) => { Array.from(selectedNodeTypes), ], queryFn: () => - apiService.groupTree.getRealizationGroupTreeData( + apiService.flowNetwork.getRealizationFlowNetwork( selectedEnsembleIdent?.getCaseUuid() ?? "", selectedEnsembleIdent?.getEnsembleName() ?? "", selectedRealizationNumber ?? 0, diff --git a/frontend/src/modules/FlowNetwork/settings/settings.tsx b/frontend/src/modules/FlowNetwork/settings/settings.tsx index 056f2c959..f96e4acf9 100644 --- a/frontend/src/modules/FlowNetwork/settings/settings.tsx +++ b/frontend/src/modules/FlowNetwork/settings/settings.tsx @@ -30,7 +30,7 @@ import { import { availableDateTimesAtom, edgeMetadataListAtom, - groupTreeQueryResultAtom, + flowNetworkQueryResultAtom, nodeMetadataListAtom, selectedDateTimeAtom, selectedEdgeKeyAtom, @@ -68,9 +68,9 @@ export function Settings({ workbenchSession, settingsContext }: ModuleSettingsPr const selectedDateTime = useAtomValue(selectedDateTimeAtom); const setUserSelectedDateTime = useSetAtom(userSelectedDateTimeAtom); - const groupTreeQueryResult = useAtomValue(groupTreeQueryResultAtom); + const FlowNetworkQueryResult = useAtomValue(flowNetworkQueryResultAtom); - usePropagateApiErrorToStatusWriter(groupTreeQueryResult, statusWriter); + usePropagateApiErrorToStatusWriter(FlowNetworkQueryResult, statusWriter); const setValidRealizationNumbersAtom = useSetAtom(validRealizationNumbersAtom); const filterEnsembleRealizationsFunc = useEnsembleRealizationFilterFunc(workbenchSession); @@ -184,9 +184,9 @@ export function Settings({ workbenchSession, settingsContext }: ModuleSettingsPr } - errorComponent={"Could not load group tree data"} + errorComponent={"Could not load flow network data"} >