From 9f6a41397bae2027c351c7560fc731cb31432d3b Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 17 Aug 2026 10:21:48 +1200 Subject: [PATCH 01/15] minimum jumping depth --- source_modelling/rupture_propagation.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/source_modelling/rupture_propagation.py b/source_modelling/rupture_propagation.py index d74b3b33..605f5943 100644 --- a/source_modelling/rupture_propagation.py +++ b/source_modelling/rupture_propagation.py @@ -497,6 +497,7 @@ def sample_rupture_propagation( def jump_points_from_rupture_tree( source_map: dict[str, sources.IsSource], rupture_causality_tree: Tree, + min_depth: float | None = None ) -> dict[str, JumpPair]: """ Extract jump points between faults from a rupture causality tree. @@ -510,6 +511,8 @@ def jump_points_from_rupture_tree( A mapping of fault names to their corresponding source objects. rupture_causality_tree : Tree A rupture causality tree. + min_depth : float | None, optional + The minimum depth to consider jumping between, or ``None`` to allow jumps at all depth. Returns ------- @@ -521,9 +524,14 @@ def jump_points_from_rupture_tree( for source, parent in rupture_causality_tree.items(): if parent is None: continue - source_point, parent_point = sources.closest_point_between_sources( - source_map[source], source_map[parent] - ) + elif min_depth: + source_point, parent_point = sources.closest_points_beneath( + source_map[source], source_map[parent], min_depth + ) + else: + source_point, parent_point = sources.closest_point_between_sources( + source_map[source], source_map[parent], + ) jump_points[source] = JumpPair(parent_point, source_point) return jump_points From c84cc9b4853d091967c52be625d7cf480952e91c Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 17 Aug 2026 10:26:47 +1200 Subject: [PATCH 02/15] ruff --- source_modelling/rupture_propagation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source_modelling/rupture_propagation.py b/source_modelling/rupture_propagation.py index 605f5943..248a95a0 100644 --- a/source_modelling/rupture_propagation.py +++ b/source_modelling/rupture_propagation.py @@ -497,7 +497,7 @@ def sample_rupture_propagation( def jump_points_from_rupture_tree( source_map: dict[str, sources.IsSource], rupture_causality_tree: Tree, - min_depth: float | None = None + min_depth: float | None = None, ) -> dict[str, JumpPair]: """ Extract jump points between faults from a rupture causality tree. @@ -530,7 +530,8 @@ def jump_points_from_rupture_tree( ) else: source_point, parent_point = sources.closest_point_between_sources( - source_map[source], source_map[parent], + source_map[source], + source_map[parent], ) jump_points[source] = JumpPair(parent_point, source_point) return jump_points From 8d1582bcf8bbf83aac28c6c5aaf6945ade2a6989 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 17 Aug 2026 10:31:37 +1200 Subject: [PATCH 03/15] fix missing attributes on top_m and bottom_m --- source_modelling/sources.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/source_modelling/sources.py b/source_modelling/sources.py index 5bffe437..a415fa4d 100644 --- a/source_modelling/sources.py +++ b/source_modelling/sources.py @@ -59,6 +59,18 @@ class Point: dip: float dip_dir: float + @property + def top_m(self): + """float: The top of the point source pseudo-geometry""" + depth = self.bounds[-1] + return depth - np.sin(np.radians(self.dip)) / 2 + + @property + def bottom_m(self): + """float: The bottom of the point source pseudo-geometry""" + depth = self.bounds[-1] + return depth + np.sin(np.radians(self.dip)) / 2 + @classmethod def from_lat_lon_depth(cls, point_coordinates: np.ndarray, **kwargs) -> Self: """Construct a point source from a lat, lon, depth format. @@ -1643,7 +1655,7 @@ def fault_coordinate_distance( def closest_points_beneath( - source_a: Fault | Plane, source_b: Fault | Plane, min_depth: float + source_a: IsSource, source_b: IsSource, min_depth: float ) -> tuple[np.ndarray, np.ndarray]: """Find the closest points between two sources beneath a minimum depth. From 8c5da951b5280362db207aaa489d0d92ff8f32d2 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 17 Aug 2026 10:35:33 +1200 Subject: [PATCH 04/15] add type annotations --- source_modelling/sources.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source_modelling/sources.py b/source_modelling/sources.py index a415fa4d..2a7a125f 100644 --- a/source_modelling/sources.py +++ b/source_modelling/sources.py @@ -60,13 +60,13 @@ class Point: dip_dir: float @property - def top_m(self): + def top_m(self) -> float: """float: The top of the point source pseudo-geometry""" depth = self.bounds[-1] return depth - np.sin(np.radians(self.dip)) / 2 @property - def bottom_m(self): + def bottom_m(self) -> float: """float: The bottom of the point source pseudo-geometry""" depth = self.bounds[-1] return depth + np.sin(np.radians(self.dip)) / 2 From 34290056ab3dee27cf4a61dbb9c0ef298a611d8b Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 17 Aug 2026 10:51:41 +1200 Subject: [PATCH 05/15] correct depth calculation --- source_modelling/sources.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/source_modelling/sources.py b/source_modelling/sources.py index 2a7a125f..02e554c4 100644 --- a/source_modelling/sources.py +++ b/source_modelling/sources.py @@ -63,13 +63,14 @@ class Point: def top_m(self) -> float: """float: The top of the point source pseudo-geometry""" depth = self.bounds[-1] - return depth - np.sin(np.radians(self.dip)) / 2 + return depth - self.width_m * np.sin(np.radians(self.dip)) / 2 @property def bottom_m(self) -> float: """float: The bottom of the point source pseudo-geometry""" - depth = self.bounds[-1] - return depth + np.sin(np.radians(self.dip)) / 2 + centroid_depth = self.bounds[-1] + + return centroid_depth + self.width_m * np.sin(np.radians(self.dip)) / 2 @classmethod def from_lat_lon_depth(cls, point_coordinates: np.ndarray, **kwargs) -> Self: From bbce118c5d40f036ff2aa2dbafb84d3bc15819bf Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 17 Aug 2026 10:52:26 +1200 Subject: [PATCH 06/15] update variables --- source_modelling/sources.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source_modelling/sources.py b/source_modelling/sources.py index 02e554c4..913ec385 100644 --- a/source_modelling/sources.py +++ b/source_modelling/sources.py @@ -62,8 +62,8 @@ class Point: @property def top_m(self) -> float: """float: The top of the point source pseudo-geometry""" - depth = self.bounds[-1] - return depth - self.width_m * np.sin(np.radians(self.dip)) / 2 + centroid_depth = self.bounds[-1] + return centroid_depth - self.width_m * np.sin(np.radians(self.dip)) / 2 @property def bottom_m(self) -> float: From 0137c531c238679bb8fb68d7aef37e5b65faeec5 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 17 Aug 2026 11:00:52 +1200 Subject: [PATCH 07/15] add tests for new top and bottom properties --- tests/test_sources.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/test_sources.py b/tests/test_sources.py index 758bad95..72b304a1 100644 --- a/tests/test_sources.py +++ b/tests/test_sources.py @@ -14,7 +14,7 @@ from qcore import coordinates, geo from source_modelling import sources -from source_modelling.sources import Fault, Plane, multi_fault_rx_ry_distance +from source_modelling.sources import Fault, Plane, Point, multi_fault_rx_ry_distance DATA_PATH = Path("tests") / "data" np.random.seed(0) @@ -74,6 +74,20 @@ def test_point_construction( assert np.allclose(point.centroid, point_coordinates) +def test_top_bottom_point(): + point = Point( + coordinates.nztm_to_wgs_depth(np.array([-43.0, 172.0, 1000.0])), + 1000.0, + 1000.0, + 0, + 60.0, + 90.0, + ) + sin_dip = np.sqrt(3) / 2 + assert point.bottom_m == 1000.0 + sin_dip / 2 * 1000.0 + assert point.top_m == 1000.0 - sin_dip / 2 * 1000.0 + + @given( point_coordinates=st.builds( coordinate, From 1f1a8dfa0597d57275376528ddc6f0618304f43d Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 17 Aug 2026 11:01:33 +1200 Subject: [PATCH 08/15] add numpydoc ignores --- source_modelling/sources.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source_modelling/sources.py b/source_modelling/sources.py index 913ec385..591bc070 100644 --- a/source_modelling/sources.py +++ b/source_modelling/sources.py @@ -60,13 +60,13 @@ class Point: dip_dir: float @property - def top_m(self) -> float: + def top_m(self) -> float: # numpydoc ignore=RT01 """float: The top of the point source pseudo-geometry""" centroid_depth = self.bounds[-1] return centroid_depth - self.width_m * np.sin(np.radians(self.dip)) / 2 @property - def bottom_m(self) -> float: + def bottom_m(self) -> float: # numpydoc ignore=RT01 """float: The bottom of the point source pseudo-geometry""" centroid_depth = self.bounds[-1] From d4fedcc89b6073fcc3c488ac8acbdb1da9e1ea5f Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 17 Aug 2026 11:27:28 +1200 Subject: [PATCH 09/15] handle depth clamping --- source_modelling/rupture_propagation.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/source_modelling/rupture_propagation.py b/source_modelling/rupture_propagation.py index 248a95a0..3fa23098 100644 --- a/source_modelling/rupture_propagation.py +++ b/source_modelling/rupture_propagation.py @@ -525,8 +525,15 @@ def jump_points_from_rupture_tree( if parent is None: continue elif min_depth: + source_a = source_map[source] + source_b = source_map[parent] + depth = min( + min_depth, + 0.99 * source_a.bottom_m / 1000, + 0.99 * source_b.bottom_m / 1000, + ) source_point, parent_point = sources.closest_points_beneath( - source_map[source], source_map[parent], min_depth + source_a, source_b, depth ) else: source_point, parent_point = sources.closest_point_between_sources( From 3c59100ee2a08d68ddeab4586ca04cc8dc1b8afe Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 17 Aug 2026 11:27:57 +1200 Subject: [PATCH 10/15] update truthiness check --- source_modelling/rupture_propagation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source_modelling/rupture_propagation.py b/source_modelling/rupture_propagation.py index 3fa23098..7e8592d8 100644 --- a/source_modelling/rupture_propagation.py +++ b/source_modelling/rupture_propagation.py @@ -524,7 +524,7 @@ def jump_points_from_rupture_tree( for source, parent in rupture_causality_tree.items(): if parent is None: continue - elif min_depth: + elif min_depth is not None: source_a = source_map[source] source_b = source_map[parent] depth = min( From 174cc933ee955cf4baaecc663d4d3645cb78ee3f Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 17 Aug 2026 11:46:09 +1200 Subject: [PATCH 11/15] add test cases --- tests/test_rupture_propagation.py | 70 ++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/tests/test_rupture_propagation.py b/tests/test_rupture_propagation.py index 3dfb7229..b50461b9 100644 --- a/tests/test_rupture_propagation.py +++ b/tests/test_rupture_propagation.py @@ -502,7 +502,7 @@ def test_sample_rupture_propagation( @pytest.mark.parametrize( - "source_map, rupture_causality_tree, expected_jump_points", + "source_map, rupture_causality_tree, min_depth, expected_jump_points", [ # Test case 1: Simple rupture causality tree ( @@ -518,6 +518,7 @@ def test_sample_rupture_propagation( ), }, {"A": None, "B": "A", "C": "B"}, + None, { "B": rupture_propagation.JumpPair( np.array([0.5, 0.5]), np.array([0.5, 0.5]) @@ -541,6 +542,7 @@ def test_sample_rupture_propagation( ), }, {"A": None, "B": "A", "C": "A"}, + None, { "B": rupture_propagation.JumpPair( np.array([0.5, 0.5]), np.array([0.5, 0.5]) @@ -555,11 +557,13 @@ def test_sample_rupture_propagation( def test_jump_points_from_rupture_tree( source_map: dict[str, sources.Point], rupture_causality_tree: dict[str, str | None], + min_depth: float | None, expected_jump_points: dict[str, rupture_propagation.JumpPair], ): result_jump_points = rupture_propagation.jump_points_from_rupture_tree( source_map, # ty: ignore[invalid-argument-type] rupture_causality_tree, + min_depth, ) # Check if the jump points match the expected values @@ -568,3 +572,67 @@ def test_jump_points_from_rupture_tree( result_jump_points[fault].from_point, expected_jump.from_point ) assert np.allclose(result_jump_points[fault].to_point, expected_jump.to_point) + + +# The bottom depth (in metres) of the sources used in the minimum depth +# tests below (planes 10km wide dipping at 45 degrees from the surface). +_DIPPING_PLANE_BOTTOM_M = 10_000 * np.sin(np.radians(45)) + + +@pytest.mark.parametrize( + "min_depth, expected_jump_depth_m", + [ + # No minimum depth: the sources dip away from each other, so the + # closest points are at the surface. + (None, 0), + # The minimum depth lies within both sources and so is respected exactly. + (5, 5000), + # The minimum depth is below the bottom of both sources and so is + # clamped to just above their bottom depth. + (100, 0.99 * _DIPPING_PLANE_BOTTOM_M), + ], +) +def test_jump_points_from_rupture_tree_min_depth( + min_depth: float | None, expected_jump_depth_m: float +): + # Two planes dipping away from each other, so that the distance + # between them increases with depth. + source_map = { + "A": sources.Plane.from_centroid_strike_dip( + np.array([-41.2865, 174.7762]), + dip=45, + length=10, + width=10, + dtop=0, + strike_nztm=0, + dip_dir_nztm=270, + ), + "B": sources.Plane.from_centroid_strike_dip( + np.array([-41.2865, 174.8762]), + dip=45, + length=10, + width=10, + dtop=0, + strike_nztm=0, + dip_dir_nztm=90, + ), + } + + result_jump_points = rupture_propagation.jump_points_from_rupture_tree( + source_map, + {"A": None, "B": "A"}, + min_depth, + ) + + jump = result_jump_points["B"] + from_depth_m = source_map["A"].fault_coordinates_to_wgs_depth_coordinates( + jump.from_point + )[-1] + to_depth_m = source_map["B"].fault_coordinates_to_wgs_depth_coordinates( + jump.to_point + )[-1] + + # The sources dip away from each other, so the jump is made as + # shallow as the minimum depth allows. + assert from_depth_m == pytest.approx(expected_jump_depth_m, abs=1) + assert to_depth_m == pytest.approx(expected_jump_depth_m, abs=1) From dd1c720b42313b710c7f8af169df9a30a1dd83c2 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Wed, 19 Aug 2026 11:19:08 +1200 Subject: [PATCH 12/15] fix ty check --- tests/test_rupture_propagation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_rupture_propagation.py b/tests/test_rupture_propagation.py index b50461b9..7ab6cf56 100644 --- a/tests/test_rupture_propagation.py +++ b/tests/test_rupture_propagation.py @@ -619,7 +619,7 @@ def test_jump_points_from_rupture_tree_min_depth( } result_jump_points = rupture_propagation.jump_points_from_rupture_tree( - source_map, + source_map, # ty: ignore[invalid-argument-type] {"A": None, "B": "A"}, min_depth, ) From 2e9832734e69f1891f35bad7e8626f89361615b8 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 24 Aug 2026 14:01:48 +1200 Subject: [PATCH 13/15] Update source_modelling/rupture_propagation.py Co-authored-by: Andrew Ridden-Harper <52001209+AndrewRidden-Harper@users.noreply.github.com> --- source_modelling/rupture_propagation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source_modelling/rupture_propagation.py b/source_modelling/rupture_propagation.py index 7e8592d8..594b2734 100644 --- a/source_modelling/rupture_propagation.py +++ b/source_modelling/rupture_propagation.py @@ -512,7 +512,7 @@ def jump_points_from_rupture_tree( rupture_causality_tree : Tree A rupture causality tree. min_depth : float | None, optional - The minimum depth to consider jumping between, or ``None`` to allow jumps at all depth. + The minimum depth to consider jumping between, or ``None`` to allow jumps at all depths. Returns ------- From bebeed93414e871644654e6cc18716c41dbd9e14 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 24 Aug 2026 14:26:39 +1200 Subject: [PATCH 14/15] document 0.99 factor --- source_modelling/rupture_propagation.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/source_modelling/rupture_propagation.py b/source_modelling/rupture_propagation.py index 594b2734..d0f8853e 100644 --- a/source_modelling/rupture_propagation.py +++ b/source_modelling/rupture_propagation.py @@ -529,6 +529,12 @@ def jump_points_from_rupture_tree( source_b = source_map[parent] depth = min( min_depth, + # HACK: factor of 0.99 is used here because the closest points + # solver will not work if the minimum depth is precisely the + # bottom-edge of the fault. If the closest point is the bottom + # depth then this will still recover that, but a proper + # treatment of the degenerate case would require specialising + # the solver. 0.99 * source_a.bottom_m / 1000, 0.99 * source_b.bottom_m / 1000, ) From 0998b3bc9113bf2550895475abc265b93d1e1f9e Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 24 Aug 2026 14:33:00 +1200 Subject: [PATCH 15/15] add diagrammatic documentation of depth --- source_modelling/sources.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/source_modelling/sources.py b/source_modelling/sources.py index 591bc070..162971c1 100644 --- a/source_modelling/sources.py +++ b/source_modelling/sources.py @@ -63,6 +63,18 @@ class Point: def top_m(self) -> float: # numpydoc ignore=RT01 """float: The top of the point source pseudo-geometry""" centroid_depth = self.bounds[-1] + # -------------------------+-------- + # \- / | + # \-- / dip | + # \-/ | + # \-- | + # fault o------------+ centroid depth + # \-- | + # \- | sin(dip) / 2 * width + # \-- | + # \- | + # \+ + return centroid_depth - self.width_m * np.sin(np.radians(self.dip)) / 2 @property