Skip to content

GEOPY-3013: Support Collar dip and Collar azimuth drillhole attributes - #962

Merged
domfournier merged 12 commits into
developfrom
GEOPY-3013
Aug 4, 2026
Merged

GEOPY-3013: Support Collar dip and Collar azimuth drillhole attributes#962
domfournier merged 12 commits into
developfrom
GEOPY-3013

Conversation

@domfournier

@domfournier domfournier commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

GEOPY-3013 - Support Collar dip and Collar azimuth drillhole attributes

Copilot AI lite review requested due to automatic review settings July 29, 2026 18:43
@github-actions github-actions Bot changed the title GEOPY-3013 GEOPY-3013: Support Collar dip and Collar azimuth drillhole attributes Jul 29, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Not ready to approve

The current surveys=None defaulting logic can produce incorrect dips (e.g., collar_dip=0.0) and can unintentionally overwrite an explicitly provided end_of_hole.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

This PR adds support for defining a drillhole’s initial orientation via collar azimuth and collar dip, and uses those values to drive desurveying when explicit survey data isn’t provided.

Changes:

  • Add collar_azimuth and collar_dip attributes to Drillhole and related attribute key maps.
  • Update Drillhole.surveys defaulting behavior to derive a single survey point from collar azimuth/dip when surveys are omitted.
  • Add a regression test validating desurvey results for collar azimuth/dip without surveys.
File summaries
File Description
tests/objects/drillhole_data_test.py Adds a test for collar azimuth/dip desurvey behavior; standardizes temp workspace filename creation.
geoh5py/shared/utils.py Extends key mapping to recognize “Collar azimuth” and “Collar dip” attributes.
geoh5py/objects/drillhole.py Introduces collar azimuth/dip attributes and uses them to default surveys when surveys are not provided.
Review details

Comments suppressed due to low confidence (1)

geoh5py/objects/drillhole.py:332

  • When surveys=None, the default dip/azimuth uses or, which treats valid collar_dip=0.0 as falsy and incorrectly falls back to -90.0. Also, the setter always overwrites end_of_hole from the (default) surveys, which clobbers any explicit end_of_hole passed during initialization.
    def surveys(self, array: np.ndarray | list | tuple | None):
        if array is None:
            array = np.c_[0, self._collar_azimuth or 0.0, self._collar_dip or -90.0]

        if not isinstance(array, (np.ndarray, list, tuple)):
            raise TypeError(
                "Input 'surveys' must be of type 'numpy.ndarray' or 'list'."
            )

        self._surveys = self.format_survey_values(array)
        self.end_of_hole = float(self._surveys["Depth"][-1])

  • Files reviewed: 3/3 changed files
  • Comments generated: 0
  • Review effort level: Low

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Copilot AI review requested due to automatic review settings July 30, 2026 17:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Not ready to approve

The updated drillhole survey handling has correctness issues (defaulting via or and structured-array formatting) and a key test assertion was commented out, reducing regression protection.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Comments suppressed due to low confidence (3)

geoh5py/objects/drillhole.py:383

  • format_survey_values currently transposes structured/recarray inputs and, when dropping the optional Info field, slices records (values[:-1]) instead of fields. This will break callers that pass a structured surveys array (e.g., tests that use a 4-field dtype with Info) and can raise at np.rec.fromarrays.
        # Already a recarray with proper type
        if values.dtype.names == dtype.names:
            return values

        if np.issubdtype(values.dtype, np.number):
            n_fields = values.shape[1]
        else:
            n_fields = len(values.dtype.names)

        if n_fields not in [3, 4]:
            raise ValueError("'surveys' requires an ndarray of shape (*, 3) or (*, 4)")

        values = values.T.tolist()

        if n_fields == 3 and len(dtype) == 4:
            values += [np.array([b""] * len(values[0]), dtype=dtype[-1])]
        elif n_fields == 4 and len(dtype) == 3:
            values = values[:-1]

        array_values = np.rec.fromarrays(values, dtype=dtype)

geoh5py/objects/drillhole.py:330

  • Using or to provide defaults for collar_azimuth / collar_dip treats valid 0.0 values as missing (because 0.0 is falsy), which can silently change the default survey direction.

This issue also appears on line 364 of the same file.

    def surveys(self, array: np.ndarray | list | tuple | None):
        if array is None:
            array = np.c_[0, self._collar_azimuth or 0.0, self._collar_dip or -90.0]

tests/objects/drillhole_v4_0_test.py:1156

  • The test is explicitly modifying surveys to include an Info field, but the assertion that verifies Info is present is commented out. This weakens the test and can allow regressions where the extra field is dropped during concatenation/saving.
    assert len(dh.parent.data["Surveys"]) == 35
    # assert "Info" in dh.parent.data["Surveys"].dtype.names

  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Low

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Copilot AI review requested due to automatic review settings July 30, 2026 17:26
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.30%. Comparing base (177344a) to head (0db4ea3).
⚠️ Report is 13 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop     #962      +/-   ##
===========================================
+ Coverage    91.16%   91.30%   +0.14%     
===========================================
  Files          129      129              
  Lines        10873    10877       +4     
  Branches      1985     1982       -3     
===========================================
+ Hits          9912     9931      +19     
+ Misses         504      497       -7     
+ Partials       457      449       -8     
Files with missing lines Coverage Δ
geoh5py/objects/drillhole.py 86.04% <100.00%> (+2.66%) ⬆️
geoh5py/shared/concatenation/concatenator.py 90.62% <100.00%> (+0.08%) ⬆️
geoh5py/shared/concatenation/drillhole.py 86.02% <100.00%> (+3.47%) ⬆️
geoh5py/shared/utils.py 91.14% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Not ready to approve

The updated drillhole survey initialization/loading and survey reformatting contain confirmed logic issues that can break persisted survey handling and structured array conversion.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Comments suppressed due to low confidence (6)

geoh5py/objects/drillhole.py:120

  • __init__ now unconditionally calls self.surveys = surveys. When surveys is None, this eagerly creates a default survey and prevents any lazy loading of an existing on-file surveys array (previously fetched on demand), which can cause reopened drillholes to ignore the surveys stored in the workspace file.
        self.collar = collar
        self.cost = cost
        self.default_collocation_distance = default_collocation_distance
        self.end_of_hole = end_of_hole
        self.planning = planning
        self.surveys = surveys

geoh5py/objects/drillhole.py:317

  • The surveys getter assumes _surveys is always populated in-memory. For entities loaded from disk/concatenation, surveys were previously lazily fetched via workspace.fetch_array_attribute(..., "surveys"); without that, reopened drillholes can return the default survey (or fail) instead of the persisted surveys.
    def surveys(self) -> np.ndarray:
        """
        Coordinates of the surveys.
        """
        surveys = np.vstack(

geoh5py/objects/drillhole.py:106

  • _surveys is no longer initialized on the instance. If __init__ stops calling the surveys setter (or if an on-file entity is constructed without surveys), surveys access will raise AttributeError or behave inconsistently. Initialize _surveys to None like the other cached arrays.

This issue also appears in the following locations of the same file:

  • line 115
  • line 313
        self._depths: FloatData | None = None
        self._trace: np.ndarray | None = None
        self._trace_depth: np.ndarray | None = None
        self._locations = None
        self._intervals: dict | None = None

geoh5py/objects/drillhole.py:370

  • format_survey_values will crash with TypeError: object of type 'NoneType' has no len() if a non-numeric, non-structured ndarray is passed (e.g., dtype '<U' from mixed inputs), because values.dtype.names is None. This should raise a clear TypeError instead.
        if np.issubdtype(values.dtype, np.number):
            n_fields = values.shape[1]
        else:
            n_fields = len(values.dtype.names)
            dtype = np.dtype(SURVEYS_FIELDS[:n_fields])
            if values.dtype.descr != dtype.descr:
                raise TypeError(f"The type of survey array must be numeric or {dtype}")

geoh5py/objects/drillhole.py:385

  • When converting a 4-column input to a 3-field dtype, the code currently does values = [val[:-1] for val in values], which removes the last row from each column array instead of dropping the 4th column. This produces misaligned survey data (length N-1) and will break np.rec.fromarrays.
        if n_fields == 3 and len(dtype) == 4:
            values += [np.array([b""] * len(values[0]), dtype=dtype[-1])]
        elif n_fields == 4 and len(dtype) == 3:
            values = [val[:-1] for val in values]

tests/objects/drillhole_v4_0_test.py:1155

  • This test currently comments out the assertion that the concatenated Surveys dtype contains the Info field. Leaving a disabled assertion makes it easy for regressions in survey-info handling to go unnoticed; update the implementation to satisfy the assertion and keep the coverage.
    assert len(dh.parent.data["Surveys"]) == 35
    # assert "Info" in dh.parent.data["Surveys"].dtype.names
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Low

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Copilot AI review requested due to automatic review settings July 30, 2026 20:12
import numpy as np
import pytest
from h5py import special_dtype
from h5py import special_dtype, string_dtype
Comment thread tests/objects/drillhole_v4_0_test.py Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Not ready to approve

Default survey construction and empty Info handling have correctness risks (e.g., collar_dip=0.0 treated as unset and bytes used for a vlen=str field) that should be fixed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Comments suppressed due to low confidence (5)

tests/objects/drillhole_v4_0_test.py:1136

  • The Info field uses special_dtype(vlen=str), which expects Python str values; appending b"" (bytes) can lead to type issues when building/writing the structured array.
    for val in surveys:
        values.append((*val, b""))

geoh5py/objects/drillhole.py:384

  • When adding an empty Info column, the code currently fills it with b"" (bytes). For special_dtype(vlen=str) (and other string dtypes), values should be Python str (e.g., "") to avoid type mismatches when constructing/writing the structured array.
        if n_fields == 3 and len(dtype) == 4:
            values += [np.array([b""] * len(values[0]))]
        elif n_fields == 4 and len(dtype) == 3:
            dtype = np.dtype(SURVEYS_FIELDS, align=True)

tests/objects/drillhole_v4_0_test.py:1131

  • SURVEYS_FIELDS is imported but not used here, and the survey dtype is duplicated inline. Using the shared constant keeps tests aligned with the production survey dtype and avoids an unused import.
    new_dtype = np.dtype(
        [
            ("Depth", "<f4"),
            ("Azimuth", "<f4"),
            ("Dip", "<f4"),
            ("Info", special_dtype(vlen=str)),
        ],
        align=True,
    )

tests/objects/drillhole_data_test.py:30

  • special_dtype is imported but not used in this test module, which can trigger unused-import checks in some lint configurations.
from h5py import special_dtype, string_dtype

geoh5py/objects/drillhole.py:331

  • Using or to apply defaults treats valid 0.0 values as falsy. In particular, collar_dip=0.0 will be replaced by -90.0 when surveys is None, producing an incorrect default survey.
        if array is None:
            array = np.c_[0, self._collar_azimuth or 0.0, self._collar_dip or -90.0]

  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Low

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Copilot AI review requested due to automatic review settings July 30, 2026 21:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Not ready to approve

The updated survey defaulting/formatting logic has edge-case bugs (e.g., dip=0.0 handling and dtype/shape handling) that can lead to incorrect surveys or runtime errors.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Comments suppressed due to low confidence (3)

geoh5py/objects/drillhole.py:366

  • format_survey_values assumes a 2D array when it computes values.shape[1]. If callers pass a 1D np.ndarray (e.g., np.array([depth, az, dip])), this raises IndexError instead of a clear validation error.

Normalize non-structured ndarrays to at least 2D before accessing shape[1].

        if isinstance(values, (list, tuple)):
            values = np.array(values, ndmin=2)

        if np.issubdtype(values.dtype, np.number) or not values.dtype.names:
            n_fields = values.shape[1]
        else:

geoh5py/objects/drillhole.py:385

  • The logic handling mismatches between n_fields and the requested dtype is backwards:
  • If the input has 4 columns but dtype has 3 fields, the current code forces dtype to 4 fields, which can break callers that explicitly requested a 3-field survey dtype (e.g., concatenation where the stored Surveys dataset is 3-field).
  • When adding an empty Info column, the filler array is created from b"" bytes without matching the vlen-str dtype.

Prefer adapting the values to the requested dtype: drop the 4th column when dtype has 3 fields, and add an empty string column when dtype has 4 fields.

        if n_fields == 3 and len(dtype) == 4:
            values += [np.array([b""] * len(values[0]))]
        elif n_fields == 4 and len(dtype) == 3:
            dtype = np.dtype(SURVEYS_FIELDS, align=True)

geoh5py/objects/drillhole.py:331

  • When surveys are not provided, the default survey uses self._collar_dip or -90.0. This treats a valid dip of 0.0 (horizontal) as falsy and incorrectly replaces it with -90.0.

Use an explicit is None check so 0.0 is preserved.

This issue also appears in the following locations of the same file:

  • line 361
  • line 381
        if array is None:
            array = np.c_[0, self._collar_azimuth or 0.0, self._collar_dip or -90.0]

  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Low

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

@gmcga
gmcga self-requested a review July 31, 2026 17:51

@gmcga gmcga left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comments. I also think it'd be good to try and increase the test coverage as there's a fair amount missing.

Comment thread geoh5py/objects/drillhole.py
Comment thread geoh5py/objects/drillhole.py Outdated
Copilot AI review requested due to automatic review settings July 31, 2026 19:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Not ready to approve

Survey formatting in concatenated drillholes can produce a dtype that doesn’t match concatenated storage (and Info padding uses bytes), risking runtime errors and inconsistent round-tripping.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (3)

geoh5py/shared/concatenation/drillhole.py:405

  • In concatenated drillholes, this override no longer normalizes incoming surveys to the concatenated 'Surveys' dtype. If the concatenated dataset has only (Depth, Azimuth, Dip) but callers provide an Info column, super().format_survey_values will currently return a 4-field recarray, which won't match the concatenated storage dtype and can break updates.
        # Check if 'Info' is in the survey array
        if "Surveys" in self.concatenator.data:
            dtype = self.concatenator.data["Surveys"].dtype

        return super().format_survey_values(values, dtype=dtype)

geoh5py/objects/drillhole.py:386

  • Padding a missing 'Info' field with a bytes array (b"") can lead to mixed bytes/str values for an h5py vlen=str dtype and inconsistent round-tripping. Prefer padding with empty Python strings and the expected Info field dtype.
        if n_fields == 3 and len(dtype) == 4:
            values += [np.array([b""] * len(values[0]))]

geoh5py/objects/drillhole.py:374

  • As written, purely numeric ndarrays with shape (, 4) are accepted and will be coerced into a dtype that includes the string 'Info' field, which is ambiguous and likely unintended. Numeric survey arrays should stay constrained to shape (, 3) (Depth/Azimuth/Dip), while 4-field inputs should be provided as structured arrays (or non-numeric arrays) with a valid Info field.
        if np.issubdtype(values.dtype, np.number) or not values.dtype.names:
            n_fields = values.shape[1]
        else:
            n_fields = len(values.dtype.names)
            dtype = np.dtype(SURVEYS_FIELDS[:n_fields], align=True)
            if values.dtype.descr != dtype.descr:
                raise TypeError(f"The type of survey array must be numeric or {dtype}")
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread tests/objects/drillhole_data_test.py
Copilot AI review requested due to automatic review settings August 4, 2026 15:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Not ready to approve

The new survey auto-expansion to include the optional “Info” field inserts a bytes default (b"") for a vlen str dtype, which can produce incorrect string typing in stored/returned survey records.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (1)

geoh5py/objects/drillhole.py:387

  • When auto-expanding 3-column survey inputs to a 4-field dtype (adding the optional "Info" field), the default value is currently created as a bytes array (b""). The declared dtype for "Info" is special_dtype(vlen=str), which is intended for Python str objects; writing bytes here can lead to mixed/incorrect string types in the resulting recarray and in the persisted HDF5 dataset.
        values = values.T.tolist()

        if n_fields == 3 and len(dtype) == 4:
            values += [np.array([b""] * len(values[0]))]
        elif n_fields == 4 and len(dtype) == 3:
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

@gmcga gmcga left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good

@domfournier
domfournier merged commit 49c733f into develop Aug 4, 2026
14 of 15 checks passed
@domfournier
domfournier deleted the GEOPY-3013 branch August 4, 2026 19:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants