Skip to content

Commit 2627845

Browse files
committed
updating docs
1 parent 4e0ee66 commit 2627845

1 file changed

Lines changed: 278 additions & 2 deletions

File tree

docs/custom_loading.rst

Lines changed: 278 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,61 @@ file. For the general case where none of the spectra are assumed to be the same
131131
length, the loader should return a `~specutils.SpectrumList`. Consider the
132132
custom JWST data loader as an example:
133133

134-
.. literalinclude:: ../specutils/io/default_loaders/jwst_reader.py
135-
:language: python
134+
.. code-block:: python
135+
136+
from specutils import Spectrum, SpectrumList
137+
from specutils.io.registers import data_loader
138+
139+
@data_loader(
140+
"JWST x1d multi", identifier=identify_jwst_x1d_multi_fits,
141+
dtype=SpectrumList, extensions=['fits'], priority=10,
142+
)
143+
def jwst_x1d_multi_loader(file_obj, **kwargs):
144+
"""Loader for JWST x1d 1-D spectral data in FITS format"""
145+
return _jwst_spec1d_loader(file_obj, extname='EXTRACT1D', **kwargs)
146+
147+
def _jwst_spec1d_loader(file_obj, extname='EXTRACT1D', flux_col=None, **kwargs):
148+
"""Implementation of loader for JWST x1d 1-D spectral data in FITS format"""
149+
150+
if extname not in ['COMBINE1D', 'EXTRACT1D']:
151+
raise ValueError('Incorrect extname given for 1d spectral data.')
152+
153+
spectra = []
154+
with read_fileobj_or_hdulist(file_obj, memmap=False, **kwargs) as hdulist:
155+
156+
primary_header = hdulist["PRIMARY"].header
157+
158+
for hdu in hdulist:
159+
# Read only the BinaryTableHDUs named COMBINE1D/EXTRACT1D and SCI
160+
if hdu.name != extname:
161+
continue
162+
163+
header = hdu.header
164+
165+
# Correct some known bad unit strings before reading the table
166+
bad_units = {"(MJy/sr)^2": "MJy2 sr-2"}
167+
for c in hdu.columns:
168+
if c.unit in bad_units:
169+
c.unit = bad_units[c.unit]
170+
171+
data = QTable.read(hdu)
172+
173+
if data[0]['WAVELENGTH'].shape != ():
174+
# In this case we have multiple spectra packed into a single extension, one target
175+
# per row of the table
176+
for row in data:
177+
if hasattr(row['WAVELENGTH'], 'mask') and np.all(row['WAVELENGTH'].mask):
178+
# If everything is masked out we don't bother to read it in at all
179+
continue
180+
srctype = row['SOURCE_TYPE']
181+
spec = _jwst_spectrum_from_table(row, header, primary_header, flux_col, srctype)
182+
spectra.append(spec)
183+
else:
184+
# Otherwise the whole table is defining a single spectrum
185+
spec = _jwst_spectrum_from_table(data, header, primary_header, flux_col)
186+
spectra.append(spec)
187+
188+
return SpectrumList(spectra)
136189
137190
Note that by default, any loader that uses ``dtype=Spectrum`` will also
138191
automatically add a reader for `~specutils.SpectrumList`. This enables user
@@ -142,6 +195,229 @@ many `~specutils.Spectrum` objects. This method is available since
142195
`~specutils.SpectrumList` makes use of the Astropy IO registry (see
143196
`astropy.io.registry.read`).
144197

198+
Lazy Loading
199+
^^^^^^^^^^^^
200+
201+
By default, `~specutils.SpectrumList` data loaders will load all spectra eagerly. Loaders optionally support
202+
lazy loading so that individual spectra are only loaded into memory when accessed. This can be useful for lists with a
203+
large number of spectra.
204+
205+
Implementation
206+
~~~~~~~~~~~~~~
207+
Lazy loading is opt-in per data loader. To implement lazy loading for a given data loader, define a custom function to be
208+
passed into the ``lazy_loader`` argument of the ``@data_loader`` decorator. The loader function should return a ``SpectrumList`` built using
209+
the :meth:`specutils.SpectrumList.from_lazy` class method. The function should:
210+
211+
* Determine the total number of spectra.
212+
* Define an index-based loader function that returns a single ``Spectrum``.
213+
* Return the resulting ``SpectrumList``.
214+
215+
See the following example for the Roman 1d spectra asdf data loader.
216+
217+
.. code-block:: python
218+
219+
def _lazy_loader(file_obj, **kwargs):
220+
"""Lazy loader for Roman spectra"""
221+
# read in the input file
222+
with read_fileobj_or_asdftree(file_obj, **kwargs) as af:
223+
roman = af["roman"]
224+
# get the roman spectral source ids
225+
sources = list(roman["data"].keys())
226+
227+
def _loader(i: int) -> Spectrum:
228+
"""Function to load a single spectra from the input file given a list index"""
229+
# select the proper source
230+
source = sources[i]
231+
with read_fileobj_or_asdftree(file_obj, **kwargs) as af2:
232+
roman2 = af2["roman"]
233+
# load a single Spectrum
234+
return _load_roman_spectrum(roman2, source)
235+
236+
# create the lazy SpectrumList, pass in the number of spectra and the individual spectrum loader
237+
sl = SpectrumList.from_lazy(length=len(sources), loader=_loader)
238+
return sl
239+
240+
241+
@data_loader(
242+
"Roman 1d combined",
243+
identifier=identify_1d_combined, # standard function for format identification
244+
dtype=SpectrumList,
245+
extensions=["asdf"],
246+
priority=10,
247+
force=True,
248+
lazy_loader=_lazy_loader, # function to handle lazy loading
249+
)
250+
def roman_1d_combined_list(file_obj, **kwargs):
251+
"""Load all Roman 1d combined extracted spectra"""
252+
# standard eager loading of all spectra
253+
spectra = SpectrumList()
254+
with read_fileobj_or_asdftree(file_obj, **kwargs) as af:
255+
roman = af["roman"]
256+
meta = roman["meta"]
257+
# load the spectra
258+
for source in roman["data"]:
259+
# load single spectrum
260+
spectrum = _load_roman_spectrum(roman, source)
261+
spectra.append(spectrum)
262+
263+
return spectra
264+
265+
266+
def _load_roman_spectrum(roman: dict, source: str) -> Spectrum:
267+
"""Load a single Roman spectrum"""
268+
meta = copy.deepcopy(roman["meta"])
269+
meta['source_id'] = source
270+
data = roman["data"][source] if source else roman["data"]
271+
flux = data['flux'] * u.Unit(meta["unit_flux"])
272+
flux_err = StdDevUncertainty(data['flux_error'])
273+
wavelength = data['wl'] * u.Unit(meta["unit_wl"])
274+
return Spectrum(spectral_axis=wavelength, flux=flux, uncertainty=flux_err, meta=meta)
275+
276+
Usage
277+
~~~~~
278+
279+
Once implmemented, lazy loading can be activated by passing ``lazy_load=True`` to ``SpectrumList.read``.
280+
This creates a list of placeholder objects of length equal to the number of spectra loaded into the list.
281+
The ``repr`` indicates a lazy list with how many spectra are currently loaded into memory
282+
283+
.. code-block:: python
284+
285+
# example file with 6 spectral sources
286+
speclist = SpectrumList.read("/path/to/roman.asdf", format="Roman 1d combined", lazy_load=True)
287+
288+
speclist
289+
lazy list: 0 items loaded; access an index to load a spectrum:
290+
[<object object at 0x127280500>, <object object at 0x127280500>,
291+
<object object at 0x127280500>, <object object at 0x127280500>,
292+
<object object at 0x127280500>, <object object at 0x127280500>]
293+
294+
# inspect the lazy list
295+
len(speclist)
296+
6
297+
298+
# verify it is lazy
299+
speclist.is_lazy
300+
True
301+
302+
# check how many are loaded
303+
speclist.n_loaded
304+
0
305+
306+
Accessing a list item will lazily load the corresponding spectrum into memory.
307+
308+
.. code-block:: python
309+
310+
# access the first spectrum
311+
speclist[0]
312+
<Spectrum(flux=[nan ... nan] W / (nm m2) (shape=(275,), mean=0.00000 W / (nm m2)); spectral_axis=<SpectralAxis [ 750. 752.47542943 754.95902919 ... 1837.848077 1843.91402794
313+
1850. ] nm> (length=275); uncertainty=StdDevUncertainty)>
314+
315+
# check the repr again
316+
speclist
317+
lazy list: 1 items loaded; access an index to load a spectrum:
318+
[<Spectrum(flux=[nan ... nan] W / (nm m2) (shape=(275,), mean=0.00000 W / (nm m2)); spectral_axis=<SpectralAxis [ 750. 752.47542943 754.95902919 ... 1837.848077 1843.91402794
319+
1850. ] nm> (length=275); uncertainty=StdDevUncertainty)>,
320+
<object object at 0x127280500>, <object object at 0x127280500>, <object object at 0x127280500>,
321+
<object object at 0x127280500>, <object object at 0x127280500>]
322+
323+
# check how many are loaded
324+
speclist.n_loaded
325+
1
326+
327+
**Optional Labels**
328+
You can optionally pass a list of labels to use as placeholder values in the lazy list repr, instead of
329+
the default pointer ``<object>``. This can be done by passing a list of strings to the ``labels`` argument
330+
of ``SpectrumList.from_lazy`` in the lazy loader function.
331+
332+
.. code-block:: python
333+
334+
def _lazy_load_roman(file_obj, **kwargs):
335+
"""Lazy loader for SpectrumList"""
336+
337+
with read_fileobj_or_asdftree(file_obj, **kwargs) as af:
338+
roman = af["roman"]
339+
# create a list of roman source ids
340+
sources = list(roman["data"].keys())
341+
342+
def _loader(i: int) -> Spectrum:
343+
...
344+
345+
# pass the source ids as placeholder labels
346+
sl = SpectrumList.from_lazy(
347+
length=len(sources), loader=_loader, labels=sources
348+
)
349+
return sl
350+
351+
Loading the lazy list with display these labels instead:
352+
353+
.. code-block:: python
354+
355+
speclist = SpectrumList.read("/path/to/roman.asdf", format="Roman 1d combined", lazy_load=True)
356+
357+
speclist
358+
lazy list: 0 items loaded; access an index to load a spectrum:
359+
['402849', '403613', '403686', '404935', '404979', '414981']
360+
361+
362+
.. note::
363+
364+
Lazy loaders can be outfitted to any existing data loader. See the example data loader for loading
365+
``SDSS-V spec`` formatted FITS files.
366+
367+
368+
Alternate List Indexing
369+
^^^^^^^^^^^^^^^^^^^^^^^
370+
371+
Alternate ID labels allow string indexing of a ``SpectrumList``. This is useful for long lists of
372+
spectra that can be more easily identified by a name or ID rather than a list index. Alternate IDs
373+
are optional, and can be added to any ``SpectrumList`` data loader with the :meth:`specutils.SpectrumList.set_id_map`
374+
class method. This method accepts a dictionary mapping of string labels to list indices.
375+
376+
For example,
377+
378+
.. code-block:: python
379+
380+
from specutils import SpectrumList
381+
382+
# instantiate a SpectrumList
383+
ss = SpectrumList(['a', 'b', 'c'])
384+
ss
385+
['a', 'b', 'c']
386+
387+
# set an alternate id indexing
388+
ss.set_id_map({'spec1': 0, 'spec2': 1, 'spec3': 2})
389+
390+
# access an item with a list index
391+
ss[0]
392+
'a'
393+
394+
# access an item with an alternate id
395+
ss['spec1']
396+
'a'
397+
398+
The example Roman data loaders uses a string target source id as alternate ids.
399+
400+
.. code-block:: python
401+
402+
def _load_roman_multisource(file_obj, **kwargs):
403+
"""Load all Roman spectra into a SpectrumList"""
404+
405+
spectra = SpectrumList()
406+
with read_fileobj_or_asdftree(file_obj, **kwargs) as af:
407+
roman = af["roman"]
408+
meta = roman["meta"]
409+
sources = list(roman['data'].keys())
410+
411+
# set the alternate ids to roman source ids
412+
source_idx_map = dict(zip(sources, range(len(sources))))
413+
spectra.set_id_map(source_idx_map)
414+
415+
# load the spectra
416+
for source in roman["data"]:
417+
spectrum = _load_roman_spectrum(roman, source)
418+
spectra.append(spectrum)
419+
420+
return spectra
145421
146422
.. _custom_writer:
147423

0 commit comments

Comments
 (0)