Skip to content

[Bug]: Cartopy 0.25.0 introduces regression in difference plot for lat_lon_land #1026

@tomvothecoder

Description

@tomvothecoder

What happened?

Overview

The colors on the difference plots appear inverted on the Y-axis for specific lat_lon_land plots.

Source: zppy testing

Example:

Related Dependency

cartopy=0.25.0 introduces a change that results in the Y-axis being inverted (or the colors). This happens with when calculating difference (test-ref) then passing 1D X/Y data ax.contourf(). Masked data at the bottom/top of the difference data might also be related

Exact Cause

  • Create a minimum reproducible example using the array data
  • Figure out what changed between cartopy versions and what we need to do in e3sm_diags.

I found the exact commit that introduces this regression and opened a related issue in the Cartopy repo here: SciTools/cartopy#2620.

Temporary Workaround

In PR #1021, we've temporarily pinned cartopy=<0.25.0 to prevent this the issue from occurring.
(before pinning, after pinning).

What did you expect to happen? Are there are possible answers you came across?

The diff plots should remain the same between cartopy=0.24.0 and cartopy=0.25.0.

Attempted Fixes (Not Used)

  1. Update _add_contour_plot() to pass X and Y as a 2D meshgrid and set transform_first=True -- fixes the difference plots for lat_lon_land, but introduces a ton of issues with other plots here
  2. Sorting the Y-axis with Xarray using ascending=False fixes the plot, but this should not be done because it will affect other plots too.

Minimal Reproducible Example (MVCE)

Note: The data includes masked values at the grid edge.
This script below was adapted based on how e3sm_diags produces plots.

Details
"""A minimum, self-contained example demonstrating Cartopy contourf() plotting
differences between cartopy 0.24.0 and 0.25.0.

Issue: The contourf() plot appears inverted (flipped vertically) on the Y-axis
when using cartopy 0.25.0 compared to cartopy 0.24.0

Cause:
- Commit https://github.com/SciTools/cartopy/commit/eb4d0423ee9940067fa18c71cb38e178471ed888
introduces changes to "FIX: create a single inverted polygon when no exteriors found".
- This commit is where the issue first appears (11/4/2024).

Variables:
- x: longitude (0.5 o 360.5)
- y: latitude (-89.5 to 89.5)
- var: variable on (y, x) grid with masked values present at the bottom edge.

Workaround:
- Converting x and y to 2D meshgrid using numpy.meshgrid() and using transform_first=True.
- However, this workaround causes issues with many other plots.

Setup Environment 1 (Without Plot Issues):
----------------------------------------------------------
- Commit: https://github.com/SciTools/cartopy/commit/5c4ef99f7093f43e387b975391dd77179a8df9ac
- 11/3/2024: DOC: add deprecation note for clip_path [skip actions]
- Hash: 5c4ef99f7093f43e387b975391dd77179a8df9ac

Commands:
    git clone https://github.com/SciTools/cartopy.git

    conda create -n cartopy_5c4e4f -c conda-forge xarray=2025.12.0 netcdf4 matplotlib-base=3.10.8 ipykernel
    conda activate cartopy_5c4e4f

    cd cartopy
    git checkout 5c4ef99f7093f43e387b975391dd77179a8df9ac
    python -m pip install .


Setup Environment 2 (With Plot Issues):
----------------------------------------------------------
- Commit: https://github.com/SciTools/cartopy/commit/eb4d0423ee9940067fa18c71cb38e178471ed888
- 11/4/2024: FIX: create a single inverted polygon when no exteriors found
- Hash: eb4d0423ee9940067fa18c71cb38e178471ed888

Commands:
    git clone https://github.com/SciTools/cartopy.git

    conda create -n cartopy_eb4d04 -c conda-forge xarray=2025.12.0 netcdf4 matplotlib-base=3.10.8 ipykernel
    conda activate cartopy_eb4d04

    cd cartopy
    git checkout eb4d0423ee9940067fa18c71cb38e178471ed888
    python -m pip install .
"""

import urllib.request
import os

import cartopy
import matplotlib
import numpy as np
import xarray as xr
from cartopy.mpl.ticker import LatitudeFormatter, LongitudeFormatter

matplotlib.use("Agg")
import matplotlib.pyplot as plt  # isort:skip  # noqa: E402

# Download the data
# --------------------------------------------------------------------------
# Source: /lcrc/group/e3sm/public_html/diagnostic_output/ac.tvo/tests/1026_cartopy_0250_mvce/
public_path = "https://web.lcrc.anl.gov/public/e3sm/diagnostic_output/ac.tvo/tests/1026_cartopy_0250_mvce/"
local_data_dir = "./data_cartopy_0250_mvce"
os.makedirs(local_data_dir, exist_ok=True)

def download_data():
    files = ["x.nc", "y.nc", "var.nc"]

    for fname in files:
        url = public_path + fname
        local_path = os.path.join(local_data_dir, fname)

        if not os.path.exists(local_path):
            print(f"Downloading {url} to {local_path} ...")
            # Set a User-Agent header to avoid HTTP 403 errors
            req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
            with urllib.request.urlopen(req) as response, open(local_path, 'wb') as out_file:
                out_file.write(response.read())
        else:
            print(f"File {local_path} already exists.")

download_data()

# Open the data
# --------------------------------------------------------------------------
x = xr.open_dataarray(os.path.join(local_data_dir, "x.nc"))
y = xr.open_dataarray(os.path.join(local_data_dir, "y.nc"))
var = xr.open_dataarray(os.path.join(local_data_dir, "var.nc"))

# Create the figure and axis with Cartopy projection
# --------------------------------------------------------------------------
fig = plt.figure(figsize=(8.5, 11.0), dpi=150)
projection = cartopy.crs.PlateCarree(central_longitude=180)
ax = fig.add_axes((0.1691, 0.1112, 0.6465, 0.2258), projection=projection)

# Set the longitude and latitude limits.
lon_west = 0
lon_east = 360
lat_south = -90
lat_north = 90

ax.set_extent([lon_west, lon_east, lat_south, lat_north], crs=projection)

# Create the contour plot
# --------------------------------------------------------------------------
transform = cartopy.crs.PlateCarree()
contour_plot = ax.contourf(
    x,
    y,
    var,
    cmap="BrBG",
    transform=transform,
    norm=None,
    levels=None,
    extend="both",
)

# Configure aspect ratio and coast lines.
# --------------------------------------------------------------------------
ax.set_aspect((lon_east - lon_west) / (2 * (lat_north - lat_south)))
ax.coastlines(lw=0.3)

# Configure x and y axes:
# --------------------------------------------------------------------------
x_ticks = np.array([0., 60., 120., 180., 240., 300., 359.5])
y_ticks = np.array([-90, -60, -30, 0, 30, 60, 90])
ax.set_xticks(x_ticks, crs=transform)
ax.set_yticks(y_ticks, crs=transform)
ax.tick_params(labelsize=8.0, direction="out", width=1)
ax.xaxis.set_ticks_position("bottom")
ax.yaxis.set_ticks_position("left")

# Add longitude and latitude formatters
lon_formatter = LongitudeFormatter(
    zero_direction_label=True, number_format=".0f"
)
lat_formatter = LatitudeFormatter()
ax.xaxis.set_major_formatter(lon_formatter)
ax.yaxis.set_major_formatter(lat_formatter)

# Add color bar
# --------------------------------------------------------------------------
cbax_rect = (0.8326, 0.13269999999999998, 0.0326, 0.1792)
cbax = fig.add_axes(cbax_rect)
cbar = fig.colorbar(contour_plot, cax=cbax)
cbar.ax.tick_params(labelsize=9.0, length=0)

# Save the figure
# --------------------------------------------------------------------------
conda_env = os.environ.get("CONDA_DEFAULT_ENV", "unknown_env")
output_path = os.path.abspath(f"{conda_env}_mvce.png")
fig.savefig(output_path)

print(f"Saved figure at {output_path}")

Relevant log output

N/A

Anything else we need to know?

No response

Environment

Latest e3sm_diags with cartopy=0.25.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugBug fix (will increment patch version)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions