Ignore:
Timestamp:
Jul 1, 2025, 11:31:08 AM (2 weeks ago)
Author:
jbclement
Message:

Mars PCM:
In the script "display_netcdf.py" ("util" folder), addition of the possibility to display a 3D globe view of 2D maps. An environment file is also added to set up quickly the libraries and modules needed for the Python script.
JBC

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/LMDZ.MARS/util/display_netcdf.py

    r3810 r3818  
    77This script can display any numeric variable from a NetCDF file.
    88It 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
    1112  - 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
    1414  - 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
     18Automatic 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
    1727
    1828Usage:
    1929  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:
    2532    --variable         : Name of the variable to visualize.
    2633    --time-index       : Index along the Time dimension (0-based, ignored for purely 1D time series).
     
    3138    --slice-lat-index  : Fixed latitude index for altitude×latitude cross-section.
    3239    --show-topo        : Overlay MOLA topography on lat/lon maps.
     40    --show-polar       :
     41    --show-3d          :
    3342    --output           : If provided, save the figure to this filename instead of displaying.
    3443    --extra-indices    : JSON string to fix indices for any other dimensions.
     
    3847  2) Interactive mode:
    3948       python display_netcdf.py
    40        (The script will prompt for everything, including averaging or slicing options.)
     49     The script will prompt for everything.
    4150"""
    4251
     
    4958import numpy as np
    5059import matplotlib.pyplot as plt
    51 import matplotlib.tri as mtri
    5260import matplotlib.path as mpath
     61import matplotlib.colors as mcolors
    5362import cartopy.crs as ccrs
     63import pandas as pd
    5464from netCDF4 import Dataset
     65
     66# Attempt vedo import early for global use
     67try:
     68    import vedo
     69    from vedo import *
     70    from scipy.interpolate import RegularGridInterpolator
     71    vedo_available = True
     72except ImportError:
     73    vedo_available = False
    5574
    5675# Constants for recognized dimension names
     
    5978LAT_DIMS  = ("latitude", "lat")
    6079LON_DIMS  = ("longitude", "lon")
     80
     81# Paths for MOLA data
     82MOLA_NPY = 'MOLA_1px_per_deg.npy'
     83MOLA_CSV = 'molaTeam_contour_31rgb_steps.csv'
    6184
    6285# Attempt to load MOLA topography
     
    6891    topo_lon2d, topo_lat2d = np.meshgrid(topo_lons, topo_lats)
    6992    topo_loaded = True
    70     print("MOLA topography loaded successfully from 'MOLA_1px_per_deg.npy'.")
    7193except Exception as e:
     94    print(f"Warning: '{MOLA_NPY}' not found: {e}")
    7295    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
     99if os.path.isfile(MOLA_CSV):
     100    color_table = pd.read_csv(MOLA_CSV)
     101    csv_loaded = True
     102else:
     103    print(f"Warning: '{MOLA_CSV}' not found. 3D view colors disabled.")
     104    csv_loaded = False
    74105
    75106
     
    204235    # Show both figures
    205236    plt.show()
     237
     238
     239def 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
    206375
    207376def plot_variable(dataset, varname, time_index=None, alt_index=None,
     
    400569
    401570                    # Prompt for polar-stereo views if interactive
    402                     if sys.stdin.isatty() and input("Display polar-stereo views? [y/n]: ").strip().lower() == "y":
     571                    if input("Display polar-stereo views? [y/n]: ").strip().lower() == "y":
    403572                        units = getattr(dataset.variables[varname], "units", None)
    404573                        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)
    405579
    406580                    if output_path:
     
    510684                plot_polar_views(lon2d, lat2d, dslice, colormap, varname, units)
    511685
     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
    512691            if output_path:
    513692                plt.savefig(output_path, bbox_inches="tight")
     
    753932def main():
    754933    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')
    764944    args = parser.parse_args()
    765945
Note: See TracChangeset for help on using the changeset viewer.