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

Fix delayed generation of composites and composite resolution #828

Merged
merged 8 commits into from
Jun 27, 2019
9 changes: 7 additions & 2 deletions satpy/composites/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -823,7 +823,10 @@ def __call__(self, projectables, nonprojectables=None, **attrs):
new_attrs.update({key: val
for (key, val) in attrs.items()
if val is not None})
resolution = new_attrs.get('resolution', None)
new_attrs.update(self.attrs)
if resolution is not None:
new_attrs['resolution'] = resolution
djhoese marked this conversation as resolved.
Show resolved Hide resolved
new_attrs["sensor"] = self._get_sensors(projectables)
new_attrs["mode"] = mode

Expand All @@ -847,8 +850,8 @@ class Filler(GenericCompositor):

def __call__(self, projectables, nonprojectables=None, **info):
"""Generate the composite."""
projectables[0] = projectables[0].fillna(projectables[1])
return super(Filler, self).__call__([projectables[0]], **info)
filled_projectable = projectables[0].fillna(projectables[1])
return super(Filler, self).__call__([filled_projectable], **info)


class RGBCompositor(GenericCompositor):
Expand Down Expand Up @@ -1403,6 +1406,7 @@ def __init__(self, name, filename=None, area=None, **kwargs):
super(StaticImageCompositor, self).__init__(name, **kwargs)

def __call__(self, *args, **kwargs):
"""Call the compositor."""
from satpy import Scene
scn = Scene(reader='generic_image', filenames=[self.filename])
scn.load(['image'])
Expand Down Expand Up @@ -1435,6 +1439,7 @@ class BackgroundCompositor(GenericCompositor):
"""A compositor that overlays one composite on top of another."""

def __call__(self, projectables, *args, **kwargs):
"""Call the compositor."""
projectables = self.check_areas(projectables)

# Get enhanced datasets
Expand Down
42 changes: 36 additions & 6 deletions satpy/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,19 @@ def __init__(self, name, data=None):

@property
def is_leaf(self):
"""Check if the node is a leaf."""
return not self.children

def flatten(self, d=None):
"""Flatten tree structure to a one level dictionary.


Args:
d (dict, optional): output dictionary to update

Returns:
dict: Node.name -> Node. The returned dictionary includes the
current Node and all its children.

"""
if d is None:
d = {}
Expand All @@ -60,9 +61,13 @@ def flatten(self, d=None):
return d

def copy(self, node_cache=None):
"""Make a copy of the node."""
if node_cache and self.name in node_cache:
return node_cache[self.name]

if self.name is EMPTY_LEAF_NAME:
return self

s = Node(self.name, self.data)
for c in self.children:
c = c.copy(node_cache=node_cache)
Expand All @@ -79,12 +84,15 @@ def __str__(self):
return self.display()

def __repr__(self):
"""Generate a representation of the node."""
return "<Node ({})>".format(repr(self.name))

def __eq__(self, other):
"""Check equality."""
return self.name == other.name

def __hash__(self):
"""Generate the hash of the node."""
return hash(self.name)

def display(self, previous=0, include_data=False):
Expand Down Expand Up @@ -124,14 +132,18 @@ def trunk(self, unique=True):


class DependencyTree(Node):
"""Structure to discover and store `Dataset` dependencies
"""Structure to discover and store `Dataset` dependencies.

Used primarily by the `Scene` object to organize dependency finding.
Dependencies are stored used a series of `Node` objects which this
class is a subclass of.

"""

# simplify future logic by only having one "sentinel" empty node
# making it a class attribute ensures it is the same across instances
empty_node = Node(EMPTY_LEAF_NAME)

def __init__(self, readers, compositors, modifiers):
"""Collect Dataset generating information.

Expand All @@ -154,8 +166,6 @@ def __init__(self, readers, compositors, modifiers):
# keep a flat dictionary of nodes contained in the tree for better
# __contains__
self._all_nodes = DatasetDict()
# simplify future logic by only having one "sentinel" empty node
self.empty_node = Node(EMPTY_LEAF_NAME)

def leaves(self, nodes=None, unique=True):
"""Get the leaves of the tree starting at this root.
Expand Down Expand Up @@ -201,6 +211,7 @@ def trunk(self, nodes=None, unique=True):
return res

def add_child(self, parent, child):
"""Add a child to the tree."""
Node.add_child(parent, child)
# Sanity check: Node objects should be unique. They can be added
# multiple times if more than one Node depends on them
Expand All @@ -213,6 +224,7 @@ def add_child(self, parent, child):
self._all_nodes[child.name] = child

def add_leaf(self, ds_id, parent=None):
"""Add a leaf to the tree."""
if parent is None:
parent = self
try:
Expand All @@ -222,7 +234,7 @@ def add_leaf(self, ds_id, parent=None):
self.add_child(parent, node)

def copy(self):
"""Copy the this node tree
"""Copy this node tree.

Note all references to readers are removed. This is meant to avoid
tree copies accessing readers that would return incompatible (Area)
Expand All @@ -237,9 +249,11 @@ def copy(self):
return new_tree

def __contains__(self, item):
"""Check if a item is in the tree."""
return item in self._all_nodes

def __getitem__(self, item):
"""Get an item of the tree."""
return self._all_nodes[item]

def contains(self, item):
Expand All @@ -251,6 +265,7 @@ def getitem(self, item):
return super(DatasetDict, self._all_nodes).__getitem__(item)

def get_compositor(self, key):
"""Get a compositor."""
for sensor_name in self.compositors.keys():
try:
return self.compositors[sensor_name][key]
Expand All @@ -264,6 +279,7 @@ def get_compositor(self, key):
raise KeyError("Could not find compositor '{}'".format(key))

def get_modifier(self, comp_id):
"""Get a modifer."""
# create a DatasetID for the compositor we are generating
modifier = comp_id.modifiers[-1]
for sensor_name in self.modifiers.keys():
Expand Down Expand Up @@ -416,6 +432,20 @@ def _find_compositor(self, dataset_key, **dfilter):

return root, set()

def get_filtered_item(self, dataset_key, **dfilter):
"""Get the item matching *dataset_key* and *dfilter*."""
try:
ds_dict = dataset_key.to_dict()
except AttributeError:
if isinstance(dataset_key, str):
ds_dict = {'name': dataset_key}
elif isinstance(dataset_key, float):
ds_dict = {'wavelength': dataset_key}
clean_filter = {key: value for key, value in dfilter.items() if value is not None}
ds_dict.update(clean_filter)
dsid = DatasetID.from_dict(ds_dict)
return self[dsid]

def _find_dependencies(self, dataset_key, **dfilter):
"""Find the dependencies for *dataset_key*.

Expand Down Expand Up @@ -456,7 +486,7 @@ def _find_dependencies(self, dataset_key, **dfilter):
# assume that there is no such thing as a "better" composite
# version so if we find any DatasetIDs already loaded then
# we want to use them
node = self[dataset_key]
node = self.get_filtered_item(dataset_key, **dfilter)
LOG.trace("Composite already loaded:\n\tRequested: {}\n\tFound: {}".format(dataset_key, node.name))
return node, set()
except KeyError:
Expand Down
30 changes: 23 additions & 7 deletions satpy/tests/compositor_tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
#
# You should have received a copy of the GNU General Public License along with
# satpy. If not, see <http://www.gnu.org/licenses/>.
"""Tests for compositors.
"""
"""Tests for compositors."""

import xarray as xr
import dask.array as da
Expand All @@ -36,7 +35,7 @@ class TestMatchDataArrays(unittest.TestCase):
"""Test the utility method 'match_data_arrays'."""

def _get_test_ds(self, shape=(50, 100), dims=('y', 'x')):
"""Helper method to get a fake DataArray."""
"""Get a fake DataArray."""
from pyresample.geometry import AreaDefinition
data = da.random.random(shape, chunks=25)
area = AreaDefinition(
Expand Down Expand Up @@ -113,6 +112,7 @@ def test_mult_ds_diff_size(self):
self.assertRaises(IncompatibleAreas, comp.match_data_arrays, (ds1, ds2))

def test_nondimensional_coords(self):
"""Test the removal of non-dimensional coordinates when compositing."""
from satpy.composites import CompositeBase
ds = self._get_test_ds(shape=(2, 2))
ds['acq_time'] = ('y', [0, 1])
Expand Down Expand Up @@ -230,6 +230,8 @@ def test_self_sharpened_basic(self):


class TestSunZenithCorrector(unittest.TestCase):
"""Test case for the zenith corrector."""

def setUp(self):
"""Create test data."""
from pyresample.geometry import AreaDefinition
Expand Down Expand Up @@ -291,6 +293,7 @@ def test_basic_lims_provided(self):


class TestDifferenceCompositor(unittest.TestCase):
"""Test case for the difference compositor."""

def setUp(self):
"""Create test data."""
Expand Down Expand Up @@ -385,7 +388,7 @@ def setUp(self):
self.sza.attrs['area'] = my_area

def test_basic_sza(self):
"""Test compositor when SZA data is included"""
"""Test compositor when SZA data is included."""
from satpy.composites import DayNightCompositor
comp = DayNightCompositor(name='dn_test')
res = comp((self.data_a, self.data_b, self.sza))
Expand All @@ -404,8 +407,10 @@ def test_basic_area(self):


class TestFillingCompositor(unittest.TestCase):
"""Test case for the filling compositor."""

def test_fill(self):
"""Test filling."""
from satpy.composites import FillingCompositor
comp = FillingCompositor(name='fill_test')
filler = xr.DataArray(np.array([1, 2, 3, 4, 3, 2, 1]))
Expand Down Expand Up @@ -586,6 +591,7 @@ class TestColormapCompositor(unittest.TestCase):
"""Test the ColormapCompositor."""

def test_build_colormap(self):
"""Test colormap building."""
from satpy.composites import ColormapCompositor
cmap_comp = ColormapCompositor('test_cmap_compositor')
palette = np.array([[0, 0, 0], [127, 127, 127], [255, 255, 255]])
Expand All @@ -605,6 +611,7 @@ class TestPaletteCompositor(unittest.TestCase):
"""Test the PaletteCompositor."""

def test_call(self):
"""Test palette compositing."""
from satpy.composites import PaletteCompositor
cmap_comp = PaletteCompositor('test_cmap_compositor')
palette = xr.DataArray(np.array([[0, 0, 0], [127, 127, 127], [255, 255, 255]]),
Expand Down Expand Up @@ -748,7 +755,7 @@ def test_get_sensors(self):
@mock.patch('satpy.composites.check_times')
@mock.patch('satpy.composites.GenericCompositor.match_data_arrays')
def test_call_with_mock(self, match_data_arrays, check_times, combine_metadata, get_sensors):
"""Test calling generic compositor"""
"""Test calling generic compositor."""
from satpy.composites import IncompatibleAreas
combine_metadata.return_value = dict()
get_sensors.return_value = 'foo'
Expand Down Expand Up @@ -777,11 +784,12 @@ def test_call_with_mock(self, match_data_arrays, check_times, combine_metadata,
match_data_arrays.assert_called_once()

def test_call(self):
"""Test calling generic compositor"""
"""Test calling generic compositor."""
# Multiple datasets with extra attributes
all_valid = self.all_valid
all_valid.attrs['sensor'] = 'foo'
attrs = {'foo': 'bar'}
attrs = {'foo': 'bar', 'resolution': 333}
self.comp.attrs['resolution'] = None
djhoese marked this conversation as resolved.
Show resolved Hide resolved
res = self.comp([self.all_valid, self.first_invalid], **attrs)
# Verify attributes
self.assertEqual(res.attrs.get('sensor'), 'foo')
Expand All @@ -792,11 +800,14 @@ def test_call(self):
self.assertTrue('modifiers' not in res.attrs)
self.assertIsNone(res.attrs['wavelength'])
self.assertEqual(res.attrs['mode'], 'LA')
self.assertEquals(res.attrs['resolution'], 333)


class TestAddBands(unittest.TestCase):
"""Test case for the `add_bands` function."""

def test_add_bands(self):
"""Test adding bands."""
from satpy.composites import add_bands
import dask.array as da
import numpy as np
Expand Down Expand Up @@ -849,9 +860,11 @@ def test_add_bands(self):


class TestStaticImageCompositor(unittest.TestCase):
"""Test case for the static compositor."""

@mock.patch('satpy.resample.get_area_def')
def test_init(self, get_area_def):
"""Test the initializiation of static compositor."""
from satpy.composites import StaticImageCompositor

# No filename given raises ValueError
Expand All @@ -872,6 +885,7 @@ def test_init(self, get_area_def):

@mock.patch('satpy.Scene')
def test_call(self, Scene):
"""Test the static compositing."""
from satpy.composites import StaticImageCompositor

class mock_scene(dict):
Expand Down Expand Up @@ -905,12 +919,14 @@ def load(self, arg):


class TestBackgroundCompositor(unittest.TestCase):
"""Test case for the background compositor."""

@mock.patch('satpy.composites.combine_metadata')
@mock.patch('satpy.composites.add_bands')
@mock.patch('satpy.composites.enhance2dataset')
@mock.patch('satpy.composites.BackgroundCompositor.check_areas')
def test_call(self, check_areas, e2d, add_bands, combine_metadata):
"""Test the background compositing."""
from satpy.composites import BackgroundCompositor
import numpy as np

Expand Down