Repository: napari/napari

Issue:
Scale and translate parameter don't broadcast correctly
## 🐛 Bug

This used to work before the new transform models:

```python
import napari
import dask.array as da

arr = da.random.random((10, 1024, 1024), chunks=(1, 1024, 1024))
v = napari.view_image(arr, scale=(10, 10))
```

Now I get a broadcast error with:

```pytb
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-5-977b5070ff3c> in <module>
----> 1 v.add_image(image, scale=(10, 10))

~/miniconda3/envs/all/lib/python3.8/site-packages/napari/components/add_layers_mixin.py in add_image(self, data, channel_axis, rgb, colormap, contrast_limits, gamma, interpolation, rendering, iso_threshold, attenuation, name, metadata, scale, translate, opacity, blending, visible, multiscale)
    226                     )
    227 
--> 228             return self.add_layer(layers.Image(data, **kwargs))
    229         else:
    230             # Determine if data is a multiscale

~/miniconda3/envs/all/lib/python3.8/site-packages/napari/layers/image/image.py in __init__(self, data, rgb, colormap, contrast_limits, gamma, interpolation, rendering, iso_threshold, attenuation, name, metadata, scale, translate, opacity, blending, visible, multiscale)
    248 
    249         # Trigger generation of view slice and thumbnail
--> 250         self._update_dims()
    251 
    252     def _calc_data_range(self):

~/miniconda3/envs/all/lib/python3.8/site-packages/napari/layers/base/base.py in _update_dims(self, event)
    400             self.dims.set_range(i, r)
    401 
--> 402         self.refresh()
    403         self._update_coordinates()
    404 

~/miniconda3/envs/all/lib/python3.8/site-packages/napari/layers/base/base.py in refresh(self, event)
    633             self.events.set_data()
    634             self._update_thumbnail()
--> 635             self._update_coordinates()
    636             self._set_highlight(force=True)
    637 

~/miniconda3/envs/all/lib/python3.8/site-packages/napari/layers/base/base.py in _update_coordinates(self)
    645         self.coordinates = tuple(coords)
    646         self._value = self.get_value()
--> 647         self.status = self.get_message()
    648 
    649     def _update_multiscale(self, corner_pixels, shape_threshold):

~/miniconda3/envs/all/lib/python3.8/site-packages/napari/layers/base/base.py in get_message(self)
    708             String containing a message that can be used as a status update.
    709         """
--> 710         coordinates = self._transforms.simplified(self.coordinates)
    711         full_coord = np.round(coordinates).astype(int)
    712 

~/miniconda3/envs/all/lib/python3.8/site-packages/napari/layers/transforms.py in simplified(self)
    101             return self[0]
    102         else:
--> 103             return tz.pipe(self[0], *[tf.compose for tf in self[1:]])
    104 
    105     def set_slice(self, axes: Sequence[int]) -> 'TransformChain':

~/miniconda3/envs/all/lib/python3.8/site-packages/toolz/functoolz.py in pipe(data, *funcs)
    632     """
    633     for func in funcs:
--> 634         data = func(data)
    635     return data
    636 

~/miniconda3/envs/all/lib/python3.8/site-packages/napari/layers/transforms.py in compose(self, transform)
    180     def compose(self, transform: 'ScaleTranslate') -> 'ScaleTranslate':
    181         """Return the composite of this transform and the provided one."""
--> 182         scale = self.scale * transform.scale
    183         translate = self.translate + self.scale * transform.translate
    184         return ScaleTranslate(scale, translate)

ValueError: operands could not be broadcast together with shapes (2,) (3,) 
```

## Expected behavior

To be honest, it's unclear to me whether this should work. But it *did* use to work, so I'm flagging it as something that we may or may not want to support. This sort of stuff makes sense for example when the time axis doesn't have a scale, but the spatial axes do.

## Environment

napari 0.3.0


Discussion:
We'll want to revisit this after #1616 merges

Resolving patch:
diff --git a/napari/utils/transforms/transforms.py b/napari/utils/transforms/transforms.py
--- a/napari/utils/transforms/transforms.py
+++ b/napari/utils/transforms/transforms.py
@@ -167,6 +167,13 @@ class ScaleTranslate(Transform):
 
     def __init__(self, scale=(1.0,), translate=(0.0,), *, name=None):
         super().__init__(name=name)
+
+        if len(scale) > len(translate):
+            translate = [0] * (len(scale) - len(translate)) + list(translate)
+
+        if len(translate) > len(scale):
+            scale = [1] * (len(translate) - len(scale)) + list(scale)
+
         self.scale = np.array(scale)
         self.translate = np.array(translate)
 


Test patch:
diff --git a/napari/layers/image/_tests/test_image.py b/napari/layers/image/_tests/test_image.py
--- a/napari/layers/image/_tests/test_image.py
+++ b/napari/layers/image/_tests/test_image.py
@@ -599,6 +599,20 @@ def test_image_translate(translate):
     Image(data, translate=translate)
 
 
+def test_image_scale_broadcast():
+    """Test scale is broadcast."""
+    data = np.random.random((5, 10, 15))
+    layer = Image(data, scale=(2, 2))
+    np.testing.assert_almost_equal(layer.scale, (1, 2, 2))
+
+
+def test_image_translate_broadcast():
+    """Test translate is broadcast."""
+    data = np.random.random((5, 10, 15))
+    layer = Image(data, translate=(2, 2))
+    np.testing.assert_almost_equal(layer.translate, (0, 2, 2))
+
+
 def test_grid_translate():
     np.random.seed(0)
     data = np.random.random((10, 15))
diff --git a/napari/utils/transforms/_tests/test_transforms.py b/napari/utils/transforms/_tests/test_transforms.py
--- a/napari/utils/transforms/_tests/test_transforms.py
+++ b/napari/utils/transforms/_tests/test_transforms.py
@@ -15,6 +15,30 @@ def test_scale_translate(Transform):
     npt.assert_allclose(new_coord, target_coord)
 
 
+@pytest.mark.parametrize('Transform', [ScaleTranslate, Affine])
+def test_scale_translate_broadcast_scale(Transform):
+    coord = [1, 10, 13]
+    transform = Transform(scale=[4, 2, 3], translate=[8, -5], name='st')
+    new_coord = transform(coord)
+    target_coord = [4, 2 * 10 + 8, 3 * 13 - 5]
+    assert transform.name == 'st'
+    npt.assert_allclose(transform.scale, [4, 2, 3])
+    npt.assert_allclose(transform.translate, [0, 8, -5])
+    npt.assert_allclose(new_coord, target_coord)
+
+
+@pytest.mark.parametrize('Transform', [ScaleTranslate, Affine])
+def test_scale_translate_broadcast_translate(Transform):
+    coord = [1, 10, 13]
+    transform = Transform(scale=[2, 3], translate=[5, 8, -5], name='st')
+    new_coord = transform(coord)
+    target_coord = [6, 2 * 10 + 8, 3 * 13 - 5]
+    assert transform.name == 'st'
+    npt.assert_allclose(transform.scale, [1, 2, 3])
+    npt.assert_allclose(transform.translate, [5, 8, -5])
+    npt.assert_allclose(new_coord, target_coord)
+
+
 @pytest.mark.parametrize('Transform', [ScaleTranslate, Affine])
 def test_scale_translate_inverse(Transform):
     coord = [10, 13]