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