Skip to content

Commit

Permalink
Structural uncertainty plugin (#605)
Browse files Browse the repository at this point in the history
Co-authored-by: Therese Natterøy <[email protected]>
  • Loading branch information
HansKallekleiv and tnatt authored Apr 27, 2021
1 parent 30a86f9 commit 1f7e1f9
Show file tree
Hide file tree
Showing 36 changed files with 4,297 additions and 18 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/subsurface.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ jobs:
TESTDATA_REPO_BRANCH: master
run: |
git clone --depth 1 --branch $TESTDATA_REPO_BRANCH https://github.com/$TESTDATA_REPO_OWNER/webviz-subsurface-testdata.git
# Copy any clientside script to the test folder before running tests
mkdir ./tests/assets && cp ./webviz_subsurface/_assets/js/* ./tests/assets
pytest ./tests --headless --forked --testdata-folder ./webviz-subsurface-testdata
rm -rf ./tests/assets
webviz docs --portable ./docs_build --skip-open
- name: 🐳 Build Docker example image
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- [#615](https://github.com/equinor/webviz-subsurface/pull/615) - Improve table performance of AssistedHistoryMatchingAnalysis.

### Added
- [#605](https://github.com/equinor/webviz-subsurface/pull/605) - New plugin to analyze structural uncertainty from FMU ensembles.

## [0.2.0] - 2021-03-28
- [#604](https://github.com/equinor/webviz-subsurface/pull/604) - Consolidates surface loading and statistical calculation of surfaces by introducing a shared
SurfaceSetModel. Refactored SurfaceViewerFMU to use SurfaceSetModel.
Expand Down
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"webviz_subsurface": [
"_abbreviations/abbreviation_data/*.json",
"_assets/css/*.css",
"_assets/js/*.js",
]
},
entry_points={
Expand All @@ -51,6 +52,7 @@
"RftPlotter = webviz_subsurface.plugins:RftPlotter",
"RunningTimeAnalysisFMU = webviz_subsurface.plugins:RunningTimeAnalysisFMU",
"SegyViewer = webviz_subsurface.plugins:SegyViewer",
"StructuralUncertainty = webviz_subsurface.plugins:StructuralUncertainty",
"SubsurfaceMap = webviz_subsurface.plugins:SubsurfaceMap",
"SurfaceViewerFMU = webviz_subsurface.plugins:SurfaceViewerFMU",
"SurfaceWithGridCrossSection = webviz_subsurface.plugins:SurfaceWithGridCrossSection",
Expand All @@ -63,6 +65,7 @@
install_requires=[
"dash>=1.11",
"dash_bootstrap_components>=0.10.3",
"dash-daq>=0.5.0",
"defusedxml>=0.6.0",
"ecl2df>=0.6.1; sys_platform=='linux'",
"fmu-ensemble>=1.2.3",
Expand Down
275 changes: 275 additions & 0 deletions tests/integration_tests/test_structural_uncertainty.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
import json

import dash_html_components as html
from dash.dependencies import Input, Output, State
from webviz_config.themes import default_theme
from webviz_config import WebvizSettings

# pylint: disable=no-name-in-module
from webviz_config.plugins import (
StructuralUncertainty,
)

# pylint: enable=no-name-in-module


def stringify_object_id(uuid) -> str:
"""Object ids must be sorted and converted to
css strings to be recognized as dom elements"""
sorted_uuid_obj = json.loads(
json.dumps(
uuid,
sort_keys=True,
separators=(",", ":"),
)
)
string = ["{"]
for idx, (key, value) in enumerate(sorted_uuid_obj.items()):
string.append(f'\\"{key}\\"\\:\\"{value}\\"\\')
if idx == len(sorted_uuid_obj) - 1:
string.append("}")
else:
string.append(",")
return ("").join(string)


def test_default_configuration(dash_duo, app, testdata_folder) -> None:
webviz_settings = WebvizSettings(
shared_settings={
"scratch_ensembles": {
"iter-0": str(
testdata_folder / "reek_history_match/realization-*/iter-0"
)
}
},
theme=default_theme,
)
plugin = StructuralUncertainty(
app,
webviz_settings,
ensembles=["iter-0"],
surface_attributes=["ds_extracted_horizons"],
surface_name_filter=[
"topupperreek",
"topmidreek",
"toplowerreek",
"baselowerreek",
],
wellfolder=testdata_folder / "observed_data" / "wells",
)

app.layout = plugin.layout
dash_duo.start_server(app)

intersection_data_id = plugin.uuid("intersection-data")
modal_id = plugin.uuid("modal")
# Check some initialization
# Check dropdowns
for element, return_val in zip(
["well", "surface_attribute"], ["OP_1", "ds_extracted_horizons"]
):
uuid = stringify_object_id(
uuid={"element": element, "id": intersection_data_id}
)
assert dash_duo.wait_for_element(f"#\\{uuid} .Select-value").text == return_val

# Check Selects
for element, return_val in zip(
["surface_names"],
[["topupperreek", "topmidreek", "toplowerreek", "baselowerreek"]],
):
uuid = stringify_object_id(
uuid={"element": element, "id": intersection_data_id}
)
assert (
dash_duo.wait_for_element(f"#\\{uuid} select").text.splitlines()
== return_val
)

# Check Calculation checkbox
uuid = stringify_object_id(
uuid={"element": "calculation", "id": intersection_data_id}
)
calculation_element = dash_duo.driver.find_elements_by_css_selector(
f"#\\{uuid} > label > input"
)
assert len(calculation_element) == len(
["Min", "Max", "Mean", "Realizations", "Uncertainty envelope"]
)
for checkbox, selected in zip(
calculation_element,
["true", "true", "true", None, None],
):
assert checkbox.get_attribute("selected") == selected

# Check realizations
real_filter_btn_uuid = stringify_object_id(
{
"id": modal_id,
"modal_id": "realization-filter",
"element": "button-open",
}
)
real_uuid = stringify_object_id(
uuid={"element": "realizations", "id": intersection_data_id}
)

### Open realization filter and check realizations
dash_duo.wait_for_element_by_id(real_filter_btn_uuid).click()
real_selector = dash_duo.wait_for_element_by_id(real_uuid)
assert real_selector.text.splitlines() == [
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
]

assert dash_duo.get_logs() == [], "browser console should contain no error"


def test_full_configuration(dash_duo, app, testdata_folder) -> None:
webviz_settings = WebvizSettings(
shared_settings={
"scratch_ensembles": {
"iter-0": str(
testdata_folder / "reek_history_match/realization-*/iter-0"
),
"iter-1": str(
testdata_folder / "reek_history_match/realization-*/iter-1"
),
"iter-2": str(
testdata_folder / "reek_history_match/realization-*/iter-2"
),
"iter-3": str(
testdata_folder / "reek_history_match/realization-*/iter-3"
),
}
},
theme=default_theme,
)
plugin = StructuralUncertainty(
app,
webviz_settings,
ensembles=["iter-0", "iter-1", "iter-2", "iter-3"],
surface_attributes=["ds_extracted_horizons"],
surface_name_filter=[
"topupperreek",
"topmidreek",
"toplowerreek",
"baselowerreek",
],
wellfolder=testdata_folder / "observed_data" / "wells",
zonelog="Zonelog",
initial_settings={
"intersection_data": {
"surface_names": ["topupperreek", "topmidreek", "baselowerreek"],
"surface_attribute": "ds_extracted_horizons",
"ensembles": [
"iter-0",
"iter-1",
],
"calculation": ["Mean", "Min", "Max"],
# - Uncertainty envelope
"well": "OP_6",
"realizations": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
"colors": {
"topupperreek": {"iter-0": "#2C82C9", "iter-1": "#2CC990"},
"topmidreek": {
"iter-0": "#512E34",
"iter-1": "#7D93C1",
},
"baselowerreek": {
"iter-0": "#EEE657",
"iter-1": "#FC6042",
},
},
},
"intersection_layout": {
"yaxis": {
"range": [1700, 1550],
"title": "True vertical depth [m]",
},
"xaxis": {"title": "Lateral distance [m]"},
},
},
)

app.layout = plugin.layout

# Injecting a div that will be updated when the plot data stores are
# changed. Since the plot data are stored in LocalStorage and Selenium
# has no functionality to wait for LocalStorage to equal some value we
# instead populate this injected div with some data before we check the content
# of Localstorage.
@app.callback(
Output(plugin.uuid("layout"), "children"),
Input(plugin.uuid("intersection-graph-layout"), "data"),
State(plugin.uuid("layout"), "children"),
)
def _add_or_update_div(data, children):
plot_is_updated = html.Div(
id=plugin.uuid("plot_is_updated"), children=data.get("title")
)
if len(children) == 6:
children[5] = plot_is_updated
else:
children.append(plot_is_updated)

return children

dash_duo.start_server(app)

intersection_data_id = plugin.uuid("intersection-data")

# Check some initialization
# Check dropdowns
for element, return_val in zip(
["well", "surface_attribute"], ["OP_6", "ds_extracted_horizons"]
):
uuid = stringify_object_id(
uuid={"element": element, "id": intersection_data_id}
)
assert dash_duo.wait_for_text_to_equal(f"#\\{uuid} .Select-value", return_val)

# Wait for the callbacks to execute
dash_duo.wait_for_text_to_equal(
f'#{plugin.uuid("plot_is_updated")}', "Intersection along well: OP_6"
)

# Check that graph data is stored
graph_data = dash_duo.get_session_storage(plugin.uuid("intersection-graph-data"))
assert len(graph_data) == 22
graph_layout = dash_duo.get_session_storage(
plugin.uuid("intersection-graph-layout")
)
assert isinstance(graph_layout, dict)
assert graph_layout.get("title") == "Intersection along well: OP_6"

### Change well and check graph
well_uuid = stringify_object_id(
uuid={"element": "well", "id": intersection_data_id}
)

apply_btn = dash_duo.wait_for_element_by_id(
plugin.uuid("apply-intersection-data-selections")
)
well_dropdown = dash_duo.wait_for_element_by_id(well_uuid)
dash_duo.select_dcc_dropdown(well_dropdown, value="OP_1")
apply_btn.click()

# dash_duo.wait_for_text_to_equal(
# f'#{plugin.uuid("plot_is_updated")}',
# "Intersection along well: OP_6",
# timeout=100,
# )
graph_layout = dash_duo.get_session_storage(
plugin.uuid("intersection-graph-layout")
)
# assert graph_layout.get("title") == "Intersection along well: OP_1"
assert dash_duo.get_logs() == [], "browser console should contain no error"
64 changes: 64 additions & 0 deletions tests/unit_tests/model_tests/test_well_set_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from pathlib import Path

import pytest
import numpy as np
import xtgeo

from webviz_subsurface._models import WellSetModel


@pytest.mark.usefixtures("app")
def test_well_set_model(testdata_folder: Path) -> None:
wellfiles = [
testdata_folder / "observed_data" / "wells" / well
for well in ["OP_1.w", "OP_2.w", "OP_3.w", "OP_4.w", "OP_5.w", "OP_6.w"]
]

wmodel = WellSetModel(wellfiles=wellfiles)
assert set(wmodel.well_names) == set(
["OP_1", "OP_2", "OP_3", "OP_4", "OP_5", "OP_6"]
)
for name, well in wmodel.wells.items():
assert isinstance(name, str)
assert isinstance(well, xtgeo.Well)
op_6 = wmodel.get_well("OP_6")
assert isinstance(op_6, xtgeo.Well)
assert op_6.name == "OP_6"


@pytest.mark.usefixtures("app")
def test_logs(testdata_folder: Path) -> None:
wmodel = WellSetModel(
wellfiles=[testdata_folder / "observed_data" / "wells" / "OP_6.w"],
zonelog="Zonelog",
)
well = wmodel.get_well("OP_6")
assert well.zonelogname == "Zonelog"


@pytest.mark.usefixtures("app")
def test_tvd_truncation(testdata_folder: Path) -> None:
wmodel = WellSetModel(
wellfiles=[testdata_folder / "observed_data" / "wells" / "OP_6.w"],
tvdmin=1000,
tvdmax=1500,
)
well = wmodel.get_well("OP_6")
assert well.dataframe["Z_TVDSS"].min() >= 1000
assert well.dataframe["Z_TVDSS"].max() <= 1501


@pytest.mark.usefixtures("app")
def test_get_fence(testdata_folder: Path) -> None:
wmodel = WellSetModel(
wellfiles=[testdata_folder / "observed_data" / "wells" / "OP_6.w"],
zonelog="Zonelog",
)
fence = wmodel.get_fence("OP_6")
assert isinstance(fence, np.ndarray)
# Test horizontal length
assert int(fence[:, 3].min()) == -40
assert int(fence[:, 3].max()) == 2713
# Test tvd
assert int(fence[:, 2].min()) == 1
assert int(fence[:, 2].max()) == 1643
Loading

0 comments on commit 1f7e1f9

Please sign in to comment.