Skip to content
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ examples/
*.pyc
__pycache__/
.coverage
# Generated at build/install time by setuptools_scm (see pyproject.toml)
potpyri/_version.py
*.pyo
*.pyd
*.pyz
Expand Down
24 changes: 0 additions & 24 deletions potpyri/_version.py

This file was deleted.

7 changes: 3 additions & 4 deletions potpyri/instruments/BINOSPEC.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,10 @@ def __init__(self):

self.out_size = 5000

def raw_format(self, proc):
def _default_raw_format(self, proc):
if proc:
return('sci_img_*proc.fits*')
else:
return('sci_img*[!proc].fits*')
return 'sci_img_*proc.fits*'
return 'sci_img*[!proc].fits*'

# Get a unique image number that can be derived only from the file header
def get_number(self, hdr):
Expand Down
9 changes: 4 additions & 5 deletions potpyri/instruments/DEIMOS.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,10 @@ def __init__(self):

self.out_size = 9000

def raw_format(self, proc):
if proc and str(proc)=='raw':
return('d*.fits')
else:
return('DE*.fits.gz')
def _default_raw_format(self, proc):
if proc and str(proc) == 'raw':
return 'd*.fits'
return 'DE*.fits.gz'

def get_number(self, hdr):
elap = Time(hdr['MJD'], format='mjd')-Time('1980-01-01')
Expand Down
5 changes: 3 additions & 2 deletions potpyri/instruments/F2.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ def get_saturation(self, hdr):
# Gives the saturation level in e-
return(self.saturation*hdr['NREADS']*hdr['GAIN'])

def raw_format(self, proc):
return('*.fits.bz2')
def _default_raw_format(self, proc):
"""Gemini archive default: ``*.fits.bz2`` (use ``--proc fits`` for ``*.fits``)."""
return '*.fits.bz2'

def get_filter(self, hdr):
filt = hdr['FILTER'].replace(' ','').split('_')[0]
Expand Down
4 changes: 2 additions & 2 deletions potpyri/instruments/FOURSTAR.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ def get_number(self, hdr):
elap = int(np.round(elap.to(u.second).value))
return(elap)

def raw_format(self, proc):
return('*.fits')
def _default_raw_format(self, proc):
return '*.fits'

def get_rdnoise(self, hdr):
return(hdr['RDNOISE'])
Expand Down
9 changes: 4 additions & 5 deletions potpyri/instruments/GMOS.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,10 @@ def get_gain(self, hdr):
except KeyError:
return(self.gain)

def raw_format(self, proc):
if str(proc).lower()=='dragons':
return('*.fits')
else:
return('*.fits.bz2')
def _default_raw_format(self, proc):
if str(proc).lower() == 'dragons':
return '*.fits'
return '*.fits.bz2'

def get_ampl(self, hdr):
nccd = hdr['NCCDS']
Expand Down
4 changes: 2 additions & 2 deletions potpyri/instruments/IMACS.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ def get_ampl(self, hdr):
return(amp)

# Raw image format for ingestion
def raw_format(self, proc):
return('iff*.fits*')
def _default_raw_format(self, proc):
return 'iff*.fits*'

def import_image(self, filename, amp, log=None):
filename = os.path.abspath(filename)
Expand Down
14 changes: 6 additions & 8 deletions potpyri/instruments/LRIS.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,12 @@ def get_number(self, header):
number = str(header['FRAMENO']).zfill(5)
return(number)

def raw_format(self, proc):

if str(proc)=='archive':
return('*.fits*')
elif str(proc)=='raw':
return('*[b,r]*.fits')
else:
return('*.fits*')
def _default_raw_format(self, proc):
if str(proc) == 'archive':
return '*.fits*'
if str(proc) == 'raw':
return '*[b,r]*.fits'
return '*.fits*'

def get_instrument_name(self, hdr):
instrument = hdr['INSTRUME']
Expand Down
4 changes: 2 additions & 2 deletions potpyri/instruments/MMIRS.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ def get_number(self, hdr):
elap = int(np.round(elap.to(u.second).value))
return(elap)

def raw_format(self, proc):
return('*.fits')
def _default_raw_format(self, proc):
return '*.fits'

def get_rdnoise(self, hdr):
return(hdr['RDNOISE'])
Expand Down
4 changes: 2 additions & 2 deletions potpyri/instruments/MOSFIRE.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ def __init__(self):
def get_saturation(self, hdr):
return(hdr['SATURATE']*hdr['SYSGAIN'])

def raw_format(self, proc):
return('*.fits.gz')
def _default_raw_format(self, proc):
return '*.fits.gz'

def get_filter(self, hdr):
filt = hdr['FILTER'].replace(' ','').split('_')[0]
Expand Down
162 changes: 110 additions & 52 deletions potpyri/instruments/__init__.py
Original file line number Diff line number Diff line change
@@ -1,71 +1,129 @@
"""Instrument implementations and factory for POTPyRI.

Each instrument module defines a subclass of Instrument with detector keywords,
calibration behavior, and file-sorting rules. New instruments must be added
here and to __all__.
Each instrument module defines a subclass of :class:`~potpyri.instruments.instrument.Instrument`
with detector keywords, calibration behavior, and file-sorting rules. To add a
new instrument, create its module and add the canonical name to ``__all__`` below.
"""
from . import BINOSPEC
from . import DEIMOS
from . import F2
from . import FOURSTAR
from . import GMOS
from . import IMACS
from . import LRIS
from . import MMIRS
from . import MOSFIRE
from __future__ import annotations

# New instruments need to be added here and to the function below
__all__ = ["BINOSPEC","DEIMOS","F2","FOURSTAR","GMOS","IMACS","LRIS","MMIRS","MOSFIRE"]
from importlib import import_module
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from .instrument import Instrument

def instrument_getter(instname, log=None):
"""Return an Instrument instance for the given instrument name.
# Canonical instrument names (single source of truth). Each name must match a
# submodule ``potpyri.instruments.<NAME>`` with class ``<NAME>``.
__all__ = [
'BINOSPEC',
'DEIMOS',
'F2',
'FOURSTAR',
'GMOS',
'IMACS',
'LRIS',
'MMIRS',
'MOSFIRE',
]

# Optional CLI / shorthand aliases (not duplicated in __all__).
_INSTRUMENT_ALIASES: dict[str, str] = {
'BINO': 'BINOSPEC',
'MMIR': 'MMIRS',
}

_MODULES = {name: import_module(f'.{name}', __name__) for name in __all__}

# Re-export instrument submodules: ``from potpyri.instruments import GMOS``
for _name, _mod in _MODULES.items():
globals()[_name] = _mod


class UnknownInstrumentError(ValueError):
"""Raised when an instrument name is not supported."""

def __init__(self, name: str, *, resolved: str | None = None) -> None:
self.name = name
self.resolved = resolved
supported = ', '.join(__all__)
aliases = ', '.join(f'{k}->{v}' for k, v in sorted(_INSTRUMENT_ALIASES.items()))
hint = f' Supported instruments: {supported}.'
if aliases:
hint += f' Aliases: {aliases}.'
if resolved and resolved != name.strip().upper():
detail = f'{name!r} (resolved to {resolved!r})'
else:
detail = repr(name)
super().__init__(f'Instrument {detail} is not supported by POTPyRI.{hint}')


def supported_instruments() -> tuple[str, ...]:
"""Return supported canonical instrument names."""
return tuple(__all__)


def resolve_instrument_name(name: str) -> str:
"""Normalize *name* to a canonical instrument key (uppercase, aliases applied).

Parameters
----------
name : str
User-supplied instrument name or alias (e.g. ``'gmos'``, ``'BINO'``).

Returns
-------
str
Canonical name from :data:`__all__` when recognized, otherwise the
uppercased input (may still be unknown to :func:`instrument_getter`).
"""
key = name.strip().upper()
if key in __all__:
return key
if key in _INSTRUMENT_ALIASES:
return _INSTRUMENT_ALIASES[key]
# Legacy substring aliases used by older scripts / partial names.
if 'BINO' in key:
return 'BINOSPEC'
if 'MMIR' in key:
return 'MMIRS'
return key


def instrument_getter(instname: str, log=None) -> Instrument | None:
"""Return an :class:`~potpyri.instruments.instrument.Instrument` for *instname*.

Parameters
----------
instname : str
Instrument name (e.g. 'GMOS', 'LRIS', 'BINOSPEC').
Instrument name or alias (e.g. ``'GMOS'``, ``'BINO'``, ``'MMIR'``).
log : ColoredLogger, optional
Logger for error messages. If None, raises Exception on unsupported
instrument.
If provided and the instrument is unknown, log an error and return
``None`` instead of raising.

Returns
-------
Instrument
Subclass instance for the requested instrument.
Instrument or None
Configured instrument instance, or ``None`` if unknown and *log* is set.

Raises
------
Exception
If instname is not in __all__ and log is None.
UnknownInstrumentError
If *instname* is not supported and *log* is ``None``.
TypeError
If *instname* is missing or not a string.
"""
instname = instname.upper()
if not instname or not isinstance(instname, str):
raise TypeError(
f'instrument name must be a non-empty string, got {instname!r}'
)

if instname not in __all__:
if log:
log.error(f'Instrument {instname} not supported by POTPyRI')
else:
raise Exception(f'Instrument {instname} not supported by POTPyRI')

tel = None

if instname=="BINOSPEC":
tel = BINOSPEC.BINOSPEC()
elif instname=="DEIMOS":
tel = DEIMOS.DEIMOS()
elif instname=="F2":
tel = F2.F2()
elif instname=="FOURSTAR":
tel = FOURSTAR.FOURSTAR()
elif instname=="GMOS":
tel = GMOS.GMOS()
elif instname=="IMACS":
tel = IMACS.IMACS()
elif instname=="LRIS":
tel = LRIS.LRIS()
elif instname=="MMIRS":
tel = MMIRS.MMIRS()
elif instname=="MOSFIRE":
tel = MOSFIRE.MOSFIRE()

return(tel)
canonical = resolve_instrument_name(instname)
if canonical not in __all__:
exc = UnknownInstrumentError(instname, resolved=canonical)
if log is not None:
log.error(str(exc))
return None
raise exc

cls = getattr(_MODULES[canonical], canonical)
return cls()
Loading
Loading