From 3dc6924f9f42b88403b48369ac5b32c083b10c62 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sat, 25 Jul 2026 14:01:41 -0500 Subject: [PATCH 1/2] Fix base mesh plotting with cartopy 0.25 and matplotlib 3.11 Creating gridline labels with draw_labels=True and then hiding some of them (gl.top_labels = False, etc.) confuses cartopy's title placement. GeoAxes._update_title_position() scans gl.top_label_artists whenever gl.geo_labels is true (the default), so the hidden top labels are still included. During a draw, their tight bounding boxes are infinite, so the title ends up at y = inf and the axes tight bounding box becomes non-finite. The NaN then cascades: tight_layout() gives the axes a NaN position, the "geo" spine path becomes all NaN, and cartopy's gridliner hands NaN vertices to shapely, which raises GEOSException: IllegalArgumentException: Points of LinearRing do not form a closed linestring Instead, only request the labels we actually want. In _plot_cell_width(), that means draw_labels=['left', 'bottom']. In the arrm10to60 and wc14 mesh plots, all four label sets were hidden, so draw_labels=False. Also drop the redundant fig.canvas.draw() and plt.tight_layout() from _plot_cell_width(), since bbox_inches='tight' already crops the figure. Removing tight_layout() alone is not sufficient: without the label fix, the title is silently dropped and bbox_inches='tight' crops to a garbage bounding box, saving only the colorbar. List-valued draw_labels requires cartopy 0.22, so add that constraint. Along the way, satisfy the pre-commit hooks on the arrm10to60 file: isort reorders its imports and flake8 flags a pre-existing F523 (a '{0} x {0}'.format(dlon, dlat) that never used dlat), now an f-string. Co-Authored-By: Claude Opus 5 --- compass/mesh/spherical.py | 14 +++++----- .../global_ocean/mesh/arrm10to60/__init__.py | 26 +++++++++---------- .../tests/global_ocean/mesh/wc14/__init__.py | 11 ++++---- deploy/pixi.toml.j2 | 2 +- pyproject.toml | 2 +- 5 files changed, 27 insertions(+), 28 deletions(-) diff --git a/compass/mesh/spherical.py b/compass/mesh/spherical.py index b186f2b1fe..c6db4ace08 100644 --- a/compass/mesh/spherical.py +++ b/compass/mesh/spherical.py @@ -142,29 +142,29 @@ def _plot_cell_width(self, cell_width): image_filename = config.get('spherical_mesh', 'cell_width_image_filename') register_sci_viz_colormaps() - fig = plt.figure(figsize=[16.0, 8.0]) + plt.figure(figsize=[16.0, 8.0]) ax = plt.axes(projection=ccrs.PlateCarree()) ax.set_global() im = ax.imshow(cell_width, origin='lower', transform=ccrs.PlateCarree(), extent=[-180, 180, -90, 90], cmap=cmap, zorder=0) ax.add_feature(cartopy.feature.LAND, edgecolor='black', zorder=1) - gl = ax.gridlines( + # only request the labels we want. Creating the top and right labels + # and then hiding them confuses cartopy's title placement, leading to + # a non-finite axes bounding box + ax.gridlines( crs=ccrs.PlateCarree(), - draw_labels=True, + draw_labels=['left', 'bottom'], linewidth=1, color='gray', alpha=0.5, linestyle='-', zorder=2) - gl.top_labels = False - gl.right_labels = False min_width = np.amin(cell_width) max_width = np.amax(cell_width) plt.title( f'Grid cell size, km, min: {min_width:.1f} max: {max_width:.1f}') plt.colorbar(im, shrink=.60) - fig.canvas.draw() - plt.tight_layout() + # no tight_layout() here: bbox_inches='tight' already crops the figure plt.savefig(image_filename, bbox_inches='tight') plt.close() diff --git a/compass/ocean/tests/global_ocean/mesh/arrm10to60/__init__.py b/compass/ocean/tests/global_ocean/mesh/arrm10to60/__init__.py index 5858892fa0..7422a119a9 100644 --- a/compass/ocean/tests/global_ocean/mesh/arrm10to60/__init__.py +++ b/compass/ocean/tests/global_ocean/mesh/arrm10to60/__init__.py @@ -1,13 +1,14 @@ -import numpy as np -import matplotlib.pyplot as plt import cartopy.crs as ccrs import cartopy.feature as cfeature - +import matplotlib.pyplot as plt import mpas_tools.mesh.creation.mesh_definition_tools as mdt -from mpas_tools.mesh.creation.signed_distance import \ - signed_distance_from_geojson, mask_from_geojson +import numpy as np from geometric_features import read_feature_collection from mpas_tools.cime.constants import constants +from mpas_tools.mesh.creation.signed_distance import ( + mask_from_geojson, + signed_distance_from_geojson, +) from mpas_tools.viz.colormaps import register_sci_viz_colormaps from compass.mesh import QuasiUniformSphericalMeshStep @@ -49,8 +50,8 @@ def build_cell_width_lat_lon(self): dlon = 0.1 dlat = dlon earth_radius = constants['SHR_CONST_REARTH'] - print('\nCreating cellWidth on a lat-lon grid of: {0:.2f} x {0:.2f} ' - 'degrees'.format(dlon, dlat)) + print(f'\nCreating cellWidth on a lat-lon grid of: {dlon:.2f} x ' + f'{dlat:.2f} degrees') print('This can be set higher for faster test generation\n') nlon = int(360. / dlon) + 1 nlat = int(180. / dlat) + 1 @@ -141,17 +142,16 @@ def _plot_cartopy(plot_number, var_name, var, map_name): extent=[-180, 180, -90, 90], cmap=map_name, zorder=0) ax.add_feature(cfeature.LAND, edgecolor='black', zorder=1) - gl = ax.gridlines( + # all labels were hidden below, so don't create them in the first place: + # hidden labels confuse cartopy's title placement, leading to a non-finite + # axes bounding box + ax.gridlines( crs=ccrs.PlateCarree(), - draw_labels=True, + draw_labels=False, linewidth=1, color='gray', alpha=0.5, linestyle='-', zorder=2) ax.coastlines() - gl.top_labels = False - gl.bottom_labels = False - gl.right_labels = False - gl.left_labels = False plt.colorbar(im, shrink=.9) plt.title(var_name) diff --git a/compass/ocean/tests/global_ocean/mesh/wc14/__init__.py b/compass/ocean/tests/global_ocean/mesh/wc14/__init__.py index 7e2ff57bf4..db456f7413 100644 --- a/compass/ocean/tests/global_ocean/mesh/wc14/__init__.py +++ b/compass/ocean/tests/global_ocean/mesh/wc14/__init__.py @@ -214,17 +214,16 @@ def _plot_cartopy(nPlot, varName, var, map_name): extent=[-180, 180, -90, 90], cmap=map_name, zorder=0) ax.add_feature(cfeature.LAND, edgecolor='black', zorder=1) - gl = ax.gridlines( + # all labels were hidden below, so don't create them in the first place: + # hidden labels confuse cartopy's title placement, leading to a non-finite + # axes bounding box + ax.gridlines( crs=ccrs.PlateCarree(), - draw_labels=True, + draw_labels=False, linewidth=1, color='gray', alpha=0.5, linestyle='-', zorder=2) ax.coastlines() - gl.top_labels = False - gl.bottom_labels = False - gl.right_labels = False - gl.left_labels = False plt.colorbar(im, shrink=.9) plt.title(varName) diff --git a/deploy/pixi.toml.j2 b/deploy/pixi.toml.j2 index 5573454792..b6d58ab460 100644 --- a/deploy/pixi.toml.j2 +++ b/deploy/pixi.toml.j2 @@ -21,7 +21,7 @@ mache = "{{ mache }}.*" jigsawpy = "*" {%- endif %} -cartopy = "*" +cartopy = ">=0.22.0" cartopy_offlinedata = "*" cmocean = "*" esmf = {version = "{{ esmf }}.*", build = "{{ mpi_prefix }}_*"} diff --git a/pyproject.toml b/pyproject.toml index 37a7b3030c..a1d690588d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ classifiers = [ "Topic :: Scientific/Engineering", ] dependencies = [ - "cartopy", + "cartopy>=0.22.0", "cmocean", "gsw", "h5py", From 820c73322eee06331d1297709ef9f89c1055f132 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Sat, 25 Jul 2026 14:31:16 -0500 Subject: [PATCH 2/2] Get mesh metadata package versions without conda The switch from conda to pixi means the `conda` command is no longer available, so getting package versions for MPAS mesh metadata with `conda list` fails with a `FileNotFoundError`. Instead, look up conda package records in the environment's `conda-meta` directory, falling back on python package metadata for packages installed with pip (namely compass itself). Also, JIGSAW is now built as part of the `jigsawpy` package, so the two share a version. Co-Authored-By: Claude Opus 5 --- compass/ocean/tests/global_ocean/metadata.py | 49 ++++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/compass/ocean/tests/global_ocean/metadata.py b/compass/ocean/tests/global_ocean/metadata.py index aa161b0f10..2e879e509f 100644 --- a/compass/ocean/tests/global_ocean/metadata.py +++ b/compass/ocean/tests/global_ocean/metadata.py @@ -1,6 +1,10 @@ +import glob +import importlib.metadata +import json import os import shutil import subprocess +import sys from datetime import datetime import numpy @@ -188,7 +192,9 @@ def _get_metadata(dsInit, config): if 'bgc' in descriptions: metadata['MPAS_Mesh_Biogeochemistry'] = descriptions['bgc'] - packages = {'compass': 'compass', 'JIGSAW': 'jigsaw', + # JIGSAW is built as part of the jigsawpy package so the two share a + # version + packages = {'compass': 'compass', 'JIGSAW': 'jigsawpy', 'JIGSAW_Python': 'jigsawpy', 'MPAS_Tools': 'mpas_tools', 'NCO': 'nco', 'ESMF': 'esmf', 'geometric_features': 'geometric_features', @@ -197,17 +203,40 @@ def _get_metadata(dsInit, config): for name in packages: package = packages[name] metadata[f'MPAS_Mesh_{name}_Version'] = \ - _get_conda_package_version(package) + _get_package_version(package) return metadata +def _get_package_version(package): + """ + Get the version of a package in the pixi environment, either from the + conda package records or, for packages installed with pip (such as compass + itself), from the python package metadata + """ + version = _get_conda_package_version(package) + if version is None: + version = _get_python_package_version(package) + if version is None: + version = 'not found' + return version + + def _get_conda_package_version(package): - conda = subprocess.check_output(['conda', 'list', package]).decode("utf-8") - lines = conda.split('\n') - for line in lines: - parts = line.split() - if len(parts) > 0 and parts[0] == package: - return parts[1] - - return 'not found' + """ Get the version of a conda package installed in this environment """ + pattern = os.path.join(sys.prefix, 'conda-meta', f'{package}-*.json') + for filename in sorted(glob.glob(pattern)): + with open(filename) as data_file: + record = json.load(data_file) + if record.get('name') == package: + return record.get('version') + + return None + + +def _get_python_package_version(package): + """ Get the version of a python package installed in this environment """ + try: + return importlib.metadata.version(package) + except importlib.metadata.PackageNotFoundError: + return None