diff --git a/satpy/composites/lightning.py b/satpy/composites/lightning.py
new file mode 100644
index 0000000000..a7931027b9
--- /dev/null
+++ b/satpy/composites/lightning.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019 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 .
+"""Composite classes for the LI instrument."""
+
+import logging
+import sys
+
+import numpy as np
+import xarray as xr
+
+from satpy.composites import CompositeBase
+
+LOG = logging.getLogger(__name__)
+
+
+class LightningTimeCompositor(CompositeBase):
+ """Class used to create the flash_age compositor usefull for lighting event visualisation.
+
+ The datas used are dates related to the lightning event that should be normalised between
+ 0 and 1. The value 1 corresponds to the latest lightning event and the value 0 corresponds
+ to the latest lightning event - time_range. The time_range is defined in the satpy/etc/composites/li.yaml
+ and is in minutes.
+ """
+ def __init__(self, name, prerequisites=None, optional_prerequisites=None, **kwargs):
+ """Initialisation of the class."""
+ self.name = name
+ super().__init__(name, prerequisites, optional_prerequisites, **kwargs)
+ # Get the time_range which is in minute
+ self.time_range = self.attrs["time_range"]
+ self.standard_name = self.attrs["standard_name"]
+ self.reference_time = self.attrs["reference_time"]
+
+
+ def _normalize_time(self,data:xr.DataArray,attrs:dict)->xr.DataArray:
+ """Normalised the time in the range between [end_time,end_time - time_range].
+
+ The range of the normalised data is between 0 and 1 where 0 corresponds to the date end_time - time_range
+ and 1 to the end_time. Where end_times represent the latest lightning event and time_range is the range of
+ time you want to see the event.The dates that are earlier to end_time - time_range are removed.
+
+ Args:
+ data (xr.DataArray): datas containing dates to be normalised
+ attrs (dict): Attributes suited to the flash_age composite
+
+ Returns:
+ xr.DataArray: Normalised time
+ """
+ # Compute the maximum time value
+ end_time = np.array(np.datetime64(data.attrs[self.reference_time]))
+ # Compute the minimum time value based on the time range
+ begin_time = end_time - np.timedelta64(self.time_range, "m")
+ # Drop values that are bellow begin_time
+ data = data.where(data >= begin_time, drop=True)
+ # exit if data is empty afer filtering
+ if data.size == 0 :
+ LOG.error(f"All the flash_age events happened before {begin_time}")
+ sys.exit(1)
+ # Normalize the time values
+ normalized_data = (data - begin_time) / (end_time - begin_time)
+ # Ensure the result is still an xarray.DataArray
+ return xr.DataArray(normalized_data, dims=data.dims, coords=data.coords,attrs=attrs)
+
+
+ @staticmethod
+ def _update_missing_metadata(existing_attrs, new_attrs):
+ for key, val in new_attrs.items():
+ if key not in existing_attrs and val is not None:
+ existing_attrs[key] = val
+
+ def _redefine_metadata(self,attrs:dict)->dict:
+ """Modify the standard_name and name metadatas.
+
+ Args:
+ attrs (dict): data's attributes
+
+ Returns:
+ dict: atualised attributes
+ """
+ attrs["name"] = self.standard_name
+ attrs["standard_name"] =self.standard_name
+ # Attributes to describe the values range
+ return attrs
+
+
+ def __call__(self,projectables, nonprojectables=None, **attrs):
+ """Normalise the dates."""
+ data = projectables[0]
+ new_attrs = data.attrs.copy()
+ self._update_missing_metadata(new_attrs, attrs)
+ new_attrs = self._redefine_metadata(new_attrs)
+ return self._normalize_time(data,new_attrs)
diff --git a/satpy/etc/composites/li.yaml b/satpy/etc/composites/li.yaml
index 4d3cc88e95..19e879590c 100644
--- a/satpy/etc/composites/li.yaml
+++ b/satpy/etc/composites/li.yaml
@@ -10,69 +10,78 @@ composites:
compositor: !!python/name:satpy.composites.SingleBandCompositor
standard_name: acc_flash
prerequisites:
- - flash_accumulation
+ - flash_accumulation
acc_flash_alpha:
description: Composite to colorise the AF product using the flash accumulation with transparency
compositor: !!python/name:satpy.composites.SingleBandCompositor
standard_name: acc_flash_alpha
prerequisites:
- - flash_accumulation
+ - flash_accumulation
acc_flash_area:
description: Composite to colorise the AFA product using the flash area
compositor: !!python/name:satpy.composites.SingleBandCompositor
standard_name: acc_flash_area
prerequisites:
- - accumulated_flash_area
+ - accumulated_flash_area
acc_flash_area_alpha:
description: Composite to colorise the AFA product using the flash area with transparency
compositor: !!python/name:satpy.composites.SingleBandCompositor
standard_name: acc_flash_area_alpha
prerequisites:
- - accumulated_flash_area
+ - accumulated_flash_area
acc_flash_radiance:
description: Composite to colorise the AFR product using the flash radiance
compositor: !!python/name:satpy.composites.SingleBandCompositor
standard_name: lightning_radiance
prerequisites:
- - flash_radiance
+ - flash_radiance
acc_flash_radiance_alpha:
description: Composite to colorise the AFR product using the flash radiance with transparency
compositor: !!python/name:satpy.composites.SingleBandCompositor
standard_name: lightning_radiance_alpha
prerequisites:
- - flash_radiance
+ - flash_radiance
flash_radiance:
description: Composite to colorise the LFL product using the flash radiance
compositor: !!python/name:satpy.composites.SingleBandCompositor
standard_name: lightning_radiance
prerequisites:
- - radiance
+ - radiance
flash_radiance_alpha:
description: Composite to colorise the LFL product using the flash radiance with transparency
compositor: !!python/name:satpy.composites.SingleBandCompositor
standard_name: lightning_radiance_alpha
prerequisites:
- - radiance
+ - radiance
group_radiance:
description: Composite to colorise the LGR product using the flash radiance
compositor: !!python/name:satpy.composites.SingleBandCompositor
standard_name: lightning_radiance
prerequisites:
- - radiance
+ - radiance
group_radiance_alpha:
description: Composite to colorise the LGR product using the flash radiance with transparency
compositor: !!python/name:satpy.composites.SingleBandCompositor
standard_name: lightning_radiance_alpha
prerequisites:
- - radiance
+ - radiance
# DEPRECATED, USE acc_flash_area INSTEAD
flash_area:
compositor: !!python/name:satpy.composites.SingleBandCompositor
standard_name: acc_flash_area
prerequisites:
- - accumulated_flash_area
+ - accumulated_flash_area
+
+ flash_age:
+ description: Composite to colorise the LFL product using the flash time
+ compositor: !!python/name:satpy.composites.lightning.LightningTimeCompositor
+ standard_name: lightning_time
+ time_range: 60 # range for colormap in minutes
+ reference_time: end_time
+ prerequisites:
+ - flash_time
diff --git a/satpy/etc/enhancements/li.yaml b/satpy/etc/enhancements/li.yaml
index 49009808eb..9aaa5c4a0b 100644
--- a/satpy/etc/enhancements/li.yaml
+++ b/satpy/etc/enhancements/li.yaml
@@ -1,60 +1,84 @@
enhancements:
-# note that the colormap parameters are tuned for 5 minutes of files accumulation
-# these are tentative recipes that will need to be further tuned as we gain experience with LI data
+ # note that the colormap parameters are tuned for 5 minutes of files accumulation
+ # these are tentative recipes that will need to be further tuned as we gain experience with LI data
acc_flash:
standard_name: acc_flash
operations:
- - name: colorize
- method: !!python/name:satpy.enhancements.colorize
- kwargs:
- palettes:
- - {colors: ylorrd, min_value: 0, max_value: 5}
+ - name: colorize
+ method: !!python/name:satpy.enhancements.colorize
+ kwargs:
+ palettes:
+ - { colors: ylorrd, min_value: 0, max_value: 5 }
acc_flash_alpha:
standard_name: acc_flash_alpha
operations:
- - name: colorize
- method: !!python/name:satpy.enhancements.colorize
- kwargs:
- palettes:
- - {colors: ylorrd, min_value: 0, max_value: 5,
- min_alpha: 120, max_alpha: 180}
+ - name: colorize
+ method: !!python/name:satpy.enhancements.colorize
+ kwargs:
+ palettes:
+ - {
+ colors: ylorrd,
+ min_value: 0,
+ max_value: 5,
+ min_alpha: 120,
+ max_alpha: 180,
+ }
acc_flash_area:
standard_name: acc_flash_area
operations:
- - name: colorize
- method: !!python/name:satpy.enhancements.colorize
- kwargs:
- palettes:
- - {colors: ylorrd, min_value: 0, max_value: 20}
+ - name: colorize
+ method: !!python/name:satpy.enhancements.colorize
+ kwargs:
+ palettes:
+ - { colors: ylorrd, min_value: 0, max_value: 20 }
acc_flash_area_alpha:
standard_name: acc_flash_area_alpha
operations:
- - name: colorize
- method: !!python/name:satpy.enhancements.colorize
- kwargs:
- palettes:
- - {colors: ylorrd, min_value: 0, max_value: 20,
- min_alpha: 120, max_alpha: 180}
+ - name: colorize
+ method: !!python/name:satpy.enhancements.colorize
+ kwargs:
+ palettes:
+ - {
+ colors: ylorrd,
+ min_value: 0,
+ max_value: 20,
+ min_alpha: 120,
+ max_alpha: 180,
+ }
lightning_radiance:
standard_name: lightning_radiance
operations:
- - name: colorize
- method: !!python/name:satpy.enhancements.colorize
- kwargs:
- palettes:
- - {colors: ylorrd, min_value: 0, max_value: 1000}
+ - name: colorize
+ method: !!python/name:satpy.enhancements.colorize
+ kwargs:
+ palettes:
+ - { colors: ylorrd, min_value: 0, max_value: 1000 }
lightning_radiance_alpha:
standard_name: lightning_radiance_alpha
operations:
- - name: colorize
- method: !!python/name:satpy.enhancements.colorize
- kwargs:
- palettes:
- - {colors: ylorrd, min_value: 0, max_value: 1000,
- min_alpha: 120, max_alpha: 180}
+ - name: colorize
+ method: !!python/name:satpy.enhancements.colorize
+ kwargs:
+ palettes:
+ - {
+ colors: ylorrd,
+ min_value: 0,
+ max_value: 1000,
+ min_alpha: 120,
+ max_alpha: 180,
+ }
+
+ lightning_time:
+ standard_name: lightning_time
+ operations:
+ - name: colorize
+ method: !!python/name:satpy.enhancements.colorize
+ kwargs:
+ palettes:
+ - { colors: ylorrd, min_value: 0, max_value: 1 }
diff --git a/satpy/tests/compositor_tests/test_lightning.py b/satpy/tests/compositor_tests/test_lightning.py
new file mode 100644
index 0000000000..4c1f8b9a8c
--- /dev/null
+++ b/satpy/tests/compositor_tests/test_lightning.py
@@ -0,0 +1,113 @@
+"""Test the flash age compositor."""
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019 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 .
+
+
+import datetime
+import logging
+from unittest import mock
+
+import numpy as np
+import xarray as xr
+
+from satpy.composites.lightning import LightningTimeCompositor
+
+
+def test_flash_age_compositor():
+ """Test the flash_age compsitor by comparing two xarrays object."""
+ comp = LightningTimeCompositor("flash_age",prerequisites=["flash_time"],
+ standard_name="ligtning_time",
+ time_range=60,
+ reference_time="end_time")
+ attrs_flash_age = {"variable_name": "flash_time","name": "flash_time",
+ "start_time": datetime.datetime(2024, 8, 1, 10, 50, 0),
+ "end_time": datetime.datetime(2024, 8, 1, 11, 0, 0),"reader": "li_l2_nc"}
+ flash_age_value = np.array(["2024-08-01T09:00:00",
+ "2024-08-01T10:00:00", "2024-08-01T10:30:00","2024-08-01T11:00:00"], dtype="datetime64[ns]")
+ flash_age = xr.DataArray(
+ flash_age_value,
+ dims=["y"],
+ coords={
+ "crs": "8B +proj=longlat +ellps=WGS84 +type=crs"
+ },attrs = attrs_flash_age,name="flash_time")
+ res = comp([flash_age])
+ expected_attrs = {"variable_name": "flash_time","name": "lightning_time",
+ "start_time": datetime.datetime(2024, 8, 1, 10, 50, 0),
+ "end_time": datetime.datetime(2024, 8, 1, 11, 0, 0),"reader": "li_l2_nc",
+ "standard_name": "ligtning_time"
+ }
+ expected_array = xr.DataArray(
+ np.array([0.0,0.5,1.0]),
+ dims=["y"],
+ coords={
+ "crs": "8B +proj=longlat +ellps=WGS84 +type=crs"
+ },attrs = expected_attrs,name="flash_time")
+ xr.testing.assert_equal(res,expected_array)
+
+def test_empty_array_error(caplog):
+ """Test when the filtered array is empty."""
+ comp = LightningTimeCompositor("flash_age",prerequisites=["flash_time"],
+ standard_name="ligtning_time",
+ time_range=60,
+ reference_time="end_time")
+ attrs_flash_age = {"variable_name": "flash_time","name": "flash_time",
+ "start_time": datetime.datetime(2024, 8, 1, 10, 50, 0),
+ "end_time": datetime.datetime(2024, 8, 1, 11, 0, 0),"reader": "li_l2_nc"}
+ flash_age_value = np.array(["2024-08-01T09:00:00"], dtype="datetime64[ns]")
+ flash_age = xr.DataArray(
+ flash_age_value,
+ dims=["y"],
+ coords={
+ "crs": "8B +proj=longlat +ellps=WGS84 +type=crs"
+ },attrs = attrs_flash_age,name="flash_time")
+ with mock.patch("sys.exit") as mock_exit:
+ # Capture logging output
+ with caplog.at_level(logging.ERROR):
+ _ = comp([flash_age])
+
+ mock_exit.assert_called_once_with(1)
+
+ assert "All the flash_age events happened before 2024-08-01T10:00:00" in caplog.text
+
+def test_update_missing_metadata():
+ """Test the _update_missing_metadata method."""
+ existing_attrs = {
+ "standard_name": "lightning_event_time",
+ "time_range": 30
+ }
+
+ # New metadata to be merged
+ new_attrs = {
+ "standard_name": None, # Should not overwrite since it's None
+ "reference_time": "2023-09-20T00:00:00Z", # Should be added
+ "units": "seconds" # Should be added
+ }
+
+ # Expected result after merging
+ expected_attrs = {
+ "standard_name": "lightning_event_time", # Should remain the same
+ "time_range": 30, # Should remain the same
+ "reference_time": "2023-09-20T00:00:00Z", # Should be added
+ "units": "seconds" # Should be added
+ }
+
+ # Call the static method
+ LightningTimeCompositor._update_missing_metadata(existing_attrs, new_attrs)
+
+ # Assert the final state of existing_attrs is as expected
+ assert existing_attrs == expected_attrs