From e1a5de080361569f0f1b83c7a9f9efe1c70405d6 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 29 May 2026 17:44:44 +0800 Subject: [PATCH 1/3] Add the benchmark for cartopy global coastline --- benchmarks/bench_cartopy_coastlines.py | 149 +++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 benchmarks/bench_cartopy_coastlines.py diff --git a/benchmarks/bench_cartopy_coastlines.py b/benchmarks/bench_cartopy_coastlines.py new file mode 100644 index 0000000..510986e --- /dev/null +++ b/benchmarks/bench_cartopy_coastlines.py @@ -0,0 +1,149 @@ +""" +Benchmark PyGMT and cartopy when plotting simple global coastlines. +""" + +import statistics +import time +from pathlib import Path + +import cartopy.crs as ccrs +import cartopy.feature as cfeature +import pygmt + +import matplotlib.pyplot as plt # noqa: E402 + + +BACKENDS = ("cartopy", "pygmt") +OUTPUT_DIR = Path("plots/coastlines") +REPEATS = 10 +RESOLUTION = "low" +CARTOPY_RESOLUTIONS = { + "low": "110m", + "medium": "50m", + "high": "10m", +} +PYGMT_RESOLUTIONS = { + "low": "c", + "medium": "l", + "high": "i", +} +LAND_COLOR = "#cccccc" +WATER_COLOR = "#b9d9ea" + + +def plot_cartopy(resolution: str): + """Create a simple global coastline plot with cartopy.""" + fig = plt.figure(figsize=(6, 4), dpi=300) + ax = fig.add_subplot(1, 1, 1, projection=ccrs.Robinson()) + ax.set_global() + ax.add_feature(cfeature.LAND, facecolor=LAND_COLOR) + ax.add_feature(cfeature.OCEAN, facecolor=WATER_COLOR) + ax.coastlines( + resolution=CARTOPY_RESOLUTIONS[resolution], + linewidth=0.5, + ) + ax.gridlines(color="0.8", linewidth=0.4) + return fig + + +def save_cartopy(fig, output: Path) -> None: + """Save a cartopy/matplotlib figure and release it.""" + fig.savefig(output) + plt.close(fig) + + +def plot_pygmt(resolution: str) -> pygmt.Figure: + """Create a simple global coastline plot with PyGMT.""" + fig = pygmt.Figure() + fig.coast( + region="g", + projection="R15c", + resolution=PYGMT_RESOLUTIONS[resolution], + land=LAND_COLOR, + water=WATER_COLOR, + shorelines="1/0.5p,black", + frame="afg", + ) + return fig + + +def save_pygmt(fig: pygmt.Figure, output: Path) -> None: + """Save a PyGMT figure.""" + fig.savefig(output) + + +def benchmark( + name: str, + plot_func, + save_func, + output_dir: Path, + repeats: int, + resolution: str, +) -> tuple[list[float], list[float]]: + """Time repeated coastline plot creation and figure export runs.""" + output_dir.mkdir(parents=True, exist_ok=True) + + # Warm up each backend once before recording timings. + fig = plot_func(resolution) + save_func(fig, output_dir / f"{name}_warmup.png") + + plot_timings = [] + save_timings = [] + for run_id in range(repeats): + output = output_dir / f"{name}_{run_id + 1}.png" + + start = time.perf_counter() + fig = plot_func(resolution) + plot_timings.append(time.perf_counter() - start) + + save_func(fig, output) + save_timings.append(time.perf_counter() - start) + + return plot_timings, save_timings + + +def format_summary(name: str, timings: list[float]) -> str: + """Format benchmark timing statistics.""" + mean = statistics.fmean(timings) + median = statistics.median(timings) + minimum = min(timings) + maximum = max(timings) + return ( + f"{name:10s} " + f"mean={mean:.4f}s " + f"median={median:.4f}s " + f"min={minimum:.4f}s " + f"max={maximum:.4f}s" + ) + + +def main() -> None: + """Run the coastline plotting benchmark.""" + plotters = { + "cartopy": plot_cartopy, + "pygmt": plot_pygmt, + } + savers = { + "cartopy": save_cartopy, + "pygmt": save_pygmt, + } + + print(f"Running {REPEATS} timed run(s) per backend") + print(f"Using {RESOLUTION} coastline resolution") + print(f"Writing PNG files to {OUTPUT_DIR}") + for backend in BACKENDS: + print(f"Benchmarking {backend}...", flush=True) + plot_timings, save_timings = benchmark( + name=f"{backend}_{RESOLUTION}", + plot_func=plotters[backend], + save_func=savers[backend], + output_dir=OUTPUT_DIR, + repeats=REPEATS, + resolution=RESOLUTION, + ) + print(format_summary(f"{backend} plot", plot_timings)) + print(format_summary(f"{backend} savefig", save_timings)) + + +if __name__ == "__main__": + main() From c6a927b4860aac204f8a19fb9d6ec21c139af0d3 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 29 May 2026 18:11:08 +0800 Subject: [PATCH 2/3] update --- benchmarks/bench_cartopy_coastlines.py | 65 ++++++++++++-------------- 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/benchmarks/bench_cartopy_coastlines.py b/benchmarks/bench_cartopy_coastlines.py index 510986e..edd4975 100644 --- a/benchmarks/bench_cartopy_coastlines.py +++ b/benchmarks/bench_cartopy_coastlines.py @@ -13,20 +13,10 @@ import matplotlib.pyplot as plt # noqa: E402 -BACKENDS = ("cartopy", "pygmt") OUTPUT_DIR = Path("plots/coastlines") REPEATS = 10 -RESOLUTION = "low" -CARTOPY_RESOLUTIONS = { - "low": "110m", - "medium": "50m", - "high": "10m", -} -PYGMT_RESOLUTIONS = { - "low": "c", - "medium": "l", - "high": "i", -} +CARTOPY_RESOLUTIONS = ("110m", "50m", "10m") +PYGMT_RESOLUTIONS = ("crude", "low", "intermediate") LAND_COLOR = "#cccccc" WATER_COLOR = "#b9d9ea" @@ -38,10 +28,7 @@ def plot_cartopy(resolution: str): ax.set_global() ax.add_feature(cfeature.LAND, facecolor=LAND_COLOR) ax.add_feature(cfeature.OCEAN, facecolor=WATER_COLOR) - ax.coastlines( - resolution=CARTOPY_RESOLUTIONS[resolution], - linewidth=0.5, - ) + ax.coastlines(resolution=resolution, linewidth=0.5) ax.gridlines(color="0.8", linewidth=0.4) return fig @@ -57,8 +44,8 @@ def plot_pygmt(resolution: str) -> pygmt.Figure: fig = pygmt.Figure() fig.coast( region="g", - projection="R15c", - resolution=PYGMT_RESOLUTIONS[resolution], + projection="R6i", + resolution=resolution, land=LAND_COLOR, water=WATER_COLOR, shorelines="1/0.5p,black", @@ -119,30 +106,36 @@ def format_summary(name: str, timings: list[float]) -> str: def main() -> None: """Run the coastline plotting benchmark.""" - plotters = { - "cartopy": plot_cartopy, - "pygmt": plot_pygmt, - } - savers = { - "cartopy": save_cartopy, - "pygmt": save_pygmt, - } - print(f"Running {REPEATS} timed run(s) per backend") - print(f"Using {RESOLUTION} coastline resolution") print(f"Writing PNG files to {OUTPUT_DIR}") - for backend in BACKENDS: - print(f"Benchmarking {backend}...", flush=True) + + print("Benchmarking cartopy...", flush=True) + for resolution in CARTOPY_RESOLUTIONS: + print(f"Resolution: {resolution}") + plot_timings, save_timings = benchmark( + name=f"cartopy_{resolution}", + plot_func=plot_cartopy, + save_func=save_cartopy, + output_dir=OUTPUT_DIR, + repeats=REPEATS, + resolution=resolution, + ) + print(format_summary("cartopy plot", plot_timings)) + print(format_summary("cartopy savefig", save_timings)) + + print("Benchmarking pygmt...", flush=True) + for resolution in PYGMT_RESOLUTIONS: + print(f"Resolution: {resolution}") plot_timings, save_timings = benchmark( - name=f"{backend}_{RESOLUTION}", - plot_func=plotters[backend], - save_func=savers[backend], + name=f"pygmt_{resolution}", + plot_func=plot_pygmt, + save_func=save_pygmt, output_dir=OUTPUT_DIR, repeats=REPEATS, - resolution=RESOLUTION, + resolution=resolution, ) - print(format_summary(f"{backend} plot", plot_timings)) - print(format_summary(f"{backend} savefig", save_timings)) + print(format_summary("pygmt plot", plot_timings)) + print(format_summary("pygmt savefig", save_timings)) if __name__ == "__main__": From 1b65324f72c849dc95112bbc1a4b29936ff0dbf4 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 29 May 2026 19:23:03 +0800 Subject: [PATCH 3/3] Update --- benchmarks/bench_cartopy_coastlines.py | 78 ++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 11 deletions(-) diff --git a/benchmarks/bench_cartopy_coastlines.py b/benchmarks/bench_cartopy_coastlines.py index edd4975..00639e0 100644 --- a/benchmarks/bench_cartopy_coastlines.py +++ b/benchmarks/bench_cartopy_coastlines.py @@ -16,12 +16,12 @@ OUTPUT_DIR = Path("plots/coastlines") REPEATS = 10 CARTOPY_RESOLUTIONS = ("110m", "50m", "10m") -PYGMT_RESOLUTIONS = ("crude", "low", "intermediate") +PYGMT_RESOLUTIONS = ("crude", "low", "intermediate", "high", "full") LAND_COLOR = "#cccccc" WATER_COLOR = "#b9d9ea" -def plot_cartopy(resolution: str): +def plot_cartopy_global(resolution: str): """Create a simple global coastline plot with cartopy.""" fig = plt.figure(figsize=(6, 4), dpi=300) ax = fig.add_subplot(1, 1, 1, projection=ccrs.Robinson()) @@ -33,17 +33,29 @@ def plot_cartopy(resolution: str): return fig +def plot_cartopy_regional(resolution: str): + """Create a regional coastline plot with cartopy.""" + fig = plt.figure(figsize=(6, 4), dpi=300) + ax = fig.add_subplot(1, 1, 1, projection=ccrs.Mercator()) + ax.set_extent([110, 160, -45, -10], crs=ccrs.PlateCarree()) + ax.add_feature(cfeature.LAND, facecolor=LAND_COLOR) + ax.add_feature(cfeature.OCEAN, facecolor=WATER_COLOR) + ax.coastlines(resolution=resolution, linewidth=0.5) + ax.gridlines(color="0.8", linewidth=0.4) + return fig + + def save_cartopy(fig, output: Path) -> None: """Save a cartopy/matplotlib figure and release it.""" fig.savefig(output) plt.close(fig) -def plot_pygmt(resolution: str) -> pygmt.Figure: +def plot_pygmt_global(resolution: str) -> pygmt.Figure: """Create a simple global coastline plot with PyGMT.""" fig = pygmt.Figure() fig.coast( - region="g", + region="d", projection="R6i", resolution=resolution, land=LAND_COLOR, @@ -53,6 +65,20 @@ def plot_pygmt(resolution: str) -> pygmt.Figure: ) return fig +def plot_pygmt_regional(resolution: str) -> pygmt.Figure: + """Create a regional coastline plot with PyGMT.""" + fig = pygmt.Figure() + fig.coast( + region=[110, 160, -45, -10], + projection="M6i", + resolution=resolution, + land=LAND_COLOR, + water=WATER_COLOR, + shorelines="1/0.5p,black", + frame="afg", + ) + return fig + def save_pygmt(fig: pygmt.Figure, output: Path) -> None: """Save a PyGMT figure.""" @@ -109,12 +135,26 @@ def main() -> None: print(f"Running {REPEATS} timed run(s) per backend") print(f"Writing PNG files to {OUTPUT_DIR}") - print("Benchmarking cartopy...", flush=True) + print("Benchmarking cartopy global...", flush=True) for resolution in CARTOPY_RESOLUTIONS: print(f"Resolution: {resolution}") plot_timings, save_timings = benchmark( - name=f"cartopy_{resolution}", - plot_func=plot_cartopy, + name=f"cartopy_global_{resolution}", + plot_func=plot_cartopy_global, + save_func=save_cartopy, + output_dir=OUTPUT_DIR, + repeats=REPEATS, + resolution=resolution, + ) + print(format_summary("cartopy plot", plot_timings)) + print(format_summary("cartopy savefig", save_timings)) + + print("Benchmarking cartopy regional...", flush=True) + for resolution in CARTOPY_RESOLUTIONS[2:]: + print(f"Resolution: {resolution}") + plot_timings, save_timings = benchmark( + name=f"cartopy_regional_{resolution}", + plot_func=plot_cartopy_regional, save_func=save_cartopy, output_dir=OUTPUT_DIR, repeats=REPEATS, @@ -123,12 +163,27 @@ def main() -> None: print(format_summary("cartopy plot", plot_timings)) print(format_summary("cartopy savefig", save_timings)) - print("Benchmarking pygmt...", flush=True) - for resolution in PYGMT_RESOLUTIONS: + + print("Benchmarking pygmt global...", flush=True) + for resolution in PYGMT_RESOLUTIONS[0:3]: + print(f"Resolution: {resolution}") + plot_timings, save_timings = benchmark( + name=f"pygmt_global_{resolution}", + plot_func=plot_pygmt_global, + save_func=save_pygmt, + output_dir=OUTPUT_DIR, + repeats=REPEATS, + resolution=resolution, + ) + print(format_summary("pygmt plot", plot_timings)) + print(format_summary("pygmt savefig", save_timings)) + + print("Benchmarking pygmt regional...", flush=True) + for resolution in PYGMT_RESOLUTIONS[2:]: print(f"Resolution: {resolution}") plot_timings, save_timings = benchmark( - name=f"pygmt_{resolution}", - plot_func=plot_pygmt, + name=f"pygmt_regional_{resolution}", + plot_func=plot_pygmt_regional, save_func=save_pygmt, output_dir=OUTPUT_DIR, repeats=REPEATS, @@ -138,5 +193,6 @@ def main() -> None: print(format_summary("pygmt savefig", save_timings)) + if __name__ == "__main__": main()