gwpy 3.0.7__py3-none-any.whl → 3.0.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of gwpy might be problematic. Click here for more details.

Files changed (50) hide show
  1. gwpy/_version.py +2 -2
  2. gwpy/astro/range.py +3 -3
  3. gwpy/astro/tests/test_range.py +4 -5
  4. gwpy/cli/qtransform.py +1 -1
  5. gwpy/detector/tests/test_units.py +3 -0
  6. gwpy/detector/units.py +12 -3
  7. gwpy/frequencyseries/frequencyseries.py +13 -4
  8. gwpy/io/cache.py +24 -1
  9. gwpy/io/datafind.py +1 -0
  10. gwpy/io/ffldatafind.py +27 -16
  11. gwpy/io/tests/test_ffldatafind.py +10 -3
  12. gwpy/plot/gps.py +7 -2
  13. gwpy/plot/tests/test_gps.py +1 -0
  14. gwpy/plot/tests/test_tex.py +3 -0
  15. gwpy/plot/tex.py +8 -6
  16. gwpy/segments/flag.py +14 -3
  17. gwpy/segments/tests/test_flag.py +53 -0
  18. gwpy/signal/filter_design.py +4 -3
  19. gwpy/signal/spectral/_lal.py +1 -1
  20. gwpy/signal/tests/test_coherence.py +9 -9
  21. gwpy/signal/tests/test_filter_design.py +3 -3
  22. gwpy/spectrogram/tests/test_spectrogram.py +2 -2
  23. gwpy/testing/fixtures.py +2 -19
  24. gwpy/testing/utils.py +43 -19
  25. gwpy/time/_tconvert.py +19 -33
  26. gwpy/time/tests/test_time.py +4 -45
  27. gwpy/timeseries/core.py +4 -2
  28. gwpy/timeseries/io/cache.py +47 -29
  29. gwpy/timeseries/io/gwf/framecpp.py +11 -2
  30. gwpy/timeseries/io/gwf/lalframe.py +21 -0
  31. gwpy/timeseries/io/losc.py +8 -2
  32. gwpy/timeseries/tests/test_io_cache.py +74 -0
  33. gwpy/timeseries/tests/test_io_gwf_lalframe.py +17 -0
  34. gwpy/timeseries/tests/test_timeseries.py +78 -36
  35. gwpy/types/array.py +16 -3
  36. gwpy/types/index.py +7 -5
  37. gwpy/types/sliceutils.py +5 -1
  38. gwpy/types/tests/test_array2d.py +4 -0
  39. gwpy/types/tests/test_series.py +26 -0
  40. gwpy/utils/shell.py +2 -2
  41. gwpy/utils/sphinx/zenodo.py +118 -61
  42. gwpy/utils/tests/test_shell.py +2 -2
  43. gwpy/utils/tests/test_sphinx_zenodo.py +175 -0
  44. {gwpy-3.0.7.dist-info → gwpy-3.0.9.dist-info}/METADATA +8 -7
  45. {gwpy-3.0.7.dist-info → gwpy-3.0.9.dist-info}/RECORD +49 -48
  46. {gwpy-3.0.7.dist-info → gwpy-3.0.9.dist-info}/WHEEL +1 -1
  47. gwpy/utils/sphinx/epydoc.py +0 -104
  48. {gwpy-3.0.7.dist-info → gwpy-3.0.9.dist-info}/LICENSE +0 -0
  49. {gwpy-3.0.7.dist-info → gwpy-3.0.9.dist-info}/entry_points.txt +0 -0
  50. {gwpy-3.0.7.dist-info → gwpy-3.0.9.dist-info}/top_level.txt +0 -0
@@ -175,11 +175,11 @@ class TestSpectrogram(_TestArray2D):
175
175
 
176
176
  # test simple filter
177
177
  a2 = array.filter(*zpk)
178
- utils.assert_array_equal(array * fresp, a2)
178
+ utils.assert_quantity_sub_equal(array * fresp, a2)
179
179
 
180
180
  # test inplace filtering
181
181
  array.filter(lti, inplace=True)
182
- utils.assert_array_equal(array, a2)
182
+ utils.assert_quantity_sub_equal(array, a2)
183
183
 
184
184
  # test errors
185
185
  with pytest.raises(TypeError):
gwpy/testing/fixtures.py CHANGED
@@ -30,7 +30,7 @@ import numpy
30
30
 
31
31
  from matplotlib import rc_context
32
32
 
33
- from ..plot.tex import HAS_TEX
33
+ from ..plot.tex import has_tex
34
34
  from ..timeseries import TimeSeries
35
35
  from ..utils.decorators import deprecated_function
36
36
  from .utils import TemporaryFilename
@@ -52,25 +52,8 @@ def tmpfile():
52
52
 
53
53
  # -- plotting -----------------------------------------------------------------
54
54
 
55
- def _test_usetex():
56
- """Return `True` if we can render figures using `text.usetex`.
57
- """
58
- from matplotlib import pyplot
59
- with rc_context(rc={'text.usetex': True}):
60
- fig = pyplot.figure()
61
- fig.gca()
62
- try:
63
- fig.canvas.draw()
64
- except RuntimeError:
65
- return False
66
- else:
67
- return True
68
- finally:
69
- pyplot.close(fig)
70
-
71
-
72
55
  SKIP_TEX = pytest.mark.skipif(
73
- not HAS_TEX or not _test_usetex(),
56
+ not has_tex(),
74
57
  reason='TeX is not available',
75
58
  )
76
59
 
gwpy/testing/utils.py CHANGED
@@ -23,7 +23,6 @@ import os.path
23
23
  import subprocess
24
24
  import tempfile
25
25
  from contextlib import contextmanager
26
- from distutils.version import LooseVersion
27
26
  from importlib import import_module
28
27
  from itertools import zip_longest
29
28
  from pathlib import Path
@@ -35,12 +34,14 @@ from numpy.testing import (assert_array_equal, assert_allclose)
35
34
 
36
35
  from astropy.time import Time
37
36
 
38
- from ..utils.decorators import deprecated_function
37
+ from gwpy.io.cache import file_segment
38
+ from gwpy.utils.decorators import deprecated_function
39
39
 
40
40
  # -- useful constants ---------------------------------------------------------
41
41
 
42
42
  TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
43
43
  TEST_GWF_FILE = os.path.join(TEST_DATA_DIR, 'HLV-HW100916-968654552-1.gwf')
44
+ TEST_GWF_SPAN = file_segment(TEST_GWF_FILE)
44
45
  TEST_HDF5_FILE = os.path.join(TEST_DATA_DIR, 'HLV-HW100916-968654552-1.hdf')
45
46
 
46
47
 
@@ -83,8 +84,9 @@ def skip_missing_dependency(module): # pragma: no cover
83
84
  "be removed in GWpy 3.1.0",
84
85
  ))
85
86
  def module_older_than(module, minversion): # pragma: no cover
87
+ from packaging.version import Version
86
88
  mod = import_module(module)
87
- return LooseVersion(mod.__version__) < LooseVersion(minversion)
89
+ return Version(mod.__version__) < Version(minversion)
88
90
 
89
91
 
90
92
  @deprecated_function(message=(
@@ -148,26 +150,42 @@ def _assert_quantity(q1, q2, array_assertion=assert_array_equal):
148
150
  array_assertion(q1.value, q2.value)
149
151
 
150
152
 
151
- def assert_quantity_sub_equal(a, b, *attrs, **kwargs):
152
- """Assert that two `~gwpy.types.Array` objects are the same (or almost)
153
+ def assert_quantity_sub_equal(
154
+ a,
155
+ b,
156
+ *attrs,
157
+ almost_equal=False,
158
+ exclude=None,
159
+ **kwargs,
160
+ ):
161
+ """Assert that two `~gwpy.types.Array` objects are the same (or almost).
153
162
 
154
163
  Parameters
155
164
  ----------
156
165
  a, b : `~gwpy.types.Array`
157
- the arrays two be tested (can be subclasses)
166
+ The arrays to be tested (can be subclasses).
158
167
 
159
168
  *attrs
160
- the list of attributes to test, defaults to all
169
+ The list of attributes to test, defaults to all.
161
170
 
162
171
  almost_equal : `bool`, optional
163
- allow the numpy array's to be 'almost' equal, default: `False`,
164
- i.e. require exact matches
172
+ Allow the numpy array's to be 'almost' equal, default: `False`,
173
+ i.e. require exact matches.
165
174
 
166
175
  exclude : `list`, optional
167
- a list of attributes to exclude from the test
176
+ A list of attributes to exclude from the test.
177
+
178
+ kwargs
179
+ Other keyword arguments are passed to the array comparison operator
180
+ `numpy.testing.assert_array_equal` or `numpy.testing.assert_allclose`.
181
+
182
+ See also
183
+ --------
184
+ numpy.testing.assert_array_equal
185
+ numpy.testing.assert_allclose
168
186
  """
169
187
  # get value test method
170
- if kwargs.pop('almost_equal', False):
188
+ if almost_equal:
171
189
  assert_array = assert_allclose
172
190
  else:
173
191
  assert_array = assert_array_equal
@@ -175,8 +193,7 @@ def assert_quantity_sub_equal(a, b, *attrs, **kwargs):
175
193
  # parse attributes to be tested
176
194
  if not attrs:
177
195
  attrs = a._metadata_slots
178
- exclude = kwargs.pop('exclude', [])
179
- attrs = [attr for attr in attrs if attr not in exclude]
196
+ attrs = [attr for attr in attrs if attr not in (exclude or [])]
180
197
 
181
198
  # don't assert indexes that don't exist for both
182
199
  def _check_index(dim):
@@ -193,7 +210,7 @@ def assert_quantity_sub_equal(a, b, *attrs, **kwargs):
193
210
 
194
211
  # test data
195
212
  assert_attributes(a, b, *attrs)
196
- assert_array(a.value, b.value)
213
+ assert_array(a.value, b.value, **kwargs)
197
214
 
198
215
 
199
216
  def assert_attributes(a, b, *attrs):
@@ -313,11 +330,18 @@ def TemporaryFilename(*args, **kwargs): # pylint: disable=invalid-name
313
330
  os.remove(name)
314
331
 
315
332
 
316
- def test_read_write(data, format,
317
- extension=None, autoidentify=True,
318
- read_args=[], read_kw={},
319
- write_args=[], write_kw={},
320
- assert_equal=assert_array_equal, assert_kw={}):
333
+ def test_read_write(
334
+ data,
335
+ format,
336
+ extension=None,
337
+ autoidentify=True,
338
+ read_args=[],
339
+ read_kw={},
340
+ write_args=[],
341
+ write_kw={},
342
+ assert_equal=assert_quantity_sub_equal,
343
+ assert_kw={},
344
+ ):
321
345
  """Test that data can be written to and read from a file in some format
322
346
 
323
347
  Parameters
gwpy/time/_tconvert.py CHANGED
@@ -23,14 +23,13 @@ Peter Shawhan.
23
23
  """
24
24
 
25
25
  import datetime
26
- import warnings
27
26
  from decimal import Decimal
28
27
  from numbers import Number
29
28
 
30
- from dateutil.parser import parse as parse_datestr
31
-
32
29
  from astropy.units import Quantity
33
30
 
31
+ from dateparser import parse as dateparser_parse
32
+
34
33
  from . import (Time, LIGOTimeGPS)
35
34
 
36
35
  __author__ = 'Duncan Macleod <duncan.macleod@ligo.org>'
@@ -224,7 +223,11 @@ def from_gps(gps):
224
223
  # special case strings
225
224
 
226
225
  def _now():
227
- return datetime.datetime.utcnow().replace(microsecond=0)
226
+ try:
227
+ now = datetime.datetime.now(datetime.UTC)
228
+ except AttributeError: # python < 3.11
229
+ now = datetime.datetime.utcnow()
230
+ return now.replace(microsecond=0)
228
231
 
229
232
 
230
233
  def _today():
@@ -260,35 +263,18 @@ def _str_to_datetime(datestr):
260
263
  except KeyError: # any other string
261
264
  pass
262
265
 
263
- # use maya
264
- try:
265
- import maya
266
- except ImportError:
267
- MAYA_ERROR = None
268
- else:
269
- try:
270
- return maya.when(datestr).datetime()
271
- except Exception as exc1:
272
- MAYA_ERROR = exc1
273
-
274
- # use dateutil.parse
275
- with warnings.catch_warnings():
276
- # don't allow lazy passing of time-zones
277
- warnings.simplefilter("error", RuntimeWarning)
278
- try:
279
- return parse_datestr(datestr)
280
- except RuntimeWarning as exc:
281
- if MAYA_ERROR:
282
- raise exc from MAYA_ERROR
283
- exc.args = (
284
- f"{str(exc).rstrip('.')}. Try installing 'maya' which can "
285
- "handle more complex date strings.",
286
- )
287
- raise
288
- except (ValueError, TypeError) as exc: # improve error reporting
289
- exc.args = ("Cannot parse date string {0!r}: {1}".format(
290
- datestr, exc.args[0]),)
291
- raise
266
+ result = dateparser_parse(
267
+ datestr,
268
+ settings={
269
+ "TIMEZONE": "UTC",
270
+ "RETURN_AS_TIMEZONE_AWARE": True,
271
+ "TO_TIMEZONE": "UTC",
272
+ "PREFER_DATES_FROM": "current_period",
273
+ }
274
+ )
275
+ if result is None:
276
+ raise ValueError("failed to parse '{datestr}' as datetime")
277
+ return result
292
278
 
293
279
 
294
280
  def _datetime_to_time(dtm):
@@ -19,11 +19,9 @@
19
19
  """Tests for :mod:`gwpy.time`
20
20
  """
21
21
 
22
- from contextlib import nullcontext
23
22
  from datetime import datetime
24
23
  from decimal import Decimal
25
24
  from operator import attrgetter
26
- from unittest import mock
27
25
 
28
26
  import pytest
29
27
 
@@ -55,10 +53,6 @@ YESTERDAY = 1126137617
55
53
 
56
54
 
57
55
  @pytest.mark.freeze_time(FREEZE)
58
- @pytest.mark.parametrize("use_maya", (
59
- False,
60
- pytest.param(True, marks=pytest.mark.requires("maya")),
61
- ))
62
56
  @pytest.mark.parametrize(("in_", "out"), [
63
57
  (1126259462, int(GW150914)),
64
58
  (1235635623.7500002, LIGOTimeGPS(1235635623, 750000200)),
@@ -66,6 +60,7 @@ YESTERDAY = 1126137617
66
60
  ('0', 0),
67
61
  ('Jan 1 2017', 1167264018),
68
62
  ('Sep 14 2015 09:50:45.391', GW150914),
63
+ ('Oct 30 2016 12:34 CST', 1161887657),
69
64
  ((2017, 1, 1), 1167264018),
70
65
  (datetime(2017, 1, 1), 1167264018),
71
66
  (Time(57754, format='mjd'), 1167264018),
@@ -83,30 +78,15 @@ YESTERDAY = 1126137617
83
78
  ('tomorrow', TOMORROW),
84
79
  ('yesterday', YESTERDAY),
85
80
  ])
86
- def test_to_gps(in_, out, use_maya):
87
- """Test that :func:`to_gps` works with and without maya.
88
- """
89
- if use_maya: # do nothing
90
- ctx = nullcontext()
91
- else: # force 'install maya' to error to use dateutil
92
- ctx = mock.patch.dict("sys.modules", {"maya": None})
93
- with ctx:
94
- assert time.to_gps(in_) == out
95
-
96
-
97
- @pytest.mark.requires("maya")
98
- @pytest.mark.parametrize(("in_", "out"), [
99
- ('Oct 30 2016 12:34 CST', 1161887657),
100
- ])
101
- def test_to_gps_maya(in_, out):
102
- """Test that :func:`gwpy.time.to_gps` works with maya.
81
+ def test_to_gps(in_, out):
82
+ """Test that :func:`to_gps` works.
103
83
  """
104
84
  assert time.to_gps(in_) == out
105
85
 
106
86
 
107
87
  @pytest.mark.parametrize(("in_", "err"), [
108
88
  (Quantity(1, 'm'), UnitConversionError),
109
- ('random string', (ValueError, TypeError)),
89
+ ('random string', ValueError),
110
90
  ])
111
91
  def test_to_gps_error(in_, err):
112
92
  """Test that :func:`gwpy.time.to_gps` errors when it should.
@@ -115,27 +95,6 @@ def test_to_gps_error(in_, err):
115
95
  time.to_gps(in_)
116
96
 
117
97
 
118
- @mock.patch.dict("sys.modules", {"maya": None})
119
- def test_to_gps_dateparser_error_propagation_nomaya():
120
- with pytest.raises(
121
- RuntimeWarning,
122
- match=(
123
- "tzname CST identified but not understood.(.*) "
124
- "Try installing 'maya' (.*)"
125
- ),
126
- ):
127
- time.to_gps("Oct 30 2016 12:34 CST")
128
-
129
-
130
- @pytest.mark.requires("maya")
131
- @mock.patch("maya.when", side_effect=ValueError("test chain"))
132
- def test_to_gps_dateparser_error_propagation_maya(_):
133
- with pytest.raises(RuntimeWarning) as exc:
134
- time.to_gps("Oct 30 2016 12:34 CST")
135
- # validate that the maya exception was chained to the dateutil one
136
- assert str(exc.value.__cause__) == "test chain"
137
-
138
-
139
98
  @pytest.mark.parametrize('in_, out', [
140
99
  (1167264018, datetime(2017, 1, 1)),
141
100
  ('1167264018', datetime(2017, 1, 1)),
gwpy/timeseries/core.py CHANGED
@@ -1288,16 +1288,18 @@ class TimeSeriesBaseDict(OrderedDict):
1288
1288
  # find observatory for this group
1289
1289
  if observatory is None:
1290
1290
  try:
1291
- observatory = ''.join(
1291
+ obs = ''.join(
1292
1292
  sorted(set(c.ifo[0] for c in channellist)))
1293
1293
  except TypeError as exc:
1294
1294
  raise ValueError(
1295
1295
  "Cannot parse list of IFOs from channel names",
1296
1296
  ) from exc
1297
1297
 
1298
+ else:
1299
+ obs = observatory
1298
1300
  # find frames
1299
1301
  cache = io_datafind.find_urls(
1300
- observatory,
1302
+ obs,
1301
1303
  frametype,
1302
1304
  start,
1303
1305
  end,
@@ -19,22 +19,34 @@
19
19
  """I/O utilities for reading `TimeSeries` from a `list` of file paths.
20
20
  """
21
21
 
22
- from ...io.cache import (FILE_LIKE, read_cache, file_segment, sieve)
22
+ from io import BytesIO
23
+ from math import inf
24
+ from os import PathLike
25
+
26
+ from ...io.cache import (
27
+ FILE_LIKE,
28
+ file_segment,
29
+ read_cache,
30
+ write_cache,
31
+ )
23
32
  from ...segments import Segment
24
33
 
25
34
  __author__ = "Duncan Macleod <duncan.macleod@ligo.org>"
26
35
 
27
36
 
28
- def preformat_cache(cache, start=None, end=None):
37
+ def preformat_cache(cache, start=None, end=None, sort=file_segment):
29
38
  """Preprocess a `list` of file paths for reading.
30
39
 
31
- - read the cache from the file (if necessary)
32
- - sieve the cache to only include data we need
40
+ This function does the following:
41
+
42
+ - read the list of paths cache file (if necessary),
43
+ - sort the cache in time order (if possible),
44
+ - sieve the cache to only include data we need.
33
45
 
34
46
  Parameters
35
47
  ----------
36
- cache : `list`, `str`
37
- List of file paths, or path to a LAL-format cache file on disk.
48
+ cache : `list`, `str`, `pathlib.Path`
49
+ List of file paths, or path to a cache file.
38
50
 
39
51
  start : `~gwpy.time.LIGOTimeGPS`, `float`, `str`, optional
40
52
  GPS start time of required data, defaults to start of data found;
@@ -44,31 +56,37 @@ def preformat_cache(cache, start=None, end=None):
44
56
  GPS end time of required data, defaults to end of data found;
45
57
  any input parseable by `~gwpy.time.to_gps` is fine.
46
58
 
59
+ sort : `callable`, optional
60
+ A callable key function by which to sort the file paths.
61
+
47
62
  Returns
48
63
  -------
49
64
  modcache : `list`
50
65
  A parsed, sieved list of paths based on the input arguments.
66
+
67
+ See also
68
+ --------
69
+ gwpy.io.cache.read_cache
70
+ For details of how the sorting and sieving is implemented
51
71
  """
52
- # open cache file
53
- if isinstance(cache, (str,) + FILE_LIKE):
54
- return read_cache(cache, sort=file_segment,
55
- segment=Segment(start, end))
56
-
57
- # format existing cache file
58
- cache = type(cache)(cache) # copy cache
59
-
60
- # sort cache
61
- try:
62
- cache.sort(key=file_segment) # sort
63
- except ValueError:
64
- # if this failed, then the sieving will also fail, but lets proceed
65
- # anyway, since the user didn't actually ask us to do this (but
66
- # its a very good idea)
67
- return cache
68
-
69
- # sieve cache
70
- if start is None: # start time of earliest file
71
- start = file_segment(cache[0])[0]
72
- if end is None: # end time of latest file
73
- end = file_segment(cache[-1])[-1]
74
- return sieve(cache, segment=Segment(start, end))
72
+ # if given a list of paths, write it to a file-like structure
73
+ # so that we can use read_cache to do all the work
74
+ if not isinstance(cache, (str, PathLike) + FILE_LIKE):
75
+ cachef = BytesIO()
76
+ write_cache(cache, cachef)
77
+ cachef.seek(0)
78
+ cache = cachef
79
+
80
+ # need start and end times to sieve the cache
81
+ if start is None:
82
+ start = -inf
83
+ if end is None:
84
+ end = +inf
85
+
86
+ # read the cache
87
+ return read_cache(
88
+ cache,
89
+ coltype=type(start),
90
+ sort=sort,
91
+ segment=Segment(start, end),
92
+ )
@@ -401,6 +401,7 @@ def read_frvect(vect, epoch, start, end, name=None, series_class=TimeSeries):
401
401
  dim = vect.GetDim(0)
402
402
  dx = dim.dx
403
403
  x0 = dim.startX
404
+ xunit = dim.GetUnitX() or None
404
405
 
405
406
  # start and end GPS times of this FrVect
406
407
  dimstart = epoch + x0
@@ -428,8 +429,16 @@ def read_frvect(vect, epoch, start, end, name=None, series_class=TimeSeries):
428
429
  unit = vect.GetUnitY() or None
429
430
 
430
431
  # create array
431
- series = series_class(arr, t0=dimstart+nxstart*dx, dt=dx, name=name,
432
- channel=name, unit=unit, copy=False)
432
+ series = series_class(
433
+ arr,
434
+ t0=dimstart+nxstart*dx,
435
+ dt=dx,
436
+ name=name,
437
+ channel=name,
438
+ xunit=xunit,
439
+ unit=unit,
440
+ copy=False,
441
+ )
433
442
 
434
443
  # add information to channel
435
444
  series.channel.sample_rate = series.sample_rate.value
@@ -149,12 +149,33 @@ def read(source, channels, start=None, end=None, series_class=TimeSeries,
149
149
  end = epoch + streamdur
150
150
  end = min(epoch + streamdur, lalutils.to_lal_ligotimegps(end))
151
151
  duration = float(end - start)
152
+ if start >= (epoch + streamdur):
153
+ raise ValueError(
154
+ "cannot read data starting after stream ends",
155
+ )
156
+ if duration < 0:
157
+ raise ValueError(
158
+ "cannot read data with negative duration",
159
+ )
152
160
 
153
161
  # read data
154
162
  out = series_class.DictClass()
155
163
  for name in channels:
156
164
  ts = _read_channel(stream, str(name), start=start, duration=duration)
157
165
  out[name] = series_class.from_lal(ts, copy=False)
166
+ tsend = ts.epoch + getattr(ts.data.data, "size", 0) * ts.deltaT
167
+ if (end - tsend) >= ts.deltaT: # one (probably) sample missing
168
+ # if the available data simply didn't go all the way to the end
169
+ # lalframe would have errored (and we protect against that above),
170
+ # so we know there is data missing.
171
+ # see https://git.ligo.org/lscsoft/lalsuite/-/issues/710
172
+ ts = _read_channel(
173
+ stream,
174
+ str(name),
175
+ start=tsend,
176
+ duration=float(end - tsend),
177
+ )
178
+ out[name] = out[name].append(ts.data.data, inplace=False)
158
179
  lalframe.FrStreamSeek(stream, epoch)
159
180
  return out
160
181
 
@@ -35,7 +35,10 @@ from gwosc.locate import get_urls
35
35
 
36
36
  from .. import (StateVector, TimeSeries)
37
37
  from ...io import (gwf as io_gwf, hdf5 as io_hdf5)
38
- from ...io.cache import file_segment
38
+ from ...io.cache import (
39
+ file_segment,
40
+ sieve as sieve_cache,
41
+ )
39
42
  from ...io.utils import file_path
40
43
  from ...detector.units import parse_unit
41
44
  from ...segments import Segment
@@ -149,7 +152,10 @@ def fetch_gwosc_data(detector, start, end, cls=TimeSeries, **kwargs):
149
152
  key in kwargs}
150
153
  if 'sample_rate' in url_kw: # format as Hertz
151
154
  url_kw['sample_rate'] = Quantity(url_kw['sample_rate'], 'Hz').value
152
- cache = get_urls(detector, int(start), int(ceil(end)), **url_kw)
155
+ cache = sieve_cache(
156
+ get_urls(detector, int(start), int(ceil(end)), **url_kw),
157
+ segment=span,
158
+ )
153
159
  # if event dataset, pick shortest file that covers the request
154
160
  # -- this is a bit hacky, and presumes that only an event dataset
155
161
  # -- would be produced with overlapping files.
@@ -0,0 +1,74 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright (C) Cardiff University 2023
3
+ #
4
+ # This file is part of GWpy.
5
+ #
6
+ # GWpy is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # GWpy is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with GWpy. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ """Tests for :mod:`gwpy.timeseries.io.cache`
20
+ """
21
+
22
+ import pytest
23
+
24
+ from ...io.cache import write_cache
25
+ from ..io import cache as ts_io_cache
26
+
27
+
28
+ @pytest.fixture
29
+ def cache():
30
+ """List of files over which to test sorting/sieving.
31
+ """
32
+ return [
33
+ "/tmp/A-TEST-0-10.tmp",
34
+ "/tmp/A-TEST-10-10.tmp",
35
+ "/tmp/A-TEST-20-10.tmp",
36
+ "/tmp/A-TEST-30-5.tmp",
37
+ "/tmp/A-TEST-35-15.tmp",
38
+ ]
39
+
40
+
41
+ @pytest.fixture
42
+ def cache_file(tmp_path, cache):
43
+ """File version of `cache()`.
44
+ """
45
+ path = tmp_path / "cache.txt"
46
+ write_cache(cache, path)
47
+ return path
48
+
49
+
50
+ @pytest.mark.parametrize("source", ("cache", "cache_file"))
51
+ @pytest.mark.parametrize(("start", "end", "idx"), [
52
+ # use everything in the cache
53
+ (None, None, slice(None)),
54
+ # use only GPS time '25' onwards, which is cache[2:]
55
+ (25, None, slice(2, None)),
56
+ # use only up to GPS time '25', which is cache[:3]
57
+ (None, 25, slice(None, 3)),
58
+ # use interval [10, 35), which needs cache[1:4]
59
+ (10, 35, slice(1, 4)),
60
+ ])
61
+ def test_preformat_cache(request, cache, source, start, end, idx):
62
+ """Test that `gwpy.timeseries.io.cache.preformat_cache` works properly.
63
+
64
+ Here `[start, end)` is a GPS segment, and `idx` the corresponding slice
65
+ needed to restrict the cache object.
66
+
67
+ Loops over a variety of input arguments, using `request` to dynamically
68
+ loop over `cache` or `cache_file` as the input.
69
+ """
70
+ assert ts_io_cache.preformat_cache(
71
+ request.getfixturevalue(source), # cache or cache_file
72
+ start=start,
73
+ end=end,
74
+ ) == cache[idx]
@@ -28,6 +28,7 @@ import numpy
28
28
  from gwdatafind.utils import file_segment
29
29
 
30
30
  from ...io.cache import write_cache
31
+ from ...segments import Segment
31
32
  from ...testing.utils import (
32
33
  assert_dict_equal,
33
34
  assert_quantity_sub_equal,
@@ -184,3 +185,19 @@ def test_write_no_ifo(tmp_path, data):
184
185
  {None: data},
185
186
  tmp
186
187
  )
188
+
189
+
190
+ def test_read_missing_sample():
191
+ """Check that giving non-sampled start time doesn't result in missing data.
192
+
193
+ See https://git.ligo.org/computing/helpdesk/-/issues/4774 and
194
+ https://git.ligo.org/lscsoft/lalsuite/-/issues/710.
195
+ """
196
+ data = TimeSeries.read(
197
+ TEST_GWF_PATH,
198
+ "H1:LDAS-STRAIN",
199
+ start=968654552.6709,
200
+ end=968654553,
201
+ format="gwf.lalframe",
202
+ )
203
+ assert data.span == Segment(968654552.670898432, 968654553.0)