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

Merge v0.2.2 to develop branch #60

Merged
merged 17 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
9 changes: 7 additions & 2 deletions las_geoh5/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@
# (see LICENSE file at the root of this source code package).
#

# flake8: noqa

from __future__ import annotations

from pathlib import Path


__version__ = "0.3.0-alpha.1"


Expand All @@ -25,3 +24,9 @@ def assets_path() -> Path:
raise RuntimeError(f"Assets folder not found: {assets_folder}")

return assets_folder


__all__ = [
"__version__",
"assets_path",
]
9 changes: 2 additions & 7 deletions las_geoh5/import_files/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@
from pathlib import Path
from shutil import move

import lasio
from geoh5py import Workspace
from geoh5py.shared.utils import fetch_active_workspace
from geoh5py.ui_json import InputFile
from tqdm import tqdm

from las_geoh5.import_files.params import ImportOptions, NameOptions
from las_geoh5.import_las import las_to_drillhole
from las_geoh5.import_las import las_to_drillhole, lasio_read


_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -113,11 +112,7 @@ def run(params_json: Path, output_geoh5: Path | None = None):
for file in tqdm(
ifile.data["files"].split(";"), desc="Reading LAS files"
):
futures.append(
pool.apply_async(
lasio.read, (file,), {"mnemonic_case": "preserve"}
)
)
futures.append(pool.apply_async(lasio_read, (file,)))

lasfiles = [future.get() for future in futures]

Expand Down
121 changes: 120 additions & 1 deletion las_geoh5/import_las.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from __future__ import annotations

import logging
import re
from pathlib import Path
from typing import Any

Expand All @@ -24,6 +25,9 @@
from las_geoh5.import_files.params import ImportOptions, NameOptions


_logger = logging.getLogger(__name__)


class LASTranslator:
"""Translator for the weakly standardized LAS file standard."""

Expand Down Expand Up @@ -182,7 +186,7 @@
survey = Path(survey)

if survey.suffix == ".las":
file = lasio.read(survey, mnemonic_case="preserve")
file = lasio_read(survey)
try:
surveys = np.c_[get_depths(file)["depth"], file["DIP"], file["AZIM"]]
if len(drillhole.surveys) == 1:
Expand Down Expand Up @@ -309,6 +313,8 @@
translator = LASTranslator(NameOptions())

name = translator.retrieve("well_name", lasfile)
if not isinstance(name, str):
name = str(name)
if not name and logger is not None:
logger.warning(
"No well name provided for LAS file. "
Expand Down Expand Up @@ -414,3 +420,116 @@
new_row = drillhole.surveys[0, :]
new_row[0] = np.max(depths)
drillhole.surveys = np.vstack([drillhole.surveys, new_row])


def _patch_lasio_reader():
"""Patch lasio.reader.configure_metadata_patterns to handle edge cases."""

# patch only once
if getattr(lasio.reader, "patched_configure_metadata_patterns", False):
return

Check warning on line 430 in las_geoh5/import_las.py

View check run for this annotation

Codecov / codecov/patch

las_geoh5/import_las.py#L430

Added line #L430 was not covered by tests

_logger.debug("Patching lasio.reader.configure_metadata_patterns")

# TODO: Propose change on lasio to fix possible version issue

def configure_metadata_patterns(line, section_name): # pylint: disable=too-many-locals
"""Configure regular-expression patterns to parse section meta-data lines.

# OVERLOAD lasio.reader.configure_metadata_patterns

Arguments:
line (str): line from LAS header section
section_name (str): Name of the section the 'line' is from.

Returns:
An array of regular-expression strings (patterns).
"""

# Default return value
patterns = []

# Default regular expressions for name, value and desc fields
name_re = r"\.?(?P<name>[^.]*)\."
value_re = r"(?P<value>.*):"
desc_re = r"(?P<descr>.*)"

# Default regular expression for unit field. Note that we
# attempt to match "1000 psi" as a special case which allows
# a single whitespace character, in contradiction to the LAS specification
# See GitHub issue #363 for details.
if "VERS" in line:
unit_re = r"(?P<unit>\D*)"
else:
unit_re = r"(?P<unit>([0-9]+\s)?[^\s]*)"

# Alternate regular expressions for special cases
name_missing_period_re = r"(?P<name>[^:]*):"
value_missing_period_re = r"(?P<value>.*)"
value_without_colon_delimiter_re = r"(?P<value>[^:]*)"
value_with_time_colon_re = (
r"(?P<value>.*?)(?:(?<!( [0-2][0-3]| hh| HH)):(?!([0-5][0-9]|mm|MM)))"
)
name_with_dots_re = r"\.?(?P<name>[^.].*[.])\."
no_desc_re = ""
no_unit_re = ""

# Configure special cases
# 1. missing period (assume that only name and value are present)
# 2. missing colon delimiter and description field
# 3. double_dots '..' caused by mnemonic abbreviation (with period)
# next to the dot delimiter.
if ":" in line:
if "." not in line[: line.find(":")]:
# If there is no period, then we assume that the colon exists and
# everything on the left is the name, and everything on the right
# is the value - therefore no unit or description field.
name_re = name_missing_period_re
value_re = value_missing_period_re
desc_re = no_desc_re
unit_re = no_unit_re
value_with_time_colon_re = value_missing_period_re

Check warning on line 491 in las_geoh5/import_las.py

View check run for this annotation

Codecov / codecov/patch

las_geoh5/import_las.py#L487-L491

Added lines #L487 - L491 were not covered by tests

if ":" not in line:
# If there isn't a colon delimiter then there isn't
# a description field either.
value_re = value_without_colon_delimiter_re
desc_re = no_desc_re

Check warning on line 497 in las_geoh5/import_las.py

View check run for this annotation

Codecov / codecov/patch

las_geoh5/import_las.py#L496-L497

Added lines #L496 - L497 were not covered by tests

if ".." in line and section_name == "Curves":
name_re = name_with_dots_re

Check warning on line 500 in las_geoh5/import_las.py

View check run for this annotation

Codecov / codecov/patch

las_geoh5/import_las.py#L499-L500

Added lines #L499 - L500 were not covered by tests
else:
if re.search(r"[^ ]\.\.", line) and section_name == "Curves":
double_dot = line.find("..")
desc_colon = line.rfind(":")

Check warning on line 504 in las_geoh5/import_las.py

View check run for this annotation

Codecov / codecov/patch

las_geoh5/import_las.py#L503-L504

Added lines #L503 - L504 were not covered by tests

# Check that a double_dot is not in the
# description string.
if double_dot < desc_colon:
name_re = name_with_dots_re

Check warning on line 509 in las_geoh5/import_las.py

View check run for this annotation

Codecov / codecov/patch

las_geoh5/import_las.py#L508-L509

Added lines #L508 - L509 were not covered by tests

if section_name == "Parameter":
# Search for a value entry with a time-value first.
pattern = name_re + unit_re + value_with_time_colon_re + desc_re
patterns.append(pattern)

Check warning on line 514 in las_geoh5/import_las.py

View check run for this annotation

Codecov / codecov/patch

las_geoh5/import_las.py#L513-L514

Added lines #L513 - L514 were not covered by tests

# Add the regular pattern for all section_names
# for the Parameter section this will run after time-value pattern
pattern = name_re + unit_re + value_re + desc_re
patterns.append(pattern)

return patterns

lasio.reader.configure_metadata_patterns = configure_metadata_patterns
lasio.reader.patched_configure_metadata_patterns = True


def lasio_read(file):
"""Read a LAS file using lasio.

Wrapper around lasio.read that patches the reader to handle some
edge cases in LAS files.
"""

_patch_lasio_reader()
return lasio.read(file, mnemonic_case="preserve")
32 changes: 32 additions & 0 deletions tests/import_las_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,3 +415,35 @@ def test_warning_no_well_name(tmp_path: Path, caplog):
)

assert match in caplog.text


def test_handle_numeric_well_name(tmp_path: Path):
with Workspace.create(tmp_path / "test.geoh5") as workspace:
dh_group = DrillholeGroup.create(workspace, name="dh_group")

lasfile = generate_lasfile(
"123",
{"UTMX": 0.0, "UTMY": 0.0, "ELEV": 10.0},
np.arange(0, 11, 1),
{"my_property": np.zeros(11)},
)
lasfile = write_lasfile(tmp_path, lasfile)

filepath = write_import_params_file(
tmp_path / "import_las_files.ui.json",
dh_group,
"my_property_group",
[lasfile],
(
"UTMX",
"UTMY",
"ELEV",
),
)

module = importlib.import_module("las_geoh5.import_files.driver")
module.run(filepath)

with workspace.open():
dh_group = workspace.get_entity("dh_group")[0]
assert "123" in [k.name for k in dh_group.children]
5 changes: 5 additions & 0 deletions tests/script_las_to_geoh5_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .helpers import generate_lasfile, write_import_params_file, write_lasfile


# pylint: disable=duplicate-code
@pytest.fixture(scope="module", name="lasfile")
def lasfile_fixture(tmp_path_factory) -> Path:
input_dir = tmp_path_factory.mktemp("input")
Expand Down Expand Up @@ -75,6 +76,7 @@ def test_las_to_geoh5_without_output_name(
"""Test the las_to_geoh5 script."""

workspace_file = input_workspace.h5file
assert isinstance(workspace_file, Path)
modified_date = workspace_file.stat().st_mtime

with patch("sys.argv", ["las_to_geoh5", str(params_filepath)]):
Expand All @@ -93,6 +95,7 @@ def test_las_to_geoh5_with_monitoring_folder(
"""Test the las_to_geoh5 script."""

workspace_file = input_workspace.h5file
assert isinstance(workspace_file, Path)
modified_date = workspace_file.stat().st_mtime

monitoring_folder = tmp_path / "monitored here"
Expand Down Expand Up @@ -124,6 +127,7 @@ def test_las_to_geoh5_with_output_name(
"""Test the las_to_geoh5 script."""

workspace_file = input_workspace.h5file
assert isinstance(workspace_file, Path)
modified_date = workspace_file.stat().st_mtime

working_dir = tmp_path / "working"
Expand All @@ -150,6 +154,7 @@ def test_las_to_geoh5_with_absolute_output_path(
"""Test the las_to_geoh5 script."""

workspace_file = input_workspace.h5file
assert isinstance(workspace_file, Path)
modified_date = workspace_file.stat().st_mtime

output_dir = tmp_path / "output"
Expand Down
Loading