Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
78894c3
Make sensor attribute a set
sfinkens May 4, 2026
dac0457
Use instrument instead of sensor attribute
sfinkens May 6, 2026
8ffb671
Add instrument attribute setter
sfinkens May 6, 2026
2d730ea
Reset ABI name
sfinkens May 6, 2026
180a040
Convert string type sensors to set for now
sfinkens May 6, 2026
46b08c2
Fix tests
sfinkens May 6, 2026
fe1d232
Update docstring
sfinkens May 6, 2026
32ee183
Restore accidental string replace
sfinkens May 6, 2026
bb04e29
Make sensor attribute a set
sfinkens May 4, 2026
fbb55cf
Use instrument instead of sensor attribute
sfinkens May 6, 2026
9633197
Add instrument attribute setter
sfinkens May 6, 2026
1d362c7
Reset ABI name
sfinkens May 6, 2026
ae1d8e7
Convert string type sensors to set for now
sfinkens May 6, 2026
d95a3f3
Fix tests
sfinkens May 6, 2026
2107f44
Update docstring
sfinkens May 6, 2026
78e0de9
Restore accidental string replace
sfinkens May 6, 2026
32a8e02
Replace direct attribute access with setter
sfinkens May 7, 2026
1e69183
Merge branch 'wmo-instruments-part1' of https://github.com/sfinkens/s…
sfinkens May 7, 2026
e4a53b6
Restore enhancer keyword arguments
sfinkens May 7, 2026
db7731d
Add deprecation warnings for sensor attribute
sfinkens May 7, 2026
8d50a8d
Add tests for instrument utilities
sfinkens May 7, 2026
f0970e3
Move instrument helpers to their own module
sfinkens May 8, 2026
9ca9b79
Remove explicit sensor usage in dependency tree
sfinkens May 8, 2026
cc6bcbd
Replace sensor with instruments in file handlers
sfinkens May 11, 2026
ad763f2
Replace sensor with instruments in file handlers
sfinkens May 13, 2026
b448d83
Replace sensors with instruments in yaml reader
sfinkens May 13, 2026
cddd659
Add helper for GOES Imager
sfinkens May 13, 2026
69d6e18
Fix test
sfinkens May 13, 2026
0e32aab
Use Enum for WMO instrument names
sfinkens May 18, 2026
da20004
Revert MIMIC changes
sfinkens May 18, 2026
4f0ff26
Adapt instrument normalization to WMO names
sfinkens May 18, 2026
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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ repos:
- types-setuptools
- types-PyYAML
- types-requests
args: ["--python-version", "3.10", "--ignore-missing-imports"]
args: ["--python-version", "3.11", "--ignore-missing-imports"]
- repo: https://github.com/pycqa/isort
rev: 9.0.0a3
hooks:
Expand Down
1 change: 1 addition & 0 deletions satpy/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"readers": {
"clip_negative_radiances": False,
},
"instruments_key": "instruments"
}

# Satpy main configuration object
Expand Down
171 changes: 171 additions & 0 deletions satpy/_instruments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Copyright (c) 2026 Satpy developers
#
# This file is part of satpy.
#
# satpy is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# satpy is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# satpy. If not, see <http://www.gnu.org/licenses/>.
"""Helpers for accessing and modifying instrument attributes."""

import logging
import warnings
from enum import StrEnum
from typing import Any

import satpy

logger = logging.getLogger(__name__)

def get_instruments_from_attrs(attrs: dict[str,Any]) -> set[str]:
"""Get instrument names from dataset attributes.

String type attributes are converted to set. This can be
removed once all file handlers provide instruments as a
set.
"""
legacy = attrs.get("sensor", set())
instruments = attrs.get("instruments", legacy)
if legacy:
warnings.warn(
"Satpy will ignore the 'sensor' attribute as of v1.1. "
"Use the 'instruments' attribute instead.",
DeprecationWarning,
stacklevel=2
)
if isinstance(instruments, str):
warnings.warn(
"Converting 'instruments' attribute from string to set. "
"This will result in an error in v1.1, when Satpy will require "
"set type instruments attributes.",
DeprecationWarning,
stacklevel=2
)
instruments = set([instruments])
return instruments



def normalize_instrument_name(instrument: str) -> str:
"""Normalize instrument name for internal usage."""
sep_map = {
"-": "",
"(": "",
")": "",
" ": "_",
"/": "-"
}
sep_trans = str.maketrans(sep_map)
return instrument.translate(sep_trans).lower()


def get_one_instrument_from_attrs(attrs: dict[str,Any]) -> str:
"""Get a single instrument name from dataset attributes."""
instruments = get_instruments_from_attrs(attrs)
if not instruments:
raise KeyError("No 'instruments' in dataset attribute")
if len(instruments) > 1:
logger.warning(f"More than one instrument in dataset attributes, will use the first value: {instruments}")
return list(instruments)[0]


def get_pyspectral_instrument_name(instrument: str) -> str:
"""Get instrument name expected by pyspectral."""
return normalize_instrument_name(instrument)


def serialize_instruments(instruments: set[str]) -> str:
"""Serialize a set of instruments."""
sep_map = {
"-": "",
"(": "",
")": "",
" ": "",
"/": ""
}
sep_trans = str.maketrans(sep_map)
return "-".join(
instr.translate(sep_trans).lower()
for instr in sorted(instruments)
)


def set_instruments_attr(attrs: dict[str,Any], instruments: set[str]|str) -> None:
"""Set 'instruments' dataset atrribute."""
key = get_instruments_key()
attrs[key] = instruments


def get_instruments_key():
"""Get key for instruments in dataset attributes."""
return satpy.config.get("instruments_key")



class OSCAR(StrEnum):
"""WMO OSCAR instrument names."""
ABI = "ABI"
AHI = "AHI"
AMSR_2 = "AMSR2"
AMSU_A = "AMSU-A"
AMSU_B = "AMSU-B"
ATMS = "ATMS"
AVHRR = "AVHRR"
AVHRR_2 = "AVHRR/2"
AVHRR_3 = "AVHRR/3"
CRIS = "CrIS"
EPIC = "EPIC"
ETM_PLUS = "ETM+"
FCI = "FCI"
GLM = "GLM"
GMI = "GMI"
IASI = "IASI"
IASI_NG = "IASI-NG"
IMAGER_GOES_12_15 = "IMAGER (GOES 12-15)"
IMAGER_GOES_8_11 = "IMAGER (GOES 8-11)"
IMAGER_INSAT = "IMAGER (INSAT)"
IMAGER_MTSAT_2 = "IMAGER (MTSAT-2)"
JAMI = "JAMI"
LI = "LI"
MERIS = "MERIS"
MERSI_1 = "MERSI-1"
MERSI_2 = "MERSI-2"
MERSI_3 = "MERSI-3"
MERSI_LL = "MERSI-LL"
MERSI_RM = "MERSI-RM"
METIMAGE = "METimage"
MHS = "MHS"
MODIS = "MODIS"
MSS = "MSS"
MSU_GS = "MSU-GS"
MSU_GS_A = "MSU-GS/A"
MVIRI = "MVIRI"
# OSCAR lists "MWR (Sterna)", "MWR (AWS)" etc.
# But to avoid enhancement/composite duplication
# we just use "MWR".
MWR = "MWR"
OCI = "OCI"
OLCI = "OLCI"
OLI = "OLI"
SEAWIFS = "SeaWiFS"
SEVIRI = "SEVIRI"
SGLI = "SGLI"
SLSTR = "SLSTR"
SSMIS = "SSMIS"
TIRS = "TIRS"
TM = "TM"
VIIRS = "VIIRS"
VISSR = "VISSR"
VISSR_HIMAWARI_5 = "VISSR (Himawari-5)"


def enum_to_str(instruments: set[StrEnum]) -> set[str]:
"""Convert OSCAR enums to string."""
return {str(i) for i in instruments}
3 changes: 2 additions & 1 deletion satpy/composites/aux_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import os

import satpy
import satpy._instruments as instru
from satpy.aux_download import DataDownloadMixin

from .core import GenericCompositor
Expand Down Expand Up @@ -156,7 +157,7 @@ def __call__(self, *args, **kwargs):
if self.area is None:
raise AttributeError("Area definition needs to be configured")
img.attrs["area"] = self.area
img.attrs["sensor"] = None
instru.set_instruments_attr(img.attrs, set())
img.attrs["mode"] = "".join(img.bands.data)
img.attrs.pop("modifiers", None)
img.attrs.pop("calibration", None)
Expand Down
3 changes: 2 additions & 1 deletion satpy/composites/config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from yaml import UnsafeLoader

import satpy
import satpy._instruments as instru
from satpy import DataID, DataQuery
from satpy._config import config_search_paths, get_entry_points_config_dirs, glob_config
from satpy.dataset.dataid import minimal_default_keys_config
Expand Down Expand Up @@ -268,7 +269,7 @@ def load_compositor_configs_for_sensor(sensor_name: str) -> tuple[dict[str, dict
DataID key -> key properties

"""
config_filename = sensor_name + ".yaml"
config_filename = instru.normalize_instrument_name(sensor_name) + ".yaml"
logger.debug("Looking for composites config file %s", config_filename)
paths = get_entry_points_config_dirs("satpy.composites")
composite_configs = config_search_paths(
Expand Down
20 changes: 6 additions & 14 deletions satpy/composites/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import numpy as np
import xarray as xr

import satpy._instruments as instru
from satpy.dataset import DataID, combine_metadata
from satpy.dataset.dataid import minimal_default_keys_config
from satpy.utils import unify_chunks
Expand Down Expand Up @@ -433,20 +434,11 @@ def _concat_datasets(self, projectables, mode):

return data

def _get_sensors(self, projectables):
sensor = set()
def _get_sensors(self, projectables) -> set[str]:
sensors = set()
for projectable in projectables:
current_sensor = projectable.attrs.get("sensor", None)
if current_sensor:
if isinstance(current_sensor, (str, bytes)):
sensor.add(current_sensor)
else:
sensor |= current_sensor
if len(sensor) == 0:
sensor = None
elif len(sensor) == 1:
sensor = list(sensor)[0]
return sensor
sensors.update(instru.get_instruments_from_attrs(projectable.attrs))
return sensors

def __call__(
self,
Expand Down Expand Up @@ -512,7 +504,7 @@ def _get_updated_attrs(self, datasets, attrs, mode):
new_attrs.update(self.attrs)
if resolution is not None:
new_attrs["resolution"] = resolution
new_attrs["sensor"] = self._get_sensors(datasets)
instru.set_instruments_attr(new_attrs, self._get_sensors(datasets))
new_attrs["mode"] = mode

return new_attrs
Expand Down
6 changes: 4 additions & 2 deletions satpy/composites/fill.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import numpy as np
import xarray as xr

import satpy._instruments as instru
from satpy.dataset import combine_metadata

from .core import (
Expand Down Expand Up @@ -406,9 +407,10 @@ def _combine_metadata_with_mode_and_sensor(self,
# 'mode' is no longer valid after we've remove the 'A'
# let the base class __call__ determine mode
attrs.pop("mode", None)
if attrs.get("sensor") is None:
if not instru.get_instruments_from_attrs(attrs):
# sensor can be a set
attrs["sensor"] = self._get_sensors([foreground, background])
instruments = self._get_sensors([foreground, background])
instru.set_instruments_attr(attrs, instruments)
return attrs

@staticmethod
Expand Down
5 changes: 2 additions & 3 deletions satpy/composites/glm.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import xarray as xr

import satpy._instruments as instru
from satpy.enhancements.enhancer import get_enhanced_image

from .core import GenericCompositor
Expand Down Expand Up @@ -96,9 +97,7 @@ def _update_attrs(self, new_data, background_layer, highlight_layer):
new_data.attrs = background_layer.attrs.copy()
new_data.attrs["units"] = 1
new_sensors = self._get_sensors((highlight_layer, background_layer))
new_data.attrs.update({
"sensor": new_sensors,
})
instru.set_instruments_attr(new_data.attrs, new_sensors)

def __call__(self, projectables, optional_datasets=None, **attrs):
"""Create RGBA image with highlighted pixels."""
Expand Down
4 changes: 3 additions & 1 deletion satpy/dependency_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import numpy as np

import satpy._instruments as instru
from satpy import DataID, DatasetDict
from satpy.dataset import ModifierTuple, create_filtered_query
from satpy.dataset.data_dict import TooManyResults, get_key
Expand Down Expand Up @@ -504,6 +505,7 @@ def get_compositor(self, key):
def get_modifier(self, comp_id):
"""Get a modifer."""
# create a DataID for the compositor we are generating
instr_key = instru.get_instruments_key()
modifier = comp_id["modifiers"][-1]
for sensor_name in sorted(self.modifiers):
modifiers = self.modifiers[sensor_name]
Expand All @@ -514,7 +516,7 @@ def get_modifier(self, comp_id):
mloader, moptions = modifiers[modifier]
moptions = moptions.copy()
moptions.update(comp_id.to_dict())
moptions["sensor"] = sensor_name
moptions[instr_key] = sensor_name
compositors[comp_id] = mloader(_satpy_id=comp_id, **moptions)
return compositors[comp_id]

Expand Down
27 changes: 14 additions & 13 deletions satpy/enhancements/enhancer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) 2025 Satpy developers

Check notice on line 1 in satpy/enhancements/enhancer.py

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

✅ Getting better: Overall Code Complexity

The mean cyclomatic complexity decreases from 4.40 to 4.30, threshold = 4. This file has many conditional statements (e.g. if, for, while) across its implementation, leading to lower code health. Avoid adding more conditionals.
#
# This file is part of satpy.
#
Expand All @@ -22,9 +22,13 @@
import yaml
from yaml import UnsafeLoader

import satpy._instruments as instru
from satpy._config import config_search_paths, get_entry_points_config_dirs
from satpy.decision_tree import DecisionTree
from satpy.utils import get_logger, recursive_dict_update
from satpy.utils import (
get_logger,
recursive_dict_update,
)

LOG = get_logger(__name__)

Expand Down Expand Up @@ -122,26 +126,23 @@

self.sensor_enhancement_configs = []

def get_sensor_enhancement_config(self, sensor):
def get_sensor_enhancement_config(self, sensors: set[str]):
"""Get the sensor-specific config."""
if isinstance(sensor, str):
# one single sensor
sensor = [sensor]

paths = get_entry_points_config_dirs("satpy.enhancements")
for sensor_name in sensor:
config_fn = os.path.join("enhancements", sensor_name + ".yaml")
for sensor_name in sensors:
basename = instru.normalize_instrument_name(sensor_name) + ".yaml"
config_fn = os.path.join("enhancements", basename)
config_files = config_search_paths(config_fn, search_dirs=paths)
# Note: Enhancement configuration files can't overwrite individual
# options, only entire sections are overwritten
for config_file in config_files:
yield config_file

def add_sensor_enhancements(self, sensor):
def add_sensor_enhancements(self, sensors: set[str]):
"""Add sensor-specific enhancements."""
# XXX: Should we just load all enhancements from the base directory?
new_configs = []
for config_file in self.get_sensor_enhancement_config(sensor):
for config_file in self.get_sensor_enhancement_config(sensors):
if config_file not in self.sensor_enhancement_configs:
self.sensor_enhancement_configs.append(config_file)
new_configs.append(config_file)
Expand Down Expand Up @@ -209,9 +210,9 @@
if enhancer is None or enhancer.enhancement_tree is None:
LOG.debug("No enhancement being applied to dataset")
else:
if dataset.attrs.get("sensor", None):
enhancer.add_sensor_enhancements(dataset.attrs["sensor"])

sensors = instru.get_instruments_from_attrs(dataset.attrs)
if sensors:
enhancer.add_sensor_enhancements(sensors)
enhancer.apply(img, **dataset.attrs)

if overlay is not None:
Expand Down
Loading
Loading