Summary
We currently define a Spectrum class that subclasses specutils.Spectrum1D and adds convenience constructors and methods (e.g., from_grid, resample, regularize, set_spectral_resolution, bin). This issue proposes we re‑evaluate whether we should:
- keep and harden our own
Spectrum subclass, or
- simplify the public API to use
specutils.Spectrum1D directly and expose our added functionality as standalone functions/utilities (and/or methods on our grid classes), avoiding inheritance.
The goal is to reduce maintenance burden, clarify responsibilities (data container vs. modeling ops), and align with the rest of speclib where grid access and interpolation already live in dedicated classes (SpectralGrid, BinnedSpectralGrid, SEDGrid).
Motivation
- Clarity of responsibilities:
Spectrum1D already provides a well‑understood container for spectral data. Our subclass mixes container concerns (data) with modeling utilities (loading model grids, resampling, binning, convolution). This can blur boundaries and complicate type expectations.
- Lower maintenance surface: Subclassing a fast‑moving upstream class can be brittle (e.g., API changes in
specutils). Composition/utility functions are typically more robust.
- Testing & dependency graph: Keeping transformations in free functions (or small utility modules) yields easier unit tests and avoids hidden side‑effects of overridden behavior.
- Consistency with recent API changes: We recently clarified naming around flux retrieval (e.g.,
get_spectrum → get_flux) and added an interpolation toggle in grid classes. Pushing grid access into grid classes, and keeping the spectrum container neutral, improves coherence.
Current state
The in‑tree Spectrum subclass extends Spectrum1D with:
@classmethod from_grid(...) — loads and optionally interpolates spectra from several model libraries (PHOENIX, NewEra variants, DRIFT‑PHOENIX, NextGen, SPHINX, MPS‑Atlas), with unit massaging and cropping.
resample(...) — synphot-based flux‑conserving resampling.
regularize(...) — regrid to uniform wavelength spacing.
set_spectral_resolution(...) — Gaussian convolution to target resolution.
bin(center, width) — boxcar binning returning BinnedSpectrum.
These are useful operations, but they are not intrinsically tied to Spectrum1D inheritance; each could be provided as a function operating on/returning Spectrum1D.
Options
A) Keep Spectrum subclass (status quo + hardening)
- Pros: Backwards compatibility; method‑chaining ergonomics.
- Cons: Ongoing maintenance of subclass; potential friction with upstream changes; mixed responsibilities.
B) Prefer composition/utilities; retire subclass
- Pros: Clearer layering; smaller public surface; easier testing; avoids fragile inheritance.
- Cons: Some user code will need migration from
spec = Spectrum.from_grid(...) to factory/utility calls returning Spectrum1D.
C) Hybrid: thin shim class (deprecated) that forwards to utilities
- Pros: Smooth migration with deprecation warnings; users keep current imports short‑term.
- Cons: Temporary duplication until removal window ends.
Proposed approach (B + C)
-
Introduce utility API (new module, e.g., speclib.ops or speclib.processing):
from_grid(..., *, grid_name, interpolate=True) -> Spectrum1D
resample_flux_synphot(spec: Spectrum1D, wavelength) -> Spectrum1D
regularize_grid(spec: Spectrum1D, delta_lambda) -> Spectrum1D
convolve_to_resolution(spec: Spectrum1D, spectral_resolution) -> Spectrum1D
bin_spectrum(spec: Spectrum1D, center, width) -> BinnedSpectrum
-
Route grid access through existing grid classes where appropriate:
- Encourage
SpectralGrid.get_flux(...) (or a get_spectrum1d(...) convenience) and remove grid fetching logic from the Spectrum class.
-
Deprecation path for Spectrum subclass (hybrid):
- Keep
class Spectrum(Spectrum1D) for one minor release window as a thin shim that immediately calls the new utilities.
- Emit
DeprecationWarning on import/instantiation and on each deprecated method (from_grid, resample, regularize, set_spectral_resolution, bin).
- Document replacements in the changelog and docs ("Migration guide").
-
Documentation refresh:
- Update examples/tutorials to import
Spectrum1D from specutils and call speclib.ops.* utilities or SpectralGrid methods.
Backwards compatibility & migration
Before
from speclib import Spectrum
spec = Spectrum.from_grid(4700, 4.6, 0.0, model_grid="newera_jwst")
spec2 = spec.resample(wavelength)
spec3 = spec.set_spectral_resolution(5 * u.AA)
bspec = spec3.bin(center, width)
After (proposed)
from specutils import Spectrum1D
from speclib import ops # new utilities
spec = ops.from_grid(4700, 4.6, 0.0, model_grid="newera_jwst") # -> Spectrum1D
spec2 = ops.resample_flux_synphot(spec, wavelength)
spec3 = ops.convolve_to_resolution(spec2, 5 * u.AA)
bspec = ops.bin_spectrum(spec3, center, width)
or via grid class (recommended when using model libraries)
from speclib.grids import SpectralGrid
grid = SpectralGrid("newera_jwst", interpolate=True)
flux = grid.get_flux(teff=4700, logg=4.6, feh=0.0)
# pack into Spectrum1D with an explicit wavelength axis if needed
spec = Spectrum1D(flux=flux, spectral_axis=grid.wavelength)
Removal plan: Mark Spectrum as deprecated in the next release (e.g., v0.x.y) and remove no sooner than two minor releases later, pending user feedback.
Impacted modules
speclib/core.py (or wherever Spectrum lives)
speclib/utils.py (grid IO helpers already exist; will be reused)
speclib/grids.py (SpectralGrid, BinnedSpectralGrid, SEDGrid)
speclib/ops.py (new)
- Docs (
docs/), examples, and tutorials
- Tests under
tests/ referencing Spectrum
Implementation plan (checklist)
Benchmarks & acceptance criteria
- Numerical parity: For a representative set of models (PHOENIX/NewEra variants), ensure the old
Spectrum methods and new ops functions agree to within numerical tolerance (e.g., <1e-10 relative for identically sampled operations; flux‑conserving resample within integration accuracy).
- Performance: No worse than previous implementation for common operations (resample, convolution) on typical array sizes (10⁴–10⁵ samples).
- Units: Round‑trip conversions preserve physical units and axis metadata; tests cover Å, nm, µm.
- Docs: Updated tutorials run cleanly; deprecation warnings are clear and actionable.
Open questions
- Should
ops.from_grid return a Spectrum1D or simply (wavelength, flux) and let callers construct the Spectrum1D? (Leaning Spectrum1D for ergonomics.)
- Do we want a convenience wrapper
SpectralGrid.get_spectrum1d(...) to pair get_flux(...) with the grid wavelength automatically?
- Where should
BinnedSpectrum live long‑term? Keep as a simple dataclass‑like container or convert to a Spectrum1D variant (e.g., center/width as custom coordinates)?
- What minimal set of dependencies do we accept in
ops (e.g., synphot optional vs. required)?
- What deprecation window is acceptable for users? One or two minor releases?
Alternatives considered
- Keep subclassing but strip grid loading and only keep pure processing methods. (Still inherits maintenance risk.)
- Provide mixin classes that extend
Spectrum1D behavior via composition rather than inheritance. (More complex; questionable value.)
Additional context
Action items / assignees
- Design review: @brackham
- Prototype utilities: @brackham (or volunteer)
- Testing & docs: (volunteer)
Proposed milestone
Target: next minor release (e.g., v0.x+1.0).
If we agree, we can mark this as accepted and start a PR series:
- introduce
ops + tests; 2) add deprecations; 3) docs & migration guide; 4) remove subclass (later release).
Summary
We currently define a
Spectrumclass that subclassesspecutils.Spectrum1Dand adds convenience constructors and methods (e.g.,from_grid,resample,regularize,set_spectral_resolution,bin). This issue proposes we re‑evaluate whether we should:Spectrumsubclass, orspecutils.Spectrum1Ddirectly and expose our added functionality as standalone functions/utilities (and/or methods on our grid classes), avoiding inheritance.The goal is to reduce maintenance burden, clarify responsibilities (data container vs. modeling ops), and align with the rest of
speclibwhere grid access and interpolation already live in dedicated classes (SpectralGrid,BinnedSpectralGrid,SEDGrid).Motivation
Spectrum1Dalready provides a well‑understood container for spectral data. Our subclass mixes container concerns (data) with modeling utilities (loading model grids, resampling, binning, convolution). This can blur boundaries and complicate type expectations.specutils). Composition/utility functions are typically more robust.get_spectrum→get_flux) and added an interpolation toggle in grid classes. Pushing grid access into grid classes, and keeping the spectrum container neutral, improves coherence.Current state
The in‑tree
Spectrumsubclass extendsSpectrum1Dwith:@classmethod from_grid(...)— loads and optionally interpolates spectra from several model libraries (PHOENIX, NewEra variants, DRIFT‑PHOENIX, NextGen, SPHINX, MPS‑Atlas), with unit massaging and cropping.resample(...)—synphot-based flux‑conserving resampling.regularize(...)— regrid to uniform wavelength spacing.set_spectral_resolution(...)— Gaussian convolution to target resolution.bin(center, width)— boxcar binning returningBinnedSpectrum.These are useful operations, but they are not intrinsically tied to
Spectrum1Dinheritance; each could be provided as a function operating on/returningSpectrum1D.Options
A) Keep
Spectrumsubclass (status quo + hardening)B) Prefer composition/utilities; retire subclass
spec = Spectrum.from_grid(...)to factory/utility calls returningSpectrum1D.C) Hybrid: thin shim class (deprecated) that forwards to utilities
Proposed approach (B + C)
Introduce utility API (new module, e.g.,
speclib.opsorspeclib.processing):from_grid(..., *, grid_name, interpolate=True) -> Spectrum1Dresample_flux_synphot(spec: Spectrum1D, wavelength) -> Spectrum1Dregularize_grid(spec: Spectrum1D, delta_lambda) -> Spectrum1Dconvolve_to_resolution(spec: Spectrum1D, spectral_resolution) -> Spectrum1Dbin_spectrum(spec: Spectrum1D, center, width) -> BinnedSpectrumRoute grid access through existing grid classes where appropriate:
SpectralGrid.get_flux(...)(or aget_spectrum1d(...)convenience) and remove grid fetching logic from theSpectrumclass.Deprecation path for
Spectrumsubclass (hybrid):class Spectrum(Spectrum1D)for one minor release window as a thin shim that immediately calls the new utilities.DeprecationWarningon import/instantiation and on each deprecated method (from_grid,resample,regularize,set_spectral_resolution,bin).Documentation refresh:
Spectrum1Dfromspecutilsand callspeclib.ops.*utilities orSpectralGridmethods.Backwards compatibility & migration
Before
After (proposed)
or via grid class (recommended when using model libraries)
Removal plan: Mark
Spectrumas deprecated in the next release (e.g.,v0.x.y) and remove no sooner than two minor releases later, pending user feedback.Impacted modules
speclib/core.py(or whereverSpectrumlives)speclib/utils.py(grid IO helpers already exist; will be reused)speclib/grids.py(SpectralGrid,BinnedSpectralGrid,SEDGrid)speclib/ops.py(new)docs/), examples, and tutorialstests/referencingSpectrumImplementation plan (checklist)
speclib/ops.pywith utility functions listed above.Spectrum.from_gridinto utilities and/or use existingSpectralGridIO helpers (utils.load_newera_flux_array, etc.).Spectrumwith deprecation warnings that forwards to utilities.Spectrum1D+ops(andSpectralGridfor model access).Benchmarks & acceptance criteria
Spectrummethods and newopsfunctions agree to within numerical tolerance (e.g., <1e-10 relative for identically sampled operations; flux‑conserving resample within integration accuracy).Open questions
ops.from_gridreturn aSpectrum1Dor simply(wavelength, flux)and let callers construct theSpectrum1D? (LeaningSpectrum1Dfor ergonomics.)SpectralGrid.get_spectrum1d(...)to pairget_flux(...)with the grid wavelength automatically?BinnedSpectrumlive long‑term? Keep as a simple dataclass‑like container or convert to aSpectrum1Dvariant (e.g., center/width as custom coordinates)?ops(e.g.,synphotoptional vs. required)?Alternatives considered
Spectrum1Dbehavior via composition rather than inheritance. (More complex; questionable value.)Additional context
get_spectrum→get_flux) and the interpolation toggle work (Feature Request: Option to Disable Interpolation inSpectralGrid,BinnedSpectralGrid, andSEDGrid#17).specutilsis widely used; deferring to its core types reduces surprises for users coming from the broader ecosystem.Action items / assignees
Proposed milestone
Target: next minor release (e.g.,
v0.x+1.0).If we agree, we can mark this as accepted and start a PR series:
ops+ tests; 2) add deprecations; 3) docs & migration guide; 4) remove subclass (later release).