Radiometry & the infrared¶
The band-integrated model (a magnitude through one zero point, photons through one scalar QE) gets you exposure estimates. Quantitative photometry and honest infrared work need more: the right magnitude system, real filter shapes, extinction, the option to let a calibrated spectrum set the rate, and the thermal background that dominates the IR. Phase 1.5 adds all of these, additively --- existing code keeps working.
Magnitude systems: Vega and AB¶
getframes ships both photometric systems as Bandpass
factories:
import getframes as gf
vega = gf.Bandpass.johnson("V") # Vega system (Johnson-Cousins UBVRI)
ab = gf.Bandpass.ab("r") # AB system (SDSS r)
The AB system references every band to a flat $f_\nu = 3631$ Jy source, so its
zero point is computed from the band's transmission shape rather than tabulated.
Supported AB bands: SDSS u g r i z, Gaia gaia_g gaia_bp gaia_rp (bp/rp also
accepted), and 2MASS J H Ks. Each carries a tophat spectral response, so
spectral mode works out of the box.
scope = gf.Telescope(2.5, 0.4, throughput=0.3, band=gf.Bandpass.ab("g"))
rate = scope.photon_rate_from_magnitude(20.0) # photons/s at the detector
Real transmission products¶
The tophat bands are coarse stand-ins. For rigour, load a measured filter, detector QE, and atmospheric-transmission curve and fold them into one response:
from getframes.spectral import SpectralBandpass, QE
filter_ = SpectralBandpass.from_file("sdss_g.txt", wavelength_to_nm=0.1) # angstroms
atmos = SpectralBandpass.from_file("atmosphere.txt")
qe = QE.from_file("detector_qe.txt")
response = SpectralBandpass.from_product(filter_, atmos, qe) # filter x atmos x QE
band = gf.Bandpass("custom g", photon_zeropoint=1.6e10, response=response)
from_file reads a two-column (wavelength, value) text file via numpy.loadtxt
(wavelength_to_nm rescales the first column). from_product (and the lower-level
product) multiply curves on their common support.
Extinction (reddening)¶
Extinction applies interstellar dust
attenuation via a Cardelli, Clayton & Mathis (1989) curve, parameterised by the
visual extinction $A_V$ and $R_V$ (3.1 for the diffuse ISM):
ext = gf.Extinction(a_v=1.0) # one magnitude of V extinction
ext.attenuation_mag(550.0) # ~1.0 mag at V
ext.transmission(440.0) # fractional throughput at B (more extinct)
reddened = ext.redden(sed) # apply to an SED (units preserved)
a_band = ext.band_attenuation_mag(gf.Bandpass.johnson("B")) # add to a magnitude
Letting a spectrum set the rate¶
In spectral mode an SED only colours the effective QE; the magnitude sets the
rate. With an absolute SED you can integrate the spectrum to set the rate
itself:
from getframes.spectral import SED
import numpy as np
# photons/s/m^2/nm above the atmosphere
flux = SED.from_flux_density(np.linspace(400, 900, 50), np.full(50, 3.0e6))
star = gf.PointSource(x=64, y=64, flux_sed=flux) # rate set by the spectrum
flux_sed is a third brightness option alongside magnitude and photon_rate on
PointSource and ExtendedSource. The
telescope integrates $\int S(\lambda)\,T(\lambda)\,d\lambda$ over the band
(Telescope.photon_rate_from_sed) to set the
rate; the same SED also colours the QE in spectral mode. Combine it with
Extinction.redden for a reddened, flux-calibrated source.
Thermal background & detector glow¶
For warm instruments and IR detectors the background is dominated by thermal
emission, not the night sky. Thermal models a
graybody of a given temperature and emissivity, integrated over the band into a
per-pixel photon rate --- the IR analogue of Sky:
scope = gf.Telescope(8.0, 0.05, throughput=0.5, band=gf.Bandpass.ab("Ks"))
scene = gf.Scene(
shape=(256, 320),
optics=scope,
psf=gf.GaussianPSF(2.0),
sources=[gf.PointSource(x=160, y=128, magnitude=15.0)],
thermal=gf.Thermal(temperature_k=283.0, emissivity=0.1),
)
cam = gf.Camera.from_preset("leonardo_saphira").with_config(resolution=(256, 320))
frame = cam.observe(scene, exposure=2.0, seed=0)
Thermal needs a band with a spectral response (it integrates the graybody over
it). It is added as a uniform background, like the sky.
Detector self-emission ("glow") is separate, a detector property on the config:
cam = cam.with_config(detector_glow_e_per_s=2.0) # e-/pixel/s, added to the dark
Glow scales with exposure and rides on the dark signal, so an exposure-matched master dark removes it --- exactly what a calibration pipeline should handle.
astropy.units interop¶
The spectral constructors accept astropy.units quantities for wavelength (and
flux), converting them as needed; plain arrays are assumed to be in nm. astropy
is optional --- import it only if you want to pass quantities.
import astropy.units as u
from getframes.spectral import QE
QE.from_arrays([400, 700, 900] * u.nm, [0.3, 0.8, 0.6]) # quantity wavelengths