From a54733c37e365d770f778bd457f7719aa1b605ad Mon Sep 17 00:00:00 2001 From: lachlangrose Date: Mon, 21 Sep 2026 18:33:04 +0300 Subject: [PATCH] fix: use circular mean when aggregating collocated dip direction measurements DipDipDirectionInterpolator.setup_interpolation averages collocated structure points (within one DBSCAN cluster) with a plain arithmetic mean. DIPDIR is a compass bearing, so this is wrong near due north: averaging 350 and 10 degrees gives 180 (the opposite direction) instead of 0. DIPDIR is now aggregated with a circular mean; X, Y and DIP (not a wrapping quantity) are unaffected. Ported from Loop3D/map2loop#249 (that repository is being archived now that map2loop lives in packages/map2loop of this monorepo). --- .../map2loop/src/map2loop/interpolators.py | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/map2loop/src/map2loop/interpolators.py b/packages/map2loop/src/map2loop/interpolators.py index 9648348ea..8e053a884 100644 --- a/packages/map2loop/src/map2loop/interpolators.py +++ b/packages/map2loop/src/map2loop/interpolators.py @@ -11,7 +11,21 @@ from .utils import strike_dip_vector, generate_grid from .logging import getLogger -logger = getLogger(__name__) +logger = getLogger(__name__) + + +def _circular_mean_degrees(angles_degrees) -> float: + """ + Mean of a set of compass bearings in degrees (e.g. dip direction), correctly + handling wraparound. A plain arithmetic mean of 350 and 10 degrees gives 180 + (the opposite direction); this gives 0, the correct answer. + """ + radians = numpy.deg2rad(numpy.asarray(angles_degrees, dtype=float)) + mean_angle = numpy.degrees( + numpy.arctan2(numpy.mean(numpy.sin(radians)), numpy.mean(numpy.cos(radians))) + ) + return float(mean_angle % 360) + class Interpolator(ABC): """ @@ -353,10 +367,17 @@ def setup_interpolation(self, structure_data: pandas.DataFrame): f"Detected {len(collocated_clusters)} collocated point clusters. Aggregating these points.\n " ) - # Aggregate data for collocated points by taking the mean of X, Y, DIP, and DIPDIR within each cluster + # Aggregate data for collocated points by taking the mean of X, Y and DIP, and the + # circular mean of DIPDIR (a compass bearing, so a plain mean is wrong near due north) + # within each cluster aggregated_data = ( structure_data.groupby("cluster") - .agg({"X": "mean", "Y": "mean", "DIP": "mean", "DIPDIR": "mean"}) + .agg( + X=("X", "mean"), + Y=("Y", "mean"), + DIP=("DIP", "mean"), + DIPDIR=("DIPDIR", _circular_mean_degrees), + ) .reset_index(drop=True) )