YAC 3.21.0
Yet Another Coupler
Loading...
Searching...
No Matches
yac_plot_weights.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2#
3# Copyright (c) 2026 The YAC Authors
4#
5# SPDX-License-Identifier: BSD-3-Clause
6#
7# /// script
8# requires-python = ">=3.9"
9# dependencies = ["numpy", "matplotlib", "netCDF4", "cartopy"]
10# ///
11
12"""Plot interpolation weights from YAC weight files on a map."""
13
14import logging
15import argparse
16import pathlib
17
18import numpy as np
19import matplotlib.pyplot as plt
20import netCDF4
21
22from _utils.plotting import (
23 setup_figure,
24 plot_grid_edges_in_ax,
25 plot_weights,
26 plot_fixed_values,
27 add_label_in_extent,
28 load_grid,
29 add_keybinding_legend,
30)
31
32
33def load_weight_file(weightfile):
34 """Load and validate YAC weight file.
35
36 Args:
37 weightfile: Path to weight file
38
39 Returns:
40 Tuple of (src_grid_name, tgt_grid_name, src_loc, tgt_loc,
41 src_address, tgt_address, weights, fixed_values)
42 where fixed_values is a list of (value, dst_address_array) tuples,
43 one entry per distinct fixed value (empty list when none are present).
44 """
45 logging.info(f"Loading weight file: {weightfile}")
46 with netCDF4.Dataset(weightfile, mode="r") as wf:
47 assert(wf.version == "yac weight file 1.0")
48 num_weights = wf.dimensions["num_wgts"].size
49 logging.debug(f"Weight file has {num_weights} weight(s)")
50 if num_weights != 1:
51 raise ValueError(
52 f"Only files with num_weights==1 are supported, got {num_weights}"
53 )
54
55 src_grid_name = wf.src_grid_name
56 tgt_grid_name = wf.dst_grid_name
57 logging.info(f"Source grid: {src_grid_name}")
58 logging.info(f"Target grid: {tgt_grid_name}")
59
60 src_loc = netCDF4.chartostring(wf["src_locations"][0, :])[()].strip()
61 logging.info(f"Source location: {src_loc}")
62
63 tgt_loc = netCDF4.chartostring(wf["dst_location"][:])[()].strip()
64 logging.info(f"Target location: {tgt_loc}")
65
66 src_address = np.asarray(wf["src_address"])
67 tgt_address = np.asarray(wf["dst_address"])
68
69 weights = np.asarray(wf["remap_matrix"])
70
71 logging.info(f"Total weight links: {len(src_address)}")
72 logging.debug(f"Weight range: [{weights.min():.6f}, {weights.max():.6f}]")
73
74 fixed_values = []
75 if "fixed_values" in wf.variables:
76 fv = np.asarray(wf["fixed_values"])
77 n_per_fv = np.asarray(wf["num_dst_per_fixed_value"])
78 dst_fixed = np.asarray(wf["dst_address_fixed"])
79 offset = 0
80 for value, count in zip(fv, n_per_fv):
81 fixed_values.append((float(value), dst_fixed[offset : offset + count]))
82 offset += count
83 total = sum(c for _, c in [(v, len(a)) for v, a in fixed_values])
84 logging.info(
85 f"Fixed values: {len(fixed_values)} distinct, "
86 f"{total} target points total"
87 )
88
89 return (
90 src_grid_name,
91 tgt_grid_name,
92 src_loc,
93 tgt_loc,
94 src_address,
95 tgt_address,
96 weights,
97 fixed_values,
98 )
99
100
102 src_idx,
103 tgt_idx,
104 src_address,
105 tgt_address,
106 weights,
107 src_ids,
108 tgt_ids,
109 src_points,
110 tgt_points,
111):
112 """Filter interpolation weights to focus on a specific source or target index.
113
114 Args:
115 src_idx: Global ID of the source cell/vertex to filter by (or None). This is
116 the value shown as a label when pressing 's', i.e. the .gid/.vid value
117 from the grid file.
118 tgt_idx: Global ID of the target cell/vertex to filter by (or None). See src_idx.
119 src_address: Array of source addresses from the weight file. Each value is
120 global_id + 1 (1-indexed global IDs, not array positions).
121 tgt_address: Array of target addresses from the weight file. Each value is
122 global_id + 1 (1-indexed global IDs, not array positions).
123 weights: Weight values
124 src_ids: Global IDs of source grid points (.gid or .vid from grid file)
125 tgt_ids: Global IDs of target grid points (.gid or .vid from grid file)
126 src_points: Source coordinates, shape (2, n)
127 tgt_points: Target coordinates, shape (2, n)
128
129 Returns:
130 Tuple of (filtered_src_address, filtered_tgt_address, filtered_weights, focus_points)
131 """
132 if src_idx is not None:
133 logging.info(f"Filtering weights for source global ID {src_idx}")
134 # Filter weight links where the source global ID matches src_idx.
135 # Addresses are global_id + 1.
136 weight_mask = src_address == src_idx + 1
137 fixed_ids = src_ids
138 fixed_global_id = src_idx
139 linked_address = tgt_address[weight_mask]
140 linked_ids = tgt_ids
141 fixed_points = src_points
142 linked_points = tgt_points
143 filter_type = "source"
144 else:
145 logging.info(f"Filtering weights for target global ID {tgt_idx}")
146 weight_mask = tgt_address == tgt_idx + 1
147 fixed_ids = tgt_ids
148 fixed_global_id = tgt_idx
149 linked_address = src_address[weight_mask]
150 linked_ids = src_ids
151 fixed_points = tgt_points
152 linked_points = src_points
153 filter_type = "target"
154
155 # linked_address values are global_id + 1; search for global_id in linked_ids
156 # to find the array position of each linked point.
157 sorter = np.argsort(linked_ids)
158 linked_idx = sorter[np.searchsorted(linked_ids, linked_address - 1, sorter=sorter)]
159
160 # Find array position of the fixed point by its global ID.
161 fixed_point_idx = np.where(fixed_ids == fixed_global_id)[0]
162
163 if len(fixed_point_idx) == 0:
164 raise ValueError(f"Global ID {fixed_global_id} not found in {filter_type} grid")
165
166 # Combine for focus points
167 focus_points = np.hstack(
168 [fixed_points[..., fixed_point_idx], linked_points[..., linked_idx]]
169 )
170
171 # Apply mask to weight data
172 filtered_src_address = src_address[weight_mask]
173 filtered_tgt_address = tgt_address[weight_mask]
174 filtered_weights = weights[weight_mask]
175
176 num_filtered = len(filtered_weights)
177 logging.info(
178 f"Found {num_filtered} weight links for {filter_type} global ID {fixed_global_id}"
179 )
180 if num_filtered > 0:
181 logging.debug(
182 f"Filtered weight range: [{filtered_weights.min():.6f}, {filtered_weights.max():.6f}]"
183 )
184 logging.debug(f"Weight sum: {filtered_weights.sum():.6f}")
185
186 return filtered_src_address, filtered_tgt_address, filtered_weights, focus_points
187
188
189def plot_grid(ax, corner_coords, coords, ids, core_mask, color, label_key):
190 """Plot a grid on the given axis with specified coordinates, IDs, and styling.
191
192 Core cells are plotted with full opacity; halo cells (core_mask==False)
193 are plotted with a dimmed, dashed style to distinguish them visually.
194
195 Args:
196 ax: Matplotlib axis to plot on
197 corner_coords: Corner coordinates of the grid
198 coords: Coordinates of grid points
199 ids: IDs of grid points
200 core_mask: Boolean array, True for core (owned) cells, False for halo
201 color: Color for core cells
202 label_key: Key for labeling
203 """
204 logging.debug(
205 f"Plotting grid with {np.sum(core_mask)} core + "
206 f"{np.sum(~core_mask)} halo cells in {color}"
207 )
208 plot_grid_edges_in_ax(
209 ax,
210 corner_coords[0, core_mask],
211 corner_coords[1, core_mask],
212 {"zorder": 1, "color": color},
213 )
214 if np.any(~core_mask):
215 plot_grid_edges_in_ax(
216 ax,
217 corner_coords[0, ~core_mask],
218 corner_coords[1, ~core_mask],
219 {"zorder": 1, "color": color, "alpha": 0.3, "linestyle": "dashed"},
220 )
221
222 add_label_in_extent(
223 ax,
224 coords[0, ...],
225 coords[1, ...],
226 ids,
227 {"color": color, "visible": False},
228 label_key,
229 )
230
231
232if __name__ == "__main__":
233 parser = argparse.ArgumentParser(
234 description="Plot YAC interpolation weights on a map"
235 )
236 parser.add_argument(
237 "-v",
238 "--verbose",
239 action="count",
240 default=0,
241 help="Increase verbosity (-v for INFO, -vv for DEBUG)",
242 )
243 parser.add_argument(
244 "--center",
245 "-c",
246 type=float,
247 nargs=2,
248 help="Center of the map projection (longitude, latitude in degrees)",
249 default=None,
250 metavar=("LON", "LAT"),
251 )
252 parser.add_argument(
253 "--radius",
254 "-r",
255 type=float,
256 help="Radius in km of the region around the center to display (default: 2000)",
257 default=2000,
258 )
259 parser.add_argument(
260 "--coast-res",
261 type=str,
262 default="50m",
263 nargs="?",
264 choices=("10m", "50m", "110m"),
265 help="Resolution of coastlines (default: 50m). Use --coast-res without value to disable.",
266 )
267 parser.add_argument(
268 "--projection",
269 type=str,
270 default="orthographic",
271 choices=("orthographic", "stereographic", "platecarree"),
272 help="Map projection type (default: orthographic)",
273 )
274
275 idx_group = parser.add_mutually_exclusive_group()
276 idx_group.add_argument(
277 "--source-index",
278 type=int,
279 default=None,
280 help="Focus on a source cell/vertex. (global index).",
281 )
282 idx_group.add_argument(
283 "--target-index",
284 type=int,
285 default=None,
286 help="Focus on a target cell/vertex. (global index)",
287 )
288
289 parser.add_argument(
290 "weightfile",
291 type=pathlib.Path,
292 help="Path to YAC weight file (NetCDF format)",
293 )
294 parser.add_argument(
295 "gridfile",
296 type=pathlib.Path,
297 help="Path to a grid file (NetCDF format). The file is searched for the grid name specified in the weight file.",
298 )
299 parser.add_argument(
300 "gridfile2",
301 type=pathlib.Path,
302 nargs="?",
303 help="Path to a second grid file to search for grid names. Use this when the source and target grids are stored in separate files.",
304 )
305
306 args = parser.parse_args()
307
308 log_level = [logging.WARNING, logging.INFO, logging.DEBUG][min(args.verbose, 2)]
309 logging.basicConfig(level=log_level, format="%(levelname)s: %(message)s")
310
311 logging.info("Starting YAC weight visualization")
312 logging.debug(f"Weight file: {args.weightfile}")
313 logging.debug(f"Grid files: {args.gridfile}, {args.gridfile2}")
314 logging.debug(f"Projection: {args.projection}")
315 if args.center:
316 logging.debug(f"Center: {args.center[0]:.2f}°E, {args.center[1]:.2f}°N")
317 logging.debug(f"Radius: {args.radius} km")
318
319 (
320 src_grid_name,
321 tgt_grid_name,
322 src_loc,
323 tgt_loc,
324 src_address,
325 tgt_address,
326 weights,
327 fixed_values,
328 ) = load_weight_file(args.weightfile)
329
330 logging.info("Loading source grid")
331 src_corners, src_points, src_ids, src_core_mask = load_grid(
332 [args.gridfile, args.gridfile2],
333 src_grid_name,
334 src_loc,
335 )
336
337 logging.info("Loading target grid")
338 tgt_corners, tgt_points, tgt_ids, tgt_core_mask = load_grid(
339 [args.gridfile, args.gridfile2],
340 tgt_grid_name,
341 tgt_loc,
342 )
343
344 focus_points = None
345
346 if args.source_index is not None or args.target_index is not None:
347 src_address, tgt_address, weights, focus_points = filter_weights_by_index(
348 args.source_index,
349 args.target_index,
350 src_address,
351 tgt_address,
352 weights,
353 src_ids,
354 tgt_ids,
355 src_points,
356 tgt_points,
357 )
358
359 logging.info("Setting up figure and map projection")
360 fig, ax = setup_figure(
361 projection=args.projection,
362 center=args.center,
363 focus_points=focus_points,
364 radius=args.radius,
365 coast_res=args.coast_res,
366 )
367
368 logging.info("Plotting source grid (green)")
369 plot_grid(ax, src_corners, src_points, src_ids, src_core_mask, "green", "S")
370
371 logging.info("Plotting target grid (blue)")
372 plot_grid(ax, tgt_corners, tgt_points, tgt_ids, tgt_core_mask, "blue", "T")
373
374 logging.info(f"Plotting {len(weights)} weight connections")
375 plot_weights(
376 ax,
377 src_points,
378 src_ids,
379 tgt_points,
380 tgt_ids,
381 src_address,
382 tgt_address,
383 weights,
384 )
385
386 if fixed_values:
387 logging.info(f"Plotting {len(fixed_values)} fixed-value group(s)")
388 plot_fixed_values(ax, tgt_points, tgt_ids, fixed_values)
389
390 add_keybinding_legend(
391 ax,
392 [
393 ("S", "toggle source labels", "green"),
394 ("T", "toggle target labels", "blue"),
395 ("W", "toggle weight labels"),
396 ],
397 )
398
399 logging.info("Opening interactive plot window")
400 logging.info("Press 's' to toggle source labels, 't' to toggle target labels")
401 plt.show()
filter_weights_by_index(src_idx, tgt_idx, src_address, tgt_address, weights, src_ids, tgt_ids, src_points, tgt_points)
Filter interpolation weights to focus on a specific source or target index.
plot_grid(ax, corner_coords, coords, ids, core_mask, color, label_key)
Plot a grid on the given axis with specified coordinates, IDs, and styling.
load_weight_file(weightfile)
Load and validate YAC weight file.