Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Well log viewer #733

Merged
merged 2 commits into from
Aug 24, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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