Inplace volumes waterfall - #1858
Conversation
New module decomposing the change in STOIIP/GIIP between two ensembles into additive contributions from its volumetric factors (BULK, PORO or NTG+PORO_NET, SW, BO/BG), rendered as a waterfall plot. The reference and comparison ensembles are selected explicitly with two ensemble dropdowns; the decomposition operates on per-ensemble means, so it does not require realization alignment between the two ensembles. Ported from the legacy webviz-subsurface waterfall.
There was a problem hiding this comment.
Pull request overview
This PR introduces a new InplaceVolumesComparison module that compares two inplace-volumes sources (across ensembles and/or table sources) and visualizes the STOIIP/GIIP mean change as a Plotly waterfall decomposition into volumetric-factor contributions.
Changes:
- Adds the new InplaceVolumesComparison module (settings + view) including factor-spec generation, decomposition math, and Plotly waterfall rendering.
- Refactors shared inplace-volumes query utilities to support querying an explicit list of (ensemble, table) sources (not only a cross-product).
- Updates inplace-volumes module registration metadata (descriptions/titles) and adds unit tests for the new decomposition and source-selection utilities.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/tests/unit/InplaceVolumesWaterfallSources.test.ts | Adds unit tests for source lookup and endpoint label behavior. |
| frontend/tests/unit/InplaceVolumesWaterfallDecomposition.test.ts | Adds unit tests for factor spec building and decomposition reconciliation/edge cases. |
| frontend/src/modules/registerAllModules.ts | Registers the new InplaceVolumesComparison module. |
| frontend/src/modules/InplaceVolumesTable/registerModule.ts | Updates module description text. |
| frontend/src/modules/InplaceVolumesPlot/registerModule.ts | Updates deprecated-module description text. |
| frontend/src/modules/InplaceVolumesNew/registerModule.ts | Updates module description and default title for the newer inplace-volumes plotting module. |
| frontend/src/modules/InplaceVolumesComparison/view/view.tsx | Adds view wrapper wiring (status, sizing, plot rendering). |
| frontend/src/modules/InplaceVolumesComparison/view/utils/waterfallSources.ts | Adds utilities for locating per-source table data and making endpoint labels. |
| frontend/src/modules/InplaceVolumesComparison/view/utils/computeVolumeChangeDecomposition.ts | Implements factor spec creation and the decomposition computation. |
| frontend/src/modules/InplaceVolumesComparison/view/utils/buildWaterfallPlot.tsx | Implements Plotly waterfall construction and subplot layout. |
| frontend/src/modules/InplaceVolumesComparison/view/hooks/useBuildWaterfallPlot.ts | Implements orchestration: selection validation, data extraction, decomposition per group, and plot building. |
| frontend/src/modules/InplaceVolumesComparison/view/atoms/queryAtoms.ts | Adds atoms for explicit source selection and statistical data queries. |
| frontend/src/modules/InplaceVolumesComparison/view/atoms/interfaceEffects.ts | Wires settings→view interface values into view atoms. |
| frontend/src/modules/InplaceVolumesComparison/view/atoms/baseAtoms.ts | Adds base atoms for view-side selections and derived state. |
| frontend/src/modules/InplaceVolumesComparison/settings/settings.tsx | Adds the module settings UI (sources, response, subplot-by, filters). |
| frontend/src/modules/InplaceVolumesComparison/settings/persistence.ts | Adds settings persistence (schema + serialize/deserialize). |
| frontend/src/modules/InplaceVolumesComparison/settings/atoms/queryAtoms.ts | Adds table-definition query for the selected ensembles. |
| frontend/src/modules/InplaceVolumesComparison/settings/atoms/persistableFixableAtoms.ts | Adds persistable/fixable atoms for settings selections. |
| frontend/src/modules/InplaceVolumesComparison/settings/atoms/derivedAtoms.ts | Adds derived atoms (available options, comparability, factor spec, query indices). |
| frontend/src/modules/InplaceVolumesComparison/settings/atoms/baseAtoms.ts | Adds base atom for index-value criteria. |
| frontend/src/modules/InplaceVolumesComparison/registerModule.ts | Registers the new module with metadata (DEV state). |
| frontend/src/modules/InplaceVolumesComparison/preview.tsx | Adds module preview thumbnail drawing. |
| frontend/src/modules/InplaceVolumesComparison/persistence.ts | Adds module-level state schema + serialization wiring. |
| frontend/src/modules/InplaceVolumesComparison/loadModule.tsx | Adds module load/init wiring for settings + view components. |
| frontend/src/modules/InplaceVolumesComparison/interfaces.ts | Adds settings→view interface definition and initialization mapping. |
| frontend/src/modules/_shared/InplaceVolumes/queryHooks.ts | Adds InplaceVolumesSource and makeAggregatedStatisticalTableDataQueryOptions for explicit source querying. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 27 changed files in this pull request and generated no new comments.
Suppressed comments (3)
frontend/src/modules/InplaceVolumesComparison/settings/settings.tsx:81
areSourcesDistinctis also false while either table is unselected. Consequently, selecting two different ensembles can mark the reference ensemble as invalid during table loading—or permanently when one ensemble has no tables—even though the ensembles already differ. Only show this duplicate-source error once both table selections are present.
const isSameSourceSelectedTwice = Boolean(
referenceEnsembleIdent.value && comparisonEnsembleIdent.value && !areSourcesDistinct,
);
frontend/src/modules/InplaceVolumesComparison/view/hooks/useBuildWaterfallPlot.ts:232
- The intersection silently drops a selected group that has rows in only one source (for example after per-ensemble realization filtering). Such a group is neither plotted nor added to
skippedGroupLabels, so the resulting comparison appears complete. Build the sorted union and treat a missing side as a skipped group.
const groupKeys = Array.from(comparisonStatisticsByGroup.keys())
.filter((groupKey) => referenceStatisticsByGroup.has(groupKey))
.sort((a, b) => a.localeCompare(b));
frontend/src/modules/InplaceVolumesComparison/settings/settings.tsx:50
- Table-definition request failures are never propagated to the settings status writer. Because failed results are converted to empty definition arrays, the UI instead reports “No inplace volumes tables in this ensemble,” which misdiagnoses an API failure as missing data. Propagate
tableDefinitionsQuery.errorsvia the shared API-error status hook, as the other inplace-volume settings modules do.
This issue also appears on line 79 of the same file.
const tableDefinitionsQuery = useAtomValue(tableDefinitionsQueryAtom);
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 31 changed files in this pull request and generated no new comments.
Suppressed comments (6)
frontend/src/modules/InplaceVolumesComparison/settings/settings.tsx:211
- A failed table-definition request also leaves
comparisonTableNameOptionsempty, causing this field to report that the ensemble has no tables. DistinguishtableDefinitionsQuery.errorsfrom a successful empty response and present a load failure.
errorOverlay={
!tableDefinitionsQuery.isLoading &&
comparisonEnsembleIdent.value &&
comparisonTableNameOptions.length === 0
? "No inplace volumes tables in this ensemble."
: undefined
frontend/src/modules/InplaceVolumesComparison/settings/settings.tsx:307
- When the only differences are columns missing from one source, enabling the switch cannot filter to a shared subset, but this branch still claims that it does and that the totals are partial. Base this message on whether there is at least one shared column with differing values, not only on the selected criterion.
{isIndexValueIntersectionEnabled
? "Only values present in both are compared, so the totals below are for that shared subset and will not match the full-field volumes."
: "These indices are left unfiltered, so the full volume of both sources is compared and the difference in coverage shows up in the BULK contribution."}
frontend/src/modules/InplaceVolumesComparison/settings/atoms/derivedAtoms.ts:182
- This marks intersection as active for missing-column differences too. Those columns cannot be intersected and never enter
availableIndicesWithValuesAtom, so enabling the switch does not narrow the query, yet the view warns that only a shared subset is plotted. Treat intersection as active only when a shared column has differing values.
export const isIndexValueIntersectionActiveAtom = atom(
(get) => get(isIndexValueIntersectionEnabledAtom) && get(indexColumnDifferencesAtom).length > 0,
);
frontend/src/modules/InplaceVolumesComparison/settings/settings.tsx:180
- A failed table-definition request is converted to an empty table list, so this condition reports “No inplace volumes tables” for network/server failures as well. Check
tableDefinitionsQuery.errorsand surface a load failure instead of misdiagnosing the ensemble as empty; the comparison-table field has the same issue.
This issue also appears on line 206 of the same file.
errorOverlay={
!tableDefinitionsQuery.isLoading &&
referenceEnsembleIdent.value &&
referenceTableNameOptions.length === 0
? "No inplace volumes tables in this ensemble."
: undefined
frontend/src/modules/InplaceVolumesComparison/settings/settings.tsx:282
missingFromnames the side where the column is absent, but this message presents it as the side where the column exists. For example, a reference-only FACIES column is stored asmissingFrom: "comparison"and is currently reported as existing only in comparison.
This issue also appears on line 305 of the same file.
<> exists only in the {difference.missingFrom}, so it cannot be used.</>
frontend/src/modules/InplaceVolumesComparison/view/hooks/useBuildWaterfallPlot.ts:225
- This branch is reached only when
getAreTablesComparable()is false, which this module's accessor configuration defines as having no common index columns; result-name intersection is checked separately below. The current message can therefore incorrectly claim there are no common result names even when STOIIP/GIIP is shared.
if (!areSelectedTablesComparable) {
return makeErrorResult(
"The selected tables are not comparable: they have no result names or index columns in common.",
);
Closes #1808