diff --git a/geomagio/TimeseriesUtility.py b/geomagio/TimeseriesUtility.py
index 8b0d3ee49d1fbcf8fbdd5f6da1fd5fe08a661056..34358454cae2b3820ab39d0e5d04426db85b380e 100644
--- a/geomagio/TimeseriesUtility.py
+++ b/geomagio/TimeseriesUtility.py
@@ -276,6 +276,33 @@ def get_channels(stream):
     return [ch for ch in channels]
 
 
+def get_trace_value(traces, time, default=None):
+    """Get a value at a specific time.
+
+    Parameters
+    ----------
+    trace : obspy.core.Trace
+    time : obspy.core.UTCDateTime
+
+    Returns
+    -------
+    nearest time in trace
+    value from trace at nearest time, or None
+    """
+    # array of UTCDateTime values corresponding
+    for trace in traces:
+        times = trace.times("utcdatetime")
+        index = times.searchsorted(time)
+        trace_time = times[index]
+        trace_value = trace.data[index]
+        if trace_time == time:
+            if numpy.isnan(trace_value):
+                return default
+            else:
+                return trace_value
+    return default
+
+
 def has_all_channels(stream, channels, starttime, endtime):
     """Check whether all channels have any data within time range.
 
diff --git a/geomagio/residual/Reading.py b/geomagio/residual/Reading.py
index 19041231fe5b4a3c33dc9ab8e675ed992ce7063a..70c99dd4da6247df4b81d7d29f17476421ec34c5 100644
--- a/geomagio/residual/Reading.py
+++ b/geomagio/residual/Reading.py
@@ -2,8 +2,11 @@ import collections
 from typing import Dict, List, Optional
 from typing_extensions import Literal
 
+from obspy import Stream
 from pydantic import BaseModel
 
+from .. import TimeseriesUtility
+from ..TimeseriesFactory import TimeseriesFactory
 from .Absolute import Absolute
 from .Measurement import AverageMeasurement, Measurement, average_measurement
 from .MeasurementType import MeasurementType
@@ -36,3 +39,60 @@ class Reading(BaseModel):
         Example: reading[MeasurementType.WEST_DOWN]
         """
         return [m for m in self.measurements if m.measurement_type == measurement_type]
+
+    def load_ordinates(
+        self,
+        observatory: str,
+        timeseries_factory: TimeseriesFactory,
+        default_existing: bool = True,
+    ):
+        """Load ordinates from a timeseries factory.
+
+        Parameters
+        ----------
+        observatory: the observatory to load.
+        timeseries_factory: source of data.
+        default_existing: keep existing values if data not found.
+        """
+        mean = average_measurement(self.measurements)
+        data = timeseries_factory.get_timeseries(
+            observatory=observatory,
+            channels=("H", "E", "Z", "F"),
+            interval="second",
+            type="variation",
+            starttime=mean.time,
+            endtime=mean.endtime,
+        )
+        self.update_measurement_ordinates(data, default_existing)
+
+    def update_measurement_ordinates(self, data: Stream, default_existing: bool = True):
+        """Update ordinates.
+
+        Parameters
+        ----------
+        data: source of data.
+        default_existing: keep existing values if data not found.
+        """
+        for measurement in self.measurements:
+            if not measurement.time:
+                continue
+            measurement.h = TimeseriesUtility.get_trace_value(
+                traces=data.select(channel="H"),
+                time=measurement.time,
+                default=default_existing and measurement.h or None,
+            )
+            measurement.e = TimeseriesUtility.get_trace_value(
+                traces=data.select(channel="E"),
+                time=measurement.time,
+                default=default_existing and measurement.e or None,
+            )
+            measurement.z = TimeseriesUtility.get_trace_value(
+                traces=data.select(channel="Z"),
+                time=measurement.time,
+                default=default_existing and measurement.z or None,
+            )
+            measurement.f = TimeseriesUtility.get_trace_value(
+                traces=data.select(channel="F"),
+                time=measurement.time,
+                default=default_existing and measurement.f or None,
+            )
diff --git a/test/TimeseriesUtility_test.py b/test/TimeseriesUtility_test.py
index 25a93f404f220d7dfad9faf9b0c2e1d006d47819..8c036f7758412d61063e61f371cb539d44679b87 100644
--- a/test/TimeseriesUtility_test.py
+++ b/test/TimeseriesUtility_test.py
@@ -170,6 +170,48 @@ def test_get_merged_gaps():
     assert_equal(gap[1], UTCDateTime("2015-01-01T00:00:07Z"))
 
 
+def test_get_trace_values():
+    """TimeseriesUtility_test.test_get_trace_values()
+    """
+    stream = Stream(
+        [
+            __create_trace("H", [numpy.nan, 1, 1, numpy.nan, numpy.nan]),
+            __create_trace("Z", [0, 0, 0, 1, 1, 1]),
+        ]
+    )
+    for trace in stream:
+        # set time of first sample
+        trace.stats.starttime = UTCDateTime("2015-01-01T00:00:00Z")
+        # set sample rate to 1 second
+        trace.stats.delta = 1
+        trace.stats.npts = len(trace.data)
+    print(stream)
+    print(stream.select(channel="H")[0].times("utcdatetime"))
+    # value that doesn't exist
+    assert_equal(
+        TimeseriesUtility.get_trace_value(
+            traces=stream.select(channel="H"), time=UTCDateTime("2015-01-01T00:00:00Z")
+        ),
+        None,
+    )
+    # value that exists
+    assert_equal(
+        TimeseriesUtility.get_trace_value(
+            traces=stream.select(channel="Z"), time=UTCDateTime("2015-01-01T00:00:00Z")
+        ),
+        0,
+    )
+    # default for value that doesn't exist
+    assert_equal(
+        TimeseriesUtility.get_trace_value(
+            traces=stream.select(channel="H"),
+            time=UTCDateTime("2015-01-01T00:00:03Z"),
+            default=4,
+        ),
+        4,
+    )
+
+
 def test_has_all_channels():
     """TimeseriesUtility_test.test_has_all_channels():
     """