@@ -131,8 +131,61 @@ file. For the general case where none of the spectra are assumed to be the same
131131length, the loader should return a `~specutils.SpectrumList `. Consider the
132132custom 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
138191automatically 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 0x 127280500> , < object object at 0x 127280500> ,
291+ < object object at 0x 127280500> , < object object at 0x 127280500> ,
292+ < object object at 0x 127280500> , < object object at 0x 127280500> ]
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 0x 127280500> , < object object at 0x 127280500> , < object object at 0x 127280500> ,
321+ < object object at 0x 127280500> , < object object at 0x 127280500> ]
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