Changeset 3839 for trunk/LMDZ.MARS/util


Ignore:
Timestamp:
Jul 7, 2025, 6:00:37 PM (4 days ago)
Author:
jbclement
Message:

Mars PCM:
In "display_netcdf.py", addition of the possibility to display altitude as function of time for 1D variables + showing value at cursor for 2D heatmaps.
JBC

Location:
trunk/LMDZ.MARS/util
Files:
2 edited

Legend:

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

    r3824 r3839  
    8585# Attempt to load MOLA topography
    8686try:
    87     MOLA = np.load('MOLA_1px_per_deg.npy')  # shape (nlat, nlon) at 1° per pixel: lat from -90 to 90, lon from 0 to 360
     87    MOLA = np.load('MOLA_1px_per_deg.npy') # shape (nlat, nlon) at 1° per pixel: lat from -90 to 90, lon from 0 to 360
    8888    nlat, nlon = MOLA.shape
    8989    topo_lats = np.linspace(90 - 0.5, -90 + 0.5, nlat)
     
    171171        transform=transform
    172172    )
     173
     174
     175def attach_format_coord(ax, mat, x, y, is_pcolormesh=True):
     176    """
     177    Attach a format_coord function to the axes to display x, y, and value at cursor.
     178    Works for both pcolormesh and imshow style grids.
     179    """
     180    # Determine dimensions
     181    if mat.ndim == 2:
     182        ny, nx = mat.shape
     183    elif mat.ndim == 3 and mat.shape[2] in (3, 4):
     184        ny, nx, nc = mat.shape
     185    else:
     186        raise ValueError(f"Unsupported mat shape {mat.shape}")
     187    # Edges or extents
     188    if is_pcolormesh:
     189        xedges, yedges = x, y
     190    else:
     191        x0, x1 = x.min(), x.max()
     192        y0, y1 = y.min(), y.max()
     193
     194    def format_coord(xp, yp):
     195        # Map to indices
     196        if is_pcolormesh:
     197            col = np.searchsorted(xedges, xp) - 1
     198            row = np.searchsorted(yedges, yp) - 1
     199        else:
     200            col = int((xp - x0) / (x1 - x0) * nx)
     201            row = int((yp - y0) / (y1 - y0) * ny)
     202        # Within bounds?
     203        if 0 <= row < ny and 0 <= col < nx:
     204            if mat.ndim == 2:
     205                v = mat[row, col]
     206                return f"x={xp:.3g}, y={yp:.3g}, val={v:.3g}"
     207            else:
     208                vals = mat[row, col]
     209                txt = ", ".join(f"{vv:.3g}" for vv in vals[:3])
     210                return f"x={xp:.3g}, y={yp:.3g}, val=({txt})"
     211        return f"x={xp:.3g}, y={yp:.3g}"
     212
     213    ax.format_coord = format_coord
    173214
    174215
     
    396437        data_full = np.where(data_full.mask, np.nan, data_full.data)
    397438
     439    # If Time and altitude are both present and neither indexed,
     440    # and every other dim has size 1:
     441    t_idx = find_dim_index(dims, TIME_DIMS)
     442    a_idx = find_dim_index(dims, ALT_DIMS)
     443    shape = var.shape
     444    if (t_idx is not None and a_idx is not None
     445        and time_index is None and alt_index is None
     446        and all(size == 1 for i, size in enumerate(shape) if i not in (t_idx, a_idx))):
     447
     448        # Build a slicer that keeps Time & altitude, drops other singletons
     449        slicer = [0] * len(dims)
     450        slicer[t_idx] = slice(None)
     451        slicer[a_idx] = slice(None)
     452        data2d = data_full[tuple(slicer)]  # shape (ntime, nalt)
     453
     454        # Coordinate arrays
     455        tvar = find_coord_var(dataset, TIME_DIMS)
     456        avar = find_coord_var(dataset, ALT_DIMS)
     457        tvals = dataset.variables[tvar][:]
     458        avals = dataset.variables[avar][:]
     459
     460        # Unmask if necessary
     461        if hasattr(tvals, "mask"):
     462            tvals = np.where(tvals.mask, np.nan, tvals.data)
     463        if hasattr(avals, "mask"):
     464            avals = np.where(avals.mask, np.nan, avals.data)
     465
     466        # Plot heatmap with x=time, y=altitude
     467        fig, ax = plt.subplots(figsize=(10, 6))
     468        T, A = np.meshgrid(tvals, avals)
     469        im = ax.pcolormesh(
     470            T, A, data2d.T,
     471            shading="auto", cmap=colormap
     472        )
     473        dt = tvals[1] - tvals[0]
     474        da = avals[1] - avals[0]
     475        x_edges = np.concatenate([tvals - dt/2, [tvals[-1] + dt/2]])
     476        y_edges = np.concatenate([avals - da/2, [avals[-1] + da/2]])
     477        attach_format_coord(ax, data2d.T, x_edges, y_edges, is_pcolormesh=True)
     478        ax.set_xlabel(tvar)
     479        ax.set_ylabel(avar)
     480        cbar = fig.colorbar(im, ax=ax)
     481        cbar.set_label(varname + (f" ({getattr(var, 'units','')})"))
     482        ax.set_title(f"{varname} — {avar} vs {tvar}", fontweight="bold")
     483
     484        if output_path:
     485            fig.savefig(output_path, bbox_inches="tight")
     486            print(f"Saved to {output_path}")
     487        else:
     488            plt.show()
     489        return
     490
    398491    # Pure 1D time series
    399492    if len(dims) == 1 and find_dim_index(dims, TIME_DIMS) is not None:
     
    434527        if hasattr(lons, "mask"):
    435528            lons = np.where(lons.mask, np.nan, lons.data)
    436         plt.figure(figsize=(10, 6))
    437         plt.pcolormesh(lons, tvals, data_avg, shading="auto", cmap=colormap)
    438         plt.xlabel(f"Longitude ({getattr(dataset.variables[lon_var], 'units', 'deg')})")
    439         plt.ylabel(time_var)
    440         cbar = plt.colorbar()
     529        fig, ax = ax.subplots(figsize=(10, 6))
     530        im = plt.pcolormesh(lons, tvals, data_avg, shading="auto", cmap=colormap)
     531        dx = lons[1] - lons[0]
     532        dy = tvals[1] - tvals[0]
     533        x_edges = np.concatenate([lons - dx/2, [lons[-1] + dx/2]])
     534        y_edges = np.concatenate([tvals - dy/2, [tvals[-1] + dy/2]])
     535        attach_format_coord(ax, data_avg.T, x_edges, y_edges, is_pcolormesh=True)
     536        ax.set_xlabel(f"Longitude ({getattr(dataset.variables[lon_var], 'units', 'deg')})")
     537        ax.set_ylabel(time_var)
     538        cbar = fig.colorbar(im, ax=ax)
    441539        cbar.set_label(varname + (f" ({var.units})" if hasattr(var, "units") else ""))
    442         plt.title(f"{varname} averaged over latitude", fontweight='bold')
     540        ax.set_title(f"{varname} averaged over latitude", fontweight='bold')
    443541        if output_path:
    444             plt.savefig(output_path, bbox_inches="tight")
     542            fig.savefig(output_path, bbox_inches="tight")
    445543            print(f"Saved to {output_path}")
    446544        else:
     
    700798
    701799        # Generic 2D
    702         plt.figure(figsize=(8, 6))
    703         plt.imshow(dslice, aspect="auto")
    704         plt.colorbar(label=varname + (f" ({var.units})" if hasattr(var, "units") else ""))
    705         plt.xlabel("Dim 2 index")
    706         plt.ylabel("Dim 1 index")
    707         plt.title(f"{varname} (2D)", fontweight='bold')
     800        fig, ax = plt.subplots(figsize=(8, 6))
     801        im = ax.imshow(
     802            dslice,
     803            aspect="auto",
     804            interpolation='nearest'
     805        )
     806        x0, x1 = 0, dslice.shape[1] - 1
     807        y0, y1 = 0, dslice.shape[0] - 1
     808        x_centers = np.linspace(x0, x1, dslice.shape[1])
     809        y_centers = np.linspace(y0, y1, dslice.shape[0])
     810        attach_format_coord(ax, dslice, x_centers, y_centers, is_pcolormesh=False)
     811        cbar = fig.colorbar(im, ax=ax, orientation='vertical')
     812        cbar.set_label(varname + (f" ({var.units})" if hasattr(var, "units") else ""))
     813        ax.set_xlabel("Dim 2 index")
     814        ax.set_ylabel("Dim 1 index")
     815        ax.set_title(f"{varname} (2D)")
     816
    708817        if output_path:
    709             plt.savefig(output_path, bbox_inches="tight")
     818            fig.savefig(output_path, bbox_inches="tight")
    710819            print(f"Saved to {output_path}")
    711820        else:
  • trunk/LMDZ.MARS/util/display_netcdf.yml

    r3824 r3839  
    88  - aiohappyeyeballs=2.6.1=pyhd8ed1ab_0
    99  - aiohttp=3.12.13=py312h178313f_0
    10   - aiosignal=1.3.2=pyhd8ed1ab_0
     10  - aiosignal=1.4.0=pyhd8ed1ab_0
    1111  - alsa-lib=1.2.14=hb9d3cd8_0
    1212  - aom=3.9.1=hac33072_0
     
    3838  - fonts-conda-ecosystem=1=0
    3939  - fonts-conda-forge=1=0
    40   - fonttools=4.58.4=py312h178313f_0
     40  - fonttools=4.58.5=py312h178313f_0
    4141  - freetype=2.13.3=ha770c72_1
    4242  - fribidi=1.0.10=h36c2ea0_0
     
    4444  - gdk-pixbuf=2.42.12=hb9ae30d_0
    4545  - geos=3.13.1=h97f6797_0
    46   - gettext=0.24.1=h5888daf_0
    47   - gettext-tools=0.24.1=h5888daf_0
     46  - gettext=0.25.1=h5888daf_0
     47  - gettext-tools=0.25.1=h5888daf_0
    4848  - gl2ps=1.4.2=hae5d5c5_1
    4949  - gmp=6.3.0=hac33072_2
     
    5656  - jsoncpp=1.9.6=hf42df4d_1
    5757  - keyutils=1.6.1=h166bdaf_0
    58   - kiwisolver=1.4.8=py312h84d6215_0
     58  - kiwisolver=1.4.8=py312h68727a3_1
    5959  - krb5=1.21.3=h659f571_0
    6060  - lame=3.100=h166bdaf_1003
    6161  - lcms2=2.17=h717163a_0
    62   - ld_impl_linux-64=2.43=h1423503_5
     62  - ld_impl_linux-64=2.44=h1423503_0
    6363  - lerc=4.0.0=h0aef613_1
    6464  - level-zero=1.23.0=h84d6215_0
    6565  - libabseil=20250127.1=cxx17_hbbce691_0
    6666  - libaec=1.1.4=h3f801dc_0
    67   - libasprintf=0.24.1=h8e693c7_0
    68   - libasprintf-devel=0.24.1=h8e693c7_0
     67  - libasprintf=0.25.1=h8e693c7_0
     68  - libasprintf-devel=0.25.1=h8e693c7_0
    6969  - libass=0.17.3=h52826cd_2
    7070  - libblas=3.9.0=32_h59b9bed_openblas
     
    9191  - libgcc-ng=15.1.0=h69a702a_3
    9292  - libgcrypt-lib=1.11.1=hb9d3cd8_0
    93   - libgettextpo=0.24.1=h5888daf_0
    94   - libgettextpo-devel=0.24.1=h5888daf_0
     93  - libgettextpo=0.25.1=h5888daf_0
     94  - libgettextpo-devel=0.25.1=h5888daf_0
    9595  - libgfortran=15.1.0=h69a702a_3
    9696  - libgfortran5=15.1.0=hcea5267_3
     
    129129  - libopus=1.5.2=hd0c01bc_0
    130130  - libpciaccess=0.18=hb9d3cd8_0
    131   - libpng=1.6.49=h943b412_0
     131  - libpng=1.6.50=h943b412_0
    132132  - libpq=17.5=h27ae623_0
    133133  - libprotobuf=5.29.3=h501fc15_1
     
    179179  - pango=1.56.4=hadf4263_0
    180180  - pcre2=10.45=hc749103_0
    181   - pillow=11.2.1=py312h80c1187_0
     181  - pillow=11.3.0=py312h80c1187_0
    182182  - pip=25.1.1=pyh8b19718_0
    183183  - pixman=0.46.2=h29eaf8c_0
     
    211211  - tk=8.6.13=noxft_hd72426e_102
    212212  - tornado=6.5.1=py312h66e93f0_0
    213   - typing_extensions=4.14.0=pyhe01879c_0
     213  - typing_extensions=4.14.1=pyhe01879c_0
    214214  - tzdata=2025b=h78e105d_0
    215215  - unicodedata2=16.0.0=py312h66e93f0_0
     
    219219  - vtk-base=9.4.2=py312h28cca35_1
    220220  - vtk-io-ffmpeg=9.4.2=he57672f_1
    221   - wayland=1.23.1=h3e06ad9_1
     221  - wayland=1.24.0=h3e06ad9_0
    222222  - wayland-protocols=1.45=hd8ed1ab_0
    223223  - wheel=0.45.1=pyhd8ed1ab_1
Note: See TracChangeset for help on using the changeset viewer.