Skip to content

Report the real cause when a vertical plane is queried without depth - #109

Merged
lispandfound merged 1 commit into
mainfrom
fix/85-vertical-plane-2d-coordinates
Sep 9, 2026
Merged

Report the real cause when a vertical plane is queried without depth#109
lispandfound merged 1 commit into
mainfrom
fix/85-vertical-plane-2d-coordinates

Conversation

@lispandfound

Copy link
Copy Markdown
Contributor

Fixes #85

Problem

coordinate_length was forced to 3 whenever dip == 90, regardless of the input's dimensionality:

# before
coordinate_length = (
    3 if global_coordinates.shape[-1] == 3 or self.dip == 90 else 2
)

A 2D (lat, lon) query against a vertical plane then sliced 3 components from self.bounds but only 2 from the input, producing ValueError: operands could not be broadcast together with shapes (2,) (3,). Fault.wgs_depth_coordinates_to_fault_coordinates caught that with a bare except ValueError and re-raised "Given coordinates are not on fault."for a point that is on the fault.

A plane dipping 84 degrees handled the identical query fine, so only the exactly-vertical case failed, and dip == 90 is common in NZ fault models.

What this does not do

The obvious one-line fix — size from the input shape alone — is not enough, and I did not do only that. A vertical plane projects onto a line in plan view, so a (lat, lon) pair maps to every depth on the plane: the dip coordinate is genuinely undetermined. With the slicing merely fixed, the 2x2 system is rank-deficient and lstsq returns a least-norm solution, i.e. a fabricated dip coordinate of roughly 0 that passes the [0, 1] bounds check. That trades a confusing error for a silently wrong answer, which is worse.

So this PR keeps the operation an error, but an honest one. What the correct answer should be — reject, or return a documented convention such as the top edge — is a design question I have left on the issue rather than deciding here.

Changes

  1. coordinate_length is derived from the input shape alone.
  2. A vertical plane queried without depth raises a specific, descriptive ValueError naming the real cause.
  3. New CoordinatesNotOnPlaneError(ValueError) for the genuine off-plane miss. Fault's per-plane loop now catches only that, so the undetermined-dip error surfaces to the caller instead of being reported as a geometric miss. It subclasses ValueError, so any external caller doing except ValueError is unaffected.
  4. Docstring Raises section updated; the "not passing depth is supported" note now records that vertical planes are the exception.

Behaviour

query before after
vertical plane, 3D [0.5, 0.5] [0.5, 0.5]
vertical plane, 2D broadcast shapes (2,) (3,) Depth is required to locate coordinates on a vertical plane...
Fault of that plane, 2D Given coordinates are not on fault. Depth is required...
plane dipping 84, 2D [0.5, 0.5] [0.5, 0.5]
point genuinely off the fault Given coordinates are not on fault. Given coordinates are not on fault.

Verification

  • 3 new tests; test_vertical_plane_without_depth_reports_the_real_cause fails without the fix.
  • Full suite: 721 passed (this touches core geometry, so I ran everything, not just test_sources.py).
  • ruff check, ruff format, numpydoc lint clean.

🤖 Generated with Claude Code

coordinate_length was forced to 3 whenever dip == 90, regardless of the
input's dimensionality, so a 2D (lat, lon) query against a vertical plane
produced a broadcast ValueError. Fault's per-plane loop caught that and
re-raised "Given coordinates are not on fault" -- for a point that is on
the fault. A plane dipping 84 degrees handled the same query fine, so only
the exactly-vertical case failed, and dip == 90 is common in NZ fault
models.

Sizes the slices from the input alone, and raises a specific error for the
vertical-without-depth case: a vertical plane projects onto a line in plan
view, so a (lat, lon) pair maps to every depth on the plane and the dip
coordinate is genuinely undetermined. Fabricating one would trade a
confusing error for a silently wrong answer.

Adds CoordinatesNotOnPlaneError (a ValueError subclass, so external
callers catching ValueError are unaffected) for the genuine off-plane
miss, so Fault's loop only swallows real misses and the undetermined-dip
error surfaces to the caller instead of being reported as a geometric one.

Fixes #85

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lispandfound
lispandfound merged commit 5b841ac into main Sep 9, 2026
7 checks passed
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.

sources.py: vertical planes (dip == 90) reject 2D coordinates as "not on fault"

1 participant