Skip to content

Commit 25a23a6

Browse files
authored
Add observations to SimulationTimeSeriesMatrix (#462)
1 parent a50083f commit 25a23a6

File tree

8 files changed

+249
-33
lines changed

8 files changed

+249
-33
lines changed

backend/src/services/sumo_access/observation_access.py

+24-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import logging
2-
from typing import List
2+
from typing import List, Dict
33

44
import json
55
from fmu.sumo.explorer.objects.dictionary import Dictionary
@@ -43,18 +43,34 @@ def _create_summary_observations(observations_dict: dict) -> List[SummaryVectorO
4343
summary_observations: List[SummaryVectorObservations] = []
4444
if ObservationType.SUMMARY not in observations_dict:
4545
return summary_observations
46-
for smry_obs in observations_dict[ObservationType.SUMMARY]:
46+
47+
summary_observations_dict: Dict[str, Dict[str, list]] = observations_dict[ObservationType.SUMMARY]
48+
49+
for vector_name, observations_data in summary_observations_dict.items():
50+
observation_names = observations_data["observation_name"]
51+
observation_values = observations_data["value"]
52+
observation_errors = observations_data["error"]
53+
observation_dates = observations_data["date"]
54+
55+
num_observations = len(observation_names)
56+
if (
57+
len(observation_values) != num_observations
58+
or len(observation_errors) != num_observations
59+
or len(observation_dates) != num_observations
60+
):
61+
raise ValueError(f"Inconsistent observations data for vector {vector_name}")
62+
4763
summary_observations.append(
4864
SummaryVectorObservations(
49-
vector_name=smry_obs["key"],
65+
vector_name=vector_name,
5066
observations=[
5167
SummaryVectorDateObservation(
52-
date=obs["date"],
53-
value=obs["value"],
54-
error=obs["error"],
55-
label=obs["label"],
68+
date=observation_dates[i],
69+
value=observation_values[i],
70+
error=observation_errors[i],
71+
label=observation_names[i],
5672
)
57-
for obs in smry_obs["observations"]
73+
for i in range(num_observations)
5874
],
5975
)
6076
)

backend/src/services/sumo_access/observation_types.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
class ObservationType(str, Enum):
88
"""The observation file in Sumo is a dictionary with these datatypes as keys."""
99

10-
SUMMARY = "smry"
10+
SUMMARY = "summary"
1111
RFT = "rft"
1212

1313

frontend/src/modules/SimulationTimeSeriesMatrix/queryHooks.tsx

+88-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Frequency_api, VectorDescription_api } from "@api";
1+
import { Frequency_api, SummaryVectorObservations_api, VectorDescription_api } from "@api";
22
import { VectorHistoricalData_api, VectorRealizationData_api, VectorStatisticData_api } from "@api";
33
import { apiService } from "@framework/ApiService";
44
import { EnsembleIdent } from "@framework/EnsembleIdent";
@@ -12,7 +12,6 @@ const CACHE_TIME = 60 * 1000;
1212
export function useVectorListQueries(
1313
caseUuidsAndEnsembleNames: EnsembleIdent[] | null
1414
): UseQueryResult<VectorDescription_api[]>[] {
15-
// Note: how to cancel queryFn if key is updated?
1615
return useQueries({
1716
queries: (caseUuidsAndEnsembleNames ?? []).map((item) => {
1817
return {
@@ -33,7 +32,6 @@ export function useVectorDataQueries(
3332
realizationsToInclude: number[] | null,
3433
allowEnable: boolean
3534
): UseQueryResult<VectorRealizationData_api[]>[] {
36-
// Note: how to cancel queryFn if key is updated?
3735
return useQueries({
3836
queries: (vectorSpecifications ?? []).map((item) => {
3937
return {
@@ -141,3 +139,90 @@ export function useHistoricalVectorDataQueries(
141139
}),
142140
});
143141
}
142+
143+
/**
144+
* Definition of ensemble vector observation data
145+
*
146+
* hasSummaryObservations: true if the ensemble has observations, i.e the summary observations array is not empty
147+
* vectorsObservationData: array of vector observation data for requested vector specifications
148+
*/
149+
export type EnsembleVectorObservationData = {
150+
hasSummaryObservations: boolean;
151+
vectorsObservationData: { vectorSpecification: VectorSpec; data: SummaryVectorObservations_api }[];
152+
};
153+
154+
/**
155+
* Definition of map of ensemble ident and ensemble vector observation data
156+
*/
157+
export type EnsembleVectorObservationDataMap = Map<EnsembleIdent, EnsembleVectorObservationData>;
158+
159+
/**
160+
* Definition of vector observations queries result for combined queries
161+
*/
162+
export type VectorObservationsQueriesResult = {
163+
isFetching: boolean;
164+
isError: boolean;
165+
ensembleVectorObservationDataMap: EnsembleVectorObservationDataMap;
166+
};
167+
168+
/**
169+
* This function takes vectorSpecifications and returns a map of ensembleIdent and the respective observations data for
170+
* the selected vectors.
171+
*
172+
* If the returned summary array from back-end is empty array, the ensemble does not have observations.
173+
* If the selected vectors are not among the returned summary array, the vector does not have observations.
174+
*/
175+
export function useVectorObservationsQueries(
176+
vectorSpecifications: VectorSpec[] | null,
177+
allowEnable: boolean
178+
): VectorObservationsQueriesResult {
179+
const uniqueEnsembleIdents = [...new Set(vectorSpecifications?.map((item) => item.ensembleIdent) ?? [])];
180+
return useQueries({
181+
queries: (uniqueEnsembleIdents ?? []).map((item) => {
182+
return {
183+
queryKey: ["getObservations", item.getCaseUuid(), item.getEnsembleName()],
184+
queryFn: () =>
185+
apiService.observations.getObservations(item.getCaseUuid() ?? "", item.getEnsembleName() ?? ""),
186+
staleTime: STALE_TIME,
187+
cacheTime: CACHE_TIME,
188+
enabled: !!(allowEnable && item.getCaseUuid() && item.getEnsembleName()),
189+
};
190+
}),
191+
combine: (results) => {
192+
const combinedResult: EnsembleVectorObservationDataMap = new Map();
193+
if (!vectorSpecifications)
194+
return { isFetching: false, isError: false, ensembleVectorObservationDataMap: combinedResult };
195+
196+
results.forEach((result, index) => {
197+
const ensembleIdent = uniqueEnsembleIdents.at(index);
198+
if (!ensembleIdent) return;
199+
200+
const ensembleVectorSpecifications = vectorSpecifications.filter(
201+
(item) => item.ensembleIdent === ensembleIdent
202+
);
203+
204+
const ensembleHasObservations = result.data?.summary.length !== 0;
205+
combinedResult.set(ensembleIdent, {
206+
hasSummaryObservations: ensembleHasObservations,
207+
vectorsObservationData: [],
208+
});
209+
for (const vectorSpec of ensembleVectorSpecifications) {
210+
const vectorObservationsData =
211+
result.data?.summary.find((elm) => elm.vector_name === vectorSpec.vectorName) ?? null;
212+
if (!vectorObservationsData) continue;
213+
214+
combinedResult.get(ensembleIdent)?.vectorsObservationData.push({
215+
vectorSpecification: vectorSpec,
216+
data: vectorObservationsData,
217+
});
218+
}
219+
});
220+
221+
return {
222+
isFetching: results.some((result) => result.isFetching),
223+
isError: results.some((result) => result.isError),
224+
ensembleVectorObservationDataMap: combinedResult,
225+
};
226+
},
227+
});
228+
}

frontend/src/modules/SimulationTimeSeriesMatrix/settings.tsx

+1-6
Original file line numberDiff line numberDiff line change
@@ -353,12 +353,7 @@ export function settings({ moduleContext, workbenchSession }: ModuleFCProps<Stat
353353
disabled={!selectedVectorNamesHasHistorical}
354354
onChange={handleShowHistorical}
355355
/>
356-
<Checkbox
357-
label="Show observations - NEED DATA IN SUMO"
358-
checked={showObservations}
359-
disabled={true}
360-
onChange={handleShowObservations}
361-
/>
356+
<Checkbox label="Show observations" checked={showObservations} onChange={handleShowObservations} />
362357
<div
363358
className={resolveClassNames({
364359
"pointer-events-none opacity-80": vectorListQueries.some((query) => query.isLoading),

frontend/src/modules/SimulationTimeSeriesMatrix/utils/PlotlyTraceUtils/createVectorTracesUtils.ts

+57
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
StatisticFunction_api,
3+
SummaryVectorDateObservation_api,
34
VectorHistoricalData_api,
45
VectorRealizationData_api,
56
VectorStatisticData_api,
@@ -150,6 +151,62 @@ export function createHistoricalVectorTrace({
150151
};
151152
}
152153

154+
/**
155+
Utility function for creating traces for vector observations
156+
*/
157+
export type CreateVectorObservationTraceOptions = {
158+
vectorObservations: Array<SummaryVectorDateObservation_api>;
159+
color?: string;
160+
yaxis?: string;
161+
xaxis?: string;
162+
legendGroup?: string;
163+
showLegend?: boolean;
164+
name?: string;
165+
type?: "scatter" | "scattergl";
166+
};
167+
export function createVectorObservationsTraces({
168+
vectorObservations,
169+
color = "black",
170+
yaxis = "y",
171+
xaxis = "x",
172+
legendGroup = "Observation",
173+
showLegend = false,
174+
name = undefined,
175+
type = "scatter",
176+
}: CreateVectorObservationTraceOptions): Partial<TimeSeriesPlotData>[] {
177+
// NB: "scattergl" does not include "+/- error" in the hover template `(%{x}, %{y})`, "scatter" does.
178+
179+
const traceName = name ? `Observation<br>${name}` : "Observation";
180+
return vectorObservations.map((observation) => {
181+
let hoverText = observation.label;
182+
let hoverData = `(%{x}, %{y})<br>`;
183+
if (observation.comment) {
184+
hoverText += `: ${observation.comment}`;
185+
}
186+
if (type === "scattergl") {
187+
hoverData = `(%{x}, %{y} ± ${observation.error})<br>`;
188+
}
189+
190+
return {
191+
name: traceName,
192+
legendgroup: legendGroup,
193+
x: [observation.date],
194+
y: [observation.value],
195+
marker: { color: color },
196+
yaxis: yaxis,
197+
xaxis: xaxis,
198+
hovertemplate: hoverText ? `${hoverData}${hoverText}` : hoverData,
199+
showlegend: showLegend,
200+
type: type,
201+
error_y: {
202+
type: "constant",
203+
value: observation.error,
204+
visible: true,
205+
},
206+
};
207+
});
208+
}
209+
153210
/**
154211
Utility function for creating traces representing statistical fanchart for given statistics data.
155212

frontend/src/modules/SimulationTimeSeriesMatrix/utils/subplotBuilder.ts

+37-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { VectorHistoricalData_api, VectorRealizationData_api, VectorStatisticData_api } from "@api";
1+
import {
2+
SummaryVectorObservations_api,
3+
VectorHistoricalData_api,
4+
VectorRealizationData_api,
5+
VectorStatisticData_api,
6+
} from "@api";
27
import { EnsembleIdent } from "@framework/EnsembleIdent";
38
import { ColorSet } from "@lib/utils/ColorSet";
49
import { simulationUnitReformat, simulationVectorDescription } from "@modules/_shared/reservoirSimulationStringUtils";
@@ -9,6 +14,7 @@ import { Annotations, Layout } from "plotly.js";
914
import {
1015
createHistoricalVectorTrace,
1116
createVectorFanchartTraces,
17+
createVectorObservationsTraces,
1218
createVectorRealizationTrace,
1319
createVectorRealizationTraces,
1420
createVectorStatisticsTraces,
@@ -466,8 +472,36 @@ export class SubplotBuilder {
466472
}
467473
}
468474

469-
addVectorObservations(): void {
470-
throw new Error("Method not implemented.");
475+
addObservationsTraces(
476+
vectorsObservationData: {
477+
vectorSpecification: VectorSpec;
478+
data: SummaryVectorObservations_api;
479+
}[]
480+
): void {
481+
// Only allow selected vectors
482+
const selectedVectorsObservationData = vectorsObservationData.filter((vec) =>
483+
this._selectedVectorSpecifications.some(
484+
(selectedVec) => selectedVec.vectorName === vec.vectorSpecification.vectorName
485+
)
486+
);
487+
488+
// Create traces for each vector
489+
for (const elm of selectedVectorsObservationData) {
490+
const subplotIndex = this.getSubplotIndex(elm.vectorSpecification);
491+
if (subplotIndex === -1) continue;
492+
493+
const name = this.makeTraceNameFromVectorSpecification(elm.vectorSpecification);
494+
const vectorObservationsTraces = createVectorObservationsTraces({
495+
vectorObservations: elm.data.observations,
496+
name: name,
497+
color: this._observationColor,
498+
type: this._scatterType,
499+
yaxis: `y${subplotIndex + 1}`,
500+
});
501+
502+
this._plotData.push(...vectorObservationsTraces);
503+
this._hasObservationTraces = true;
504+
}
471505
}
472506

473507
private getSubplotIndex(vectorSpecification: VectorSpec) {

frontend/src/modules/SimulationTimeSeriesMatrix/utils/vectorSpecificationsAndQueriesUtils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { FanchartStatisticOption, VectorSpec } from "../state";
1111
*/
1212
export function createLoadedVectorSpecificationAndDataArray<T>(
1313
vectorSpecifications: VectorSpec[],
14-
queryResults: UseQueryResult<T>[]
14+
queryResults: UseQueryResult<T | null>[]
1515
): { vectorSpecification: VectorSpec; data: T }[] {
1616
if (vectorSpecifications.length !== queryResults.length) {
1717
throw new Error(

0 commit comments

Comments
 (0)