Repository: SciTools/cartopy

Issue:
Vector handling
Consider the ability to plot vector fields in Cartopy. The initial usecase should:
- Take data on a rotated pole grid (for example Met Office [NAE output](http://www.metoffice.gov.uk/research/modelling-systems/unified-model/weather-forecasting)) of wind data (u and v component) which is co-located and where the u and v component are in the direction of the grid rather than east/north.
- Plot the data on its native grid (no transformation necessary)
- Plot the data on a PlateCarree map (the vector magnitude is preserved, but its direction needs to be transformed).
- Plot the data on an arbitrary map (may be no more difficult than the PlateCarree case)

**To others**: Please feel free to update this issue with clarifications/questions.



Discussion:
This feature will be pretty important to have in order for Cartopy to become more widely used. I was just wondering if work has begun on this? 

I'm not aware of any work on this yet, but @pelson might have something cooking in his basement... ?

Hi @ajdawson - no, I've not got anything done on this yet. Plotting the data in native _should_ just work, though I've not tried it - the real difficulty is in handling the projected vectors. Do you have any experience in this field? (Given your work on windspharm, I'm guessing you do). If so, I'd love to hear some ideas on what you think needs implementing, if your keen?

Cheers,

@pelson I actually don't have much experience with this.

Indeed it does seem to work for the native projection. In an ideal world it would be possible to take a vector field defined on any projection and plot it on any other projections. For instance, I'd like to plot vector fields defined on a PlateCarree map in a polar stereographic projection.

As for implementing this... I've never looked deeply into how projections for scalar data are handled in cartopy, so I'm not sure where to start!

> Take data on a rotated pole grid (for example Met Office NAE output) of wind data (u and v component) which is co-located and where the u and v component are in the direction of the grid rather than east/north.

@pelson - I've got a prototype of this working. My bare bones implementation will transform from PlateCarree to other projections (I've tested NorthPolarStereo and Robinson so far) but I'm thinking round in circles about whether it should work for a rotated pole grid as you describe. Can you point me in the direction of any data like this?

I guess you could try the data at: http://scitools.org.uk/iris/docs/latest/examples/graphics/rotated_pole_mapping.html#rotated-pole-mapping-03

I could get hold of some vector data on this projection (actually, it would be a slightly different domain, but in principle the same) if that would be helpful.

I guess I could invent some data on that grid to test things out. I can think about real data if I get it working.

> I can think about real data if I get it working.

If you remind me on Monday, I'll get hold of some real data for you (it'll have to be by email for now though).

OK thanks @pelson.

OK @pelson, I made some dummy data to test with rotated pole grid and things look good. If you could get hold of some real data for me it would be very useful. Email is fine.


Resolving patch:
diff --git a/lib/cartopy/mpl/geoaxes.py b/lib/cartopy/mpl/geoaxes.py
--- a/lib/cartopy/mpl/geoaxes.py
+++ b/lib/cartopy/mpl/geoaxes.py
@@ -1339,6 +1339,38 @@ def _pcolor_patched(self, *args, **kwargs):
         self.add_collection(collection)
         return collection
 
+    def quiver(self, x, y, u, v, *args, **kwargs):
+        """
+        Plot a 2-D field of arrows.
+
+        See :func:`matplotlib.pyplot.quiver` for details on arguments
+        and keyword arguments.
+
+        .. note::
+
+           The vector components must be defined as grid eastward and
+           grid northward.
+
+        """
+        t = kwargs.get('transform', None)
+        if t is None:
+            t = self.projection
+        if isinstance(t, ccrs.CRS) and not isinstance(t, ccrs.Projection):
+            raise ValueError('invalid transform:'
+                             ' Spherical quiver is not supported - '
+                             ' consider using PlateCarree/RotatedPole.')
+        if isinstance(t, ccrs.Projection):
+            kwargs['transform'] = t._as_mpl_transform(self)
+        else:
+            kwargs['transform'] = t
+        if t != self.projection:
+            # Transform the vectors if the projection is not the same as the
+            # data transform.
+            if x.ndim == 1 and y.ndim == 1:
+                x, y = np.meshgrid(x, y)
+            u, v = self.projection.transform_vectors(t, x, y, u, v)
+        return matplotlib.axes.Axes.quiver(self, x, y, u, v, *args, **kwargs)
+
 
 def _trigger_patch_reclip(event):
     """


Test patch:
diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_plate_carree.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_plate_carree.png
new file mode 100644
Binary files /dev/null and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_plate_carree.png differ
diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_rotated_pole.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_rotated_pole.png
new file mode 100644
Binary files /dev/null and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_rotated_pole.png differ
diff --git a/lib/cartopy/tests/mpl/test_mpl_integration.py b/lib/cartopy/tests/mpl/test_mpl_integration.py
--- a/lib/cartopy/tests/mpl/test_mpl_integration.py
+++ b/lib/cartopy/tests/mpl/test_mpl_integration.py
@@ -327,6 +327,52 @@ def test_pcolormesh_limited_area_wrap():
     ax.coastlines()
 
 
+@ImageTesting(['quiver_plate_carree'])
+def test_quiver_plate_carree():
+    x = np.arange(-60, 42.5, 2.5)
+    y = np.arange(30, 72.5, 2.5)
+    x2d, y2d = np.meshgrid(x, y)
+    u = np.cos(np.deg2rad(y2d))
+    v = np.cos(2. * np.deg2rad(x2d))
+    mag = (u**2 + v**2)**.5
+    plot_extent = [-60, 40, 30, 70]
+    plt.figure(figsize=(6, 6))
+    # plot on native projection
+    ax = plt.subplot(211, projection=ccrs.PlateCarree())
+    ax.set_extent(plot_extent, crs=ccrs.PlateCarree())
+    ax.coastlines()
+    ax.quiver(x, y, u, v, mag)
+    # plot on a different projection
+    ax = plt.subplot(212, projection=ccrs.NorthPolarStereo())
+    ax.set_extent(plot_extent, crs=ccrs.PlateCarree())
+    ax.coastlines()
+    ax.quiver(x, y, u, v, mag, transform=ccrs.PlateCarree())
+
+
+@ImageTesting(['quiver_rotated_pole'])
+def test_quiver_rotated_pole():
+    nx, ny = 22, 36
+    x = np.linspace(311.91998291, 391.11999512, nx, endpoint=True)
+    y = np.linspace(-23.59000015, 24.81000137, ny, endpoint=True)
+    x2d, y2d = np.meshgrid(x, y)
+    u = np.cos(np.deg2rad(y2d))
+    v = -2. * np.cos(2. * np.deg2rad(y2d)) * np.sin(np.deg2rad(x2d))
+    mag = (u**2 + v**2)**.5
+    rp = ccrs.RotatedPole(pole_longitude=177.5, pole_latitude=37.5)
+    plot_extent = [x[0], x[-1], y[0], y[-1]]
+    # plot on native projection
+    plt.figure(figsize=(6, 6))
+    ax = plt.subplot(211, projection=rp)
+    ax.set_extent(plot_extent, crs=rp)
+    ax.coastlines()
+    ax.quiver(x, y, u, v, mag)
+    # plot on different projection
+    ax = plt.subplot(212, projection=ccrs.PlateCarree())
+    ax.set_extent(plot_extent, crs=rp)
+    ax.coastlines()
+    ax.quiver(x, y, u, v, mag, transform=rp)
+
+
 if __name__ == '__main__':
     import nose
     nose.runmodule(argv=['-s', '--with-doctest'], exit=False)
diff --git a/lib/cartopy/tests/test_crs_transform_vectors.py b/lib/cartopy/tests/test_crs_transform_vectors.py
new file mode 100644
--- /dev/null
+++ b/lib/cartopy/tests/test_crs_transform_vectors.py
@@ -0,0 +1,170 @@
+# (C) British Crown Copyright 2013, Met Office
+#
+# This file is part of cartopy.
+#
+# cartopy is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by the
+# Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# cartopy 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with cartopy.  If not, see <http://www.gnu.org/licenses/>.
+
+
+import unittest
+import warnings
+
+import numpy as np
+from numpy.testing import assert_array_almost_equal
+
+import cartopy.crs as ccrs
+
+
+class TestTransformVectors(unittest.TestCase):
+
+    def test_transform(self):
+        # Test some simple vectors to make sure they are transformed
+        # correctly.
+        rlons = np.array([-90., 0, 90., 180.])
+        rlats = np.array([0., 0., 0., 0.])
+        src_proj = ccrs.PlateCarree()
+        target_proj = ccrs.Stereographic(central_latitude=90,
+                                         central_longitude=0)
+        # transform grid eastward vectors
+        ut, vt = target_proj.transform_vectors(src_proj,
+                                               rlons,
+                                               rlats,
+                                               np.ones([4]),
+                                               np.zeros([4]))
+        assert_array_almost_equal(ut, np.array([0, 1, 0, -1]), decimal=2)
+        assert_array_almost_equal(vt, np.array([-1, 0, 1, 0]), decimal=2)
+        # transform grid northward vectors
+        ut, vt = target_proj.transform_vectors(src_proj,
+                                               rlons,
+                                               rlats,
+                                               np.zeros([4]),
+                                               np.ones([4]))
+        assert_array_almost_equal(ut, np.array([1, 0, -1, 0]), decimal=2)
+        assert_array_almost_equal(vt, np.array([0, 1, 0, -1]), decimal=2)
+        # transform grid north-eastward vectors
+        ut, vt = target_proj.transform_vectors(src_proj,
+                                               rlons,
+                                               rlats,
+                                               np.ones([4]),
+                                               np.ones([4]))
+        assert_array_almost_equal(ut, np.array([1, 1, -1, -1]), decimal=2)
+        assert_array_almost_equal(vt, np.array([-1, 1, 1, -1]), decimal=2)
+
+    def test_transform_and_inverse(self):
+        # Check a full circle transform back to the native projection.
+        x = np.arange(-60, 42.5, 2.5)
+        y = np.arange(30, 72.5, 2.5)
+        x2d, y2d = np.meshgrid(x, y)
+        u = np.cos(np.deg2rad(y2d))
+        v = np.cos(2. * np.deg2rad(x2d))
+        src_proj = ccrs.PlateCarree()
+        target_proj = ccrs.Stereographic(central_latitude=90,
+                                         central_longitude=0)
+        proj_xyz = target_proj.transform_points(src_proj, x2d, y2d)
+        xt, yt = proj_xyz[..., 0], proj_xyz[..., 1]
+        ut, vt = target_proj.transform_vectors(src_proj, x2d, y2d, u, v)
+        utt, vtt = src_proj.transform_vectors(target_proj, xt, yt, ut, vt)
+        assert_array_almost_equal(u, utt, decimal=4)
+        assert_array_almost_equal(v, vtt, decimal=4)
+
+    def test_invalid_input_domain(self):
+        # If an input coordinate is outside the input projection domain
+        # we should be able to handle it correctly.
+        rlon = np.array([270.])
+        rlat = np.array([0.])
+        u = np.array([1.])
+        v = np.array([0.])
+        src_proj = ccrs.PlateCarree()
+        target_proj = ccrs.Stereographic(central_latitude=90,
+                                         central_longitude=0)
+        ut, vt = target_proj.transform_vectors(src_proj, rlon, rlat, u, v)
+        assert_array_almost_equal(ut, np.array([0]), decimal=2)
+        assert_array_almost_equal(vt, np.array([-1]), decimal=2)
+
+    def test_invalid_x_domain(self):
+        # If the point we need to calculate the vector angle falls outside the
+        # source projection x-domain it should be handled correctly as long as
+        # it is not a corner point.
+        rlon = np.array([180.])
+        rlat = np.array([0.])
+        u = np.array([1.])
+        v = np.array([0.])
+        src_proj = ccrs.PlateCarree()
+        target_proj = ccrs.Stereographic(central_latitude=90,
+                                         central_longitude=0)
+        ut, vt = target_proj.transform_vectors(src_proj, rlon, rlat, u, v)
+        assert_array_almost_equal(ut, np.array([-1]), decimal=2)
+        assert_array_almost_equal(vt, np.array([0.]), decimal=2)
+
+    def test_invalid_y_domain(self):
+        # If the point we need to calculate the vector angle falls outside the
+        # source projection y-domain it should be handled correctly as long as
+        # it is not a corner point.
+        rlon = np.array([0.])
+        rlat = np.array([90.])
+        u = np.array([0.])
+        v = np.array([1.])
+        src_proj = ccrs.PlateCarree()
+        target_proj = ccrs.Stereographic(central_latitude=90,
+                                         central_longitude=0)
+        ut, vt = target_proj.transform_vectors(src_proj, rlon, rlat, u, v)
+        assert_array_almost_equal(ut, np.array([0.]), decimal=2)
+        assert_array_almost_equal(vt, np.array([1.]), decimal=2)
+
+    def test_invalid_xy_domain_corner(self):
+        # If the point we need to calculate the vector angle falls outside the
+        # source projection x and y-domain it should be handled correctly.
+        rlon = np.array([180.])
+        rlat = np.array([90.])
+        u = np.array([1.])
+        v = np.array([1.])
+        src_proj = ccrs.PlateCarree()
+        target_proj = ccrs.Stereographic(central_latitude=90,
+                                         central_longitude=0)
+        ut, vt = target_proj.transform_vectors(src_proj, rlon, rlat, u, v)
+        assert_array_almost_equal(ut, np.array([0.]), decimal=2)
+        assert_array_almost_equal(vt, np.array([-2**.5]), decimal=2)
+
+    def test_invalid_x_domain_corner(self):
+        # If the point we need to calculate the vector angle falls outside the
+        # source projection x-domain and is a corner point, it may be handled
+        # incorrectly and a warning should be raised.
+        rlon = np.array([180.])
+        rlat = np.array([90.])
+        u = np.array([1.])
+        v = np.array([-1.])
+        src_proj = ccrs.PlateCarree()
+        target_proj = ccrs.Stereographic(central_latitude=90,
+                                         central_longitude=0)
+        with warnings.catch_warnings():
+            warnings.simplefilter('error')
+            with self.assertRaises(UserWarning):
+                ut, vt = target_proj.transform_vectors(
+                    src_proj, rlon, rlat, u, v)
+
+    def test_invalid_y_domain_corner(self):
+        # If the point we need to calculate the vector angle falls outside the
+        # source projection y-domain and is a corner point, it may be handled
+        # incorrectly and a warning should be raised.
+        rlon = np.array([180.])
+        rlat = np.array([90.])
+        u = np.array([-1.])
+        v = np.array([1.])
+        src_proj = ccrs.PlateCarree()
+        target_proj = ccrs.Stereographic(central_latitude=90,
+                                         central_longitude=0)
+        with warnings.catch_warnings():
+            warnings.simplefilter('error')
+            with self.assertRaises(UserWarning):
+                ut, vt = target_proj.transform_vectors(
+                    src_proj, rlon, rlat, u, v)