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

GEOPY-1320: Add collocation tolerance option in ui.json and import_files driver #26

Merged
merged 14 commits into from
Mar 5, 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
10 changes: 9 additions & 1 deletion las_geoh5/import_directories/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from geoh5py.shared.utils import fetch_active_workspace
from geoh5py.ui_json import InputFile

from las_geoh5.import_files.params import ImportOptions
from las_geoh5.import_las import las_to_drillhole


Expand Down Expand Up @@ -63,7 +64,14 @@ def import_las_directory(
for file in [k for k in prop.iterdir() if k.suffix == ".las"]:
lasfiles.append(lasio.read(file, mnemonic_case="preserve"))
print(f"Importing property group data from to {prop.name}")
las_to_drillhole(workspace, lasfiles, dh_group, prop.name, surveys)
las_to_drillhole(
workspace,
lasfiles,
dh_group,
prop.name,
surveys,
options=ImportOptions(),
)

return dh_group

Expand Down
17 changes: 6 additions & 11 deletions las_geoh5/import_files/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
from geoh5py.ui_json import InputFile
from tqdm import tqdm

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

logger = logging.getLogger("Import Files")
logger.setLevel(logging.INFO)
Expand Down Expand Up @@ -61,13 +62,6 @@ def run(filepath: str): # pylint: disable=too-many-locals
ifile.data["geoh5"].h5file.stem,
)

translator = LASTranslator(
depth=ifile.data["depths_name"],
collar_x=ifile.data["collar_x_name"],
collar_y=ifile.data["collar_y_name"],
collar_z=ifile.data["collar_z_name"],
)

workspace = Workspace()
begin_reading = time()

Expand Down Expand Up @@ -95,14 +89,15 @@ def run(filepath: str): # pylint: disable=too-many-locals
ifile.data["name"],
)
begin_saving = time()

name_options = NameOptions(**ifile.data)
import_options = ImportOptions(names=name_options, **ifile.data)
las_to_drillhole(
workspace,
lasfiles,
dh_group,
ifile.data["name"],
translator=translator,
skip_empty_header=ifile.data["skip_empty_header"],
logger=logger if ifile.data["warnings"] else None,
options=import_options,
)
end_saving = time()
logger.info(
Expand Down
55 changes: 55 additions & 0 deletions las_geoh5/import_files/params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright (c) 2024 Mira Geoscience Ltd.
#
# This file is part of las-geoh5 package.
#
# All rights reserved.
#

from pydantic import BaseModel, ConfigDict, model_validator

LAS_GEOH5_STANDARD = {
"depth_name": "DEPTH",
"collar_x_name": "X",
"collar_y_name": "Y",
"collar_z_name": "ELEV",
}


class NameOptions(BaseModel):
"""
Stores options for naming of dillhole parameters in las files.

:param depth_name: Name of the depth field.
:param collar_x_name: Name of the collar x field.
:param collar_y_name: Name of the collar y field.
:param collar_z_name: Name of the collar z field.
"""

well_name: str = "WELL"
depth_name: str = "DEPTH"
collar_x_name: str = "X"
collar_y_name: str = "Y"
collar_z_name: str = "ELEV"

@model_validator(mode="before")
@classmethod
def skip_none_value(cls, data: dict) -> dict:
return {k: v for k, v in data.items() if v is not None}

domfournier marked this conversation as resolved.
Show resolved Hide resolved

class ImportOptions(BaseModel):
"""
Stores options for the drillhole import.

:param names: Options for naming of dillhole parameters in las files.
:param collocation_tolerance: Tolerance for collocation of collar and depth data.
:param warnings: Whether to show warnings.
:param skip_empty_header: Whether to skip empty headers.
"""

model_config = ConfigDict(arbitrary_types_allowed=True)

names: NameOptions = NameOptions()
collocation_tolerance: float = 0.01
warnings: bool = True
skip_empty_header: bool = False
10 changes: 9 additions & 1 deletion las_geoh5/import_files/uijson.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,22 @@
"optional": True,
"enabled": False,
},
"collocation_tolerance": {
"label": "Collocation tolerance",
"value": 0.01,
"tooltip": (
"Tolerance for determining collocation of data locations "
"and ultimately deciding if incoming data should belong to "
"an existing property group.",
),
},
"skip_empty_header": {
"label": "Skip empty header",
"value": False,
"tooltip": (
"Importing files without collar information "
"results in drillholes placed at the origin. "
"Check this box to skip these files."
""
),
},
"warnings": {
Expand Down
99 changes: 47 additions & 52 deletions las_geoh5/import_las.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from __future__ import annotations

import logging
from enum import Enum
from pathlib import Path
from typing import Any

Expand All @@ -22,61 +21,45 @@
from geoh5py.shared.concatenation import ConcatenatedDrillhole
from tqdm import tqdm

from las_geoh5.import_files.params import ImportOptions, NameOptions


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

class Standards(Enum):
"""Standardized field names for las files."""

WELL = "well"
DEPTH = "depth"
X = "collar_x"
Y = "collar_y"
ELEV = "collar_z"

def __init__(
self,
well: str | None = None,
depth: str | None = None,
collar_x: str | None = None,
collar_y: str | None = None,
collar_z: str | None = None,
):
self.well = well or self.Standards.WELL.name
self.depth = depth or self.Standards.DEPTH.name
self.collar_x = collar_x or self.Standards.X.name
self.collar_y = collar_y or self.Standards.Y.name
self.collar_z = collar_z or self.Standards.ELEV.name
def __init__(self, names: NameOptions):
self.names = names

def translate(self, field: str):
"""
Return translated field name or raise KeyError if field not recognized.
Return translated field name or rais KeyError if field not recognized.

:param field: Standardized field name.

:return: Name of corresponding field in las file.
"""
if field not in [s.value for s in self.Standards]:
if field not in dict(self.names):
raise KeyError(f"'{field}' is not a recognized field.")

return getattr(self, field)
return getattr(self.names, field)

def retrieve(self, field, lasfile):
def retrieve(self, field: str, lasfile: lasio.LASFile):
"""
Access las data using translation.

:param field: Name of field to retrieve.
:param lasfile: lasio file object.

:return: data stored in las file under translated field name.
"""
if getattr(self, field) in lasfile.well:
out = lasfile.well[getattr(self, field)].value
elif getattr(self, field) in lasfile.curves:
out = lasfile.curves[getattr(self, field)].data
elif getattr(self, field) in lasfile.params:
out = lasfile.params[getattr(self, field)].value
if getattr(self.names, field) in lasfile.well:
out = lasfile.well[getattr(self.names, field)].value
elif getattr(self.names, field) in lasfile.curves:
out = lasfile.curves[getattr(self.names, field)].data
elif getattr(self.names, field) in lasfile.params:
out = lasfile.params[getattr(self.names, field)].value
else:
msg = f"'{field}' field: '{getattr(self, field)}' not found in las file."
msg = f"'{field}' field: '{getattr(self.names, field)}' not found in las file."
raise KeyError(msg)

return out
Expand Down Expand Up @@ -129,10 +112,10 @@
"""

if translator is None:
translator = LASTranslator()
translator = LASTranslator(names=NameOptions())

collar = []
for field in ["collar_x", "collar_y", "collar_z"]:
for field in ["collar_x_name", "collar_y_name", "collar_z_name"]:
collar_coord = 0.0
try:
collar_coord = translator.retrieve(field, lasfile)
Expand All @@ -146,7 +129,7 @@
if logger is not None:
logger.warning(
f"{field.replace('_', ' ').capitalize()} field "
f"'{getattr(translator, field)}' not found in las file."
f"'{getattr(translator.names, field)}' not found in las file."
f" Setting coordinate to 0.0. Non-null header fields include: "
f"{options}."
)
Expand Down Expand Up @@ -229,13 +212,15 @@
drillhole: ConcatenatedDrillhole,
lasfile: lasio.LASFile,
group_name: str,
collocation_tolerance: float = 0.01,
) -> ConcatenatedDrillhole:
"""
Add data from las file curves to drillhole.

:param drillhole: Drillhole object to append data to.
:param lasfile: Las file object.
:param property_group: Property group.
:param group_name: Property group name.
:param collocation_tolerance: Tolerance for determining collocation of data.

:return: Updated drillhole object.
"""
Expand Down Expand Up @@ -284,7 +269,9 @@
]
if root_name_matches:
group = [
g for g in root_name_matches if g.is_collocated(locations, 0.01)
g
for g in root_name_matches
if g.is_collocated(locations, collocation_tolerance)
]
if group:
group_name = group[0].name
Expand All @@ -296,12 +283,13 @@
return drillhole


def create_or_append_drillhole(
def create_or_append_drillhole( # pylint: disable=too-many-arguments
workspace: Workspace,
lasfile: lasio.LASFile,
drillhole_group: DrillholeGroup,
group_name: str,
translator: LASTranslator | None = None,
collocation_tolerance: float = 0.01,
logger: logging.Logger | None = None,
) -> ConcatenatedDrillhole:
"""
Expand All @@ -312,20 +300,22 @@
:param drillhole_group: Drillhole group container.
:param group_name: Property group name.
:param translator: Translator for las file.
:param collocation_tolerance: Tolerance for determining collocation of data.
:param logger: Logger object if warnings are enabled.

:return: Created or augmented drillhole.
"""

if translator is None:
translator = LASTranslator()
translator = LASTranslator(NameOptions())

Check warning on line 310 in las_geoh5/import_las.py

View check run for this annotation

Codecov / codecov/patch

las_geoh5/import_las.py#L310

Added line #L310 was not covered by tests

name = translator.retrieve("well", lasfile)
name = translator.retrieve("well_name", lasfile)
if not name and logger is not None:
logger.warning(
"No well name provided for las file. Saving drillhole with "
"name 'Unknown'."
"No well name provided for las file. "
"Saving drillhole with name 'Unknown'."
)

collar = get_collar(lasfile, translator, logger)
drillhole = drillhole_group.get_entity(name)[0] # type: ignore

Expand All @@ -348,7 +338,9 @@
f"Drillhole {name} exists in workspace but is not a Drillhole object."
)

drillhole = add_data(drillhole, lasfile, group_name)
drillhole = add_data(
drillhole, lasfile, group_name, collocation_tolerance=collocation_tolerance
)

return drillhole

Expand All @@ -359,9 +351,8 @@
drillhole_group: DrillholeGroup,
property_group: str,
survey: Path | list[Path] | None = None,
translator: LASTranslator | None = None,
skip_empty_header: bool = False,
logger: logging.Logger | None = None,
options: ImportOptions | None = None,
):
"""
Import a las file containing collocated datasets for a single drillhole.
Expand All @@ -371,23 +362,26 @@
:param drillhole_group: Drillhole group container.
:param property_group: Property group name.
:param survey: Path to a survey file stored as .csv or .las format.
:param translator: Translator for las file.
:param skip_empty_header: Skip empty header data.
:param logger: Logger object if warnings are enabled.
:param options: Import options covering name translations, collocation
tolerance, and warnings control.

:return: A :obj:`geoh5py.objects.Drillhole` object
"""

if options is None:
options = ImportOptions()

Check warning on line 373 in las_geoh5/import_las.py

View check run for this annotation

Codecov / codecov/patch

las_geoh5/import_las.py#L373

Added line #L373 was not covered by tests

translator = LASTranslator(names=options.names)

if not isinstance(data, list):
data = [data]
if not isinstance(survey, list):
survey = [survey] if survey else []
if translator is None:
translator = LASTranslator()

for datum in tqdm(data, desc="Adding drillholes and data to workspace"):
collar = get_collar(datum, translator, logger)
if all(k == 0 for k in collar) and skip_empty_header:
if all(k == 0 for k in collar) and options.skip_empty_header:
continue

drillhole = create_or_append_drillhole(
Expand All @@ -397,6 +391,7 @@
property_group,
translator=translator,
logger=logger,
collocation_tolerance=options.collocation_tolerance,
)
ind = [drillhole.name == s.name.rstrip(".las") for s in survey]
if any(ind):
Expand Down
Loading
Loading