[3783] | 1 | #!/usr/bin/env python3 |
---|
[3459] | 2 | ############################################################## |
---|
| 3 | ### Python script to visualize a variable in a NetCDF file ### |
---|
| 4 | ############################################################## |
---|
| 5 | |
---|
[3783] | 6 | """ |
---|
[3798] | 7 | This script can display any numeric variable from a NetCDF file. |
---|
| 8 | It supports the following cases: |
---|
| 9 | - 1D time series (Time) |
---|
| 10 | - 1D vertical profiles (e.g., subsurface_layers) |
---|
| 11 | - 2D latitude/longitude map |
---|
| 12 | - 2D (Time × another dimension) |
---|
| 13 | - Variables with dimension “physical_points” as 2D map if lat/lon present, |
---|
| 14 | or generic 2D plot if the remaining axes are spatial |
---|
| 15 | - Scalar output (ndim == 0 after slicing) |
---|
[3459] | 16 | |
---|
[3783] | 17 | Usage: |
---|
| 18 | 1) Command-line mode: |
---|
[3798] | 19 | python display_netcdf.py /path/to/your_file.nc --variable VAR_NAME \ |
---|
| 20 | [--time-index 0] [--cmap viridis] [--output out.png] \ |
---|
| 21 | [--extra-indices '{"dim1": idx1, "dim2": idx2}'] |
---|
[3783] | 22 | |
---|
[3798] | 23 | --variable : Name of the variable to visualize. |
---|
| 24 | --time-index : Index along the Time dimension (ignored for purely 1D time series). |
---|
| 25 | --alt-index : Index along the altitude dimension, if present. |
---|
| 26 | --cmap : Matplotlib colormap for contourf (default: "jet"). |
---|
| 27 | --output : If provided, save the figure to this filename instead of displaying. |
---|
| 28 | --extra-indices: JSON string to fix indices for any dimensions other than Time, lat, lon, or altitude. |
---|
| 29 | Example: '{"nslope": 0, "physical_points": 2}' |
---|
| 30 | Omitting a dimension means it remains unfixed (useful to plot a 1D profile). |
---|
| 31 | |
---|
| 32 | 2) Interactive mode: |
---|
[3783] | 33 | python display_netcdf.py |
---|
[3798] | 34 | (The script will prompt for the NetCDF file, the variable, etc.) |
---|
[3783] | 35 | """ |
---|
| 36 | |
---|
[3459] | 37 | import os |
---|
[3783] | 38 | import sys |
---|
| 39 | import glob |
---|
[3459] | 40 | import readline |
---|
[3783] | 41 | import argparse |
---|
[3798] | 42 | import json |
---|
[3783] | 43 | import numpy as np |
---|
| 44 | import matplotlib.pyplot as plt |
---|
[3459] | 45 | from netCDF4 import Dataset |
---|
| 46 | |
---|
[3798] | 47 | # Constants to recognize dimension names |
---|
| 48 | TIME_DIMS = ("Time", "time", "time_counter") |
---|
| 49 | ALT_DIMS = ("altitude",) |
---|
| 50 | LAT_DIMS = ("latitude", "lat") |
---|
| 51 | LON_DIMS = ("longitude", "lon") |
---|
[3783] | 52 | |
---|
[3798] | 53 | |
---|
[3783] | 54 | def complete_filename(text, state): |
---|
| 55 | """ |
---|
| 56 | Readline tab-completion function for filesystem paths. |
---|
| 57 | """ |
---|
| 58 | if "*" not in text: |
---|
| 59 | pattern = text + "*" |
---|
| 60 | else: |
---|
| 61 | pattern = text |
---|
| 62 | matches = glob.glob(os.path.expanduser(pattern)) |
---|
| 63 | matches = [m + "/" if os.path.isdir(m) else m for m in matches] |
---|
[3459] | 64 | try: |
---|
| 65 | return matches[state] |
---|
| 66 | except IndexError: |
---|
| 67 | return None |
---|
| 68 | |
---|
[3783] | 69 | |
---|
| 70 | def make_varname_completer(varnames): |
---|
| 71 | """ |
---|
| 72 | Returns a readline completer function for the given list of variable names. |
---|
| 73 | """ |
---|
[3459] | 74 | def completer(text, state): |
---|
[3783] | 75 | options = [name for name in varnames if name.startswith(text)] |
---|
| 76 | try: |
---|
[3459] | 77 | return options[state] |
---|
[3783] | 78 | except IndexError: |
---|
[3459] | 79 | return None |
---|
| 80 | return completer |
---|
| 81 | |
---|
[3783] | 82 | |
---|
| 83 | def find_dim_index(dims, candidates): |
---|
| 84 | """ |
---|
| 85 | Search through dims tuple for any name in candidates. |
---|
| 86 | Returns the index if found, else returns None. |
---|
| 87 | """ |
---|
| 88 | for idx, dim in enumerate(dims): |
---|
| 89 | for cand in candidates: |
---|
| 90 | if cand.lower() == dim.lower(): |
---|
| 91 | return idx |
---|
| 92 | return None |
---|
| 93 | |
---|
| 94 | |
---|
| 95 | def find_coord_var(dataset, candidates): |
---|
| 96 | """ |
---|
| 97 | Among dataset variables, return the first variable whose name matches any candidate. |
---|
| 98 | Returns None if none found. |
---|
| 99 | """ |
---|
| 100 | for name in dataset.variables: |
---|
| 101 | for cand in candidates: |
---|
| 102 | if cand.lower() == name.lower(): |
---|
| 103 | return name |
---|
| 104 | return None |
---|
| 105 | |
---|
| 106 | |
---|
[3798] | 107 | def plot_variable(dataset, varname, time_index=None, alt_index=None, colormap="jet", |
---|
| 108 | output_path=None, extra_indices=None): |
---|
[3783] | 109 | """ |
---|
[3798] | 110 | Extracts the requested slice from the variable and plots it according to the data shape: |
---|
[3783] | 111 | |
---|
[3798] | 112 | - Pure 1D time series → time-series plot |
---|
| 113 | - After slicing: |
---|
| 114 | • If data_slice.ndim == 0 → print the scalar value |
---|
| 115 | • If data_slice.ndim == 1: |
---|
| 116 | • If the remaining dimension is “subsurface_layers” (or another known coordinate) → vertical profile |
---|
| 117 | • Else → simple plot vs. index |
---|
| 118 | • If data_slice.ndim == 2: |
---|
| 119 | • If lat/lon exist → contourf map |
---|
| 120 | • Else → imshow generic 2D plot |
---|
| 121 | - If data_slice.ndim is neither 0, 1, nor 2 → error message |
---|
| 122 | |
---|
[3783] | 123 | Parameters |
---|
| 124 | ---------- |
---|
[3798] | 125 | dataset : netCDF4.Dataset object (already open) |
---|
| 126 | varname : name of the variable to plot |
---|
| 127 | time_index : int or None (if variable has a time dimension, ignored for pure time series) |
---|
| 128 | alt_index : int or None (if variable has an altitude dimension) |
---|
| 129 | colormap : string colormap name (passed to plt.contourf) |
---|
| 130 | output_path : string filepath to save figure, or None to display interactively |
---|
| 131 | extra_indices : dict { dimension_name (str) : index (int) } for slicing all |
---|
| 132 | dimensions except Time/lat/lon/alt. If a dimension is not |
---|
| 133 | included, it remains “slice(None)” (useful for 1D plots). |
---|
[3783] | 134 | """ |
---|
| 135 | var = dataset.variables[varname] |
---|
[3798] | 136 | dims = var.dimensions # tuple of dimension names |
---|
[3783] | 137 | |
---|
[3798] | 138 | # Read the full data (could be a masked array) |
---|
[3459] | 139 | try: |
---|
[3783] | 140 | data_full = var[:] |
---|
| 141 | except Exception as e: |
---|
| 142 | print(f"Error: Cannot read data for variable '{varname}': {e}") |
---|
[3459] | 143 | return |
---|
| 144 | |
---|
[3783] | 145 | # Convert masked array to NaN |
---|
| 146 | if hasattr(data_full, "mask"): |
---|
| 147 | data_full = np.where(data_full.mask, np.nan, data_full.data) |
---|
| 148 | |
---|
[3798] | 149 | # ------------------------------------------------------------------------ |
---|
| 150 | # 1) Pure 1D time series (dims == ('Time',) or equivalent) |
---|
| 151 | if len(dims) == 1 and find_dim_index(dims, TIME_DIMS) is not None: |
---|
| 152 | # Plot the time series directly |
---|
| 153 | time_varname = find_coord_var(dataset, TIME_DIMS) |
---|
| 154 | if time_varname: |
---|
| 155 | time_vals = dataset.variables[time_varname][:] |
---|
| 156 | if hasattr(time_vals, "mask"): |
---|
| 157 | time_vals = np.where(time_vals.mask, np.nan, time_vals.data) |
---|
| 158 | else: |
---|
| 159 | time_vals = np.arange(data_full.shape[0]) |
---|
| 160 | |
---|
| 161 | plt.figure() |
---|
| 162 | plt.plot(time_vals, data_full, marker='o') |
---|
| 163 | xlabel = time_varname if time_varname else "Time Index" |
---|
| 164 | plt.xlabel(xlabel) |
---|
| 165 | ylabel = varname |
---|
| 166 | if hasattr(var, "units"): |
---|
| 167 | ylabel += f" ({var.units})" |
---|
| 168 | plt.ylabel(ylabel) |
---|
| 169 | plt.title(f"{varname} vs {xlabel}") |
---|
| 170 | |
---|
| 171 | if output_path: |
---|
| 172 | try: |
---|
| 173 | plt.savefig(output_path, bbox_inches="tight") |
---|
| 174 | print(f"Figure saved to '{output_path}'") |
---|
| 175 | except Exception as e: |
---|
| 176 | print(f"Error saving figure: {e}") |
---|
| 177 | else: |
---|
| 178 | plt.show() |
---|
| 179 | plt.close() |
---|
| 180 | return |
---|
| 181 | # ------------------------------------------------------------------------ |
---|
| 182 | |
---|
| 183 | # Identify special dimension indices |
---|
[3783] | 184 | t_idx = find_dim_index(dims, TIME_DIMS) |
---|
| 185 | a_idx = find_dim_index(dims, ALT_DIMS) |
---|
| 186 | lat_idx = find_dim_index(dims, LAT_DIMS) |
---|
| 187 | lon_idx = find_dim_index(dims, LON_DIMS) |
---|
| 188 | |
---|
[3798] | 189 | # Build the slicer list |
---|
| 190 | slicer = [slice(None)] * len(dims) |
---|
[3783] | 191 | |
---|
[3798] | 192 | # Apply slicing on Time and altitude if specified |
---|
[3783] | 193 | if t_idx is not None: |
---|
| 194 | if time_index is None: |
---|
| 195 | print("Error: Variable has a time dimension; please supply a time index.") |
---|
| 196 | return |
---|
| 197 | slicer[t_idx] = time_index |
---|
| 198 | if a_idx is not None: |
---|
| 199 | if alt_index is None: |
---|
| 200 | print("Error: Variable has an altitude dimension; please supply an altitude index.") |
---|
| 201 | return |
---|
| 202 | slicer[a_idx] = alt_index |
---|
| 203 | |
---|
[3798] | 204 | # Apply slicing on all “extra” dimensions (except Time/lat/lon/alt) |
---|
| 205 | if extra_indices is None: |
---|
| 206 | extra_indices = {} |
---|
| 207 | for dim_name, idx_val in extra_indices.items(): |
---|
| 208 | if dim_name in dims: |
---|
| 209 | dim_index = dims.index(dim_name) |
---|
| 210 | slicer[dim_index] = idx_val |
---|
| 211 | |
---|
| 212 | # Extract the sliced data |
---|
[3783] | 213 | try: |
---|
| 214 | data_slice = data_full[tuple(slicer)] |
---|
| 215 | except Exception as e: |
---|
| 216 | print(f"Error: Could not slice variable '{varname}': {e}") |
---|
| 217 | return |
---|
| 218 | |
---|
[3798] | 219 | # CASE: After slicing, if data_slice.ndim == 0 → scalar |
---|
| 220 | if np.ndim(data_slice) == 0: |
---|
| 221 | try: |
---|
| 222 | scalar_val = float(data_slice) |
---|
| 223 | except Exception: |
---|
| 224 | scalar_val = data_slice |
---|
| 225 | print(f"Scalar result for '{varname}': {scalar_val}") |
---|
[3783] | 226 | return |
---|
| 227 | |
---|
[3798] | 228 | # CASE: After slicing, if data_slice.ndim == 1 (vertical profile or simple vector) |
---|
| 229 | if data_slice.ndim == 1: |
---|
| 230 | # Identify the remaining dimension |
---|
| 231 | rem_dim = None |
---|
| 232 | for di, dname in enumerate(dims): |
---|
| 233 | if slicer[di] == slice(None): |
---|
| 234 | rem_dim = (di, dname) |
---|
| 235 | break |
---|
| 236 | |
---|
| 237 | if rem_dim is not None: |
---|
| 238 | di, dname = rem_dim |
---|
| 239 | coord_var = None |
---|
| 240 | |
---|
| 241 | # If it's "subsurface_layers", look for coordinate "soildepth" |
---|
| 242 | if dname.lower() == "subsurface_layers" and "soildepth" in dataset.variables: |
---|
| 243 | coord_var = "soildepth" |
---|
| 244 | # If there is a variable with the same name, use it |
---|
| 245 | elif dname in dataset.variables: |
---|
| 246 | coord_var = dname |
---|
| 247 | |
---|
| 248 | if coord_var: |
---|
| 249 | coord_vals = dataset.variables[coord_var][:] |
---|
| 250 | if hasattr(coord_vals, "mask"): |
---|
| 251 | coord_vals = np.where(coord_vals.mask, np.nan, coord_vals.data) |
---|
| 252 | x = data_slice |
---|
| 253 | y = coord_vals |
---|
| 254 | |
---|
[3783] | 255 | plt.figure() |
---|
| 256 | plt.plot(x, y, marker='o') |
---|
[3798] | 257 | # Invert Y-axis if it's a depth coordinate |
---|
| 258 | if dname.lower() == "subsurface_layers": |
---|
| 259 | plt.gca().invert_yaxis() |
---|
| 260 | |
---|
| 261 | xlabel = varname |
---|
| 262 | if hasattr(var, "units"): |
---|
| 263 | xlabel += f" ({var.units})" |
---|
| 264 | plt.xlabel(xlabel) |
---|
| 265 | |
---|
| 266 | ylabel = coord_var |
---|
| 267 | if hasattr(dataset.variables[coord_var], "units"): |
---|
| 268 | ylabel += f" ({dataset.variables[coord_var].units})" |
---|
| 269 | plt.ylabel(ylabel) |
---|
| 270 | |
---|
| 271 | plt.title(f"{varname} vs {coord_var}") |
---|
| 272 | |
---|
| 273 | if output_path: |
---|
| 274 | try: |
---|
| 275 | plt.savefig(output_path, bbox_inches="tight") |
---|
| 276 | print(f"Figure saved to '{output_path}'") |
---|
| 277 | except Exception as e: |
---|
| 278 | print(f"Error saving figure: {e}") |
---|
| 279 | else: |
---|
| 280 | plt.show() |
---|
| 281 | plt.close() |
---|
| 282 | return |
---|
| 283 | else: |
---|
| 284 | # No known coordinate found → simple plot vs index |
---|
[3783] | 285 | plt.figure() |
---|
[3798] | 286 | plt.plot(data_slice, marker='o') |
---|
| 287 | plt.xlabel("Index") |
---|
| 288 | ylabel = varname |
---|
| 289 | if hasattr(var, "units"): |
---|
| 290 | ylabel += f" ({var.units})" |
---|
| 291 | plt.ylabel(ylabel) |
---|
| 292 | plt.title(f"{varname} (1D)") |
---|
| 293 | |
---|
| 294 | if output_path: |
---|
| 295 | try: |
---|
| 296 | plt.savefig(output_path, bbox_inches="tight") |
---|
| 297 | print(f"Figure saved to '{output_path}'") |
---|
| 298 | except Exception as e: |
---|
| 299 | print(f"Error saving figure: {e}") |
---|
| 300 | else: |
---|
| 301 | plt.show() |
---|
| 302 | plt.close() |
---|
| 303 | return |
---|
| 304 | |
---|
| 305 | else: |
---|
| 306 | # Unable to identify the remaining dimension → error |
---|
| 307 | print(f"Error: After slicing, data for '{varname}' is 1D but remaining dimension is unknown.") |
---|
| 308 | return |
---|
| 309 | |
---|
| 310 | # CASE: After slicing, if data_slice.ndim == 2 |
---|
| 311 | if data_slice.ndim == 2: |
---|
| 312 | # If lat and lon exist in the original dims, re-find their indices |
---|
| 313 | lat_idx2 = find_dim_index(dims, LAT_DIMS) |
---|
| 314 | lon_idx2 = find_dim_index(dims, LON_DIMS) |
---|
| 315 | |
---|
| 316 | if lat_idx2 is not None and lon_idx2 is not None: |
---|
| 317 | # We have a 2D variable on a lat×lon grid |
---|
| 318 | lat_varname = find_coord_var(dataset, LAT_DIMS) |
---|
| 319 | lon_varname = find_coord_var(dataset, LON_DIMS) |
---|
| 320 | if lat_varname is None or lon_varname is None: |
---|
| 321 | print("Error: Could not locate latitude/longitude variables in the dataset.") |
---|
| 322 | return |
---|
| 323 | |
---|
| 324 | lat_var = dataset.variables[lat_varname][:] |
---|
| 325 | lon_var = dataset.variables[lon_varname][:] |
---|
| 326 | if hasattr(lat_var, "mask"): |
---|
| 327 | lat_var = np.where(lat_var.mask, np.nan, lat_var.data) |
---|
| 328 | if hasattr(lon_var, "mask"): |
---|
| 329 | lon_var = np.where(lon_var.mask, np.nan, lon_var.data) |
---|
| 330 | |
---|
| 331 | # Build 2D coordinate arrays |
---|
| 332 | if lat_var.ndim == 1 and lon_var.ndim == 1: |
---|
| 333 | lon2d, lat2d = np.meshgrid(lon_var, lat_var) |
---|
| 334 | elif lat_var.ndim == 2 and lon_var.ndim == 2: |
---|
| 335 | lat2d, lon2d = lat_var, lon_var |
---|
[3783] | 336 | else: |
---|
[3798] | 337 | print("Error: Latitude and longitude must both be either 1D or 2D.") |
---|
[3783] | 338 | return |
---|
[3798] | 339 | |
---|
| 340 | plt.figure(figsize=(10, 6)) |
---|
| 341 | cf = plt.contourf(lon2d, lat2d, data_slice, cmap=colormap) |
---|
| 342 | cbar = plt.colorbar(cf) |
---|
| 343 | if hasattr(var, "units"): |
---|
| 344 | cbar.set_label(f"{varname} ({var.units})") |
---|
| 345 | else: |
---|
| 346 | cbar.set_label(varname) |
---|
| 347 | |
---|
| 348 | lon_label = f"Longitude ({getattr(dataset.variables[lon_varname], 'units', 'degrees')})" |
---|
| 349 | lat_label = f"Latitude ({getattr(dataset.variables[lat_varname], 'units', 'degrees')})" |
---|
| 350 | plt.xlabel(lon_label) |
---|
| 351 | plt.ylabel(lat_label) |
---|
| 352 | plt.title(f"{varname} (lat × lon)") |
---|
| 353 | |
---|
[3783] | 354 | if output_path: |
---|
[3798] | 355 | try: |
---|
| 356 | plt.savefig(output_path, bbox_inches="tight") |
---|
| 357 | print(f"Figure saved to '{output_path}'") |
---|
| 358 | except Exception as e: |
---|
| 359 | print(f"Error saving figure: {e}") |
---|
[3783] | 360 | else: |
---|
| 361 | plt.show() |
---|
| 362 | plt.close() |
---|
| 363 | return |
---|
| 364 | |
---|
[3798] | 365 | else: |
---|
| 366 | # No lat/lon → two non-geographical dimensions; plot with imshow |
---|
| 367 | plt.figure(figsize=(8, 6)) |
---|
| 368 | plt.imshow(data_slice, aspect='auto') |
---|
| 369 | cb_label = varname |
---|
| 370 | if hasattr(var, "units"): |
---|
| 371 | cb_label += f" ({var.units})" |
---|
| 372 | plt.colorbar(label=cb_label) |
---|
| 373 | plt.xlabel("Dimension 2 Index") |
---|
| 374 | plt.ylabel("Dimension 1 Index") |
---|
| 375 | plt.title(f"{varname} (2D without lat/lon)") |
---|
[3783] | 376 | |
---|
[3798] | 377 | if output_path: |
---|
| 378 | try: |
---|
| 379 | plt.savefig(output_path, bbox_inches="tight") |
---|
| 380 | print(f"Figure saved to '{output_path}'") |
---|
| 381 | except Exception as e: |
---|
| 382 | print(f"Error saving figure: {e}") |
---|
| 383 | else: |
---|
| 384 | plt.show() |
---|
| 385 | plt.close() |
---|
| 386 | return |
---|
[3783] | 387 | |
---|
[3798] | 388 | # CASE: data_slice.ndim is neither 0, 1, nor 2 |
---|
| 389 | print(f"Error: After slicing, data for '{varname}' has ndim={data_slice.ndim}, which is not supported.") |
---|
| 390 | return |
---|
[3783] | 391 | |
---|
| 392 | |
---|
| 393 | def visualize_variable_interactive(nc_path=None): |
---|
| 394 | """ |
---|
[3798] | 395 | Interactive mode: prompts for the NetCDF file if not provided, then for the variable, |
---|
| 396 | then for Time/altitude indices (skipped entirely if variable is purely 1D over Time), |
---|
| 397 | and for each other dimension offers to fix an index or to plot along that dimension (by typing 'f'). |
---|
| 398 | |
---|
| 399 | If a dimension has length 1, the index 0 is chosen automatically. |
---|
[3783] | 400 | """ |
---|
| 401 | # Determine file path |
---|
| 402 | if nc_path: |
---|
| 403 | file_input = nc_path |
---|
| 404 | else: |
---|
| 405 | readline.set_completer(complete_filename) |
---|
| 406 | readline.parse_and_bind("tab: complete") |
---|
| 407 | file_input = input("Enter the path to the NetCDF file: ").strip() |
---|
| 408 | |
---|
| 409 | if not file_input: |
---|
| 410 | print("No file specified. Exiting.") |
---|
| 411 | return |
---|
| 412 | if not os.path.isfile(file_input): |
---|
| 413 | print(f"Error: '{file_input}' not found.") |
---|
| 414 | return |
---|
| 415 | |
---|
| 416 | try: |
---|
| 417 | ds = Dataset(file_input, mode="r") |
---|
| 418 | except Exception as e: |
---|
| 419 | print(f"Error: Unable to open '{file_input}': {e}") |
---|
| 420 | return |
---|
| 421 | |
---|
| 422 | varnames = list(ds.variables.keys()) |
---|
| 423 | if not varnames: |
---|
| 424 | print("Error: No variables found in the dataset.") |
---|
| 425 | ds.close() |
---|
| 426 | return |
---|
| 427 | |
---|
| 428 | # Auto-select if only one variable |
---|
| 429 | if len(varnames) == 1: |
---|
| 430 | var_input = varnames[0] |
---|
| 431 | print(f"Automatically selected the only variable: '{var_input}'") |
---|
| 432 | else: |
---|
| 433 | print("\nAvailable variables:") |
---|
| 434 | for name in varnames: |
---|
| 435 | print(f" - {name}") |
---|
| 436 | print() |
---|
| 437 | readline.set_completer(make_varname_completer(varnames)) |
---|
| 438 | var_input = input("Enter the name of the variable to visualize: ").strip() |
---|
| 439 | if var_input not in ds.variables: |
---|
| 440 | print(f"Error: Variable '{var_input}' not found. Exiting.") |
---|
| 441 | ds.close() |
---|
| 442 | return |
---|
| 443 | |
---|
[3798] | 444 | dims = ds.variables[var_input].dimensions # tuple of dimension names |
---|
| 445 | |
---|
| 446 | # If the variable is purely 1D over Time, plot immediately without asking for time index |
---|
| 447 | if len(dims) == 1 and find_dim_index(dims, TIME_DIMS) is not None: |
---|
| 448 | plot_variable( |
---|
| 449 | dataset=ds, |
---|
| 450 | varname=var_input, |
---|
| 451 | time_index=None, |
---|
| 452 | alt_index=None, |
---|
| 453 | colormap="jet", |
---|
| 454 | output_path=None, |
---|
| 455 | extra_indices=None |
---|
| 456 | ) |
---|
| 457 | ds.close() |
---|
| 458 | return |
---|
| 459 | |
---|
| 460 | # Otherwise, proceed to prompt for time/altitude and other dimensions |
---|
[3783] | 461 | time_idx = None |
---|
| 462 | alt_idx = None |
---|
| 463 | |
---|
[3798] | 464 | # Prompt for time index if applicable |
---|
[3783] | 465 | t_idx = find_dim_index(dims, TIME_DIMS) |
---|
| 466 | if t_idx is not None: |
---|
[3798] | 467 | time_len = ds.variables[var_input].shape[t_idx] |
---|
| 468 | if time_len > 1: |
---|
[3783] | 469 | while True: |
---|
| 470 | try: |
---|
[3798] | 471 | user_t = input(f"Enter time index [0..{time_len - 1}]: ").strip() |
---|
[3783] | 472 | if user_t == "": |
---|
| 473 | print("No time index entered. Exiting.") |
---|
| 474 | ds.close() |
---|
| 475 | return |
---|
[3798] | 476 | ti = int(user_t) |
---|
| 477 | if 0 <= ti < time_len: |
---|
| 478 | time_idx = ti |
---|
[3783] | 479 | break |
---|
| 480 | except ValueError: |
---|
| 481 | pass |
---|
[3798] | 482 | print(f"Invalid index. Enter an integer between 0 and {time_len - 1}.") |
---|
[3783] | 483 | else: |
---|
| 484 | time_idx = 0 |
---|
| 485 | print("Only one time step available; using index 0.") |
---|
| 486 | |
---|
[3798] | 487 | # Prompt for altitude index if applicable |
---|
[3783] | 488 | a_idx = find_dim_index(dims, ALT_DIMS) |
---|
| 489 | if a_idx is not None: |
---|
[3798] | 490 | alt_len = ds.variables[var_input].shape[a_idx] |
---|
| 491 | if alt_len > 1: |
---|
[3783] | 492 | while True: |
---|
| 493 | try: |
---|
[3798] | 494 | user_a = input(f"Enter altitude index [0..{alt_len - 1}]: ").strip() |
---|
[3783] | 495 | if user_a == "": |
---|
| 496 | print("No altitude index entered. Exiting.") |
---|
| 497 | ds.close() |
---|
| 498 | return |
---|
[3798] | 499 | ai = int(user_a) |
---|
| 500 | if 0 <= ai < alt_len: |
---|
| 501 | alt_idx = ai |
---|
[3783] | 502 | break |
---|
| 503 | except ValueError: |
---|
| 504 | pass |
---|
[3798] | 505 | print(f"Invalid index. Enter an integer between 0 and {alt_len - 1}.") |
---|
[3783] | 506 | else: |
---|
| 507 | alt_idx = 0 |
---|
| 508 | print("Only one altitude level available; using index 0.") |
---|
| 509 | |
---|
[3798] | 510 | # Identify other dimensions (excluding Time/lat/lon/alt) |
---|
| 511 | other_dims = [] |
---|
| 512 | for idx_dim, dim_name in enumerate(dims): |
---|
| 513 | if idx_dim == t_idx or idx_dim == a_idx: |
---|
| 514 | continue |
---|
| 515 | if dim_name.lower() in (d.lower() for d in LAT_DIMS + LON_DIMS): |
---|
| 516 | continue |
---|
| 517 | other_dims.append((idx_dim, dim_name)) |
---|
| 518 | |
---|
| 519 | # For each other dimension, ask user to fix an index or type 'f' to plot along that dimension |
---|
| 520 | extra_indices = {} |
---|
| 521 | for idx_dim, dim_name in other_dims: |
---|
| 522 | dim_len = ds.variables[var_input].shape[idx_dim] |
---|
| 523 | if dim_len == 1: |
---|
| 524 | extra_indices[dim_name] = 0 |
---|
| 525 | print(f"Dimension '{dim_name}' has length 1; using index 0.") |
---|
| 526 | else: |
---|
| 527 | while True: |
---|
| 528 | prompt = ( |
---|
| 529 | f"Enter index for '{dim_name}' [0..{dim_len - 1}] " |
---|
| 530 | f"or 'f' to plot along '{dim_name}': " |
---|
| 531 | ) |
---|
| 532 | user_i = input(prompt).strip().lower() |
---|
| 533 | if user_i == 'f': |
---|
| 534 | # Leave this dimension unfixed → no key in extra_indices |
---|
| 535 | break |
---|
| 536 | if user_i == "": |
---|
| 537 | print("No index entered. Exiting.") |
---|
| 538 | ds.close() |
---|
| 539 | return |
---|
| 540 | try: |
---|
| 541 | idx_val = int(user_i) |
---|
| 542 | if 0 <= idx_val < dim_len: |
---|
| 543 | extra_indices[dim_name] = idx_val |
---|
| 544 | break |
---|
| 545 | except ValueError: |
---|
| 546 | pass |
---|
| 547 | print(f"Invalid index. Enter an integer between 0 and {dim_len - 1}, or 'f'.") |
---|
| 548 | |
---|
| 549 | # Finally, call plot_variable with collected indices |
---|
[3783] | 550 | plot_variable( |
---|
| 551 | dataset=ds, |
---|
| 552 | varname=var_input, |
---|
| 553 | time_index=time_idx, |
---|
| 554 | alt_index=alt_idx, |
---|
| 555 | colormap="jet", |
---|
[3798] | 556 | output_path=None, |
---|
| 557 | extra_indices=extra_indices |
---|
[3783] | 558 | ) |
---|
| 559 | ds.close() |
---|
| 560 | |
---|
| 561 | |
---|
[3798] | 562 | def visualize_variable_cli(nc_path, varname, time_index, alt_index, colormap, output_path, extra_json): |
---|
[3783] | 563 | """ |
---|
[3798] | 564 | Command-line mode: visualize directly, parsing the --extra-indices argument (JSON string). |
---|
[3783] | 565 | """ |
---|
| 566 | if not os.path.isfile(nc_path): |
---|
| 567 | print(f"Error: '{nc_path}' not found.") |
---|
| 568 | return |
---|
[3798] | 569 | |
---|
[3783] | 570 | try: |
---|
| 571 | ds = Dataset(nc_path, mode="r") |
---|
| 572 | except Exception as e: |
---|
| 573 | print(f"Error: Unable to open '{nc_path}': {e}") |
---|
| 574 | return |
---|
| 575 | |
---|
| 576 | if varname not in ds.variables: |
---|
| 577 | print(f"Error: Variable '{varname}' not found in '{nc_path}'.") |
---|
| 578 | ds.close() |
---|
| 579 | return |
---|
| 580 | |
---|
[3798] | 581 | # Parse extra_indices if provided |
---|
| 582 | extra_indices = {} |
---|
| 583 | if extra_json: |
---|
| 584 | try: |
---|
| 585 | parsed = json.loads(extra_json) |
---|
| 586 | if isinstance(parsed, dict): |
---|
| 587 | for k, v in parsed.items(): |
---|
| 588 | if isinstance(k, str) and isinstance(v, int): |
---|
| 589 | extra_indices[k] = v |
---|
| 590 | else: |
---|
| 591 | print("Warning: --extra-indices is not a JSON object. Ignored.") |
---|
| 592 | except json.JSONDecodeError: |
---|
| 593 | print("Warning: --extra-indices is not valid JSON. Ignored.") |
---|
| 594 | |
---|
[3783] | 595 | plot_variable( |
---|
| 596 | dataset=ds, |
---|
| 597 | varname=varname, |
---|
| 598 | time_index=time_index, |
---|
| 599 | alt_index=alt_index, |
---|
| 600 | colormap=colormap, |
---|
[3798] | 601 | output_path=output_path, |
---|
| 602 | extra_indices=extra_indices |
---|
[3783] | 603 | ) |
---|
| 604 | ds.close() |
---|
| 605 | |
---|
| 606 | |
---|
| 607 | def main(): |
---|
| 608 | parser = argparse.ArgumentParser( |
---|
[3798] | 609 | description="Visualize a 1D/2D slice of a NetCDF variable on various dimension types." |
---|
[3783] | 610 | ) |
---|
| 611 | parser.add_argument( |
---|
| 612 | "nc_file", |
---|
| 613 | nargs="?", |
---|
| 614 | help="Path to the NetCDF file (interactive if omitted)." |
---|
| 615 | ) |
---|
| 616 | parser.add_argument( |
---|
| 617 | "--variable", "-v", |
---|
| 618 | help="Name of the variable to visualize." |
---|
| 619 | ) |
---|
| 620 | parser.add_argument( |
---|
| 621 | "--time-index", "-t", |
---|
| 622 | type=int, |
---|
[3798] | 623 | help="Index on the Time dimension, if applicable (ignored for pure 1D time series)." |
---|
[3783] | 624 | ) |
---|
| 625 | parser.add_argument( |
---|
| 626 | "--alt-index", "-a", |
---|
| 627 | type=int, |
---|
[3798] | 628 | help="Index on the altitude dimension, if applicable." |
---|
[3783] | 629 | ) |
---|
| 630 | parser.add_argument( |
---|
| 631 | "--cmap", "-c", |
---|
| 632 | default="jet", |
---|
| 633 | help="Matplotlib colormap (default: 'jet')." |
---|
| 634 | ) |
---|
| 635 | parser.add_argument( |
---|
| 636 | "--output", "-o", |
---|
[3798] | 637 | help="If provided, save the figure to this file instead of displaying it." |
---|
[3783] | 638 | ) |
---|
[3798] | 639 | parser.add_argument( |
---|
| 640 | "--extra-indices", "-e", |
---|
| 641 | help="JSON string to fix indices of dimensions outside Time/lat/lon/alt. " |
---|
| 642 | "Example: '{\"nslope\":0, \"physical_points\":2}'." |
---|
| 643 | ) |
---|
[3783] | 644 | |
---|
| 645 | args = parser.parse_args() |
---|
| 646 | |
---|
[3798] | 647 | # If both nc_file and variable are provided → CLI mode |
---|
| 648 | if args.nc_file and args.variable: |
---|
[3783] | 649 | visualize_variable_cli( |
---|
| 650 | nc_path=args.nc_file, |
---|
| 651 | varname=args.variable, |
---|
| 652 | time_index=args.time_index, |
---|
| 653 | alt_index=args.alt_index, |
---|
| 654 | colormap=args.cmap, |
---|
[3798] | 655 | output_path=args.output, |
---|
| 656 | extra_json=args.extra_indices |
---|
[3783] | 657 | ) |
---|
[3798] | 658 | else: |
---|
| 659 | # Otherwise → fully interactive mode |
---|
| 660 | visualize_variable_interactive(nc_path=args.nc_file) |
---|
[3783] | 661 | |
---|
| 662 | |
---|
| 663 | if __name__ == "__main__": |
---|
| 664 | main() |
---|
| 665 | |
---|