Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiscene blend with weights #2275

Merged
merged 34 commits into from Jan 30, 2023
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1ec54cf
Extend blend method and add a new blend function using weights
Nov 12, 2022
7eba546
refactored new function weighted function and accompanying unittests …
Nov 14, 2022
a7d5ee7
Fixing the stacked blend functions taking care of fillevalue and impr…
Nov 16, 2022
d4bec06
Improve unittest code: Use fixture with autouse to avoid hardcoding c…
Nov 17, 2022
6a3b52e
Change standardname for cloudtype to ct
Dec 21, 2022
d20d4de
Merge branch 'main' into multiscene_blend_with_weights
Jan 13, 2023
51df85e
Refactor and maintain one stack function available publicly, and make…
Jan 23, 2023
d32eeeb
Use setup method rather than autouse fixture
Jan 23, 2023
4091526
Merge branch 'main' into multiscene_blend_with_weights
Jan 23, 2023
1abfc5a
Re-arrange multiscene tests into three modules in a sub-directory
Jan 23, 2023
b28df3f
Update satpy/modifiers/angles.py
adybbroe Jan 24, 2023
e262e12
Update satpy/multiscene.py
adybbroe Jan 24, 2023
24b9153
Improve doc string in satpy/multiscene.py
adybbroe Jan 24, 2023
695b98d
Clarify doc string in satpy/multiscene.py
adybbroe Jan 24, 2023
6627d48
Update satpy/tests/multiscene_tests/test_save_animation.py
adybbroe Jan 24, 2023
80ff2a0
Make sure module headers are consistent
adybbroe Jan 24, 2023
818b0f1
Update satpy/tests/multiscene_tests/test_blend.py
adybbroe Jan 24, 2023
c2a90d8
Update copyright in module header
adybbroe Jan 24, 2023
248d449
Update satpy/multiscene.py
adybbroe Jan 24, 2023
f4b0874
Use attrs keys to access the fill value
adybbroe Jan 24, 2023
0def84a
Remove dupplicate code, and use make_dataid from utils instead
Jan 24, 2023
3b718ab
Merge branch 'multiscene_blend_with_weights' of github.com:adybbroe/s…
Jan 24, 2023
505aa24
Moving test utilities from __init__ to separate module
Jan 24, 2023
2d38257
Merge branch 'main' into multiscene_blend_with_weights
Jan 25, 2023
b5adf6a
Minor refactoring of the blend/stack functions and add combining of t…
Jan 26, 2023
5e91383
Add a bit of documenation on using weights when blending with the sta…
Jan 26, 2023
897df3d
Improve formulation on the motive of blending geo and polar scenes
adybbroe Jan 27, 2023
0b187dd
Fix typo
adybbroe Jan 27, 2023
c987b5d
Refactor function using one for loop for efficiency and make private
Jan 27, 2023
933de7b
Fix code example for blending with weihts - getting the area-id corre…
Jan 27, 2023
3c125dc
Only try combine start and end times if they are already there
Jan 27, 2023
238c71c
Revert back on standard_name for cloudtype dataset
Jan 27, 2023
c76e790
Be more explicit in the doc pages when storing the blended cloudtype …
Jan 27, 2023
8baca79
Fix minor editorial in RTDs
Jan 27, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 43 additions & 0 deletions doc/source/multiscene.rst
Expand Up @@ -85,6 +85,49 @@ iteratively overlays the remaining datasets on top.
>>> blended_scene = new_mscn.blend()
>>> blended_scene.save_datasets()


Stacking scenes using weights
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

It is also possible to blend scenes together in a bit more sophisticated manner
using pixel based weighting instead of just stacking the scenes on top of each
other as described above. This can for instance be useful to make a cloud
parameter (cover, height, etc) composite combining cloud parameters derived
from both geostationary and polar orbiting satellite data over aa given area
(for instance at high latitudes where geostatioonary data degrade quickly with
latitude and polar data are more frequent) scenes valid close in time to each
other.
adybbroe marked this conversation as resolved.
Show resolved Hide resolved

This weighted blending can be accomplished via the use of the builtin
:func:`~functools.partial` function (see `Partial
<https://docs.python.org/3/library/functools.html#partial-objects>`_) and the
default :func:`~satpy.multiscene.stack` function. The
:func:`~satpy.multiscene.stack` function can take the optional argument
`weights` (`None` on default) which should be a sequence (of length equal to
the number of scenes being blended) of arrays with pixel weights.

The code below gives an example of how two clouud scenes can be blended using
adybbroe marked this conversation as resolved.
Show resolved Hide resolved
the satellite zenith angles to weight which pixels to take from each of the two
scenes. The idea being that the reliability of the cloud parameter is higher
when the satellite zenith angle is small.

>>> from satpy import Scene, MultiScene, DataQuery
>>> from functools import partial
>>> geo_scene = Scene(filenames=glob('/data/to/nwcsaf/geo/files/*nc'), reader='nwcsaf-geo')
>>> geo_scene.load(['ct'])
>>> polar_scene = Scene(filenames=glob('/data/to/nwcsaf/pps/noaa18/files/*nc'), reader='nwcsaf-pps_nc')
>>> polar_scene.load(['cma', 'ct'])
>>> mscn = MultiScene([geo_scene, polar_scene])
>>> groups = {DataQuery(name='CTY_group'): ['ct']}
>>> mscn.group(groups)
>>> resampled = mscn.resample(areaid, reduce_data=False)
>>> weights = [1./geo_satz, 1./n18_satz]
>>> stack_with_weights = partial(stack, weights=weights)
>>> blended = resampled.blend(blend_function=stack_with_weights)
>>> blended_scene.save_datasets()



Grouping Similar Datasets
^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
8 changes: 4 additions & 4 deletions satpy/etc/readers/nwcsaf-pps_nc.yaml
Expand Up @@ -120,25 +120,25 @@ datasets:
name: ct
file_type: nc_nwcsaf_ct
coordinates: [lon, lat]
standard_name: cloudtype
standard_name: ct
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the name change here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it was because I wanted to distinguish between the composite name and the dataset name, but probably I am not mastering the use/need of the standard_name? I can keep the cloudtype but not sure of the impact.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is no CF standard name then this name only matters for matching enhancements at this point. If you update this standard_name here then an enhancement configuration should be added to match this standard_name (or have one for this specific product name ct). Or you can just let it find the default enhancement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I retverted back using cloudtype.


ct_conditions:
name: ct_conditions
file_type: nc_nwcsaf_ct
coordinates: [lon, lat]
standard_name: cloudtype_conditions
standard_name: ct_conditions

ct_quality:
name: ct_quality
file_type: nc_nwcsaf_ct
coordinates: [lon, lat]
standard_name: cloudtype_quality
standard_name: ct_quality

ct_status_flag:
name: ct_status_flag
file_type: nc_nwcsaf_ct
coordinates: [lon, lat]
standard_name: cloudtype_status_flag
standard_name: ct_status_flag

ct_pal:
name: ct_pal
Expand Down
1 change: 1 addition & 0 deletions satpy/modifiers/angles.py
Expand Up @@ -447,6 +447,7 @@ def _get_sensor_angles(data_arr: xr.DataArray) -> tuple[xr.DataArray, xr.DataArr
sat_lon, sat_lat, sat_alt = get_satpos(data_arr, preference=preference)
area_def = data_arr.attrs["area"]
chunks = _geo_chunks_from_data_arr(data_arr)

sata, satz = _get_sensor_angles_from_sat_pos(sat_lon, sat_lat, sat_alt,
data_arr.attrs["start_time"],
area_def, chunks)
Expand Down
73 changes: 65 additions & 8 deletions satpy/multiscene.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2016-2019 Satpy developers
# Copyright (c) 2016-2023 Satpy developers
#
# This file is part of satpy.
#
Expand All @@ -20,6 +20,7 @@
import copy
import logging
import warnings
from datetime import datetime
from queue import Queue
from threading import Thread

Expand All @@ -45,14 +46,70 @@
log = logging.getLogger(__name__)


def stack(datasets):
"""Overlay series of datasets on top of each other."""
def stack(datasets, weights=None, combine_times=True):
"""Overlay a series of datasets together.

By default, datasets are stacked on top of each other, so the last one applied is
on top. If a sequence of weights arrays are provided the datasets will
be combined according to those weights. The result will be a composite
dataset where the data in each pixel is coming from the dataset having the
highest weight.

"""
if weights:
return _stack_weighted(datasets, weights, combine_times)

base = datasets[0].copy()
for dataset in datasets[1:]:
base = base.where(dataset.isnull(), dataset)
try:
base = base.where(dataset == dataset.attrs["_FillValue"], dataset)
except KeyError:
base = base.where(dataset.isnull(), dataset)

return base


def _stack_weighted(datasets, weights, combine_times):
"""Stack datasets using weights."""
weights = set_weights_to_zero_where_invalid(datasets, weights)

indices = da.argmax(da.dstack(weights), axis=-1)
attrs = combine_metadata(*[x.attrs for x in datasets])

if combine_times:
attrs['start_time'], attrs['end_time'] = get_combined_start_end_times(*[x.attrs for x in datasets])

dims = datasets[0].dims
weighted_array = xr.DataArray(da.choose(indices, datasets), dims=dims, attrs=attrs)
return weighted_array


def set_weights_to_zero_where_invalid(datasets, weights):
"""Go through the weights and set to zero where corresponding datasets have a value equals _FillValue or nan."""
for i, dataset in enumerate(datasets):
try:
weights[i] = xr.where(dataset == dataset.attrs["_FillValue"], 0, weights[i])
except KeyError:
weights[i] = xr.where(dataset.isnull(), 0, weights[i])

return weights


def get_combined_start_end_times(*metadata_objects):
adybbroe marked this conversation as resolved.
Show resolved Hide resolved
"""Get the start and end times attributes valid for the entire dataset series."""
start_time = datetime.now()
for md_obj in metadata_objects:
if md_obj['start_time'] < start_time:
start_time = md_obj['start_time']

end_time = datetime.fromtimestamp(0)
for md_obj in metadata_objects:
if md_obj['end_time'] > end_time:
end_time = md_obj['end_time']
adybbroe marked this conversation as resolved.
Show resolved Hide resolved

return start_time, end_time


def timeseries(datasets):
"""Expand dataset with and concatenate by time dimension."""
expanded_ds = []
Expand Down Expand Up @@ -508,7 +565,7 @@ def _get_single_frame(self, ds, enh_args, fill_value):
enh_args = enh_args.copy() # don't change caller's dict!
if "decorate" in enh_args:
enh_args["decorate"] = self._format_decoration(
ds, enh_args["decorate"])
ds, enh_args["decorate"])
img = get_enhanced_image(ds, **enh_args)
data, mode = img.finalize(fill_value=fill_value)
if data.ndim == 3:
Expand Down Expand Up @@ -632,7 +689,7 @@ def _get_writers_and_frames(
info_datasets = [scn.get(dataset_id) for scn in info_scenes]
this_fn, shape, this_fill = self._get_animation_info(info_datasets, filename, fill_value=fill_value)
data_to_write = self._get_animation_frames(
all_datasets, shape, this_fill, ignore_missing, enh_args)
all_datasets, shape, this_fill, ignore_missing, enh_args)

writer = imageio.get_writer(this_fn, **imio_args)
frames[dataset_id] = data_to_write
Expand Down Expand Up @@ -703,8 +760,8 @@ def save_animation(self, filename, datasets=None, fps=10, fill_value=None,
raise ImportError("Missing required 'imageio' library")

(writers, frames) = self._get_writers_and_frames(
filename, datasets, fill_value, ignore_missing,
enh_args, imio_args={"fps": fps, **kwargs})
filename, datasets, fill_value, ignore_missing,
enh_args, imio_args={"fps": fps, **kwargs})

client = self._get_client(client=client)
# get an ordered list of frames
Expand Down
18 changes: 18 additions & 0 deletions satpy/tests/multiscene_tests/__init__.py
@@ -0,0 +1,18 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2018-2023 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/>.
"""Unit tests for Multiscene."""