Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
40 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
6fa9743
Use better name for instruments module
sfinkens May 20, 2026
677a388
Choose better method names
sfinkens May 20, 2026
0f9c99c
Add internal to WMO conversion method
sfinkens May 20, 2026
5ecfa7f
Make scene.sensor_names return WMO names
sfinkens May 20, 2026
020a1d9
Convert WMO to internal in enhancer
sfinkens May 20, 2026
18744ea
Fix sensor_names expectations in tests
sfinkens May 20, 2026
98eaa15
Merge branch 'main' into wmo-instruments-part1
sfinkens May 20, 2026
8afd9f2
Wrap deprecation warning with v1.0 scissors
sfinkens May 22, 2026
bd2a355
Change scissor version to 1.1
sfinkens May 22, 2026
e0e76de
Add config switch for legacy sensor attribute
sfinkens May 22, 2026
f6a5fee
Update documentation
sfinkens May 22, 2026
02c455a
Change scissors again
sfinkens May 22, 2026
c147e09
Change scissors again
sfinkens May 22, 2026
e9e3e9f
Remove config switch
sfinkens May 22, 2026
bbe02e1
Merge branch 'wmo-instruments-part2-config-switch' into wmo-instrumen…
sfinkens May 22, 2026
831938d
Fix exceptions in sensor compatibility
sfinkens May 22, 2026
da4b460
Update documentation
sfinkens May 22, 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
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": "sensor"
}

# Satpy main configuration object
Expand Down
3 changes: 2 additions & 1 deletion satpy/composites/aux_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import satpy
from satpy.aux_download import DataDownloadMixin
from satpy.utils import set_instruments_attr

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
set_instruments_attr(img.attrs, set())
Comment thread
djhoese marked this conversation as resolved.
Outdated
img.attrs["mode"] = "".join(img.bands.data)
img.attrs.pop("modifiers", None)
img.attrs.pop("calibration", None)
Expand Down
4 changes: 2 additions & 2 deletions satpy/composites/config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
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
from satpy.utils import recursive_dict_update
from satpy.utils import normalize_instrument_name, recursive_dict_update

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -268,7 +268,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 = 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
21 changes: 6 additions & 15 deletions satpy/composites/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

from satpy.dataset import DataID, combine_metadata
from satpy.dataset.dataid import minimal_default_keys_config
from satpy.utils import unify_chunks
from satpy.utils import get_instruments_from_attrs, set_instruments_attr, unify_chunks

LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -433,20 +433,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(get_instruments_from_attrs(projectable.attrs))
return sensors

def __call__(
self,
Expand Down Expand Up @@ -512,7 +503,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)
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 @@ -26,6 +26,7 @@
import xarray as xr

from satpy.dataset import combine_metadata
from satpy.utils import get_instruments_from_attrs, set_instruments_attr

from .core import (
GenericCompositor,
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 get_instruments_from_attrs(attrs):
# sensor can be a set
attrs["sensor"] = self._get_sensors([foreground, background])
instruments = self._get_sensors([foreground, background])
set_instruments_attr(attrs, instruments)
return attrs

@staticmethod
Expand Down
2 changes: 1 addition & 1 deletion satpy/composites/glm.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def _update_attrs(self, new_data, background_layer, highlight_layer):
new_data.attrs["units"] = 1
new_sensors = self._get_sensors((highlight_layer, background_layer))
new_data.attrs.update({
"sensor": new_sensors,
"instruments": new_sensors,
Comment thread
sfinkens marked this conversation as resolved.
Outdated
})

def __call__(self, projectables, optional_datasets=None, **attrs):
Expand Down
34 changes: 19 additions & 15 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 @@ -24,7 +24,13 @@

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_instruments_from_attrs,
get_instruments_key,
get_logger,
normalize_instrument_name,
recursive_dict_update,
)

LOG = get_logger(__name__)

Expand All @@ -34,16 +40,17 @@

def __init__(self, *decision_dicts, **kwargs):
"""Init the decision tree."""
instr_key = get_instruments_key()
match_keys = kwargs.pop("match_keys",
("name",
"reader",
"platform_name",
"sensor",
instr_key,
Comment thread
sfinkens marked this conversation as resolved.
Outdated
"standard_name",
"units",
))
self.prefix = kwargs.pop("config_section", "enhancements")
multival_keys = kwargs.pop("multival_keys", ["sensor"])
multival_keys = kwargs.pop("multival_keys", [instr_key])
super(EnhancementDecisionTree, self).__init__(
decision_dicts, match_keys, multival_keys)

Expand Down Expand Up @@ -122,26 +129,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 = 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 +213,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 = 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
9 changes: 6 additions & 3 deletions satpy/modifiers/_crefl_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
import xarray as xr

from satpy.dataset.dataid import WavelengthRange
from satpy.utils import get_one_instrument_from_attrs, normalize_instrument_name

LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -282,7 +283,8 @@ def run_crefl(refl,
:param avg_elevation: average elevation (usually pre-calculated and stored in CMGDEM.hdf)

"""
runner_cls = _runner_class_for_sensor(refl.attrs["sensor"])
sensor = get_one_instrument_from_attrs(refl.attrs)
runner_cls = _runner_class_for_sensor(sensor)
runner = runner_cls(refl)
corr_refl = runner(sensor_azimuth, sensor_zenith, solar_azimuth, solar_zenith, avg_elevation)
return corr_refl
Expand Down Expand Up @@ -348,8 +350,9 @@ def _run_crefl(self, mus, muv, phi, solar_zenith, sensor_zenith, height, coeffs)

class _VIIRSMODISCREFLRunner(_CREFLRunner):
def _run_crefl(self, mus, muv, phi, solar_zenith, sensor_zenith, height, coeffs):
instrument = get_one_instrument_from_attrs(self._refl.attrs)
return da.map_blocks(_run_crefl, self._refl.data, mus.data, muv.data, phi.data,
height, self._refl.attrs.get("sensor"), *coeffs,
height, instrument, *coeffs,
meta=np.ndarray((), dtype=self._refl.dtype),
chunks=self._refl.chunks, dtype=self._refl.dtype,
)
Expand Down Expand Up @@ -384,7 +387,7 @@ def _run_crefl(self, mus, muv, phi, solar_zenith, sensor_zenith, height, coeffs)

def _runner_class_for_sensor(sensor_name: str) -> Type[_CREFLRunner]:
try:
return _SENSOR_TO_RUNNER[sensor_name]
return _SENSOR_TO_RUNNER[normalize_instrument_name(sensor_name)]
except KeyError:
raise NotImplementedError(f"Don't know how to apply CREFL to data from sensor {sensor_name}.")

Expand Down
11 changes: 9 additions & 2 deletions satpy/modifiers/atmosphere.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from satpy.modifiers import ModifierBase
from satpy.modifiers._crefl import ReflectanceCorrector # noqa
from satpy.modifiers.angles import compute_relative_azimuth, get_angles, get_satellite_zenith_angle
from satpy.utils import get_one_instrument_from_attrs, get_pyspectral_instrument_name

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -104,7 +105,10 @@ def __call__(self, projectables, optional_datasets=None, **info):
logger.info("Removing Rayleigh scattering with atmosphere '%s' and "
"aerosol type '%s' for '%s'",
atmosphere, aerosol_type, vis.attrs["name"])
corrector = Rayleigh(vis.attrs["platform_name"], vis.attrs["sensor"],
sensor = get_pyspectral_instrument_name(
get_one_instrument_from_attrs(vis.attrs)
)
corrector = Rayleigh(vis.attrs["platform_name"], sensor,
atmosphere=atmosphere,
aerosol_type=aerosol_type)

Expand Down Expand Up @@ -158,8 +162,11 @@ def __call__(self, projectables, optional_datasets=None, **info):
satz = satz.data # get dask array underneath

logger.info("Correction for limb cooling")
sensor = get_pyspectral_instrument_name(
get_one_instrument_from_attrs(band.attrs)
)
corrector = AtmosphericalCorrection(band.attrs["platform_name"],
band.attrs["sensor"])
sensor)

atm_corr = da.map_blocks(_call_mapped_correction, satz, band.data,
corrector=corrector,
Expand Down
7 changes: 5 additions & 2 deletions satpy/modifiers/spectral.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import xarray as xr

from satpy.modifiers import ModifierBase
from satpy.utils import get_one_instrument_from_attrs, get_pyspectral_instrument_name

try:
from pyspectral.near_infrared_reflectance import Calculator
Expand Down Expand Up @@ -131,8 +132,10 @@ def _init_reflectance_calculator(self, metadata):
if not Calculator:
logger.info("Couldn't load pyspectral")
raise ImportError("No module named pyspectral.near_infrared_reflectance")

reflectance_3x_calculator = Calculator(metadata["platform_name"], metadata["sensor"], metadata["name"],
sensor = get_pyspectral_instrument_name(
get_one_instrument_from_attrs(metadata)
)
reflectance_3x_calculator = Calculator(metadata["platform_name"], sensor, metadata["name"],
sunz_threshold=self.sun_zenith_threshold,
masking_limit=self.masking_limit)
return reflectance_3x_calculator
Expand Down
2 changes: 1 addition & 1 deletion satpy/readers/abi_l1b.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def get_dataset(self, key, info):

def _adjust_attrs(self, data, key):
data.attrs.update({"platform_name": self.platform_name,
"sensor": self.sensor})
"sensor": {self.sensor}})
# Add orbital parameters
projection = self.nc["goes_imager_projection"]
data.attrs["orbital_parameters"] = {
Expand Down
2 changes: 1 addition & 1 deletion satpy/readers/core/file_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def end_time(self):
return self.filename_info.get("end_time", self.start_time)

@property
def sensor_names(self):
def sensor_names(self) -> set:
"""List of sensors represented in this file."""
raise NotImplementedError

Expand Down
13 changes: 6 additions & 7 deletions satpy/scene.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env python

Check notice on line 1 in satpy/scene.py

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

✅ Getting better: Lines of Code in a Single File

The lines of code decreases from 1321 to 1316, improve code health by reducing it to 600. The number of Lines of Code in a single file. More Lines of Code lowers the code health.
# -*- coding: utf-8 -*-
# Copyright (c) 2010-2022 Satpy developers
#
Expand Down Expand Up @@ -36,7 +36,11 @@
from satpy.dependency_tree import DependencyTree
from satpy.node import CompositorNode, MissingDependencies, ReaderNode
from satpy.readers.core.loading import load_readers
from satpy.utils import convert_remote_files_to_fsspec, get_storage_options_from_reader_kwargs
from satpy.utils import (
convert_remote_files_to_fsspec,
get_instruments_from_attrs,
get_storage_options_from_reader_kwargs,
)

LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -197,12 +201,7 @@
def _contained_sensor_names(self) -> set[str]:
sensor_names = set()
for data_arr in self.values():
if "sensor" not in data_arr.attrs:
continue
if isinstance(data_arr.attrs["sensor"], str):
sensor_names.add(data_arr.attrs["sensor"])
elif isinstance(data_arr.attrs["sensor"], set):
sensor_names.update(data_arr.attrs["sensor"])
sensor_names.update(get_instruments_from_attrs(data_arr.attrs))
return sensor_names

@property
Expand Down
4 changes: 2 additions & 2 deletions satpy/tests/compositor_tests/test_aux_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def load(self, arg):
filenames=[IMAGE_FILENAME])
register.assert_not_called()
retrieve.assert_not_called()
assert res.attrs["sensor"] is None
assert res.attrs["sensor"] == set()
assert "modifiers" not in res.attrs
assert "calibration" not in res.attrs

Expand All @@ -95,7 +95,7 @@ def load(self, arg):
res = comp()
Scene.assert_called_once_with(reader="generic_image",
filenames=[os.path.join("data_dir", "foo.tif")])
assert res.attrs["sensor"] is None
assert res.attrs["sensor"] == set()
assert "modifiers" not in res.attrs
assert "calibration" not in res.attrs

Expand Down
10 changes: 5 additions & 5 deletions satpy/tests/compositor_tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,13 +277,13 @@ def test_concat_datasets(self):
def test_get_sensors(self):
"""Test getting sensors from the dataset attributes."""
res = self.comp._get_sensors([self.all_valid])
assert res is None
assert res == set()
dset1 = self.all_valid
dset1.attrs["sensor"] = "foo"
res = self.comp._get_sensors([dset1])
assert res == "foo"
assert res == {"foo"}
dset2 = self.first_invalid
dset2.attrs["sensor"] = "bar"
dset2.attrs["sensor"] = {"bar"}
res = self.comp._get_sensors([dset1, dset2])
assert "foo" in res
assert "bar" in res
Expand Down Expand Up @@ -327,12 +327,12 @@ def test_call(self):
"""Test calling generic compositor."""
# Multiple datasets with extra attributes
all_valid = self.all_valid
all_valid.attrs["sensor"] = "foo"
all_valid.attrs["sensor"] = {"foo"}
attrs = {"foo": "bar", "resolution": 333}
self.comp.attrs["resolution"] = None
res = self.comp([self.all_valid, self.first_invalid], **attrs)
# Verify attributes
assert res.attrs.get("sensor") == "foo"
assert res.attrs.get("sensor") == {"foo"}
assert "foo" in res.attrs
assert res.attrs.get("foo") == "bar"
assert "units" not in res.attrs
Expand Down
2 changes: 1 addition & 1 deletion satpy/tests/reader_tests/test_abi_l1b.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ def test_get_dataset(self, c01_data_arr):
"scan_mode": "M4",
"scene_abbr": "C",
"scene_id": None,
"sensor": "abi",
"sensor": {"abi"},
"timeline_ID": None,
"suffix": "suffix",
"units": "W m-2 um-1 sr-1",
Expand Down
Loading
Loading