Skip to content

Commit

Permalink
Well Log Viewer
Browse files Browse the repository at this point in the history
  • Loading branch information
HansKallekleiv committed Aug 24, 2021
1 parent c2e9e62 commit 4229d7a
Show file tree
Hide file tree
Showing 10 changed files with 288 additions and 9 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/subsurface.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ jobs:
env:
# If you want the CI to (temporarily) run against your fork of the testdada,
# change the value her from "equinor" to your username.
TESTDATA_REPO_OWNER: equinor
TESTDATA_REPO_OWNER: hanskallekleiv
# If you want the CI to (temporarily) run against another branch than master,
# change the value her from "master" to the relevant branch name.
TESTDATA_REPO_BRANCH: master
TESTDATA_REPO_BRANCH: well-log-viewer
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
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [UNRELEASED] - YYYY-MM-DD
### Added
- [#733](https://github.com/equinor/webviz-subsurface/pull/733) - Added plugin to visualize well logs from files using [videx-welllog](https://github.com/equinor/videx-wellog).
- [#708](https://github.com/equinor/webviz-subsurface/pull/708) - Added support for new report format for `DiskUsage`, which improves the estimate of free disk space.

### Changed
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"WellCrossSectionFMU = webviz_subsurface.plugins:WellCrossSectionFMU",
"AssistedHistoryMatchingAnalysis = webviz_subsurface.plugins:AssistedHistoryMatchingAnalysis",
"WellCompletions = webviz_subsurface.plugins:WellCompletions",
"WellLogViewer = webviz_subsurface.plugins:WellLogViewer",
],
"ert": ["webviz_subsurface_jobs = webviz_subsurface.ert_jobs.jobs"],
"console_scripts": [
Expand Down
8 changes: 1 addition & 7 deletions webviz_subsurface/_models/well_set_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,6 @@ def load_well(
lognames: Optional[List[str]] = None,
) -> xtgeo.Well:
lognames = [] if not lognames else lognames
if zonelogname is not None and zonelogname not in lognames:
lognames.append(zonelogname)
if mdlogname is not None and mdlogname not in lognames:
lognames.append(mdlogname)
well = xtgeo.Well(
wfile=wfile, zonelogname=zonelogname, mdlogname=mdlogname, lognames=lognames
)
well = xtgeo.Well(wfile=wfile, zonelogname=zonelogname, mdlogname=mdlogname)

return well
1 change: 1 addition & 0 deletions webviz_subsurface/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,6 @@
from ._volumetric_analysis import VolumetricAnalysis
from ._well_cross_section import WellCrossSection
from ._well_cross_section_fmu import WellCrossSectionFMU
from ._well_log_viewer import WellLogViewer
from ._assisted_history_matching_analysis import AssistedHistoryMatchingAnalysis
from ._well_completions import WellCompletions
1 change: 1 addition & 0 deletions webviz_subsurface/plugins/_well_log_viewer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .well_log_viewer import WellLogViewer
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from typing import List, Dict, Any
from pathlib import Path

import yaml


def load_and_validate_log_templates(log_templates: List[Path]) -> Dict[str, Any]:
validated_templates = {}
for idx, template_path in enumerate(log_templates):
template = yaml.safe_load(template_path.read_text())
# Validate against json schema here when available
# https://github.com/equinor/webviz-subsurface-components/issues/508
template_name = template.get("name", f"template_{idx+1}")
validated_templates[template_name] = template
return validated_templates
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from ._well_controller import well_controller
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from typing import List, Dict, Callable, Tuple, Any, Optional
import xtgeo
import dash
from dash.dependencies import Input, Output
from webviz_subsurface._models.well_set_model import WellSetModel


def well_controller(
app: dash.Dash,
well_set_model: WellSetModel,
log_templates: Dict,
get_uuid: Callable,
) -> None:
@app.callback(
Output(get_uuid("well-log-viewer"), "welllog"),
Output(get_uuid("well-log-viewer"), "template"),
Input(get_uuid("well"), "value"),
Input(get_uuid("template"), "value"),
)
def _update_log_data(well_name: str, template: str) -> Tuple[Any, Any]:
well = well_set_model.get_well(well_name)
return xtgeo_well_logs_to_json_format(well), log_templates.get(template)


def xtgeo_well_logs_to_json_format(well: xtgeo.Well) -> List[Dict]:

header = generate_header(well_name=well.name)
curves = []

# Calculate well geometrics if MD log is not provided
if well.mdlogname is None:
well.geometrics()

# Add MD and TVD curves
curves.append(generate_curve(log_name="MD", description="Measured depth"))
curves.append(
generate_curve(log_name="TVD", description="True vertical depth (SS)")
)
# Add additonal logs, skipping geometrical logs if calculated
lognames = [
logname
for logname in well.lognames
if logname not in ["Q_MDEPTH", "Q_AZI", "Q_INCL", "R_HLEN"]
]
for logname in lognames:
curves.append(generate_curve(log_name=logname.upper()))

# Filter dataframe to only include relevant logs
curve_names = [well.mdlogname, "Z_TVDSS"] + lognames
dframe = well.dataframe[curve_names]
dframe = dframe.reindex(curve_names, axis=1)

return [{"header": header, "curves": curves, "data": dframe.values.tolist()}]


def generate_header(well_name: str, logrun_name: str = "log") -> Dict[str, Any]:
return {
"name": logrun_name,
"well": well_name,
"wellbore": None,
"field": None,
"country": None,
"date": None,
"operator": None,
"serviceCompany": None,
"runNumber": None,
"elevation": None,
"source": None,
"startIndex": None,
"endIndex": None,
"step": None,
"dataUri": None,
}


def generate_curve(
log_name: str, description: Optional[str] = None, value_type: str = "float"
) -> Dict[str, Any]:
return {
"name": log_name,
"description": description,
"valueType": value_type,
"dimensions": 1,
"unit": "m",
"quantity": None,
"axis": None,
"maxSize": 20,
}
177 changes: 177 additions & 0 deletions webviz_subsurface/plugins/_well_log_viewer/well_log_viewer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
from typing import List, Dict, Union, Tuple, Callable
from pathlib import Path
import json

import dash
import dash_html_components as html
from webviz_config import WebvizPluginABC
import webviz_core_components as wcc
from webviz_subsurface_components import WellLogViewer as WellLogViewerComponent

from webviz_subsurface._models.well_set_model import WellSetModel
from webviz_subsurface._utils.webvizstore_functions import find_files, get_path
from .controllers import well_controller
from ._validate_log_templates import load_and_validate_log_templates


class WellLogViewer(WebvizPluginABC):
"""Uses [videx-welllog](https://github.com/equinor/videx-wellog) to visualize well logs
from files stored in RMS well format.
?> Currently tracks for visualizing discrete logs are not included. This will
be added in later releases.
---
* **`wellfolder`:** Path to a folder with well files stored in RMS well format.
* **`wellsuffix`:** File suffix of well files
* **`logtemplates`:** List of yaml based log template configurations. \
See the data section for description of the format.
* **`mdlog`:** Name of the md log. If not specified, MD will be calculated.
* **`well_tvdmin`:** Truncate well data values above this depth.
* **`well_tvdmax`:** Truncate well data values below this depth.
* **`well_downsample_interval`:** Sampling interval used for coarsening a well trajectory
* **`initial_settings`:** Configuration for initializing the plugin with various \
properties set. All properties are optional.
See the data section for available properties.
---
?> The format and documentation of the log template configuration will be improved \
in later releases. A small configuration sample is provided below.
```yaml
name: All logs # Name of the log template
scale:
primary: MD # Which reference track to visualize as default (MD/TVD)
allowSecondary: False # Set to True to show both MD and TVD reference tracks.
tracks: # The list of log tracks
- title: Porosity # Optional title of log track
plots: # List of which logs to include in the track
- name: PHIT # Upper case name of log
type: area # Type of visualiation (area, line, linestep, dot)
color: green # Color of log
- name: PHIT_ORIG
type: line
- plots:
- name: ZONE
type: area
- plots:
- name: FACIES
type: area
- plots:
- name: VSH
type: area
- plots:
- name: SW
type: dot
styles: # List of styles that can be added to tracks
```
Format of the `initial_settings` argument:
```yaml
initial_settings:
well: str # Name of well
logtemplate: str # Name of log template
```
"""

# pylint: disable=too-many-arguments
def __init__(
self,
app: dash.Dash,
wellfolder: Path,
logtemplates: List[Path],
wellsuffix: str = ".w",
mdlog: str = None,
well_tvdmin: Union[int, float] = None,
well_tvdmax: Union[int, float] = None,
well_downsample_interval: int = None,
initial_settings: Dict = None,
):

super().__init__()
self._wellfolder = wellfolder
self._wellsuffix = wellsuffix
self._logtemplatefiles = logtemplates
self._wellfiles: List = json.load(
find_files(folder=self._wellfolder, suffix=self._wellsuffix)
)
self._log_templates = load_and_validate_log_templates(
[get_path(fn) for fn in self._logtemplatefiles]
)
self._well_set_model = WellSetModel(
self._wellfiles,
mdlog=mdlog,
tvdmin=well_tvdmin,
tvdmax=well_tvdmax,
downsample_interval=well_downsample_interval,
)
self._initial_settings = initial_settings if initial_settings else {}
self.set_callbacks(app)

@property
def layout(self) -> html.Div:
return wcc.FlexBox(
[
wcc.Frame(
style={"height": "90vh", "flex": 1},
children=[
wcc.Dropdown(
label="Well",
id=self.uuid("well"),
options=[
{"label": name, "value": name}
for name in self._well_set_model.well_names
],
value=self._initial_settings.get(
"well_name", self._well_set_model.well_names[0]
),
clearable=False,
),
wcc.Dropdown(
label="Log template",
id=self.uuid("template"),
options=[
{"label": name, "value": name}
for name in list(self._log_templates.keys())
],
value=self._initial_settings.get(
"logtemplate", list(self._log_templates.keys())[0]
),
clearable=False,
),
],
),
wcc.Frame(
style={"flex": 6, "height": "90vh"},
children=[
WellLogViewerComponent(
id=self.uuid("well-log-viewer"),
template=self._log_templates[
list(self._log_templates.keys())[0]
],
)
],
),
]
)

def set_callbacks(self, app: dash.Dash) -> None:
well_controller(
app=app,
well_set_model=self._well_set_model,
log_templates=self._log_templates,
get_uuid=self.uuid,
)

def add_webvizstore(self) -> List[Tuple[Callable, list]]:
store_functions = [
(find_files, [{"folder": self._wellfolder, "suffix": self._wellsuffix}])
]
store_functions.extend([(get_path, [{"path": fn}]) for fn in self._wellfiles])
store_functions.extend(
[(get_path, [{"path": fn}]) for fn in self._logtemplatefiles]
)
return store_functions

0 comments on commit 4229d7a

Please sign in to comment.