Repository: regionmask/regionmask

Issue:
unstructured grids
I tried `regionmask` on unstructured grids and it works with a tweak: https://gist.github.com/aaronspring/1b45cf22f80360ddc4ef13af1cadec63

Unstructured grids like ICON just have one spatial dimension like `ncells` and 2 spatial coordinates `clon` and `clat`.

To avoid:
```python
/work/mh0727/m300524/conda-envs/pymistral/lib/python3.8/site-packages/regionmask/core/mask.py in _get_LON_LAT_out_shape(lon, lat, fill)
    467 
    468     if ndim == 1:
--> 469         LON, LAT = np.meshgrid(lon, lat)
    470     elif ndim == 2:
    471         LON, LAT = lon, lat
```
and use L470 instead, I add a fake dimension `dummy`:
```python
ds = ds.assign_coords(clon=ds.coords['clon'].expand_dims('dummy'))
ds = ds.assign_coords(clat=ds.coords['clat'].expand_dims('dummy'))
```

To identify unstructured grids, we could maybe use the metadata `CDI_grid_type` from the `xr.DataArrays` variables:
```
ds[v].attrs
{'standard_name': 'sea_water_potential_temperature',
 'long_name': 'sea water potential temperature',
 'units': 'deg C',
 'code': 2,
 'CDI_grid_type': 'unstructured',
 'number_of_grid_in_reference': 1}
```
or we just check for the number of dimensions used in `lon` and `lat` and add such a fake dimension.

Do you think `regionmask` could handle such unstructured grids without such tweaks?
A solution should also work for other unstructured output from MPAS @mgrover1, FESOM @koldunovn, etc

Discussion:
codecov-commenter: # [Codecov](https://codecov.io/gh/regionmask/regionmask/pull/280?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask) Report
> Merging [#280](https://codecov.io/gh/regionmask/regionmask/pull/280?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask) (fc4fba3) into [master](https://codecov.io/gh/regionmask/regionmask/commit/0872147942af9070a6d33b4e417ecc8cf7320159?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask) (0872147) will **decrease** coverage by `0.81%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/regionmask/regionmask/pull/280/graphs/tree.svg?width=650&height=150&src=pr&token=WI9PSJZIB7&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask)](https://codecov.io/gh/regionmask/regionmask/pull/280?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask)

```diff
@@            Coverage Diff             @@
##           master     #280      +/-   ##
==========================================
- Coverage   98.96%   98.15%   -0.82%     
==========================================
  Files          14       14              
  Lines        1067     1083      +16     
==========================================
+ Hits         1056     1063       +7     
- Misses         11       20       +9     
```

| Flag | Coverage Δ | |
|---|---|---|
| py36-bare-minimum | `?` | |
| py36-min-all-deps | `?` | |
| unittests | `98.15% <100.00%> (-0.82%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/regionmask/regionmask/pull/280?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask) | Coverage Δ | |
|---|---|---|
| [regionmask/core/mask.py](https://codecov.io/gh/regionmask/regionmask/pull/280/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask#diff-cmVnaW9ubWFzay9jb3JlL21hc2sucHk=) | `97.93% <100.00%> (-1.63%)` | :arrow_down: |
| [regionmask/defined\_regions/natural\_earth.py](https://codecov.io/gh/regionmask/regionmask/pull/280/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask#diff-cmVnaW9ubWFzay9kZWZpbmVkX3JlZ2lvbnMvbmF0dXJhbF9lYXJ0aC5weQ==) | `96.33% <0.00%> (-2.76%)` | :arrow_down: |
| [regionmask/core/plot.py](https://codecov.io/gh/regionmask/regionmask/pull/280/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask#diff-cmVnaW9ubWFzay9jb3JlL3Bsb3QucHk=) | `98.50% <0.00%> (-1.50%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/regionmask/regionmask/pull/280?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/regionmask/regionmask/pull/280?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask). Last update [0872147...fc4fba3](https://codecov.io/gh/regionmask/regionmask/pull/280?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask).

aaronspring: @mathause most CI Workflows pass. I am unsure what to do with the minimum versions etc. I needed to add the netcdf4 package to test with real data. Or should I rather try to create some fake unstructured grid with numpy and without files?
aaronspring: for later reference for ICON or MPAS output:

```python
import os
import warnings

import numpy as np
import pytest
import xarray as xr

import regionmask

from .utils import dummy_region, expected_mask_2D

def rad_to_deg(ds):
    """Convert radian units to deg."""
    with xr.set_options(keep_attrs=True):
        for c in ds.coords:
            if "units" in ds[c].attrs:
                if ds[c].attrs["units"] == "radian":
                    ds[c] = np.rad2deg(ds[c])
                    ds[c].attrs["units"] = "degrees"
                    warnings.warn(f"convert {c} from radian to degrees")
    return ds


@pytest.mark.parametrize(
    "url,lon_name,lat_name,nregions",
    [
        (
            "https://github.com/psyplot/psy-simple/raw/master/tests/icon_test.nc",
            "clon",
            "clat",
            58,
        ),
        (
            "https://github.com/NCAR/geocat-datafiles/raw/main/netcdf_files/MPAS.nc",
            "lonCell",
            "latCell",
            2,
        ),
        (
            "https://github.com/NCAR/geocat-datafiles/raw/main/netcdf_files/camse_unstructured_grid.nc",
            "lon",
            "lat",
            40,
        ),
    ],
)
@pytest.mark.parametrize("method", ["shapely", "pygeos"])
def test_unstructured_icon(url, lon_name, lat_name, nregions, method):
    """Test regionmask for unstructured grids."""
    path = url.split("/")[-1]

    if not os.path.exists(path):
        os.system(f"wget {url}")

    ds = xr.open_dataset(path)
    ds = rad_to_deg(ds)

    region = regionmask.defined_regions.ar6.all

    masked = region.mask(
        ds, lon_name=lon_name, lat_name=lat_name, wrap_lon=False, method=method
    )

    ds_regions = ds.groupby(masked).mean()
    assert ds_regions.region.size == nregions
```
aaronspring: Thanks for the review @mathause. I simplified the testing data without any downloading and `netcdf4` use. 

Patch:
diff --git a/.github/workflows/ci-additional.yaml b/.github/workflows/ci-additional.yaml
index 721628e..34950fd 100644
--- a/.github/workflows/ci-additional.yaml
+++ b/.github/workflows/ci-additional.yaml
@@ -9,7 +9,7 @@ on:
 
 jobs:
   test:
-    name: ${{ matrix.env }}
+    name: py${{ matrix.python-version }}-${{ matrix.env }}
     runs-on: ${{ matrix.os }}
     defaults:
       run:
@@ -20,9 +20,10 @@ jobs:
         os: ["ubuntu-latest"]
         env:
           [
-            "py36-bare-minimum",
-            "py36-min-all-deps",
+            "bare-minimum",
+            "min-all-deps",
           ]
+        python-version: ["3.7"]
     steps:
       - uses: actions/checkout@v2
         with:
@@ -30,7 +31,10 @@ jobs:
 
       - name: Set environment variables
         run: |
-            echo "CONDA_ENV_FILE=ci/requirements/${{ matrix.env }}.yml" >> $GITHUB_ENV
+            PY=${{ matrix.python-version }}
+            # replace "." in matrix.python-version
+            PY=${PY//./}
+            echo "CONDA_ENV_FILE=ci/requirements/py${PY}-${{ matrix.env }}.yml" >> $GITHUB_ENV
       - name: Cache conda
         uses: actions/cache@v2
         with:
@@ -39,9 +43,9 @@ jobs:
             ~/.cache/regionmask/
             ~/.local/share/cartopy/
           key:
-            ${{ runner.os }}-conda-${{ matrix.env }}-${{ hashFiles('ci/requirements/**.yml') }}
+            ${{ runner.os }}-conda-py${{ matrix.python-version }}-${{ matrix.env }}-${{ hashFiles('ci/requirements/**.yml') }}
           restore-keys: |
-            ${{ runner.os }}-conda-${{ matrix.env }}
+            ${{ runner.os }}-conda-py${{ matrix.python-version }}-
             ${{ runner.os }}-
 
       - uses: conda-incubator/setup-miniconda@v2
@@ -51,7 +55,7 @@ jobs:
           mamba-version: "*"
           activate-environment: regionmask-tests
           auto-update-conda: false
-          python-version: 3.6
+          python-version: ${{ matrix.python-version }}
           use-only-tar-bz2: true
 
       - name: Install conda dependencies
@@ -70,7 +74,7 @@ jobs:
         run: |
           python -c "import regionmask"
       - name: Run tests
-        timeout-minutes: 5
+        timeout-minutes: 15
         run: python -u -m pytest
           --cov=regionmask
           --cov-report=xml
@@ -106,4 +110,4 @@ jobs:
       - name: minimum versions policy
         run: |
           mamba install -y pyyaml conda python-dateutil
-          python ci/min_deps_check.py ci/requirements/py36-min-all-deps.yml
+          python ci/min_deps_check.py ci/requirements/py37-min-all-deps.yml
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 7795f18..46f911f 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -38,7 +38,7 @@ jobs:
             ~/.local/share/cartopy/
           key: ${{ runner.os }}-conda-py${{ matrix.python-version }}-${{ hashFiles('ci/requirements/**.yml') }}
           restore-keys: |
-            ${{ runner.os }}-conda-${{ matrix.python-version }}-
+            ${{ runner.os }}-conda-py${{ matrix.python-version }}-
             ${{ runner.os }}-
       - uses: conda-incubator/setup-miniconda@v2
         with:
diff --git a/.github/workflows/linting.yaml b/.github/workflows/linting.yaml
index c887f15..3a2f583 100644
--- a/.github/workflows/linting.yaml
+++ b/.github/workflows/linting.yaml
@@ -33,8 +33,10 @@ jobs:
         uses: actions/cache@v2
         with:
           path: ~/conda_pkgs_dir
-          key: ${{ runner.os }}-conda-py${{ matrix.python-version }}-${{
-            hashFiles('ci/requirements/**.yml') }}
+          key: ${{ runner.os }}-conda-py${{ matrix.python-version }}-${{ hashFiles('ci/requirements/**.yml') }}
+          restore-keys: |
+            ${{ runner.os }}-conda-py${{ matrix.python-version }}-
+            ${{ runner.os }}-
       - uses: conda-incubator/setup-miniconda@v2
         with:
           channels: conda-forge
diff --git a/HOW_TO_RELEASE.md b/HOW_TO_RELEASE.md
index 0481718..f4a23d2 100644
--- a/HOW_TO_RELEASE.md
+++ b/HOW_TO_RELEASE.md
@@ -100,10 +100,11 @@ These instructions assume that `upstream` refers to the main repository
 17. Issue the release on GitHub. Click on "Draft a new release" at
     <https://github.com/regionmask/regionmask/releases>. Type in the version number
     and paste the release summary in the notes.
-18. Update the docs. Login to <https://readthedocs.org/projects/regionmask/versions/>
+18. Update zenodo link.
+19. Update the docs. Login to <https://readthedocs.org/projects/regionmask/versions/>
     and switch your new release tag (at the bottom) from "Inactive" to "Active".
     It should now build automatically.
-19. Release regionmask on conda - also update the requirements in meta.yaml <https://github.com/conda-forge/regionmask-feedstock>
+20. Release regionmask on conda - also update the requirements in meta.yaml <https://github.com/conda-forge/regionmask-feedstock>
 
 <!-- markdownlint-enable MD013 -->
 
diff --git a/README.rst b/README.rst
index 48d91b1..43fbbf5 100644
--- a/README.rst
+++ b/README.rst
@@ -18,8 +18,8 @@ plotting and creation of masks of spatial regions
    :target: https://conda.anaconda.org/conda-forge
 .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
     :target: https://github.com/ambv/black
-.. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.3585543.svg
-   :target: https://doi.org/10.5281/zenodo.3585543
+.. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.5532848.svg
+   :target: https://doi.org/10.5281/zenodo.5532848
 
 
 Documentation
diff --git a/ci/requirements/py36-bare-minimum.yml b/ci/requirements/py37-bare-minimum.yml
similarity index 93%
rename from ci/requirements/py36-bare-minimum.yml
rename to ci/requirements/py37-bare-minimum.yml
index 02999d9..7f1472b 100644
--- a/ci/requirements/py36-bare-minimum.yml
+++ b/ci/requirements/py37-bare-minimum.yml
@@ -3,7 +3,7 @@ channels:
   - conda-forge
   - nodefaults
 dependencies:
-  - python=3.6
+  - python=3.7
   - geopandas
   - numpy
   - pooch
diff --git a/ci/requirements/py36-min-all-deps.yml b/ci/requirements/py37-min-all-deps.yml
similarity index 95%
rename from ci/requirements/py36-min-all-deps.yml
rename to ci/requirements/py37-min-all-deps.yml
index 47acfd4..98d59a3 100644
--- a/ci/requirements/py36-min-all-deps.yml
+++ b/ci/requirements/py37-min-all-deps.yml
@@ -3,7 +3,7 @@ channels:
   - conda-forge
   - nodefaults
 dependencies:
-  - python=3.6
+  - python=3.7
   - cartopy=0.17
   - geopandas=0.6
   - matplotlib-base=3.1
diff --git a/docs/installation.rst b/docs/installation.rst
index 7778022..4774069 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -4,7 +4,7 @@ Installation
 Required dependencies
 ---------------------
 
-- Python (3.6 or later)
+- Python (3.7 or later)
 - `geopandas <http://geopandas.org/>`__ (0.6 or later)
 - `numpy <http://www.numpy.org/>`__ (1.17 or later)
 - `pooch <https://www.fatiando.org/pooch/latest/>`__ (1.0 or later)
diff --git a/docs/whats_new.rst b/docs/whats_new.rst
index 28599a1..929598b 100644
--- a/docs/whats_new.rst
+++ b/docs/whats_new.rst
@@ -16,8 +16,19 @@ v0.9.0 (unreleased)
 Breaking Changes
 ~~~~~~~~~~~~~~~~
 
+- Removed support for Python 3.6.
+
 Enhancements
 ~~~~~~~~~~~~
+- Works with unstructured 1D grids such as:
+
+  - `ICON <https://code.mpimet.mpg.de/projects/iconpublic>`_
+  - `FESOM <https://fesom.de/>`_
+  - `MPAS <https://mpas-dev.github.io/>`_
+
+  with 1-dimensional coordinates of the form ``lon(cell)`` and ``lat(cell)``.
+  Note that only xarray arrays can be detected as unstructured grids.
+  (:issue:`278`, :pull:`280`). By `Aaron Spring <https://github.com/aaronspring>`_.
 
 Deprecations
 ~~~~~~~~~~~~
@@ -34,6 +45,9 @@ Docs
 Internal Changes
 ~~~~~~~~~~~~~~~~
 
+- Fix downloading naturalearth regions part 2 (see :pull:`261`): Monkeypatch the correct
+  download URL and catch all ``URLError``, not only timeouts (:pull:`289`).
+
 v0.8.0 (08.09.2021)
 -------------------
 
diff --git a/regionmask/core/mask.py b/regionmask/core/mask.py
index 0eabd96..7ba8167 100644
--- a/regionmask/core/mask.py
+++ b/regionmask/core/mask.py
@@ -123,6 +123,16 @@ def _mask(
 
     lon, lat = _extract_lon_lat(lon_or_obj, lat, lon_name, lat_name)
 
+    # determine whether unstructured grid
+    # have to do this before np.asarray
+    is_unstructured = False
+    if isinstance(lon, xr.DataArray) and isinstance(lat, xr.DataArray):
+        if len(lon.dims) == 1 and len(lat.dims) == 1:
+            if lon.name != lon.dims[0] and lat.name != lat.dims[0]:
+                is_unstructured = True
+                lat_orig = lat
+                lon_orig = lon
+
     lon = np.asarray(lon)
     lat = np.asarray(lat)
 
@@ -137,7 +147,7 @@ def _mask(
 
     lon_orig = lon.copy()
     if wrap_lon_:
-        lon = _wrapAngle(lon, wrap_lon_)
+        lon = _wrapAngle(lon, wrap_lon_, is_unstructured=is_unstructured)
 
     if method not in (None, "rasterize", "shapely", "pygeos"):
         msg = "Method must be None or one of 'rasterize', 'shapely' and 'pygeos'."
@@ -160,18 +170,31 @@ def _mask(
     elif method == "rasterize_split":
         mask = _mask_rasterize_split(lon, lat, outlines, numbers=numbers)
     elif method == "pygeos":
-        mask = _mask_pygeos(lon, lat, outlines, numbers=numbers)
+        mask = _mask_pygeos(
+            lon, lat, outlines, numbers=numbers, is_unstructured=is_unstructured
+        )
     elif method == "shapely":
-        mask = _mask_shapely(lon, lat, outlines, numbers=numbers)
+        mask = _mask_shapely(
+            lon, lat, outlines, numbers=numbers, is_unstructured=is_unstructured
+        )
 
     # not False required
     if wrap_lon is not False:
         # treat the points at -180°E/0°E and -90°N
-        mask = _mask_edgepoints_shapely(mask, lon, lat, outlines, numbers)
+        mask = _mask_edgepoints_shapely(
+            mask, lon, lat, outlines, numbers, is_unstructured=is_unstructured
+        )
 
     # create an xr.DataArray
     if lon.ndim == 1:
-        mask = _create_xarray(mask, lon_orig, lat, lon_name, lat_name)
+        if is_unstructured:
+            mask = _create_xarray(
+                mask, lon_orig, lat_orig, lon_name, lat_name, is_unstructured
+            )
+        else:
+            mask = _create_xarray(
+                mask, lon_orig, lat, lon_name, lat_name, is_unstructured
+            )
     else:
         mask = _create_xarray_2D(mask, lon_or_obj, lat_orig, lon_name, lat_name)
 
@@ -297,12 +320,26 @@ def _extract_lon_lat(lon_or_obj, lat, lon_name, lat_name):
     return lon, lat
 
 
-def _create_xarray(mask, lon, lat, lon_name, lat_name):
+def _create_xarray(mask, lon, lat, lon_name, lat_name, is_unstructured):
     """create an xarray DataArray"""
-
-    # create the xarray output
-    coords = {lat_name: lat, lon_name: lon}
-    mask = xr.DataArray(mask, coords=coords, dims=(lat_name, lon_name), name="region")
+    if is_unstructured:
+        # lat is xr.DataArray to retrieve cell_name
+        cell_name = lat.dims[0]
+        mask = xr.DataArray(
+            mask,
+            coords={
+                cell_name: lat.coords[cell_name],
+                "lat": (cell_name, lat.values),
+                "lon": (cell_name, lon),
+            },
+            dims=cell_name,
+            name="region",
+        )
+    else:
+        coords = {lat_name: lat, lon_name: lon}
+        mask = xr.DataArray(
+            mask, coords=coords, dims=(lat_name, lon_name), name="region"
+        )
 
     return mask
 
@@ -340,14 +377,18 @@ def _create_xarray_2D(mask, lon_or_obj, lat, lon_name, lat_name):
     return mask
 
 
-def _mask_edgepoints_shapely(mask, lon, lat, polygons, numbers, fill=np.NaN):
+def _mask_edgepoints_shapely(
+    mask, lon, lat, polygons, numbers, fill=np.NaN, is_unstructured=False
+):
 
     import shapely.vectorized as shp_vect
 
     # not sure if this is really necessary
     lon, lat, numbers = _parse_input(lon, lat, polygons, fill, numbers)
 
-    LON, LAT, out, shape = _get_LON_LAT_out_shape(lon, lat, fill)
+    LON, LAT, out, shape = _get_LON_LAT_out_shape(
+        lon, lat, fill, is_unstructured=is_unstructured
+    )
 
     mask = mask.flatten()
     mask_unassigned = np.isnan(mask)
@@ -385,12 +426,14 @@ def _mask_edgepoints_shapely(mask, lon, lat, polygons, numbers, fill=np.NaN):
     return mask.reshape(shape)
 
 
-def _mask_pygeos(lon, lat, polygons, numbers, fill=np.NaN):
+def _mask_pygeos(lon, lat, polygons, numbers, fill=np.NaN, is_unstructured=False):
     """create a mask using pygeos.STRtree"""
 
     lon, lat, numbers = _parse_input(lon, lat, polygons, fill, numbers)
 
-    LON, LAT, out, shape = _get_LON_LAT_out_shape(lon, lat, fill)
+    LON, LAT, out, shape = _get_LON_LAT_out_shape(
+        lon, lat, fill, is_unstructured=is_unstructured
+    )
 
     # add a tiny offset to get a consistent edge behaviour
     LON = LON - 1 * 10 ** -8
@@ -410,14 +453,16 @@ def _mask_pygeos(lon, lat, polygons, numbers, fill=np.NaN):
     return out.reshape(shape)
 
 
-def _mask_shapely(lon, lat, polygons, numbers, fill=np.NaN):
+def _mask_shapely(lon, lat, polygons, numbers, fill=np.NaN, is_unstructured=False):
     """create a mask using shapely.vectorized.contains"""
 
     import shapely.vectorized as shp_vect
 
     lon, lat, numbers = _parse_input(lon, lat, polygons, fill, numbers)
 
-    LON, LAT, out, shape = _get_LON_LAT_out_shape(lon, lat, fill)
+    LON, LAT, out, shape = _get_LON_LAT_out_shape(
+        lon, lat, fill, is_unstructured=is_unstructured
+    )
 
     # add a tiny offset to get a consistent edge behaviour
     LON = LON - 1 * 10 ** -8
@@ -449,7 +494,7 @@ def _parse_input(lon, lat, coords, fill, numbers):
     return lon, lat, numbers
 
 
-def _get_LON_LAT_out_shape(lon, lat, fill):
+def _get_LON_LAT_out_shape(lon, lat, fill, is_unstructured=False):
 
     if lon.ndim != lat.ndim:
         raise ValueError(
@@ -465,7 +510,9 @@ def _get_LON_LAT_out_shape(lon, lat, fill):
             f"lon.shape={lon.shape} & lat.shape={lat.shape}."
         )
 
-    if ndim == 1:
+    if is_unstructured:
+        LON, LAT = lon, lat
+    elif ndim == 1:
         LON, LAT = np.meshgrid(lon, lat)
     elif ndim == 2:
         LON, LAT = lon, lat
diff --git a/regionmask/core/utils.py b/regionmask/core/utils.py
index 7392542..52f1dea 100644
--- a/regionmask/core/utils.py
+++ b/regionmask/core/utils.py
@@ -75,7 +75,7 @@ def _wrapAngle180(lon):
     return lon
 
 
-def _wrapAngle(lon, wrap_lon=True):
+def _wrapAngle(lon, wrap_lon=True, is_unstructured=False):
     """wrap the angle to the other base
 
     If lon is from -180 to 180 wraps them to 0..360
@@ -100,7 +100,7 @@ def _wrapAngle(lon, wrap_lon=True):
         new_lon = _wrapAngle360(lon.copy())
 
     # check if they are still unique
-    if new_lon.ndim == 1:
+    if new_lon.ndim == 1 and not is_unstructured:
         if new_lon.shape != np.unique(new_lon).shape:
             raise ValueError("There are equal longitude coordinates (when wrapped)!")
 
diff --git a/setup.cfg b/setup.cfg
index 1201a1e..e40faa8 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -14,7 +14,6 @@ classifiers =
     Intended Audience :: Science/Research
     Programming Language :: Python
     Programming Language :: Python :: 3
-    Programming Language :: Python :: 3.6
     Programming Language :: Python :: 3.7
     Programming Language :: Python :: 3.8
     Programming Language :: Python :: 3.9
@@ -26,7 +25,7 @@ classifiers =
 packages = find:
 zip_safe = False  # https://mypy.readthedocs.io/en/latest/installed_packages.html
 include_package_data = True
-python_requires = >=3.6
+python_requires = >=3.7
 install_requires =
     geopandas >= 0.6
     numpy >= 1.17


Test patch:
diff --git a/regionmask/tests/test_defined_regions.py b/regionmask/tests/test_defined_regions.py
index d9d5025..45e3f58 100644
--- a/regionmask/tests/test_defined_regions.py
+++ b/regionmask/tests/test_defined_regions.py
@@ -48,12 +48,13 @@ def test_defined_regions_natural_earth(monkeypatch, region_name, n_regions):
 
 
 @requires_cartopy
-def test_natural_earth_loaded_as_utf8():
+def test_natural_earth_loaded_as_utf8(monkeypatch):
     # GH 95
-    regions = defined_regions.natural_earth.ocean_basins_50
-    r = regions[90]
+    regions = get_naturalearth_region_or_skip(
+        monkeypatch, "natural_earth.ocean_basins_50"
+    )
 
-    assert r.name == "Río de la Plata"
+    assert regions[90].name == "Río de la Plata"
 
 
 def test_maybe_get_column():
diff --git a/regionmask/tests/test_unstructured.py b/regionmask/tests/test_unstructured.py
new file mode 100644
index 0000000..d2faa9a
--- /dev/null
+++ b/regionmask/tests/test_unstructured.py
@@ -0,0 +1,24 @@
+import numpy as np
+import pytest
+import xarray as xr
+
+from . import requires_pygeos
+from .utils import dummy_region, expected_mask_2D
+
+
+@pytest.mark.parametrize(
+    "method", ["shapely", pytest.param("pygeos", marks=requires_pygeos)]
+)
+def test_unstructured_dummy(method):
+    """Test for unstructured output."""
+    lat = [0.5, 0.5, 1.5, 1.5]
+    lon = [0.5, 1.5, 0.5, 1.5]
+    grid = xr.Dataset(coords={"lon": ("cells", lon), "lat": ("cells", lat)})
+
+    result = dummy_region.mask(grid, method=method)
+
+    expected = expected_mask_2D().flatten()
+    assert isinstance(result, xr.DataArray)
+    assert np.allclose(result, expected, equal_nan=True)
+    for c in ["lon", "lat"]:
+        assert np.all(np.equal(grid.coords[c], result.coords[c]))
diff --git a/regionmask/tests/utils.py b/regionmask/tests/utils.py
index ae1076e..9e1262c 100644
--- a/regionmask/tests/utils.py
+++ b/regionmask/tests/utils.py
@@ -69,7 +69,6 @@ REGIONS_REQUIRING_CARTOPY = {
 
 def get_naturalearth_region_or_skip(monkeypatch, region_name):
 
-    from socket import timeout
     from urllib.request import URLError, urlopen
 
     import cartopy
@@ -83,22 +82,19 @@ def get_naturalearth_region_or_skip(monkeypatch, region_name):
     # https://github.com/nvkelso/natural-earth-vector/issues/445
     # remove again once the minimum cartopy version is v0.19
 
-    _NE_URL_TEMPLATE = (
+    url_template = (
         "https://naturalearth.s3.amazonaws.com/"
         "{resolution}_{category}/ne_{resolution}_{name}.zip"
     )
 
-    monkeypatch.setattr(
-        cartopy.io.shapereader.NEShpDownloader, "_NE_URL_TEMPLATE", _NE_URL_TEMPLATE
-    )
+    downloader = cartopy.config["downloaders"][("shapefiles", "natural_earth")]
+    monkeypatch.setattr(downloader, "url_template", url_template)
 
     try:
         region = attrgetter(region_name)(defined_regions)
     except URLError as e:
-        if isinstance(e.reason, timeout):
-            warnings.warn("naturalearth donwload timeout - test not run!")
-            pytest.skip()
-        else:
-            raise
+        warnings.warn(str(e))
+        warnings.warn("naturalearth donwload timeout - test not run!")
+        pytest.skip()
 
     return region