Source code for bldfm.plotting.diagnostics

"""Diagnostic and analysis plots: convergence, vertical profiles, vertical slices."""

import numpy as np
import matplotlib.pyplot as plt

from ._common import ensure_ax


[docs] def plot_convergence( grid_sizes, errors, fits=None, marker="o", ax=None, xlabel="$h$ [m]", ylabel="Relative RMSE", title=None, ): """Log-log convergence plot. Parameters ---------- grid_sizes : array-like Effective grid spacings (h). errors : array-like Error values (e.g., RMSE). fits : list of (func, params, label), optional Curve fits to overlay. Each element is a tuple: (function, parameter_dict, label_string). Example: (lambda h, a, b: a * h**b, {"a": 0.1, "b": 2.0}, "$O(h^2)$") marker : str Marker style for data points (default "o"). ax : matplotlib Axes, optional xlabel : str X-axis label (default "$h$ [m]"). ylabel : str Y-axis label (default "Relative RMSE"). title : str, optional Returns ------- ax : matplotlib Axes """ ax = ensure_ax(ax) grid_sizes = np.asarray(grid_sizes) errors = np.asarray(errors) ax.loglog(grid_sizes, errors, marker=marker, linestyle="-", label="Data") if fits is not None: for func, params, label in fits: h_fit = np.linspace(grid_sizes.min(), grid_sizes.max(), 100) y_fit = func(h_fit, **params) ax.loglog(h_fit, y_fit, linestyle="--", label=label) ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) ax.legend() ax.grid(True, which="both", alpha=0.3) if title: ax.set_title(title) return ax
[docs] def plot_vertical_profiles( z_list, profiles_list, labels, meas_height=None, figsize=None, title=None ): r"""Multi-panel (1x2) profile plot: wind speed and diffusivity vs height. Left panel: wind speed \|U\| = sqrt(u² + v²) vs z. Right panel: Kz vs z. Parameters ---------- z_list : list of array-like List of z arrays (one per stability condition). profiles_list : list of tuple List of profile tuples (u, v, Kx, Ky, Kz), one per stability condition. labels : list of str Legend labels for each profile (e.g., "L = -10 m"). meas_height : float, optional Measurement height to mark with a horizontal dashed line. figsize : tuple, optional Figure size (width, height). Default (10, 5). title : str, optional Overall figure title. Returns ------- fig : matplotlib Figure axes : ndarray of Axes (1x2) """ if figsize is None: figsize = (10, 5) fig, axes = plt.subplots(1, 2, figsize=figsize, layout="constrained") cmap = plt.get_cmap("tab10") colors = [cmap(i) for i in range(len(z_list))] for z, profs, label, color in zip(z_list, profiles_list, labels, colors): u, v, Kx, Ky, Kz = profs speed = np.sqrt(np.asarray(u) ** 2 + np.asarray(v) ** 2) axes[0].plot(speed, z, marker="+", linestyle="-", label=label, color=color) axes[1].plot(Kz, z, marker="", linestyle="-", label=label, color=color) if meas_height is not None: for ax in axes: ax.axhline(meas_height, color="gray", linestyle="--", linewidth=1, zorder=0) axes[0].set_xlabel("|U| [m/s]") axes[0].set_ylabel("z [m]") axes[0].set_title("Wind speed profile") axes[0].legend() axes[0].grid(True, alpha=0.3) axes[1].set_xlabel("Kz [m\u00b2/s]") axes[1].set_ylabel("z [m]") axes[1].set_title("Diffusivity profile") axes[1].legend() axes[1].grid(True, alpha=0.3) if title: fig.suptitle(title) return fig, axes
[docs] def plot_vertical_slice( field, grid, slice_axis, slice_index, ax=None, cmap="viridis", title=None, xlabel=None, ylabel=None, ): """2D slice from a 3D field. Parameters ---------- field : ndarray (nz, ny, nx) 3-D field to slice. grid : tuple (X, Y, Z) 3-D coordinate arrays from the solver. slice_axis : str Axis to slice along: "x", "y", or "z". slice_index : int Index along the slice axis. ax : matplotlib Axes, optional cmap : str Colormap name (default "viridis"). title : str, optional xlabel : str, optional X-axis label (auto-generated if None). ylabel : str, optional Y-axis label (auto-generated if None). Returns ------- ax : matplotlib Axes """ X, Y, Z = grid ax = ensure_ax(ax) if slice_axis == "y": slice_data = field[:, slice_index, :] x_coord = X[:, slice_index, :] if X.ndim == 3 else X z_coord = Z[:, slice_index, :] if Z.ndim == 3 else Z pm = ax.pcolormesh(x_coord, z_coord, slice_data, cmap=cmap, shading="auto") if xlabel is None: xlabel = "x [m]" if ylabel is None: ylabel = "z [m]" elif slice_axis == "x": slice_data = field[:, :, slice_index] y_coord = Y[:, :, slice_index] if Y.ndim == 3 else Y z_coord = Z[:, :, slice_index] if Z.ndim == 3 else Z pm = ax.pcolormesh(y_coord, z_coord, slice_data, cmap=cmap, shading="auto") if xlabel is None: xlabel = "y [m]" if ylabel is None: ylabel = "z [m]" elif slice_axis == "z": slice_data = field[slice_index, :, :] x_coord = X[slice_index, :, :] if X.ndim == 3 else X y_coord = Y[slice_index, :, :] if Y.ndim == 3 else Y pm = ax.pcolormesh(x_coord, y_coord, slice_data, cmap=cmap, shading="auto") if xlabel is None: xlabel = "x [m]" if ylabel is None: ylabel = "y [m]" else: raise ValueError(f"slice_axis must be 'x', 'y', or 'z', got {slice_axis}") ax.figure.colorbar(pm, ax=ax) ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) if title: ax.set_title(title) return ax