Repository: kujaku11/mth5

Issue:
FC dataset End time discrepancy
The end time stored in the metadata of an FC dataset may have discrepancies occasionally (due to rounding error, perhaps related to nanosecond time managing), through the use of  the following equation:

end_time = start_time + (n_samples - 1) / sample_rate

to recalculate the end time, from the start time, number of samples and sample rate, and the subsequent use of pd.date_range(start_time, end_time, n_samples)

Hints from issue comments:

Solution patch:
diff --git a/mth5/groups/fc_dataset.py b/mth5/groups/fc_dataset.py
index 702201a..b0c9666 100644
--- a/mth5/groups/fc_dataset.py
+++ b/mth5/groups/fc_dataset.py
@@ -208,6 +208,7 @@ class FCChannelDataset:
             self.metadata.time_period.start,
             1.0 / self.metadata.sample_rate_window_step,
             self.n_windows,
+            end_time=self.metadata.time_period.end
         )
 
     @property
diff --git a/mth5/timeseries/ts_helpers.py b/mth5/timeseries/ts_helpers.py
index ee949bf..cc6a389 100644
--- a/mth5/timeseries/ts_helpers.py
+++ b/mth5/timeseries/ts_helpers.py
@@ -57,7 +57,7 @@ def _count_decimal_sig_figs(digits):
     return len(fractional.rstrip("0"))
 
 
-def make_dt_coordinates(start_time, sample_rate, n_samples):
+def make_dt_coordinates(start_time, sample_rate, n_samples, end_time=None):
     """
     get the date time index from the data
 
@@ -89,6 +89,7 @@ def make_dt_coordinates(start_time, sample_rate, n_samples):
         raise ValueError(msg)
     if not isinstance(start_time, MTime):
         start_time = MTime(start_time)
+
     # there is something screwy that happens when your sample rate is not a
     # nice value that can easily fit into the 60 base.  For instance if you
     # have a sample rate of 24000 the dt_freq will be '41667N', but that is
@@ -97,8 +98,10 @@ def make_dt_coordinates(start_time, sample_rate, n_samples):
     # FIX: therefore estimate the end time based on the decimal sample rate.
     # need to account for the fact that the start time is the first sample
     # need n_samples - 1
-    end_time = start_time + (n_samples - 1) / sample_rate
-
+    if end_time is None:
+        end_time = start_time + (n_samples - 1) / sample_rate
+    else:
+        end_time = MTime(end_time)
     # dt_freq = "{0:.0f}N".format(1.0e9 / (sample_rate))
 
     dt_index = pd.date_range(


Test patch:
diff --git a/tests/timeseries/test_ts_helpers.py b/tests/timeseries/test_ts_helpers.py
index 50e7b10..28f9a05 100644
--- a/tests/timeseries/test_ts_helpers.py
+++ b/tests/timeseries/test_ts_helpers.py
@@ -7,6 +7,8 @@ Created on Wed Mar 29 14:30:08 2023
 # =============================================================================
 # Imports
 # =============================================================================
+import pandas as pd
+import sys
 import unittest
 
 from mt_metadata.utils.mttime import MTime
@@ -114,6 +116,37 @@ class TestMakeDtCoordinates(unittest.TestCase):
         with self.subTest("length"):
             self.assertEqual(16, len(dt))
 
+    def test_fix_issue_263(self):
+        """
+            Note that passing endtime explicitly vs not can causing different values in time coordinates.
+
+
+        Returns
+        -------
+
+        """
+        end_str = "2023-10-14T19:47:31.176479359+00:00"
+        start_str = "2023-10-14T19:47:23.978079359+00:00"
+
+        dt = 0.13088
+        sr = 1 / dt
+        n_samples = 56
+
+        tmp1 = make_dt_coordinates(start_str, sample_rate=sr, n_samples=n_samples, end_time=end_str)
+        tmp2 = make_dt_coordinates(start_str, sample_rate=sr, n_samples=n_samples, end_time=None)
+
+        if sys.version_info >= (3, 9):
+            delta_t1 = tmp1.diff()[1:]  # fails in python 3.8
+            delta_t2 = tmp2.diff()[1:]  # fails in python 3.8
+
+            # This assertion indicates that delta_t1 is uniform, whereas delta_t2 is not.
+            assert len(delta_t1.unique()) == 1
+            assert len(delta_t2.unique()) == 2
+
+            # This assertion indicates that the difference is in the first delta.
+            assert delta_t1[0] != delta_t2[0]
+            assert (delta_t1[1:] == delta_t2[1:]).all()
+
 
 class TestDecimalSigFigs(unittest.TestCase):
     def test_sig_figs(self):