diff --git a/cxotime/cxotime.py b/cxotime/cxotime.py index 2298370..ac99a90 100644 --- a/cxotime/cxotime.py +++ b/cxotime/cxotime.py @@ -199,7 +199,7 @@ def linspace( num : int | None Number of time bins. step_max : u.Quantity (timelike) - Maximum time interval step.. Should be positive nonzero. + Maximum time interval step. Should be positive nonzero. Returns ------- @@ -225,6 +225,14 @@ def linspace( times = np.linspace(start, stop, num + 1) + if step_max is not None: + # confirm that all the time deltas are less than or equal to step_max + deltas_lt = [t <= step_max for t in np.diff(times)] + if np.any(deltas_lt): + # increase num and try again + num += 1 + times = np.linspace(start, stop, num + 1) + return times @classmethod diff --git a/cxotime/tests/test_cxotime_linspace.py b/cxotime/tests/test_cxotime_linspace.py index 268423b..24303d9 100644 --- a/cxotime/tests/test_cxotime_linspace.py +++ b/cxotime/tests/test_cxotime_linspace.py @@ -15,10 +15,13 @@ def run_linspace_step_test(start, stop, dt_max, expected_len, expected_values): dt_max, abs(CxoTime(stop) - CxoTime(start)) ) - # Confirm that all the intervals are the same duration + # Confirm that all the intervals are the basically the same duration interval1 = result[1] - result[0] assert all(np.isclose((result[1:] - result[:-1]).sec, interval1.sec)) + # And that they are all less than or equal to dt_max + assert all((result[1:] - result[:-1]) <= dt_max) + # Confirm that the time range is covered assert result[0] == start assert result[-1] == stop @@ -28,6 +31,54 @@ def run_linspace_step_test(start, stop, dt_max, expected_len, expected_values): assert np.allclose(CxoTime(result).secs, CxoTime(expected_values).secs) +def test_linspace_step_bug(): + """Test that the steps are equal to or less than the maximum step for one case + described in https://github.com/sot/cxotime/issues/45 . For this case, the + maximum step is 1 minute and the range is 30 minutes. One would expect 30 + intervals, but due to tiny numeric issues with np.linspace, we need an extra + interval to avoid having any intervals just over the step_max.""" + run_linspace_step_test( + "2023:001:00:00:01.000", + "2023:001:00:30:01.000", + 1 * u.min, + 32, + [ + "2023:001:00:00:01.000", + "2023:001:00:00:59.065", + "2023:001:00:01:57.129", + "2023:001:00:02:55.194", + "2023:001:00:03:53.258", + "2023:001:00:04:51.323", + "2023:001:00:05:49.387", + "2023:001:00:06:47.452", + "2023:001:00:07:45.516", + "2023:001:00:08:43.581", + "2023:001:00:09:41.645", + "2023:001:00:10:39.710", + "2023:001:00:11:37.774", + "2023:001:00:12:35.839", + "2023:001:00:13:33.903", + "2023:001:00:14:31.968", + "2023:001:00:15:30.032", + "2023:001:00:16:28.097", + "2023:001:00:17:26.161", + "2023:001:00:18:24.226", + "2023:001:00:19:22.290", + "2023:001:00:20:20.355", + "2023:001:00:21:18.419", + "2023:001:00:22:16.484", + "2023:001:00:23:14.548", + "2023:001:00:24:12.613", + "2023:001:00:25:10.677", + "2023:001:00:26:08.742", + "2023:001:00:27:06.806", + "2023:001:00:28:04.871", + "2023:001:00:29:02.935", + "2023:001:00:30:01.000", + ], + ) + + def test_linspace_step_with_zero_range(): """Test that the result is correct when start==stop.""" run_linspace_step_test(