# Photerr

> Guide to photerr, a Python library for photometric error modeling in astronomical imaging surveys (LSST/Rubin, Euclid, Roman). Covers ErrorModel/ErrorParams architecture, survey-specific models (LsstErrorModel, EuclidErrorModel, RomanErrorModel and variants), parameter customization (nYrObs, nVisYr, m5, theta, gamma, scale, sigmaSys), non-detection handling (ndMode, sigLim, ndFlag), extended-source aperture corrections (extendedType="auto"/"gaap"), and getLimitingMags. Use this skill whenever the user imports photerr; works with LsstErrorModel, EuclidErrorModel, RomanErrorModel, or ErrorModel; calls getLimitingMags; simulates photometric errors or survey depth; applies photometric noise to a magnitude catalog; handles non-detections or limiting magnitudes; models extended-source (galaxy) photometry; compares survey depths or error budgets across LSST, Euclid, or Roman; or asks how to add realistic photometric scatter to a simulated catalog. For RAIL pipeline stages that wrap photerr (LSSTErrorModel degradatio

- Skill: `jfcrenshaw/photerr` (Agent Skill)
- Install (CLI): `npx skillmds@latest add jfcrenshaw/photerr`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jfcrenshaw/photerr/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: jfcrenshaw (https://skillmd.com/u/jfcrenshaw)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/jfcrenshaw/photerr

---


# photerr — photometric error modeling

You're helping a user work with [photerr](https://github.com/jfcrenshaw/photerr), a library for simulating realistic photometric errors for LSST/Rubin, Euclid, and Roman surveys.
The package extends the Ivezic (2019) point-source error model to low-SNR regimes and extended sources, computing errors in flux space rather than magnitude space.

**Install:** `pip install photerr` (requires Python ≥ 3.10, NumPy ≥ 1.23, pandas ≥ 1.4)

**Citation:** Crenshaw et al. 2024, AJ, 168, 80

## Core concepts

- Input: a pandas `DataFrame` with columns named for each photometric band, containing **true source magnitudes**.
- Output: a `DataFrame` with the same columns containing **simulated observed magnitudes** (true + noise), plus error columns.
- Noise is drawn in flux space; errors are Gaussian and include a systematic floor (`sigmaSys=0.005` mag by default).
- Non-detections are flagged with `np.inf` by default (configurable).

## Survey models

| Class | Survey | Default bands |
|---|---|---|
| `LsstErrorModel` (= V2) | Rubin/LSST | u g r i z y |
| `LsstErrorModelV1` | Rubin/LSST (old) | u g r i z y |
| `EuclidErrorModel` (= Wide) | Euclid wide | VIS Y J H |
| `EuclidDeepErrorModel` | Euclid deep | VIS Y J H |
| `RomanErrorModel` (= Medium) | Roman medium | Y J H |
| `RomanWideErrorModel` | Roman wide | H |
| `RomanDeepErrorModel` | Roman deep | Z Y J H F K W |
| `RomanUltraDeepErrorModel` | Roman ultra-deep | Y J H |

```python
from photerr import (
    LsstErrorModel,
    EuclidErrorModel, EuclidDeepErrorModel,
    RomanErrorModel, RomanWideErrorModel, RomanDeepErrorModel,
)
```

## Basic usage

```python
import pandas as pd
from photerr import LsstErrorModel

# Catalog of true magnitudes
catalog = pd.DataFrame({
    "u": [23.5, 24.0], "g": [23.2, 24.1],
    "r": [23.0, 23.9], "i": [22.9, 23.8],
    "z": [22.8, 23.7], "y": [22.7, 23.6],
})

errModel = LsstErrorModel()
obs = errModel(catalog, random_state=42)
# obs has the same band columns (observed noisy magnitudes) plus
# u_err, g_err, ... error columns; non-detections are np.inf
```

## Key parameters

All parameters can be overridden at construction time as keyword args.
Dict values are per-band; a scalar applies to all bands.

| Parameter | Default (LSST) | Meaning |
|---|---|---|
| `nYrObs` | 10 | Years of observation |
| `nVisYr` | per-band | Mean visits per year |
| `m5` | computed | 5σ single-visit depth (mag) |
| `gamma` | 0.039 | Ivezic (2019) band parameter |
| `theta` | per-band | PSF FWHM (arcsec) |
| `airmass` | per-band | Effective airmass |
| `km` | per-band | Atmospheric extinction |
| `sigmaSys` | 0.005 | Systematic error floor (mag) |
| `scale` | 1.0 | Per-band error scaling factor |
| `sigLim` | 0 | Detection threshold (σ); 0 = keep all |
| `ndMode` | "flag" | Non-detection handling (see below) |
| `ndFlag` | `np.inf` | Value for non-detected sources |
| `extendedType` | "point" | Aperture type for galaxies |
| `decorrelate` | `True` | Decorrelate inter-band errors |
| `highSNR` | `False` | Use high-SNR Gaussian approx |
| `errLoc` | "after" | "after": append `band_err` cols; "alone": return only errors |
| `absFlux` | `False` | Work in absolute flux units instead of magnitudes |
| `majorCol` | "major" | Catalog column name for galaxy semi-major axis (arcsec) |
| `minorCol` | "minor" | Catalog column name for galaxy semi-minor axis (arcsec) |
| `renameDict` | `{}` | Rename bands, e.g. `{"u": "lsst_u"}` |

### Common customisations

```python
# Year-1 depth
errModel = LsstErrorModel(nYrObs=1)

# Custom limiting magnitudes
errModel = LsstErrorModel(m5={"u": 23.5, "g": 24.0})

# Double errors in u and y (sensitivity study)
errModel = LsstErrorModel(scale={"u": 2.0, "y": 2.0})

# Rename bands to match catalog column names
errModel = LsstErrorModel(renameDict={"u": "lsst_u", "g": "lsst_g"})
```

## Limiting magnitudes

```python
# 5σ coadded depth (default)
m5 = errModel.getLimitingMags()

# Single-visit, 1σ
m1_single = errModel.getLimitingMags(nSigma=1, coadded=False)

# Extended source with 1-arcsec aperture
m5_ext = errModel.getLimitingMags(aperture=1.0)
```

## Non-detection handling

```python
# Default: flag non-detections as np.inf
errModel = LsstErrorModel(sigLim=0, ndMode="flag")

# Replace non-detections with the limiting magnitude
errModel = LsstErrorModel(sigLim=1, ndMode="sigLim")

# Keep only > 5σ detections; flag the rest
errModel = LsstErrorModel(sigLim=5, ndMode="flag")
```

## Extended sources (galaxies)

The catalog must include galaxy half-light radii columns (`major`, `minor` in arcsec by default, overridable with `majorCol`/`minorCol`).

```python
galaxy_catalog = pd.DataFrame({
    "u": [24.0], "g": [23.8], "r": [23.5],
    "i": [23.3], "z": [23.1], "y": [23.0],
    "major": [0.5],   # half-light radius (arcsec)
    "minor": [0.4],
})

# AUTO: aperture scales with galaxy size (Rubin-like)
errModel = LsstErrorModel(extendedType="auto")

# GAAP: Gaussian-aperture-and-PSF (KiDS-like)
errModel = LsstErrorModel(extendedType="gaap")

obs = errModel(galaxy_catalog)
```

## Multi-survey comparison

```python
from photerr import LsstErrorModel, EuclidErrorModel, RomanDeepErrorModel

models = {
    "LSST": LsstErrorModel(nYrObs=10),
    "Euclid": EuclidErrorModel(),
    "Roman deep": RomanDeepErrorModel(),
}
for name, m in models.items():
    print(name, m.getLimitingMags())
```

## Notes

- `errModel(catalog)` returns a new `DataFrame`; it does not modify the input.
- Pass `random_state` for reproducibility.
- `LsstErrorModel` is an alias for `LsstErrorModelV2`; `LsstErrorModelV1` uses the older single-visit depth calculation.
- The `ErrorModel` and `ErrorParams` base classes are available for building custom survey models.

