From 7c5440b6bcc105c03443882b50494681705bb731 Mon Sep 17 00:00:00 2001
From: pcain-usgs <pcain@usgs.gov>
Date: Fri, 30 Apr 2021 14:31:22 -0600
Subject: [PATCH 1/6] add residual entrypoint, raise errors for missing
 measurements

---
 geomagio/api/ws/algorithms.py    | 36 +++++++++++++++++++++++++++++++-
 geomagio/residual/Calculation.py |  9 +++++---
 geomagio/residual/Measurement.py |  3 ++-
 3 files changed, 43 insertions(+), 5 deletions(-)

diff --git a/geomagio/api/ws/algorithms.py b/geomagio/api/ws/algorithms.py
index 572045ba8..af957ee43 100644
--- a/geomagio/api/ws/algorithms.py
+++ b/geomagio/api/ws/algorithms.py
@@ -1,8 +1,17 @@
-from fastapi import APIRouter, Depends
+from typing import List
+
+from fastapi import APIRouter, Depends, HTTPException
 from starlette.responses import Response
 
 from ... import TimeseriesFactory
 from ...algorithm import DbDtAlgorithm
+from ...residual import (
+    calculate,
+    Reading,
+    MARK_TYPES,
+    INCLINATION_TYPES,
+    DECLINATION_TYPES,
+)
 from .DataApiQuery import DataApiQuery
 from .data import format_timeseries, get_data_factory, get_data_query, get_timeseries
 
@@ -25,3 +34,28 @@ def get_dbdt(
     return format_timeseries(
         timeseries=timeseries, format=query.format, elements=elements
     )
+
+
+@router.post("/algorithms/residual", response_model=Reading)
+def calculate_residual(reading: Reading, adjust_reference: bool = True):
+    missing_types = get_missing_measurement_types(reading=reading)
+    if len(missing_types) != 0:
+        error_message = ", ".join(t.value for t in missing_types)
+        raise HTTPException(
+            status_code=400,
+            detail=f"Missing {error_message} measurements in input reading",
+        )
+    return calculate(reading=reading, adjust_reference=adjust_reference)
+
+
+def get_missing_measurement_types(reading: Reading) -> List[str]:
+    measurement_types = [m.measurement_type for m in reading.measurements]
+    missing_types = []
+    missing_types.extend(
+        [type for type in DECLINATION_TYPES if type not in measurement_types]
+    )
+    missing_types.extend(
+        [type for type in INCLINATION_TYPES if type not in measurement_types]
+    )
+    missing_types.extend([type for type in MARK_TYPES if type not in measurement_types])
+    return missing_types
diff --git a/geomagio/residual/Calculation.py b/geomagio/residual/Calculation.py
index ddc108a4d..c18fe228e 100644
--- a/geomagio/residual/Calculation.py
+++ b/geomagio/residual/Calculation.py
@@ -28,7 +28,10 @@ def calculate(reading: Reading, adjust_reference: bool = True) -> Reading:
     NOTE: rest of reading object is shallow copy.
     """
     # reference measurement, used to adjust absolutes
-    reference = reading[mt.WEST_DOWN][0]
+    try:
+        reference = adjust_reference and reading[mt.WEST_DOWN][0] or None
+    except:
+        raise ValueError(f"Missing {mt.WEST_DOWN.value} measurement")
     # calculate inclination
     inclination, f, i_mean = calculate_I(
         hemisphere=reading.hemisphere, measurements=reading.measurements
@@ -39,13 +42,13 @@ def calculate(reading: Reading, adjust_reference: bool = True) -> Reading:
         corrected_f=corrected_f,
         inclination=inclination,
         mean=i_mean,
-        reference=adjust_reference and reference or None,
+        reference=reference,
     )
     absoluteD, meridian = calculate_D_absolute(
         azimuth=reading.azimuth,
         h_baseline=absoluteH.baseline,
         measurements=reading.measurements,
-        reference=adjust_reference and reference or None,
+        reference=reference,
     )
     # populate diagnostics object with averaged measurements
     diagnostics = Diagnostics(
diff --git a/geomagio/residual/Measurement.py b/geomagio/residual/Measurement.py
index f8be19437..bbf2f420a 100644
--- a/geomagio/residual/Measurement.py
+++ b/geomagio/residual/Measurement.py
@@ -54,7 +54,8 @@ def average_measurement(
         measurements = [m for m in measurements if m.measurement_type in types]
     if len(measurements) == 0:
         # no measurements to average
-        return None
+        error_message = ", ".join(t.value for t in types)
+        raise ValueError(f"Missing {error_message} measurements")
     starttime = safe_min([m.time.timestamp for m in measurements if m.time])
     endtime = safe_max([m.time.timestamp for m in measurements if m.time])
     measurement = AverageMeasurement(
-- 
GitLab


From 81ee0797d69815f921754fc2f61cc02d7a7e1662 Mon Sep 17 00:00:00 2001
From: pcain-usgs <pcain@usgs.gov>
Date: Fri, 30 Apr 2021 14:39:15 -0600
Subject: [PATCH 2/6] Order type imports, rename error message

---
 geomagio/api/ws/algorithms.py    | 8 ++++----
 geomagio/residual/Measurement.py | 4 ++--
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/geomagio/api/ws/algorithms.py b/geomagio/api/ws/algorithms.py
index af957ee43..f904dd9a0 100644
--- a/geomagio/api/ws/algorithms.py
+++ b/geomagio/api/ws/algorithms.py
@@ -8,9 +8,9 @@ from ...algorithm import DbDtAlgorithm
 from ...residual import (
     calculate,
     Reading,
-    MARK_TYPES,
-    INCLINATION_TYPES,
     DECLINATION_TYPES,
+    INCLINATION_TYPES,
+    MARK_TYPES,
 )
 from .DataApiQuery import DataApiQuery
 from .data import format_timeseries, get_data_factory, get_data_query, get_timeseries
@@ -40,10 +40,10 @@ def get_dbdt(
 def calculate_residual(reading: Reading, adjust_reference: bool = True):
     missing_types = get_missing_measurement_types(reading=reading)
     if len(missing_types) != 0:
-        error_message = ", ".join(t.value for t in missing_types)
+        missing_types = ", ".join(t.value for t in missing_types)
         raise HTTPException(
             status_code=400,
-            detail=f"Missing {error_message} measurements in input reading",
+            detail=f"Missing {missing_types} measurements in input reading",
         )
     return calculate(reading=reading, adjust_reference=adjust_reference)
 
diff --git a/geomagio/residual/Measurement.py b/geomagio/residual/Measurement.py
index bbf2f420a..8b0fe6b3c 100644
--- a/geomagio/residual/Measurement.py
+++ b/geomagio/residual/Measurement.py
@@ -54,8 +54,8 @@ def average_measurement(
         measurements = [m for m in measurements if m.measurement_type in types]
     if len(measurements) == 0:
         # no measurements to average
-        error_message = ", ".join(t.value for t in types)
-        raise ValueError(f"Missing {error_message} measurements")
+        missing_types = ", ".join(t.value for t in types)
+        raise ValueError(f"Missing {missing_types} measurements")
     starttime = safe_min([m.time.timestamp for m in measurements if m.time])
     endtime = safe_max([m.time.timestamp for m in measurements if m.time])
     measurement = AverageMeasurement(
-- 
GitLab


From 0266b40715cb98f42f1e767b59905b22de8b1202 Mon Sep 17 00:00:00 2001
From: pcain-usgs <pcain@usgs.gov>
Date: Fri, 30 Apr 2021 15:00:14 -0600
Subject: [PATCH 3/6] Use missing measurements method in calculation

---
 geomagio/api/ws/algorithms.py    | 17 +----------------
 geomagio/residual/Calculation.py | 22 ++++++++++++++++++----
 geomagio/residual/Measurement.py |  4 ----
 geomagio/residual/__init__.py    |  2 ++
 4 files changed, 21 insertions(+), 24 deletions(-)

diff --git a/geomagio/api/ws/algorithms.py b/geomagio/api/ws/algorithms.py
index f904dd9a0..c67c463fe 100644
--- a/geomagio/api/ws/algorithms.py
+++ b/geomagio/api/ws/algorithms.py
@@ -8,9 +8,7 @@ from ...algorithm import DbDtAlgorithm
 from ...residual import (
     calculate,
     Reading,
-    DECLINATION_TYPES,
-    INCLINATION_TYPES,
-    MARK_TYPES,
+    get_missing_measurement_types,
 )
 from .DataApiQuery import DataApiQuery
 from .data import format_timeseries, get_data_factory, get_data_query, get_timeseries
@@ -46,16 +44,3 @@ def calculate_residual(reading: Reading, adjust_reference: bool = True):
             detail=f"Missing {missing_types} measurements in input reading",
         )
     return calculate(reading=reading, adjust_reference=adjust_reference)
-
-
-def get_missing_measurement_types(reading: Reading) -> List[str]:
-    measurement_types = [m.measurement_type for m in reading.measurements]
-    missing_types = []
-    missing_types.extend(
-        [type for type in DECLINATION_TYPES if type not in measurement_types]
-    )
-    missing_types.extend(
-        [type for type in INCLINATION_TYPES if type not in measurement_types]
-    )
-    missing_types.extend([type for type in MARK_TYPES if type not in measurement_types])
-    return missing_types
diff --git a/geomagio/residual/Calculation.py b/geomagio/residual/Calculation.py
index c18fe228e..6c44d4af3 100644
--- a/geomagio/residual/Calculation.py
+++ b/geomagio/residual/Calculation.py
@@ -28,10 +28,11 @@ def calculate(reading: Reading, adjust_reference: bool = True) -> Reading:
     NOTE: rest of reading object is shallow copy.
     """
     # reference measurement, used to adjust absolutes
-    try:
-        reference = adjust_reference and reading[mt.WEST_DOWN][0] or None
-    except:
-        raise ValueError(f"Missing {mt.WEST_DOWN.value} measurement")
+    missing_types = get_missing_measurement_types(reading=reading)
+    if len(missing_types) != 0:
+        missing_types = ", ".join(t.value for t in missing_types)
+        raise ValueError(f"Missing {missing_types} measurements in input reading")
+    reference = adjust_reference and reading[mt.WEST_DOWN][0] or None
     # calculate inclination
     inclination, f, i_mean = calculate_I(
         hemisphere=reading.hemisphere, measurements=reading.measurements
@@ -281,3 +282,16 @@ def calculate_scale_value(
     residual_change = m2.residual - m1.residual
     scale_value = corrected_f * field_change / np.abs(residual_change)
     return scale_value
+
+
+def get_missing_measurement_types(reading: Reading) -> List[str]:
+    measurement_types = [m.measurement_type for m in reading.measurements]
+    missing_types = []
+    missing_types.extend(
+        [type for type in DECLINATION_TYPES if type not in measurement_types]
+    )
+    missing_types.extend(
+        [type for type in INCLINATION_TYPES if type not in measurement_types]
+    )
+    missing_types.extend([type for type in MARK_TYPES if type not in measurement_types])
+    return missing_types
diff --git a/geomagio/residual/Measurement.py b/geomagio/residual/Measurement.py
index 8b0fe6b3c..e052f53bc 100644
--- a/geomagio/residual/Measurement.py
+++ b/geomagio/residual/Measurement.py
@@ -52,10 +52,6 @@ def average_measurement(
     """
     if types:
         measurements = [m for m in measurements if m.measurement_type in types]
-    if len(measurements) == 0:
-        # no measurements to average
-        missing_types = ", ".join(t.value for t in types)
-        raise ValueError(f"Missing {missing_types} measurements")
     starttime = safe_min([m.time.timestamp for m in measurements if m.time])
     endtime = safe_max([m.time.timestamp for m in measurements if m.time])
     measurement = AverageMeasurement(
diff --git a/geomagio/residual/__init__.py b/geomagio/residual/__init__.py
index ec2de5e8a..352c66b32 100644
--- a/geomagio/residual/__init__.py
+++ b/geomagio/residual/__init__.py
@@ -9,6 +9,7 @@ from .Calculation import (
     calculate_HZ_absolutes,
     calculate_I,
     calculate_scale_value,
+    get_missing_measurement_types,
 )
 from .CalFileFactory import CalFileFactory
 from .Measurement import Measurement, AverageMeasurement, average_measurement
@@ -35,6 +36,7 @@ __all__ = [
     "calculate_I",
     "calculate_scale_value",
     "DECLINATION_TYPES",
+    "get_missing_measurement_types",
     "INCLINATION_TYPES",
     "MARK_TYPES",
     "Measurement",
-- 
GitLab


From 39606901d475826d1e31161add2f7cbc622574f3 Mon Sep 17 00:00:00 2001
From: pcain-usgs <pcain@usgs.gov>
Date: Fri, 30 Apr 2021 16:21:25 -0600
Subject: [PATCH 4/6] Move missing type method to residual

---
 geomagio/api/ws/algorithms.py    |  5 +----
 geomagio/residual/Calculation.py | 15 +--------------
 geomagio/residual/Reading.py     |  7 +++++++
 geomagio/residual/__init__.py    |  2 --
 4 files changed, 9 insertions(+), 20 deletions(-)

diff --git a/geomagio/api/ws/algorithms.py b/geomagio/api/ws/algorithms.py
index c67c463fe..f2eeee314 100644
--- a/geomagio/api/ws/algorithms.py
+++ b/geomagio/api/ws/algorithms.py
@@ -1,5 +1,3 @@
-from typing import List
-
 from fastapi import APIRouter, Depends, HTTPException
 from starlette.responses import Response
 
@@ -8,7 +6,6 @@ from ...algorithm import DbDtAlgorithm
 from ...residual import (
     calculate,
     Reading,
-    get_missing_measurement_types,
 )
 from .DataApiQuery import DataApiQuery
 from .data import format_timeseries, get_data_factory, get_data_query, get_timeseries
@@ -36,7 +33,7 @@ def get_dbdt(
 
 @router.post("/algorithms/residual", response_model=Reading)
 def calculate_residual(reading: Reading, adjust_reference: bool = True):
-    missing_types = get_missing_measurement_types(reading=reading)
+    missing_types = reading.get_missing_measurement_types()
     if len(missing_types) != 0:
         missing_types = ", ".join(t.value for t in missing_types)
         raise HTTPException(
diff --git a/geomagio/residual/Calculation.py b/geomagio/residual/Calculation.py
index 6c44d4af3..fe1ab1c1e 100644
--- a/geomagio/residual/Calculation.py
+++ b/geomagio/residual/Calculation.py
@@ -28,7 +28,7 @@ def calculate(reading: Reading, adjust_reference: bool = True) -> Reading:
     NOTE: rest of reading object is shallow copy.
     """
     # reference measurement, used to adjust absolutes
-    missing_types = get_missing_measurement_types(reading=reading)
+    missing_types = reading.get_missing_measurement_types()
     if len(missing_types) != 0:
         missing_types = ", ".join(t.value for t in missing_types)
         raise ValueError(f"Missing {missing_types} measurements in input reading")
@@ -282,16 +282,3 @@ def calculate_scale_value(
     residual_change = m2.residual - m1.residual
     scale_value = corrected_f * field_change / np.abs(residual_change)
     return scale_value
-
-
-def get_missing_measurement_types(reading: Reading) -> List[str]:
-    measurement_types = [m.measurement_type for m in reading.measurements]
-    missing_types = []
-    missing_types.extend(
-        [type for type in DECLINATION_TYPES if type not in measurement_types]
-    )
-    missing_types.extend(
-        [type for type in INCLINATION_TYPES if type not in measurement_types]
-    )
-    missing_types.extend([type for type in MARK_TYPES if type not in measurement_types])
-    return missing_types
diff --git a/geomagio/residual/Reading.py b/geomagio/residual/Reading.py
index 06c8efb97..519245b69 100644
--- a/geomagio/residual/Reading.py
+++ b/geomagio/residual/Reading.py
@@ -8,6 +8,7 @@ from pydantic import BaseModel
 from .. import TimeseriesUtility
 from ..TimeseriesFactory import TimeseriesFactory
 from .Absolute import Absolute
+from .Calculation import DECLINATION_TYPES, INCLINATION_TYPES, MARK_TYPES
 from .Measurement import Measurement, average_measurement
 from .Diagnostics import Diagnostics
 from .MeasurementType import MeasurementType
@@ -52,6 +53,12 @@ class Reading(BaseModel):
                 return absolute
         return None
 
+    def get_missing_measurement_types(self) -> List[str]:
+        measurement_types = [m.measurement_type for m in self.measurements]
+        all_types = DECLINATION_TYPES + INCLINATION_TYPES + MARK_TYPES
+        missing = [t for t in all_types if t not in measurement_types]
+        return missing
+
     def load_ordinates(
         self,
         observatory: str,
diff --git a/geomagio/residual/__init__.py b/geomagio/residual/__init__.py
index 352c66b32..ec2de5e8a 100644
--- a/geomagio/residual/__init__.py
+++ b/geomagio/residual/__init__.py
@@ -9,7 +9,6 @@ from .Calculation import (
     calculate_HZ_absolutes,
     calculate_I,
     calculate_scale_value,
-    get_missing_measurement_types,
 )
 from .CalFileFactory import CalFileFactory
 from .Measurement import Measurement, AverageMeasurement, average_measurement
@@ -36,7 +35,6 @@ __all__ = [
     "calculate_I",
     "calculate_scale_value",
     "DECLINATION_TYPES",
-    "get_missing_measurement_types",
     "INCLINATION_TYPES",
     "MARK_TYPES",
     "Measurement",
-- 
GitLab


From 3a9d06c984aa5fa785956e91af5db32bf99d611a Mon Sep 17 00:00:00 2001
From: pcain-usgs <pcain@usgs.gov>
Date: Fri, 30 Apr 2021 16:26:13 -0600
Subject: [PATCH 5/6] return None if no types are found to average

---
 geomagio/residual/Measurement.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/geomagio/residual/Measurement.py b/geomagio/residual/Measurement.py
index e052f53bc..f8be19437 100644
--- a/geomagio/residual/Measurement.py
+++ b/geomagio/residual/Measurement.py
@@ -52,6 +52,9 @@ def average_measurement(
     """
     if types:
         measurements = [m for m in measurements if m.measurement_type in types]
+    if len(measurements) == 0:
+        # no measurements to average
+        return None
     starttime = safe_min([m.time.timestamp for m in measurements if m.time])
     endtime = safe_max([m.time.timestamp for m in measurements if m.time])
     measurement = AverageMeasurement(
-- 
GitLab


From 1f85191863c7a31da0173a22f0d9cdf618b70586 Mon Sep 17 00:00:00 2001
From: pcain-usgs <pcain@usgs.gov>
Date: Tue, 4 May 2021 09:30:22 -0600
Subject: [PATCH 6/6] Raise value error with HTTPException

---
 geomagio/api/ws/algorithms.py | 12 ++++--------
 geomagio/residual/Reading.py  |  8 ++++++--
 2 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/geomagio/api/ws/algorithms.py b/geomagio/api/ws/algorithms.py
index f2eeee314..bcc42a359 100644
--- a/geomagio/api/ws/algorithms.py
+++ b/geomagio/api/ws/algorithms.py
@@ -33,11 +33,7 @@ def get_dbdt(
 
 @router.post("/algorithms/residual", response_model=Reading)
 def calculate_residual(reading: Reading, adjust_reference: bool = True):
-    missing_types = reading.get_missing_measurement_types()
-    if len(missing_types) != 0:
-        missing_types = ", ".join(t.value for t in missing_types)
-        raise HTTPException(
-            status_code=400,
-            detail=f"Missing {missing_types} measurements in input reading",
-        )
-    return calculate(reading=reading, adjust_reference=adjust_reference)
+    try:
+        return calculate(reading=reading, adjust_reference=adjust_reference)
+    except ValueError as e:
+        raise HTTPException(status_code=400, detail=str(e))
diff --git a/geomagio/residual/Reading.py b/geomagio/residual/Reading.py
index 519245b69..437da4992 100644
--- a/geomagio/residual/Reading.py
+++ b/geomagio/residual/Reading.py
@@ -8,10 +8,14 @@ from pydantic import BaseModel
 from .. import TimeseriesUtility
 from ..TimeseriesFactory import TimeseriesFactory
 from .Absolute import Absolute
-from .Calculation import DECLINATION_TYPES, INCLINATION_TYPES, MARK_TYPES
 from .Measurement import Measurement, average_measurement
 from .Diagnostics import Diagnostics
-from .MeasurementType import MeasurementType
+from .MeasurementType import (
+    MeasurementType,
+    DECLINATION_TYPES,
+    INCLINATION_TYPES,
+    MARK_TYPES,
+)
 
 
 class Reading(BaseModel):
-- 
GitLab