Skip to content

Commit

Permalink
Merge branch 'master' into sensible_info_outputs
Browse files Browse the repository at this point in the history
  • Loading branch information
weiji14 committed Sep 7, 2020
2 parents b87ceb3 + 53eb1b5 commit 6b99727
Show file tree
Hide file tree
Showing 23 changed files with 250 additions and 92 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/cache_data.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
# Install GMT
- name: Install GMT
shell: bash -l {0}
run: conda install -c conda-forge gmt=6.1.0
run: conda install -c conda-forge gmt=6.1.1

# Download remote files
- name: Download remote data
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/ci_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,11 @@ jobs:

# Setup Miniconda
- name: Setup Miniconda
uses: goanpeca/setup-miniconda@v1.6.0
uses: conda-incubator/setup-miniconda@v1.7.0
with:
python-version: ${{ matrix.python-version }}
channels: conda-forge
miniconda-version: "latest"

# Install GMT and other required dependencies from conda-forge
- name: Install GMT and required dependencies
Expand All @@ -77,7 +78,7 @@ jobs:
requirements_file=full-conda-requirements.txt
cat requirements.txt requirements-dev.txt > $requirements_file
cat << EOF >> $requirements_file
gmt=6.1.0
gmt=6.1.1
make
codecov
EOF
Expand Down
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ env:
# The file with the listed requirements to be installed by conda
- CONDA_REQUIREMENTS=requirements.txt
- CONDA_REQUIREMENTS_DEV=requirements-dev.txt
- CONDA_INSTALL_EXTRA="codecov twine gmt=6.1.0"
- CONDA_INSTALL_EXTRA="codecov twine gmt=6.1.1"
# These variables control which actions are performed in a build
- DEPLOY=false

Expand Down
34 changes: 32 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,38 @@ Leave a comment in the PR and we'll help you out.

### Testing plots

We use the [pytest-mpl](https://github.com/matplotlib/pytest-mpl) plug-in to test plot
generating code.
Writing an image-based test is only slightly more difficult than a simple test.
The main consideration is that you must specify the "baseline" or reference
image, and compare it with a "generated" or test image. This is handled using
the *decorator* functions `@check_figures_equal` and
`@pytest.mark.mpl_image_compare` whose usage are further described below.

#### Using check_figures_equal

This approach draws the same figure using two different methods (the reference
method and the tested method), and checks that both of them are the same.
It takes two `pygmt.Figure` objects ('fig_ref' and 'fig_test'), generates a png
image, and checks for the Root Mean Square (RMS) error between the two.
Here's an example:

```python
@check_figures_equal()
def test_my_plotting_case(fig_ref, fig_test):
"Test that my plotting function works"
fig_ref.grdimage("@earth_relief_01d_g", projection="W120/15c", cmap="geo")
fig_test.grdimage(grid, projection="W120/15c", cmap="geo")
```

Note: This is the recommended way to test plots whenever possible, such as when
we want to compare a reference GMT plot created from NetCDF files with one
generated by PyGMT that passes through several layers of virtualfile machinery.
Using this method will help save space in the git repository by not having to
store baseline images as with the other method below.

#### Using mpl_image_compare

This method uses the [pytest-mpl](https://github.com/matplotlib/pytest-mpl)
plug-in to test plot generating code.
Every time the tests are run, `pytest-mpl` compares the generated plots with known
correct ones stored in `pygmt/tests/baseline`.
If your test created a `pygmt.Figure` object, you can test it by adding a *decorator* and
Expand Down
6 changes: 1 addition & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,7 @@ test:
@echo ""
@cd $(TESTDIR); python -c "import $(PROJECT); $(PROJECT).show_versions()"
@echo ""
# There are two steps to the test here because `test_grdimage_over_dateline`
# passes only when it runs before the other tests.
# See also https://github.com/GenericMappingTools/pygmt/pull/476
cd $(TESTDIR); pytest -m runfirst $(PYTEST_ARGS) $(PROJECT)
cd $(TESTDIR); pytest -m 'not runfirst' $(PYTEST_ARGS) $(PROJECT)
cd $(TESTDIR); pytest $(PYTEST_ARGS) $(PROJECT)
cp $(TESTDIR)/coverage.xml .
cp -r $(TESTDIR)/htmlcov .
rm -r $(TESTDIR)
Expand Down
2 changes: 1 addition & 1 deletion doc/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Which GMT?
PyGMT requires Generic Mapping Tools (GMT) version 6 as a minimum, which is the latest
released version that can be found at
the `GMT official site <https://www.generic-mapping-tools.org>`__.
We need the latest GMT (>=6.1.0) since there are many changes being made to GMT itself in
We need the latest GMT (>=6.1.1) since there are many changes being made to GMT itself in
response to the development of PyGMT, mainly the new
`modern execution mode <https://docs.generic-mapping-tools.org/latest/cookbook/introduction.html#modern-and-classic-mode>`__.

Expand Down
2 changes: 1 addition & 1 deletion environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ channels:
dependencies:
- python=3.7
- pip
- gmt=6.1.0
- gmt=6.1.1
- numpy
- pandas
- xarray
Expand Down
2 changes: 1 addition & 1 deletion examples/gallery/grid/track_sampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

fig = pygmt.Figure()
# Plot the earth relief grid on Cylindrical Stereographic projection, masking land areas
fig.basemap(region="d", frame=True, projection="Cyl_stere/8i")
fig.basemap(region="g", frame=True, projection="Cyl_stere/150/-20/8i")
fig.grdimage(grid=grid, cmap="gray")
fig.coast(land="#666666")
# Plot using circles (c) of 0.15cm, the sampled bathymetry points
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"scripts": {
"build:miniconda": "curl -o ~/miniconda.sh https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh && bash ~/miniconda.sh -b -p $HOME/miniconda",
"build:pygmt": "conda env create -f environment.yml && source activate pygmt && conda install -c conda-forge -y gmt==6.1.0 && make install",
"build:pygmt": "conda env create -f environment.yml && source activate pygmt && conda install -c conda-forge -y gmt==6.1.1 && make install",
"build:docs": "source activate pygmt && cd doc && make all && mv _build/html ../public",
"build": "export PATH=$HOME/miniconda/bin:$PATH && npm run build:miniconda && npm run build:pygmt && npm run build:docs"
}
Expand Down
34 changes: 10 additions & 24 deletions pygmt/base_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
Does not define any special non-GMT methods (savefig, show, etc).
"""
import contextlib
import csv
import numpy as np
import pandas as pd

Expand All @@ -14,7 +13,6 @@
dummy_context,
data_kind,
fmt_docstring,
GMTTempFile,
use_alias,
kwargs_to_strings,
)
Expand Down Expand Up @@ -986,28 +984,16 @@ def text(
if position is not None and isinstance(position, str):
kwargs["F"] += f'+c{position}+t"{text}"'

with GMTTempFile(suffix=".txt") as tmpfile:
with Session() as lib:
fname = textfiles if kind == "file" else ""
if kind == "vectors":
if position is not None:
fname = ""
else:
pd.DataFrame.from_dict(
{
"x": np.atleast_1d(x),
"y": np.atleast_1d(y),
"text": np.atleast_1d(text),
}
).to_csv(
tmpfile.name,
sep="\t",
header=False,
index=False,
quoting=csv.QUOTE_NONE,
)
fname = tmpfile.name

with Session() as lib:
file_context = dummy_context(textfiles) if kind == "file" else ""
if kind == "vectors":
if position is not None:
file_context = dummy_context("")
else:
file_context = lib.virtualfile_from_vectors(
np.atleast_1d(x), np.atleast_1d(y), np.atleast_1d(text)
)
with file_context as fname:
arg_str = " ".join([fname, build_arg_string(kwargs)])
lib.call_module("text", arg_str)

Expand Down
2 changes: 1 addition & 1 deletion pygmt/clib/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class Session:
"""

# The minimum version of GMT required
required_version = "6.1.0"
required_version = "6.1.1"

@property
def session_pointer(self):
Expand Down
6 changes: 6 additions & 0 deletions pygmt/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,9 @@ class GMTVersionError(GMTError):
"""
Raised when an incompatible version of GMT is being used.
"""


class GMTImageComparisonFailure(AssertionError):
"""
Raised when a comparison between two images fails.
"""
8 changes: 0 additions & 8 deletions pygmt/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,14 +316,6 @@ def shift_origin(self, xshift=None, yshift=None):
Shift plot origin in x direction.
yshift : str
Shift plot origin in y direction.
Notes
-----
For GMT 6.1.0, this function can't be used as the first plotting
function of :meth:`pygmt.Figure`, since it relies the *region* and
*projection* settings from previous commands.
.. TODO: Remove the notes when PyGMT bumps to GMT>=6.1.1.
"""
self._preprocess()
args = ["-T"]
Expand Down
113 changes: 113 additions & 0 deletions pygmt/helpers/testing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""
Helper functions for testing.
"""

import inspect
import os

from matplotlib.testing.compare import compare_images

from ..exceptions import GMTImageComparisonFailure
from ..figure import Figure


def check_figures_equal(*, tol=0.0, result_dir="result_images"):
"""
Decorator for test cases that generate and compare two figures.
The decorated function must take two arguments, *fig_ref* and *fig_test*,
and draw the reference and test images on them. After the function
returns, the figures are saved and compared.
This decorator is practically identical to matplotlib's check_figures_equal
function, but adapted for PyGMT figures. See also the original code at
https://matplotlib.org/3.3.1/api/testing_api.html#
matplotlib.testing.decorators.check_figures_equal
Parameters
----------
tol : float
The RMS threshold above which the test is considered failed.
result_dir : str
The directory where the figures will be stored.
Examples
--------
>>> import pytest
>>> import shutil
>>> @check_figures_equal(result_dir="tmp_result_images")
... def test_check_figures_equal(fig_ref, fig_test):
... fig_ref.basemap(projection="X5c", region=[0, 5, 0, 5], frame=True)
... fig_test.basemap(projection="X5c", region=[0, 5, 0, 5], frame="af")
>>> test_check_figures_equal()
>>> assert len(os.listdir("tmp_result_images")) == 0
>>> shutil.rmtree(path="tmp_result_images") # cleanup folder if tests pass
>>> @check_figures_equal(result_dir="tmp_result_images")
... def test_check_figures_unequal(fig_ref, fig_test):
... fig_ref.basemap(projection="X5c", region=[0, 5, 0, 5], frame=True)
... fig_test.basemap(projection="X5c", region=[0, 3, 0, 3], frame=True)
>>> with pytest.raises(GMTImageComparisonFailure):
... test_check_figures_unequal()
>>> for suffix in ["", "-expected", "-failed-diff"]:
... assert os.path.exists(
... os.path.join(
... "tmp_result_images",
... f"test_check_figures_unequal{suffix}.png",
... )
... )
>>> shutil.rmtree(path="tmp_result_images") # cleanup folder if tests pass
"""

def decorator(func):

os.makedirs(result_dir, exist_ok=True)
old_sig = inspect.signature(func)

def wrapper(*args, **kwargs):
try:
fig_ref = Figure()
fig_test = Figure()
func(*args, fig_ref=fig_ref, fig_test=fig_test, **kwargs)
ref_image_path = os.path.join(
result_dir, func.__name__ + "-expected.png"
)
test_image_path = os.path.join(result_dir, func.__name__ + ".png")
fig_ref.savefig(ref_image_path)
fig_test.savefig(test_image_path)

# Code below is adapted for PyGMT, and is originally based on
# matplotlib.testing.decorators._raise_on_image_difference
err = compare_images(
expected=ref_image_path,
actual=test_image_path,
tol=tol,
in_decorator=True,
)
if err is None: # Images are the same
os.remove(ref_image_path)
os.remove(test_image_path)
else: # Images are not the same
for key in ["actual", "expected", "diff"]:
err[key] = os.path.relpath(err[key])
raise GMTImageComparisonFailure(
"images not close (RMS %(rms).3f):\n\t%(actual)s\n\t%(expected)s "
% err
)
finally:
del fig_ref
del fig_test

parameters = [
param
for param in old_sig.parameters.values()
if param.name not in {"fig_test", "fig_ref"}
]
new_sig = old_sig.replace(parameters=parameters)
wrapper.__signature__ = new_sig

return wrapper

return decorator
10 changes: 7 additions & 3 deletions pygmt/helpers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def data_kind(data, x=None, y=None, z=None):
Possible types:
* a file name provided as 'data'
* an xarray.DataArray provided as 'data'
* a matrix provided as 'data'
* 1D arrays x and y (and z, optionally)
Expand All @@ -28,8 +29,8 @@ def data_kind(data, x=None, y=None, z=None):
Parameters
----------
data : str, 2d array, or None
Data file name or numpy array.
data : str, xarray.DataArray, 2d array, or None
Data file name, xarray.DataArray or numpy array.
x/y : 1d arrays or None
x and y columns as numpy arrays.
z : 1d array or None
Expand All @@ -39,18 +40,21 @@ def data_kind(data, x=None, y=None, z=None):
Returns
-------
kind : str
One of: ``'file'``, ``'matrix'``, ``'vectors'``.
One of: ``'file'``, ``'grid'``, ``'matrix'``, ``'vectors'``.
Examples
--------
>>> import numpy as np
>>> import xarray as xr
>>> data_kind(data=None, x=np.array([1, 2, 3]), y=np.array([4, 5, 6]))
'vectors'
>>> data_kind(data=np.arange(10).reshape((5, 2)), x=None, y=None)
'matrix'
>>> data_kind(data='my-data-file.txt', x=None, y=None)
'file'
>>> data_kind(data=xr.DataArray(np.random.rand(4, 3)))
'grid'
"""
if data is None and x is None and y is None:
Expand Down
Loading

0 comments on commit 6b99727

Please sign in to comment.