Skip to content

Commit

Permalink
Use colorpalette for stratigraphy colors in front-end for `WellComple…
Browse files Browse the repository at this point in the history
…tions`-module (#495)
  • Loading branch information
jorgenherje authored Dec 18, 2023
1 parent 38ae7e9 commit d9a23e1
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 112 deletions.
94 changes: 26 additions & 68 deletions backend/src/services/sumo_access/well_completions_access.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import itertools
from typing import Dict, Iterator, List, Optional, Set, Tuple
from typing import Dict, List, Optional, Set, Tuple

import pandas as pd

Expand Down Expand Up @@ -113,33 +112,19 @@ def __init__(self, well_completions_df: pd.DataFrame) -> None:
self._kh_unit = "mDm" # NOTE: How to find metadata?
self._kh_decimal_places = 2
self._datemap = {dte: i for i, dte in enumerate(sorted(self._well_completions_df["DATE"].unique()))}
self._zones = list(sorted(self._well_completions_df["ZONE"].unique()))
self._zone_name_list = list(sorted(self._well_completions_df["ZONE"].unique()))

# NOTE: The zone tree structure should be provided by server in the future
# to obtain parent/child relationship between zones
self._zones_tree = None

self._well_completions_df["TIMESTEP"] = self._well_completions_df["DATE"].map(self._datemap)

# NOTE:
# - How to handle well attributes? Should be provided by Sumo?
# - How to handle theme colors?
self._well_attributes: Dict[
str, Dict[str, WellCompletionsAttributeType]
] = {} # Each well has dict of attributes
self._theme_colors = ["#6EA35A", "#EDAF4C", "#CA413D"] # Hard coded

def _dummy_stratigraphy(self) -> List[WellCompletionsZone]:
"""
Returns a default stratigraphy for TESTING, should be provided by Sumo
"""
return [
WellCompletionsZone(
name="TopVolantis_BaseVolantis",
color="#6EA35A",
subzones=[
WellCompletionsZone(name="Valysar", color="#6EA35A"),
WellCompletionsZone(name="Therys", color="#EDAF4C"),
WellCompletionsZone(name="Volon", color="#CA413D"),
],
),
]

def create_data(self) -> WellCompletionsData:
"""Creates well completions dataset for front-end"""
Expand All @@ -149,7 +134,7 @@ def create_data(self) -> WellCompletionsData:
units=WellCompletionsUnits(
kh=WellCompletionsUnitInfo(unit=self._kh_unit, decimalPlaces=self._kh_decimal_places)
),
stratigraphy=self._extract_stratigraphy(self._dummy_stratigraphy(), self._zones),
zones=self._extract_well_completions_zones(zones=self._zones_tree, zone_name_list=self._zone_name_list),
timeSteps=[pd.to_datetime(str(dte)).strftime("%Y-%m-%d") for dte in self._datemap.keys()],
wells=self._extract_wells(),
)
Expand Down Expand Up @@ -187,63 +172,36 @@ def _extract_well(self, well_group: pd.DataFrame, well_name: str, no_real: int)
well.completions = completions
return well

def _extract_stratigraphy(
self, stratigraphy: Optional[List[WellCompletionsZone]], zones: List[str]
def _extract_well_completions_zones(
self, zones: Optional[List[WellCompletionsZone]], zone_name_list: List[str]
) -> List[WellCompletionsZone]:
"""Returns the stratigraphy part of the dataset to front-end"""
color_iterator = itertools.cycle(self._theme_colors)
"""Returns the well completions zone objects of the dataset to front-end
If optional zones definition is provided, it is filtered to only include zones from zone_name_list.
If no zones definition is provided, a flat zones definition made from zone_name_list is returned.
"""

# If no stratigraphy file is found then the stratigraphy is
# If no well completions zones is found then the well completions zone are
# created from the unique zones in the well completions data input.
# They will then probably not come in the correct order.
if stratigraphy is None:
return [WellCompletionsZone(name=zone, color=next(color_iterator)) for zone in zones]
# They will then probably not come in the correct, and no sone/subzone relationship will be defined.
if zones is None:
return [WellCompletionsZone(name=zone) for zone in zone_name_list]

# If stratigraphy is not None the following is done:
stratigraphy, remaining_valid_zones = self._filter_valid_nodes(stratigraphy, zones)
# If the input zones are not None then filter the zones to only include
zones, remaining_valid_zones = self._filter_valid_nodes(zones, zone_name_list)

if remaining_valid_zones:
raise ValueError(
"The following zones are defined in the well completions data, "
f"but not in the stratigraphy: {remaining_valid_zones}"
f"but not in the list of zone names: {remaining_valid_zones}"
)

return self._add_colors_to_stratigraphy(stratigraphy, color_iterator)

def _add_colors_to_stratigraphy(
self,
stratigraphy: List[WellCompletionsZone],
color_iterator: Iterator,
zone_color_mapping: Optional[Dict[str, str]] = None,
) -> List[WellCompletionsZone]:
"""Add colors to the stratigraphy tree. The function will recursively parse the tree.
There are tree sources of color:
1. The color is given in the stratigraphy list, in which case nothing is done to the node
2. The color is the optional the zone->color map
3. If none of the above applies, the color will be taken from the theme color iterable for \
the leaves. For other levels, a dummy color grey is used
"""
for zone in stratigraphy:
if zone.color == "":
if zone_color_mapping is not None and zone.name in zone_color_mapping:
zone.color = zone_color_mapping[zone.name]
elif zone.subzones is None:
zone = next(color_iterator) # theme colors only applied on leaves
else:
zone.color = "#808080" # grey
if zone.subzones is not None:
zone.subzones = self._add_colors_to_stratigraphy(
zone.subzones,
color_iterator,
zone_color_mapping=zone_color_mapping,
)
return stratigraphy
return zones

def _filter_valid_nodes(
self, stratigraphy: List[WellCompletionsZone], valid_zone_names: List[str]
self, zones: List[WellCompletionsZone], valid_zone_names: List[str]
) -> Tuple[List[WellCompletionsZone], List[str]]:
"""Returns the stratigraphy tree with only valid nodes.
"""Returns the zones tree with only valid nodes.
A node is considered valid if it self or one of it's subzones are in the
valid zone names list (passed from the lyr file)
Expand All @@ -252,14 +210,14 @@ def _filter_valid_nodes(

output = []
remaining_valid_zones = valid_zone_names
for zone in stratigraphy:
for zone in zones:
if zone.subzones is not None:
zone.subzones, remaining_valid_zones = self._filter_valid_nodes(zone.subzones, remaining_valid_zones)
if zone.name in remaining_valid_zones:
output.append(zone)
remaining_valid_zones = [
elm for elm in remaining_valid_zones if elm != zone.name
] # remove zone name from valid zones if it is found in the stratigraphy
] # remove zone name from valid zones if it is found in the zones tree
elif zone.subzones is not None:
output.append(zone)

Expand Down
3 changes: 1 addition & 2 deletions backend/src/services/sumo_access/well_completions_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ class WellCompletionsWell(BaseModel):

class WellCompletionsZone(BaseModel):
name: str
color: str
subzones: Optional[List["WellCompletionsZone"]] = None


Expand All @@ -40,6 +39,6 @@ class WellCompletionsData(BaseModel):

version: str
units: WellCompletionsUnits
stratigraphy: List[WellCompletionsZone]
zones: List[WellCompletionsZone]
timeSteps: List[str]
wells: List[WellCompletionsWell]
2 changes: 1 addition & 1 deletion frontend/src/api/models/WellCompletionsData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { WellCompletionsZone } from './WellCompletionsZone';
export type WellCompletionsData = {
version: string;
units: WellCompletionsUnits;
stratigraphy: Array<WellCompletionsZone>;
zones: Array<WellCompletionsZone>;
timeSteps: Array<string>;
wells: Array<WellCompletionsWell>;
};
Expand Down
1 change: 0 additions & 1 deletion frontend/src/api/models/WellCompletionsZone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

export type WellCompletionsZone = {
name: string;
color: string;
subzones: (Array<WellCompletionsZone> | null);
};

4 changes: 2 additions & 2 deletions frontend/src/modules/WellCompletions/loadModule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ModuleRegistry } from "@framework/ModuleRegistry";

import { Settings } from "./settings";
import { DataLoadingStatus, State } from "./state";
import { view } from "./view";
import { View } from "./view";

const initialState: State = {
dataLoadingStatus: DataLoadingStatus.Idle,
Expand All @@ -12,5 +12,5 @@ const initialState: State = {

const module = ModuleRegistry.initModule<State>("WellCompletions", initialState);

module.viewFC = view;
module.viewFC = View;
module.settingsFC = Settings;
75 changes: 50 additions & 25 deletions frontend/src/modules/WellCompletions/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from "react";
import { Ensemble } from "@framework/Ensemble";
import { EnsembleIdent } from "@framework/EnsembleIdent";
import { ModuleFCProps } from "@framework/Module";
import { useSettingsStatusWriter } from "@framework/StatusWriter";
import { SyncSettingKey, SyncSettingsHelper } from "@framework/SyncSettings";
import { useEnsembleSet } from "@framework/WorkbenchSession";
import { SingleEnsembleSelect } from "@framework/components/SingleEnsembleSelect";
Expand All @@ -27,12 +28,21 @@ enum RealizationSelection {
Single = "Single",
}

export const Settings = ({ moduleContext, workbenchSession, workbenchServices }: ModuleFCProps<State>) => {
export const Settings = ({
moduleContext,
workbenchSession,
workbenchServices,
workbenchSettings,
}: ModuleFCProps<State>) => {
const ensembleSet = useEnsembleSet(workbenchSession);
const statusWriter = useSettingsStatusWriter(moduleContext);

const [availableTimeSteps, setAvailableTimeSteps] = moduleContext.useStoreState("availableTimeSteps");
const setDataLoadingStatus = moduleContext.useSetStoreValue("dataLoadingStatus");
const setPlotData = moduleContext.useSetStoreValue("plotData");

const stratigraphyColorSet = workbenchSettings.useColorSet();

const [realizationSelection, setRealizationSelection] = React.useState<RealizationSelection>(
RealizationSelection.Aggregated
);
Expand Down Expand Up @@ -71,11 +81,19 @@ export const Settings = ({ moduleContext, workbenchSession, workbenchServices }:
realizationSelection === RealizationSelection.Single ? selectedRealizationNumber : undefined
);

if (wellCompletionsQuery.isError) {
let message = "Error loading well completions data for ensemble";
if (realizationSelection === RealizationSelection.Single) {
message += ` and realization ${selectedRealizationNumber}`;
}
statusWriter.addError(message);
}

// Use ref to prevent new every render
const wellCompletionsDataAccessor = React.useRef<WellCompletionsDataAccessor>(new WellCompletionsDataAccessor());

const createAndSetPlotData = React.useCallback(
function createAndSetPlotData(
const createAndPropagatePlotDataToView = React.useCallback(
function createAndPropagatePlotDataToView(
availableTimeSteps: string[] | null,
timeStepIndex: number | [number, number] | null,
timeAggregation: TimeAggregationType
Expand All @@ -100,6 +118,7 @@ export const Settings = ({ moduleContext, workbenchSession, workbenchServices }:
typeof timeStepIndex === "number"
? availableTimeSteps[timeStepIndex]
: [availableTimeSteps[timeStepIndex[0]], availableTimeSteps[timeStepIndex[1]]];

setPlotData(wellCompletionsDataAccessor.current.createPlotData(timeStepSelection, timeAggregation));
},
[setPlotData]
Expand All @@ -114,7 +133,10 @@ export const Settings = ({ moduleContext, workbenchSession, workbenchServices }:
return;
}

wellCompletionsDataAccessor.current.parseWellCompletionsData(wellCompletionsQuery.data);
wellCompletionsDataAccessor.current.parseWellCompletionsData(
wellCompletionsQuery.data,
stratigraphyColorSet
);

// Update available time steps
const allTimeSteps = wellCompletionsDataAccessor.current.getTimeSteps();
Expand Down Expand Up @@ -146,13 +168,21 @@ export const Settings = ({ moduleContext, workbenchSession, workbenchServices }:
setPlotData(null);
return;
}
createAndSetPlotData(allTimeSteps, timeStepIndex, selectedTimeStepOptions.timeAggregationType);

createAndPropagatePlotDataToView(allTimeSteps, timeStepIndex, selectedTimeStepOptions.timeAggregationType);
},
[wellCompletionsQuery.data, selectedTimeStepOptions, setPlotData, setAvailableTimeSteps, createAndSetPlotData]
[
wellCompletionsQuery.data,
selectedTimeStepOptions,
stratigraphyColorSet,
setPlotData,
setAvailableTimeSteps,
createAndPropagatePlotDataToView,
]
);

React.useEffect(
function handleQueryStateChange() {
function propagateQueryStatesToView() {
if (wellCompletionsQuery.isFetching) {
setDataLoadingStatus(DataLoadingStatus.Loading);
} else if (wellCompletionsQuery.status === "error") {
Expand Down Expand Up @@ -212,7 +242,7 @@ export const Settings = ({ moduleContext, workbenchSession, workbenchServices }:
});
}

createAndSetPlotData(availableTimeSteps, newTimeStepIndex, newTimeAggregation);
createAndPropagatePlotDataToView(availableTimeSteps, newTimeStepIndex, newTimeAggregation);
}

function handleSelectedTimeStepIndexChange(e: Event, newIndex: number | number[]) {
Expand All @@ -228,14 +258,18 @@ export const Settings = ({ moduleContext, workbenchSession, workbenchServices }:
}));
}

createAndSetPlotData(availableTimeSteps, newTimeStepIndex, selectedTimeStepOptions.timeAggregationType);
createAndPropagatePlotDataToView(
availableTimeSteps,
newTimeStepIndex,
selectedTimeStepOptions.timeAggregationType
);
}

function handleSearchWellChange(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value;
wellCompletionsDataAccessor.current?.setSearchWellText(value);

createAndSetPlotData(
createAndPropagatePlotDataToView(
availableTimeSteps,
selectedTimeStepOptions.timeStepIndex,
selectedTimeStepOptions.timeAggregationType
Expand All @@ -246,7 +280,7 @@ export const Settings = ({ moduleContext, workbenchSession, workbenchServices }:
const checked = e.target.checked;
wellCompletionsDataAccessor.current?.setHideZeroCompletions(checked);

createAndSetPlotData(
createAndPropagatePlotDataToView(
availableTimeSteps,
selectedTimeStepOptions.timeStepIndex,
selectedTimeStepOptions.timeAggregationType
Expand Down Expand Up @@ -275,20 +309,11 @@ export const Settings = ({ moduleContext, workbenchSession, workbenchServices }:
text="Ensemble:"
labelClassName={syncHelper.isSynced(SyncSettingKey.ENSEMBLE) ? "bg-indigo-700 text-white" : ""}
>
<>
<SingleEnsembleSelect
ensembleSet={ensembleSet}
value={computedEnsembleIdent}
onChange={handleEnsembleSelectionChange}
/>
{
<div className="text-red-500 text-sm h-4">
{wellCompletionsQuery.isError
? "Current ensemble does not contain well completions data"
: ""}
</div>
}
</>
<SingleEnsembleSelect
ensembleSet={ensembleSet}
value={computedEnsembleIdent}
onChange={handleEnsembleSelectionChange}
/>
</Label>
<div className={resolveClassNames({ "pointer-events-none opacity-40": wellCompletionsQuery.isError })}>
<Label text="Realization selection">
Expand Down
Loading

0 comments on commit d9a23e1

Please sign in to comment.