Changeset 3818 for trunk/LMDZ.MARS/util/display_netcdf.py
- Timestamp:
- Jul 1, 2025, 11:31:08 AM (2 weeks ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/LMDZ.MARS/util/display_netcdf.py
r3810 r3818 7 7 This script can display any numeric variable from a NetCDF file. 8 8 It supports the following cases: 9 - 1D time series (Time) 10 - 1D vertical profiles (e.g., subsurface_layers) 9 - Scalar output 10 - 1D time series 11 - 1D vertical profiles 11 12 - 2D latitude/longitude map 12 - 2D (Time × another dimension) 13 - Variables with dimension “physical_points” displayed on a 2D map if lat/lon are present 13 - 2D cross-sections 14 14 - Optionally average over latitude and plot longitude vs. time heatmap 15 - Scalar output (ndim == 0 after slicing) 16 - 2D cross-sections (altitude × latitude or altitude × longitude) 15 - Optionally display polar stereographic view of 2D maps 16 - Optionally display 3D globe view of 2D maps 17 18 Automatic setup from the environment file found in the "LMDZ.MARS/util" folder: 19 1. Make sure Conda is installed. 20 2. In terminal, navigate to the folder containing this script. 21 4. Create the environment: 22 conda env create -f display_netcdf.yml 23 5. Activate the environment: 24 conda activate my_env 25 6. Run the script: 26 python display_netcdf.py 17 27 18 28 Usage: 19 29 1) Command-line mode: 20 python display_netcdf.py /path/to/your_file.nc --variable VAR_NAME \ 21 [--time-index 0] [--alt-index 0] [--cmap viridis] [--avg-lat] \ 22 [--slice-lon-index 10] [--slice-lat-index 20] [--show-topo] \ 23 [--output out.png] [--extra-indices '{"nslope": 1}'] 24 30 python display_netcdf.py /path/to/your_file.nc --variable VAR_NAME [options] 31 Options: 25 32 --variable : Name of the variable to visualize. 26 33 --time-index : Index along the Time dimension (0-based, ignored for purely 1D time series). … … 31 38 --slice-lat-index : Fixed latitude index for altitude×latitude cross-section. 32 39 --show-topo : Overlay MOLA topography on lat/lon maps. 40 --show-polar : 41 --show-3d : 33 42 --output : If provided, save the figure to this filename instead of displaying. 34 43 --extra-indices : JSON string to fix indices for any other dimensions. … … 38 47 2) Interactive mode: 39 48 python display_netcdf.py 40 (The script will prompt for everything, including averaging or slicing options.)49 The script will prompt for everything. 41 50 """ 42 51 … … 49 58 import numpy as np 50 59 import matplotlib.pyplot as plt 51 import matplotlib.tri as mtri52 60 import matplotlib.path as mpath 61 import matplotlib.colors as mcolors 53 62 import cartopy.crs as ccrs 63 import pandas as pd 54 64 from netCDF4 import Dataset 65 66 # Attempt vedo import early for global use 67 try: 68 import vedo 69 from vedo import * 70 from scipy.interpolate import RegularGridInterpolator 71 vedo_available = True 72 except ImportError: 73 vedo_available = False 55 74 56 75 # Constants for recognized dimension names … … 59 78 LAT_DIMS = ("latitude", "lat") 60 79 LON_DIMS = ("longitude", "lon") 80 81 # Paths for MOLA data 82 MOLA_NPY = 'MOLA_1px_per_deg.npy' 83 MOLA_CSV = 'molaTeam_contour_31rgb_steps.csv' 61 84 62 85 # Attempt to load MOLA topography … … 68 91 topo_lon2d, topo_lat2d = np.meshgrid(topo_lons, topo_lats) 69 92 topo_loaded = True 70 print("MOLA topography loaded successfully from 'MOLA_1px_per_deg.npy'.")71 93 except Exception as e: 94 print(f"Warning: '{MOLA_NPY}' not found: {e}") 72 95 topo_loaded = False 73 print(f"Warning: failed to load MOLA topography ('MOLA_1px_per_deg.npy'): {e}") 96 97 98 # Attempt to load contour color table 99 if os.path.isfile(MOLA_CSV): 100 color_table = pd.read_csv(MOLA_CSV) 101 csv_loaded = True 102 else: 103 print(f"Warning: '{MOLA_CSV}' not found. 3D view colors disabled.") 104 csv_loaded = False 74 105 75 106 … … 204 235 # Show both figures 205 236 plt.show() 237 238 239 def plot_3D_globe(lon2d, lat2d, data2d, colormap, varname, units=None): 240 """ 241 Plot a 3D globe view of the data using vedo, with surface coloring based on data2d 242 and overlaid contour lines from MOLA topography. 243 """ 244 if not vedo_available: 245 print("3D view skipped: vedo missing.") 246 return 247 if not csv_loaded: 248 print("3D view skipped: color table missing.") 249 return 250 251 # Prepare MOLA grid 252 nlat, nlon = MOLA.shape 253 lats = np.linspace(90, -90, nlat) 254 lons = np.linspace(-180, 180, nlon) 255 lon_grid, lat_grid = np.meshgrid(lons, lats) 256 257 # Interpolate data2d onto MOLA grid 258 lat_data = np.linspace(-90, 90, data2d.shape[0]) 259 lon_data = np.linspace(-180, 180, data2d.shape[1]) 260 interp2d = RegularGridInterpolator((lat_data, lon_data), data2d, 261 bounds_error=False, fill_value=None) 262 newdata2d = interp2d((lat_grid, lon_grid)) 263 264 # Generate contour lines from MOLA 265 cs = plt.contour(lon_grid, lat_grid, MOLA, levels=10, linewidths=0) 266 plt.clf() 267 contour_lines = [] 268 radius = 3389500 # Mars average radius [m] 269 for segs, level in zip(cs.allsegs, cs.levels): 270 for verts in segs: 271 lon_c = verts[:, 0] 272 lat_c = verts[:, 1] 273 phi_c = np.radians(90 - lat_c) 274 theta_c = np.radians(lon_c) 275 elev = RegularGridInterpolator((lats, lons), MOLA, 276 bounds_error=False, 277 fill_value=0.0)((lat_c, lon_c)) 278 r_cont = radius + elev * 10 279 x_c = r_cont * np.sin(phi_c) * np.cos(theta_c) * 1.002 280 y_c = r_cont * np.sin(phi_c) * np.sin(theta_c) * 1.002 281 z_c = r_cont * np.cos(phi_c) * 1.002 282 pts = np.column_stack([x_c, y_c, z_c]) 283 if pts.shape[0] > 1: 284 contour_lines.append(Line(pts, c='k', lw=0.5)) 285 286 # Create sphere surface mesh 287 phi = np.deg2rad(90 - lat_grid) 288 theta = np.deg2rad(lon_grid) 289 r = radius + MOLA * 10 290 x = r * np.sin(phi) * np.cos(theta) 291 y = r * np.sin(phi) * np.sin(theta) 292 z = r * np.cos(phi) 293 pts = np.stack([x.ravel(), y.ravel(), z.ravel()], axis=1) 294 295 # Build mesh faces 296 faces = [] 297 for i in range(nlat - 1): 298 for j in range(nlon - 1): 299 p0 = i * nlon + j 300 p1 = p0 + 1 301 p2 = p0 + nlon 302 p3 = p2 + 1 303 faces.extend([(p0, p2, p1), (p1, p2, p3)]) 304 305 mesh = Mesh([pts, faces]) 306 mesh.cmap(colormap, newdata2d.ravel()) 307 mesh.add_scalarbar(title=varname + (f' [{units}]' if units else ''), c='white') 308 mesh.lighting('default') 309 310 # Geographic grid lines 311 meridians, parallels, labels = [], [], [] 312 zero_lon_offset = radius * 0.03 313 for lon in range(-150, 181, 30): 314 lat_line = np.linspace(-90, 90, nlat) 315 lon_line = np.full_like(lat_line, lon) 316 phi = np.deg2rad(90 - lat_line) 317 theta = np.deg2rad(lon_line) 318 elev = RegularGridInterpolator((lats, lons), MOLA)((lat_line, lon_line)) 319 rr = radius + elev * 10 320 pts_line = np.column_stack([ 321 rr * np.sin(phi) * np.cos(theta), 322 rr * np.sin(phi) * np.sin(theta), 323 rr * np.cos(phi) 324 ]) * 1.005 325 label_pos = pts_line[len(pts_line)//2] 326 norm = np.linalg.norm(label_pos) 327 label_pos_out = label_pos / norm * (norm + radius * 0.02) 328 if lon == 0: 329 label_pos_out[1] += zero_lon_offset 330 meridians.append(Line(pts_line, c='k', lw=1)#.flagpole( 331 #f"{lon}°", 332 #point=label_pos_out, 333 #offset=[0, 0, radius * 0.05], 334 #s=radius*0.01, 335 #c='yellow' 336 #).follow_camera() 337 ) 338 339 for lat in range(-60, 91, 30): 340 lon_line = np.linspace(-180, 180, nlon) 341 lat_line = np.full_like(lon_line, lat) 342 phi = np.deg2rad(90 - lat_line) 343 theta = np.deg2rad(lon_line) 344 elev = RegularGridInterpolator((lats, lons), MOLA)((lat_line, lon_line)) 345 rr = radius + elev * 10 346 pts_line = np.column_stack([ 347 rr * np.sin(phi) * np.cos(theta), 348 rr * np.sin(phi) * np.sin(theta), 349 rr * np.cos(phi) 350 ]) * 1.005 351 label_pos = pts_line[len(pts_line)//2] 352 norm = np.linalg.norm(label_pos) 353 label_pos_out = label_pos / norm * (norm + radius * 0.02) 354 parallels.append(Line(pts_line, c='k', lw=1)#.flagpole( 355 #f"{lat}°", 356 #point=label_pos_out, 357 #offset=[0, 0, radius * 0.05], 358 #s=radius*0.01, 359 #c='yellow' 360 #).follow_camera() 361 ) 362 363 # Create plotter 364 plotter = Plotter(title="3D topography view", bg="bb", axes=0) 365 366 # Configure camera 367 cam_dist = radius * 3 368 plotter.camera.SetPosition([cam_dist, 0, 0]) 369 plotter.camera.SetFocalPoint([0, 0, 0]) 370 plotter.camera.SetViewUp([0, 0, 1]) 371 372 # Show the globe 373 plotter.show(mesh, *contour_lines, *meridians, *parallels) 374 206 375 207 376 def plot_variable(dataset, varname, time_index=None, alt_index=None, … … 400 569 401 570 # Prompt for polar-stereo views if interactive 402 if sys.stdin.isatty() andinput("Display polar-stereo views? [y/n]: ").strip().lower() == "y":571 if input("Display polar-stereo views? [y/n]: ").strip().lower() == "y": 403 572 units = getattr(dataset.variables[varname], "units", None) 404 573 plot_polar_views(lon2d, lat2d, data2d, colormap, varname, units) 574 575 # Prompt for 3D globe view if interactive 576 if input("Display 3D globe view? [y/n]: ").strip().lower() == "y": 577 units = getattr(dataset.variables[varname], "units", None) 578 plot_3D_globe(lon2d, lat2d, data2d, colormap, varname, units) 405 579 406 580 if output_path: … … 510 684 plot_polar_views(lon2d, lat2d, dslice, colormap, varname, units) 511 685 686 # Prompt for 3D globe view if interactive 687 if sys.stdin.isatty() and input("Display 3D globe view? [y/n]: ").strip().lower() == "y": 688 units = getattr(dataset.variables[varname], "units", None) 689 plot_3D_globe(lon2d, lat2d, dslice, colormap, varname, units) 690 512 691 if output_path: 513 692 plt.savefig(output_path, bbox_inches="tight") … … 753 932 def main(): 754 933 parser = argparse.ArgumentParser() 755 parser.add_argument("nc_file", nargs="?", help="NetCDF file (omit for interactive)") 756 parser.add_argument("-v", "--variable", help="Variable name") 757 parser.add_argument("-t", "--time-index", type=int, help="Time index (0-based)") 758 parser.add_argument("-a", "--alt-index", type=int, help="Altitude index (0-based)") 759 parser.add_argument("-c", "--cmap", default="jet", help="Colormap") 760 parser.add_argument("--avg-lat", action="store_true", 761 help="Average over latitude (time × lon heatmap)") 762 parser.add_argument("-o", "--output", help="Save figure path") 763 parser.add_argument("-e", "--extra-indices", help="JSON for other dims") 934 parser.add_argument('nc_file', nargs='?', help='NetCDF file (omit for interactive)') 935 parser.add_argument('-v','--variable', help='Variable name') 936 parser.add_argument('-t','--time-index', type=int, help='Time index (0-based)') 937 parser.add_argument('-a','--alt-index', type=int, help='Altitude index (0-based)') 938 parser.add_argument('-c','--cmap', default='jet', help='Colormap') 939 parser.add_argument('--avg-lat', action='store_true', help='Average over latitude') 940 parser.add_argument('--show-polar', action='store_true', help='Enable polar-stereo views') 941 parser.add_argument('--show-3d', action='store_true', help='Enable 3D globe view') 942 parser.add_argument('-o','--output', help='Save figure path') 943 parser.add_argument('-e','--extra-indices', help='JSON string for other dims') 764 944 args = parser.parse_args() 765 945
Note: See TracChangeset
for help on using the changeset viewer.