Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 155 additions & 6 deletions docs/source/cookbook/photometry_and_magnitude.rst
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,139 @@ predict magnitudes per exposure.
composition="NEO",
)

Supported Canonical Filters
---------------------------

``convert_magnitude`` accepts the following canonical filter IDs as either the
source or target. These filters have vendored throughput curves from the SVO
Filter Profile Service. ``V`` is the canonical Bessell V filter retained for
backwards compatibility.

.. list-table::
:header-rows: 1
:widths: 18 82

* - System or instrument
- Canonical filter IDs
* - Bessell
- ``V``, ``Bessell_U``, ``Bessell_B``, ``Bessell_R``, ``Bessell_I``
* - Rubin/LSST
- ``LSST_u``, ``LSST_g``, ``LSST_r``, ``LSST_i``, ``LSST_z``, ``LSST_y``
* - SDSS
- ``SDSS_u``, ``SDSS_g``, ``SDSS_r``, ``SDSS_i``, ``SDSS_z``
* - Pan-STARRS1
- ``PS1_g``, ``PS1_r``, ``PS1_i``, ``PS1_z``, ``PS1_y``, ``PS1_w``
* - ZTF
- ``ZTF_g``, ``ZTF_r``, ``ZTF_i``
* - DECam
- ``DECam_u``, ``DECam_g``, ``DECam_r``, ``DECam_i``, ``DECam_z``,
``DECam_Y``, ``DECam_VR``
* - Mosaic3
- ``Mosaic3_z``
* - BASS/Bok 90Prime
- ``BASS_g``, ``BASS_r``
* - SkyMapper
- ``SkyMapper_u``, ``SkyMapper_v``, ``SkyMapper_g``, ``SkyMapper_r``,
``SkyMapper_i``, ``SkyMapper_z``
* - ATLAS
- ``ATLAS_c``, ``ATLAS_o``

Reported-Band Resolution
------------------------

``map_to_canonical_filter_bands`` first passes through an already-canonical
filter ID. Otherwise it uses the MPC observatory code and reported band to apply
the following explicit mappings. The observatory code is needed only to resolve
a reported band; once both source and target are canonical,
``convert_magnitude`` does not use observatory metadata.

.. list-table:: Explicit observatory mappings
:header-rows: 1
:widths: 18 62 20

* - MPC observatory code
- Reported band → canonical filter
- Instrument
* - ``W84``
- ``u`` → ``DECam_u``; ``g`` → ``DECam_g``; ``r`` → ``DECam_r``;
``i`` → ``DECam_i``; ``z`` → ``DECam_z``; ``Y`` → ``DECam_Y``;
``y`` → ``DECam_Y``; ``VR`` → ``DECam_VR``; ``vr`` → ``DECam_VR``
- DECam
* - ``695``
- ``z`` → ``Mosaic3_z``
- Mosaic3/MzLS
* - ``I41``
- ``g`` → ``ZTF_g``; ``r`` → ``ZTF_r``; ``i`` → ``ZTF_i``
- ZTF
* - ``Q55``
- ``u`` → ``SkyMapper_u``; ``v`` → ``SkyMapper_v``;
``g`` → ``SkyMapper_g``; ``r`` → ``SkyMapper_r``;
``i`` → ``SkyMapper_i``; ``z`` → ``SkyMapper_z``
- SkyMapper
* - ``X05``
- ``u`` → ``LSST_u``; ``g`` → ``LSST_g``; ``r`` → ``LSST_r``;
``i`` → ``LSST_i``; ``z`` → ``LSST_z``; ``y`` → ``LSST_y``;
``Y`` → ``LSST_y``
- Rubin/LSST
* - ``T05``, ``T08``, ``M22``, ``W68``
- ``c`` → ``ATLAS_c``; ``Ac`` → ``ATLAS_c``; ``o`` → ``ATLAS_o``;
``Ao`` → ``ATLAS_o``
- ATLAS
* - ``V00``
- ``g`` → ``BASS_g``; ``r`` → ``BASS_r``
- BASS/Bok 90Prime
* - ``F51``, ``F52``
- ``w`` → ``PS1_w``; ``Pw`` → ``PS1_w``
- Pan-STARRS1

For ``X05``, MPC/ADES forms ``Lu``, ``Lg``, ``Lr``, ``Li``, ``Lz``, ``Ly``
(and their uppercase variants), ``LSST_<band>``, and ``Y`` are also normalized
to the corresponding LSST filter.

If no explicit observatory mapping exists and ``allow_fallback_filters=True``,
the following conservative generic fallbacks are available:

.. list-table:: Generic reported-band fallbacks
:header-rows: 1
:widths: 40 60

* - Generic reported band
- Canonical filter ID
* - ``u``
- ``SDSS_u``
* - ``g``
- ``SDSS_g``
* - ``r``
- ``SDSS_r``
* - ``i``
- ``SDSS_i``
* - ``z``
- ``SDSS_z``
* - ``y``
- ``PS1_y``

Generic fallback matching is currently case-insensitive, so an unresolved
uppercase label such as ``U``, ``R``, ``I``, or ``Y`` would also select the
corresponding lowercase fallback above. Because uppercase labels can represent
different photometric systems, use a canonical ID such as ``Bessell_U``,
``Bessell_R``, or ``Bessell_I`` directly when that is the intended filter. Set
``allow_fallback_filters=False`` whenever an exact instrument mapping is
required. Canonical IDs and explicit observatory mappings still resolve in this
strict mode, while rows that would need a generic fallback raise. Combine strict
mode with ``on_unknown="skip"`` to retain those rows as ``None`` instead.

Generic ``c``, ``o``, and ``w`` labels, clear/unfiltered or blank labels, and
other unknown bands are intentionally not assigned a canonical response. The
explicit ATLAS and Pan-STARRS1 mappings above remain supported because their
observatory codes disambiguate those bands. Unknowns raise by default or can be
retained as ``None`` with ``on_unknown="skip"``.

Bandpass Conversion and Color Terms
-----------------------------------

Use these when you already have magnitudes in one canonical filter and need
another.
another. The conversion is a composition-dependent synthetic color term, not an
empirical observatory or catalog debiasing correction.

.. code-block:: python

Expand All @@ -153,14 +281,35 @@ another.
from adam_core.photometry.bandpasses import (
bandpass_color_terms,
bandpass_delta_mag,
map_to_canonical_filter_bands,
register_custom_template,
)

m_v = np.array([20.1, 20.4, 20.8], dtype=float)
m_r = convert_magnitude(
magnitude=m_v,
source_filter_id=np.array(["V", "V", "V"], dtype=object),
target_filter_id=np.array(["LSST_r", "LSST_r", "LSST_r"], dtype=object),
m_ztf_r = np.array([20.1, 20.4], dtype=float)

# Resolve I41/r to the canonical ZTF_r filter, then convert to Bessell V.
ztf_r = map_to_canonical_filter_bands(
["I41", "I41"],
["r", "r"],
allow_fallback_filters=False,
)
m_v = convert_magnitude(
magnitude=m_ztf_r,
source_filter_id=ztf_r,
target_filter_id=np.array(["V", "V"], dtype=object),
composition="NEO",
)

# Resolve X05/i to LSST_i and convert the same ZTF measurements directly.
lsst_i = map_to_canonical_filter_bands(
["X05", "X05"],
["i", "i"],
allow_fallback_filters=False,
)
m_lsst_i = convert_magnitude(
magnitude=m_ztf_r,
source_filter_id=ztf_r,
target_filter_id=lsst_i,
composition="NEO",
)

Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ norecursedirs = ["__pypackages__"]
[tool.ruff]
line-length = 110
target-version = "py311"
# Pin the rule set to ruff's pre-0.16 defaults: newer ruff enables the UP/RUF
# modernization rules by default, which flag ~770 pre-existing sites. Adopt
# those deliberately in a dedicated PR rather than through a default change.
lint.select = ["E4", "E7", "E9", "F"]
lint.ignore = []
exclude = ["build"]

Expand Down
62 changes: 62 additions & 0 deletions src/adam_core/photometry/tests/test_bandpasses.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import re
from pathlib import Path

import numpy as np
import pyarrow.compute as pc
import pytest
Expand Down Expand Up @@ -260,6 +263,65 @@ def test_on_unknown_invalid_value_raises_immediately():
map_to_canonical_filter_bands(["W84"], ["g"], on_unknown="warn") # type: ignore[arg-type]


def test_documented_bandpass_inventory_matches_vendored_data():
docs_path = (
Path(__file__).parents[4]
/ "docs"
/ "source"
/ "cookbook"
/ "photometry_and_magnitude.rst"
)
docs = docs_path.read_text()

canonical_section = docs.split("Supported Canonical Filters", maxsplit=1)[1].split(
"Reported-Band Resolution", maxsplit=1
)[0]
canonical_table = canonical_section.split(".. list-table::", maxsplit=1)[1]
documented_filter_ids = set(re.findall(r"``([A-Za-z0-9_]+)``", canonical_table))
curves = load_bandpass_curves()
assert documented_filter_ids == set(curves.filter_id.to_pylist())

mapping_section = docs.split("Reported-Band Resolution", maxsplit=1)[1].split(
"Bandpass Conversion and Color Terms", maxsplit=1
)[0]
explicit_table = mapping_section.split(
".. list-table:: Explicit observatory mappings", maxsplit=1
)[1].split("For ``X05``", maxsplit=1)[0]
documented_mappings = set()
for row in explicit_table.split("\n * - ")[2:]:
cells = row.split("\n - ")
observatory_codes = re.findall(r"``([A-Z0-9]+)``", cells[0])
band_mappings = re.findall(r"``([^`]+)``\s+→\s+``([A-Za-z0-9_]+)``", cells[1])
documented_mappings.update(
(observatory_code, reported_band, filter_id)
for observatory_code in observatory_codes
for reported_band, filter_id in band_mappings
)

mapping = load_observatory_band_map()
vendored_mappings = set(
zip(
mapping.observatory_code.to_pylist(),
mapping.reported_band.to_pylist(),
mapping.filter_id.to_pylist(),
)
)
assert documented_mappings == vendored_mappings

fallback_table = mapping_section.split(
".. list-table:: Generic reported-band fallbacks", maxsplit=1
)[1].split("Generic fallback matching", maxsplit=1)[0]
documented_fallbacks = dict(
re.findall(r"\* - ``([^`]+)``\s+- ``([A-Za-z0-9_]+)``", fallback_table)
)
unknown_codes = [f"Z{i:02d}" for i in range(len(documented_fallbacks))]
resolved_fallbacks = map_to_canonical_filter_bands(
unknown_codes,
documented_fallbacks.keys(),
)
assert dict(zip(documented_fallbacks, resolved_fallbacks)) == documented_fallbacks


def test_bandpass_curves_are_sane():
curves = load_bandpass_curves()
assert len(curves) > 0
Expand Down
Loading