blpd.plot
Create plots of LPD model results.
The functions here all take xr.Dataset
s as their first argument.
1""" 2Create plots of LPD model results. 3 4The functions here all take `xr.Dataset`s as their first argument. 5""" 6 7from itertools import cycle 8 9import matplotlib as mpl 10import matplotlib.pyplot as plt 11import numpy as np 12from matplotlib.collections import LineCollection as _LineCollection 13from mpl_toolkits.mplot3d import Axes3D as _Axes3D # noqa: F401 unused import 14 15from . import utils 16from .utils import _add_snippets 17 18# ^ could add `as _{}` to all of these 19# to indicate that these are not intended to be public parts of the module name space 20# since __all__ is not respected by linters or autocompleters 21 22__all__ = ( 23 "conc_2d", 24 "conc_scatter", 25 "conc_xline", 26 "final_pos_hist", 27 "final_pos_hist2d", 28 "final_pos_scatter", 29 "trajectories", 30 "ws_hist", 31) 32 33# TODO: create some base classes for plots to reduce repeating of code 34# - allow passing fig and ax kwargs in __init__ 35# - stars to mark sources; check_fig_num, labeling, etc. 36# - alpha calculation based on number of particles & spread? 37 38# TODO: really all plots could have the auto-bounds stuff. and (optionally?) print message about it? 39 40 41_SOURCE_MARKER_PROPS = dict(marker="*", c="gold", ms=11, mec="0.35", mew=1.0) 42 43utils._SNIPPETS.update( 44 spc_params=""" 45spc : str 46 ASCII identifier of a chemical species. For example, `'apinene'`. 47 """.strip(), 48 ds_lpd_params=""" 49ds : xr.Dataset 50 Dataset of Lagrangian particle positions, e.g., from `blpd.model.Model.to_xr`. 51 """.strip(), 52 ds_chem_params=""" 53ds : xr.Dataset 54 Dataset of Lagrangian particle positions + chemistry, e.g., from 55 `blpd.chem.calc_relative_levels_fix_oxidants`. 56 """.strip(), 57 log_cnorm_params=""" 58log_cnorm : bool 59 Whether to use a logarithmic colormap normalization. 60 """.strip(), 61) 62 63 64@_add_snippets 65def final_pos_scatter(ds, sdim="xy"): 66 """Scatter plot of particle end positions. 67 68 Parameters 69 ---------- 70 %(ds_lpd_params)s 71 %(sdim_params)s 72 """ 73 p = utils.load_p(ds) 74 75 dims = utils.dims_from_sdim(sdim) 76 if len(dims) == 3: 77 sdim = "xyz" 78 x = ds.x.values 79 y = ds.y.values 80 z = ds.z.values 81 subplot_kw = {"projection": "3d"} 82 coords = (x, y, z) 83 plot_kw = dict(alpha=0.5, mew=0, ms=7) 84 elif len(dims) == 2: 85 x = ds[dims[0]].values 86 y = ds[dims[1]].values 87 subplot_kw = {} 88 coords = (x, y) 89 plot_kw = dict(alpha=0.5, mfc="none", mew=0.8, ms=5) 90 else: 91 raise ValueError("invalid `sdim`") 92 93 num = utils.check_fig_num(f"final-pos-scatter-{sdim}") 94 fig, ax = plt.subplots(num=num, subplot_kw=subplot_kw) 95 96 ax.plot(*coords, "o", **plot_kw) 97 98 ax.set_xlabel(f"${dims[0]}$") 99 ax.set_ylabel(f"${dims[1]}$") 100 if len(dims) == 3: 101 ax.set_zlabel(f"${dims[2]}$") 102 ax.set_title(utils.s_t_info(p), loc="left") 103 ax.set_title(f"$N_p = {p['Np_tot']}$", loc="right") 104 105 for xs, ys in p["source_positions"]: 106 sp = dict(x=xs, y=ys, z=p["release_height"]) 107 if len(dims) == 3: 108 ax.plot([sp[dims[0]]], [sp[dims[1]]], [sp[dims[2]]], "*", c="gold", ms=10) 109 else: 110 ax.plot(sp[dims[0]], sp[dims[1]], "*", c="gold", ms=10) 111 112 fig.set_tight_layout(True) 113 114 115# TODO: add option to do trajectories for continuous release runs, colored by time out 116# TODO: trajectories for hist run in 3-D? 117 118 119@_add_snippets 120def trajectories(ds, *, smooth=False, smooth_window_size=None, color_sources=False): 121 """Plot particle trajectories. 122 123 Parameters 124 ---------- 125 %(ds_lpd_params)s 126 smooth : bool 127 Whether to smooth the trajectories. 128 smooth_window_size : int, optional 129 How many points to include in the moving average window. 130 color_sources : bool 131 Whether to color particle trajectories by their sources 132 to help in differentiating them. 133 """ 134 if "t" not in ds.dims: 135 raise ValueError("time 't' must be a dimension") 136 137 p = utils.load_p(ds) 138 pos = np.stack((ds.x.values, ds.y.values, ds.z.values), axis=-1) # same shape as hist["pos"] 139 140 t_tot = p["t_tot"] 141 dt = p["dt_out"] # note output timestep not that of the integration 142 # N_t = p["N_t"] 143 Np = p["Np_tot"] 144 # N = Np * N_t 145 assert pos.shape[0] == Np 146 147 ltitle = utils.s_t_info(p) 148 rtitle = utils.s_sample_size(p) 149 150 # allow specifying smooth_window_size only 151 if smooth_window_size is not None and not smooth: 152 smooth = True 153 if smooth: 154 if smooth_window_size is None: 155 n = 100 156 else: 157 n = int(smooth_window_size) 158 159 if n * dt > 0.5 * t_tot: 160 raise ValueError( 161 "can't do any smoothing with the requested window size (not enough points)" 162 ) 163 pos0 = pos[:, 0, :][:, np.newaxis, :] 164 pos = np.swapaxes(utils.moving_average(np.swapaxes(pos, 0, 1), n=n, axis=0), 0, 1) 165 # ^ `np.swapaxes` should return views, not create new arrays (ver >= 1.10.0) 166 167 # preserve starting point 168 pos = np.concatenate((pos0, pos), axis=1) # axis=1 => basically `hstack` 169 170 ltitle = f"$N_{{smooth}} = {n}$ ({n*dt:} s)\n{ltitle}" # add smoothing info to left title 171 172 num = utils.check_fig_num("trajectories") 173 fig, ax = plt.subplots(num=num) 174 175 if color_sources: 176 if isinstance(color_sources, (list, np.ndarray)): 177 colors = color_sources # assume is a list of colors (TODO: should check) 178 else: 179 colors = plt.get_cmap("Dark2").colors 180 # Dark2 is a ListedColormap with 8 colors; `plt.cm.Dark2` same but pylint complains plt.cm `no-member` Dark2 181 182 N_sources = p["N_sources"] 183 for i_source, color in zip(range(N_sources), cycle(colors)): 184 segs = [pos[i, :, :2] for i in range(i_source, Np, N_sources)] 185 186 lc = _LineCollection( 187 segs, 188 linewidths=0.5, 189 colors=color, 190 linestyles="solid", 191 alpha=0.3, 192 ) 193 ax.add_collection(lc) 194 195 else: 196 segs = [pos[i, :, :2] for i in range(Np)] 197 198 lc = _LineCollection( 199 segs, 200 linewidths=0.5, 201 colors="0.6", 202 linestyles="solid", 203 alpha=0.5, 204 ) 205 ax.add_collection(lc) 206 207 for x, y in p["source_positions"]: 208 ax.plot(x, y, **_SOURCE_MARKER_PROPS) 209 210 ax.autoscale() # `ax.add_collection` won't do this automatically 211 ax.set_xlabel("$x$") 212 ax.set_ylabel("$y$") 213 ax.set_title(ltitle, loc="left") 214 ax.set_title(rtitle, loc="right") 215 216 fig.set_tight_layout(True) 217 218 219@_add_snippets 220def conc_scatter( 221 ds, 222 spc="apinene", 223 sdim="xy", 224 *, 225 cmap="gnuplot", 226 log_cnorm=False, 227 vmax=100, 228 vmin=None, 229 ax=None, 230): 231 """Plot species relative level in particle as a scatter (2- or 3-d). 232 233 Parameters 234 ---------- 235 %(ds_chem_params)s 236 %(spc_params)s 237 %(sdim_params)s 238 %(log_cnorm_params)s 239 """ 240 p = utils.load_p(ds) 241 242 dims = utils.dims_from_sdim(sdim) 243 if len(dims) == 3: 244 sdim = "xyz" 245 x = ds.x.values 246 y = ds.y.values 247 z = ds.z.values 248 subplot_kw = {"projection": "3d"} 249 coords = (x, y, z) 250 # plot_kw = dict(alpha=0.5, mew=0, ms=7) 251 elif len(dims) == 2: 252 x = ds[dims[0]].values 253 y = ds[dims[1]].values 254 subplot_kw = {} 255 coords = (x, y) 256 # plot_kw = dict(alpha=0.5, mfc="none", mew=0.8, ms=5) 257 else: 258 raise ValueError("invalid `sdim`") 259 260 conc = ds.f_r.sel(spc=spc).values 261 spc_display_name = str(ds.display_name.sel(spc=spc).values) 262 263 num = utils.check_fig_num(f"horizontal-end-positions-with-conc_{spc}_scatter_{sdim}") 264 fig, ax = utils.maybe_new_figure(num, ax=ax, subplot_kw=subplot_kw) 265 266 norm, _ = utils.maybe_log_cnorm(log_cnorm=log_cnorm, levels=None, vmin=vmin, vmax=vmax) 267 268 im = ax.scatter( 269 *coords, 270 c=conc, 271 s=7, 272 marker="o", 273 alpha=0.4, 274 linewidths=0, 275 cmap=cmap, 276 norm=norm, 277 ) 278 cb = fig.colorbar(im, ax=ax, drawedges=False) 279 cb.set_label(f"{spc_display_name} relative conc. (%)") 280 281 for xs, ys in p["source_positions"]: 282 sp = dict(x=xs, y=ys, z=p["release_height"]) 283 ax.plot(*tuple(sp[dim] for dim in dims), **_SOURCE_MARKER_PROPS) 284 285 ax.set_title(utils.s_t_info(p), loc="left") 286 ax.set( 287 xlabel=f"{ds[dims[0]].attrs['long_name']} [{ds[dims[0]].attrs['units']}]", 288 ylabel=f"{ds[dims[1]].attrs['long_name']} [{ds[dims[1]].attrs['units']}]", 289 ) 290 if len(dims) == 3: 291 ax.set_zlabel(f"{ds[dims[2]].attrs['long_name']} [{ds[dims[2]].attrs['units']}]") 292 fig.set_tight_layout(True) 293 294 295@_add_snippets 296def conc_2d( 297 ds, 298 spc="apinene", 299 *, 300 bins=(20, 10), 301 plot_type="pcolor", 302 levels=30, # for contourf 303 cmap="gnuplot", 304 log_cnorm=False, 305 vmax=100, 306 vmin=None, 307 ax=None, 308): 309 """Plot species 2-d (*xy*) binned average species relative level. 310 311 Parameters 312 ---------- 313 %(ds_chem_params)s 314 %(spc_params)s 315 %(bins_params)s 316 plot_type : {'pcolor', 'contourf'} 317 levels : int 318 Only used for plot type `'contourf'`. 319 %(log_cnorm_params)s 320 """ 321 p = utils.load_p(ds) 322 X = ds.x.values 323 Y = ds.y.values 324 conc = ds.f_r.sel(spc=spc).values 325 spc_display_name = str(ds.display_name.sel(spc=spc).values) 326 327 if "x" not in ds.dims: 328 # We were passed the particles dataset, need to do the binning 329 binned = utils.bin_values_xy(X, Y, conc, bins=bins) 330 else: 331 raise NotImplementedError("already binned?") 332 333 fig, ax = utils.maybe_new_figure(f"horizontal-end-positions-with-conc_{spc}_{plot_type}", ax=ax) 334 335 norm, locator = utils.maybe_log_cnorm( 336 log_cnorm=log_cnorm, 337 levels=levels if plot_type == "contourf" else None, 338 vmin=vmin, 339 vmax=vmax, 340 ) 341 342 if plot_type == "pcolor": 343 im = ax.pcolormesh(binned.xe, binned.ye, binned.v, cmap=cmap, norm=norm) 344 elif plot_type == "contourf": 345 im = ax.contourf( 346 binned.x, binned.y, binned.v, levels, cmap=cmap, norm=norm, locator=locator 347 ) 348 else: 349 raise ValueError("`plot_type` should be 'pcolor' or 'contourf'") 350 351 cb = fig.colorbar(im, ax=ax, drawedges=False) 352 cb.set_label(f"{spc_display_name} relative conc. (%)") 353 354 for x, y in p["source_positions"]: 355 ax.plot(x, y, **_SOURCE_MARKER_PROPS) 356 357 ax.autoscale(enable=True, axis="both", tight=True) 358 ax.set_title(utils.s_t_info(p), loc="left") 359 ax.set( 360 xlabel=f"{ds.x.attrs['long_name']} [{ds.x.attrs['units']}]", 361 ylabel=f"{ds.y.attrs['long_name']} [{ds.y.attrs['units']}]", 362 ) 363 fig.set_tight_layout(True) 364 365 366@_add_snippets 367def conc_xline( 368 ds, 369 spc="apinene", 370 y=0.0, 371 *, 372 dy=1.0, 373 ax=None, # TODO: select z as well? 374 legend=True, 375 label=None, 376 legend_title="Chemical species", 377): 378 r"""Plot species average relative level in the *x*-direction at a certain approximate y value. 379 `spc` can be ``'all'`` (all will fit in one plot since we are plotting lines). 380 381 Parameters 382 ---------- 383 %(ds_chem_params)s 384 %(spc_params)s 385 y : float 386 *y* value of our "line". 387 dy : float 388 We will include Lagrangian particles within $[-\delta y / 2, \delta y / 2]$. 389 """ 390 p = utils.load_p(ds) 391 X = ds.x.values 392 Y = ds.y.values 393 394 fig, ax = utils.maybe_new_figure(f"horizontal-end-positions-with-conc_{spc}_line", ax=ax) 395 396 spc_to_plot = ds.spc.values if spc == "all" else [spc] 397 398 # Define bins (edges) 399 xe = utils._auto_bins_1d(X, nx_max=100, std_mult=2.0, method="sqrt") 400 ye = np.r_[y - 0.5 * dy, y + 0.5 * dy] 401 # ze = np.r_[z - 0.5*dz, z + 0.5*dz] 402 bins = [xe, ye] 403 404 for spc in spc_to_plot: 405 spc_display_name = str(ds.display_name.sel(spc=spc).values) 406 conc = ds.f_r.sel(spc=spc).values 407 binned = utils.bin_values_xy(X, Y, conc, bins=bins) 408 ax.plot(binned.x, binned.v.squeeze(), label=spc_display_name if label is None else label) 409 410 for xs, ys in p["source_positions"]: 411 if abs(ys - y) <= 5: 412 ax.plot(xs, np.nanmin(binned.v), **_SOURCE_MARKER_PROPS) 413 414 if legend: 415 fig.legend( 416 ncol=2, 417 fontsize="small", 418 title=legend_title, 419 ) 420 ax.set_title(utils.s_t_info(p), loc="left") 421 ax.set( 422 xlabel=f"{ds.x.attrs['long_name']} [{ds.x.attrs['units']}]", 423 ylabel="Relative concentration", 424 ) 425 fig.set_tight_layout(True) 426 427 428# TODO: x-y (or u-v) hists for different height (z) bins 429 430 431@_add_snippets 432def ws_hist( 433 ds, 434 *, 435 bounds=None, 436): 437 """Plot histograms of particle wind speed components (one subplot for each). 438 For a single- or continuous-release run. 439 440 Parameters 441 ---------- 442 %(ds_lpd_params)s 443 bounds : 2-tuple(float), optional 444 If provided, used as wind speed limits for all histograms. 445 """ 446 p = utils.load_p(ds) 447 u_all = np.ravel(ds.u.values) # or can do `.ravel()` 448 v_all = np.ravel(ds.v.values) 449 w_all = np.ravel(ds.w.values) 450 451 num = utils.check_fig_num("ws-hist-all") 452 fig, axs = plt.subplots(3, 1, num=num, sharex=True) 453 454 if bounds is None: 455 bins = 100 456 else: 457 bins = np.linspace(bounds[0], bounds[1], 100) 458 459 labels = ["$u$", "$v$", "$w$"] 460 for i, (ui, ax) in enumerate(zip([u_all, v_all, w_all], axs.flat)): 461 ax.hist(ui, bins) 462 ax.text(0.01, 0.98, labels[i], va="top", ha="left", fontsize=13, transform=ax.transAxes) 463 464 if bounds: 465 axs[0].set_xlim(bounds) 466 467 axs[0].set_title(utils.s_t_info(p), loc="left") 468 axs[0].set_title(utils.s_sample_size(p), loc="right") 469 axs[0].set_xlabel(f"[{ds.u.attrs['units']}]") 470 fig.set_tight_layout(True) 471 472 473@_add_snippets 474def final_pos_hist( 475 ds, 476 *, 477 bounds=None, 478): 479 """Plot histograms of final position components (*x*, *y*, and *z*). 480 481 Parameters 482 ---------- 483 %(ds_lpd_params)s 484 bounds : 2-tuple(float), optional 485 If provided, used as position component limits for all histograms. 486 """ 487 p = utils.load_p(ds) 488 if "t" in ds.dims: 489 xf = ds.x.isel(t=-1).values 490 yf = ds.y.isel(t=-1).values 491 zf = ds.z.isel(t=-1).values 492 else: 493 xf = ds.x.values 494 yf = ds.y.values 495 zf = ds.z.values 496 497 num = utils.check_fig_num("final-pos-hist") 498 fig, axs = plt.subplots(3, 1, num=num, sharex=True) 499 500 if bounds is None: 501 bins = 100 502 else: 503 bins = np.linspace(bounds[0], bounds[1], 100) 504 505 labels = ["$x$", "$y$", "$z$"] 506 for i, (xi, ax) in enumerate(zip([xf, yf, zf], axs.flat)): 507 ax.hist(xi, bins) 508 ax.text(0.01, 0.98, labels[i], va="top", ha="left", fontsize=13, transform=ax.transAxes) 509 510 if bounds: 511 axs[0].set_xlim(bounds) 512 513 axs[0].set_title(utils.s_t_info(p), loc="left") 514 axs[0].set_title(utils.s_sample_size(p, N_p_only=True), loc="right") 515 axs[-1].set_xlabel(f"[{ds.x.attrs['units']}]") 516 fig.set_tight_layout(True) 517 518 519@_add_snippets 520def final_pos_hist2d( 521 ds, 522 *, 523 sdim="xy", 524 bins=50, 525 plot_type="pcolor", 526 log_cnorm=False, 527 vmax=None, 528): 529 """Plot 2-d histogram of selected final position components. 530 531 Parameters 532 ---------- 533 %(ds_lpd_params)s 534 %(sdim_params)s 535 %(bins_params)s 536 %(log_cnorm_params)s 537 """ 538 p = utils.load_p(ds) 539 540 dims = utils.dims_from_sdim(sdim) 541 if "t" in ds.dims: 542 x = ds[dims[0]].isel(t=-1).values 543 y = ds[dims[1]].isel(t=-1).values 544 else: 545 x = ds[dims[0]].values 546 y = ds[dims[1]].values 547 548 num = utils.check_fig_num(f"final-pos-hist-{sdim}") 549 fig, ax = plt.subplots(num=num) 550 551 if bins == "auto": 552 bins = utils.auto_bins_xy([x, y]) 553 554 if log_cnorm: 555 norm = mpl.colors.LogNorm(vmin=1.0, vmax=vmax) 556 else: 557 norm = mpl.colors.Normalize(vmin=1.0, vmax=vmax) 558 559 _, _, _, im = ax.hist2d(x, y, bins=bins, norm=norm) 560 # ^ returns h (nx, ny), xedges, yedges, image 561 562 cb = fig.colorbar(im, ax=ax) 563 cb.set_label("Particle count") 564 565 for x, y in p["source_positions"]: 566 ax.plot(x, y, "*", c="gold", ms=11, mec="0.35", mew=1.0) 567 568 ax.set_xlabel(f"${dims[0]}$ [{ds.x.attrs['units']}]") 569 ax.set_ylabel(f"${dims[1]}$ [{ds.x.attrs['units']}]") 570 ax.autoscale(enable=True, axis="both", tight=True) 571 ax.set_title(utils.s_t_info(p), loc="left") 572 ax.set_title(utils.s_sample_size(p, N_p_only=True), loc="right") 573 fig.set_tight_layout(True)
296@_add_snippets 297def conc_2d( 298 ds, 299 spc="apinene", 300 *, 301 bins=(20, 10), 302 plot_type="pcolor", 303 levels=30, # for contourf 304 cmap="gnuplot", 305 log_cnorm=False, 306 vmax=100, 307 vmin=None, 308 ax=None, 309): 310 """Plot species 2-d (*xy*) binned average species relative level. 311 312 Parameters 313 ---------- 314 %(ds_chem_params)s 315 %(spc_params)s 316 %(bins_params)s 317 plot_type : {'pcolor', 'contourf'} 318 levels : int 319 Only used for plot type `'contourf'`. 320 %(log_cnorm_params)s 321 """ 322 p = utils.load_p(ds) 323 X = ds.x.values 324 Y = ds.y.values 325 conc = ds.f_r.sel(spc=spc).values 326 spc_display_name = str(ds.display_name.sel(spc=spc).values) 327 328 if "x" not in ds.dims: 329 # We were passed the particles dataset, need to do the binning 330 binned = utils.bin_values_xy(X, Y, conc, bins=bins) 331 else: 332 raise NotImplementedError("already binned?") 333 334 fig, ax = utils.maybe_new_figure(f"horizontal-end-positions-with-conc_{spc}_{plot_type}", ax=ax) 335 336 norm, locator = utils.maybe_log_cnorm( 337 log_cnorm=log_cnorm, 338 levels=levels if plot_type == "contourf" else None, 339 vmin=vmin, 340 vmax=vmax, 341 ) 342 343 if plot_type == "pcolor": 344 im = ax.pcolormesh(binned.xe, binned.ye, binned.v, cmap=cmap, norm=norm) 345 elif plot_type == "contourf": 346 im = ax.contourf( 347 binned.x, binned.y, binned.v, levels, cmap=cmap, norm=norm, locator=locator 348 ) 349 else: 350 raise ValueError("`plot_type` should be 'pcolor' or 'contourf'") 351 352 cb = fig.colorbar(im, ax=ax, drawedges=False) 353 cb.set_label(f"{spc_display_name} relative conc. (%)") 354 355 for x, y in p["source_positions"]: 356 ax.plot(x, y, **_SOURCE_MARKER_PROPS) 357 358 ax.autoscale(enable=True, axis="both", tight=True) 359 ax.set_title(utils.s_t_info(p), loc="left") 360 ax.set( 361 xlabel=f"{ds.x.attrs['long_name']} [{ds.x.attrs['units']}]", 362 ylabel=f"{ds.y.attrs['long_name']} [{ds.y.attrs['units']}]", 363 ) 364 fig.set_tight_layout(True)
Plot species 2-d (xy) binned average species relative level.
Parameters
- ds (xr.Dataset):
Dataset of Lagrangian particle positions + chemistry, e.g., from
blpd.chem.calc_relative_levels_fix_oxidants
. - spc (str):
ASCII identifier of a chemical species. For example,
'apinene'
. - bins: If
bins='auto'
,blpd.utils.auto_bins
is used to construct the bins. Alternatively, you can pass bin specifications in any of the forms described here. plot_type ({'pcolor', 'contourf'}):
levels (int): Only used for plot type
'contourf'
.- log_cnorm (bool): Whether to use a logarithmic colormap normalization.
220@_add_snippets 221def conc_scatter( 222 ds, 223 spc="apinene", 224 sdim="xy", 225 *, 226 cmap="gnuplot", 227 log_cnorm=False, 228 vmax=100, 229 vmin=None, 230 ax=None, 231): 232 """Plot species relative level in particle as a scatter (2- or 3-d). 233 234 Parameters 235 ---------- 236 %(ds_chem_params)s 237 %(spc_params)s 238 %(sdim_params)s 239 %(log_cnorm_params)s 240 """ 241 p = utils.load_p(ds) 242 243 dims = utils.dims_from_sdim(sdim) 244 if len(dims) == 3: 245 sdim = "xyz" 246 x = ds.x.values 247 y = ds.y.values 248 z = ds.z.values 249 subplot_kw = {"projection": "3d"} 250 coords = (x, y, z) 251 # plot_kw = dict(alpha=0.5, mew=0, ms=7) 252 elif len(dims) == 2: 253 x = ds[dims[0]].values 254 y = ds[dims[1]].values 255 subplot_kw = {} 256 coords = (x, y) 257 # plot_kw = dict(alpha=0.5, mfc="none", mew=0.8, ms=5) 258 else: 259 raise ValueError("invalid `sdim`") 260 261 conc = ds.f_r.sel(spc=spc).values 262 spc_display_name = str(ds.display_name.sel(spc=spc).values) 263 264 num = utils.check_fig_num(f"horizontal-end-positions-with-conc_{spc}_scatter_{sdim}") 265 fig, ax = utils.maybe_new_figure(num, ax=ax, subplot_kw=subplot_kw) 266 267 norm, _ = utils.maybe_log_cnorm(log_cnorm=log_cnorm, levels=None, vmin=vmin, vmax=vmax) 268 269 im = ax.scatter( 270 *coords, 271 c=conc, 272 s=7, 273 marker="o", 274 alpha=0.4, 275 linewidths=0, 276 cmap=cmap, 277 norm=norm, 278 ) 279 cb = fig.colorbar(im, ax=ax, drawedges=False) 280 cb.set_label(f"{spc_display_name} relative conc. (%)") 281 282 for xs, ys in p["source_positions"]: 283 sp = dict(x=xs, y=ys, z=p["release_height"]) 284 ax.plot(*tuple(sp[dim] for dim in dims), **_SOURCE_MARKER_PROPS) 285 286 ax.set_title(utils.s_t_info(p), loc="left") 287 ax.set( 288 xlabel=f"{ds[dims[0]].attrs['long_name']} [{ds[dims[0]].attrs['units']}]", 289 ylabel=f"{ds[dims[1]].attrs['long_name']} [{ds[dims[1]].attrs['units']}]", 290 ) 291 if len(dims) == 3: 292 ax.set_zlabel(f"{ds[dims[2]].attrs['long_name']} [{ds[dims[2]].attrs['units']}]") 293 fig.set_tight_layout(True)
Plot species relative level in particle as a scatter (2- or 3-d).
Parameters
- ds (xr.Dataset):
Dataset of Lagrangian particle positions + chemistry, e.g., from
blpd.chem.calc_relative_levels_fix_oxidants
. - spc (str):
ASCII identifier of a chemical species. For example,
'apinene'
. - sdim (str):
String representation of the set of dimensions.
Valid options:
["xy", "xz", "yz", "xyz", "3d", "3-d"]
. - log_cnorm (bool): Whether to use a logarithmic colormap normalization.
367@_add_snippets 368def conc_xline( 369 ds, 370 spc="apinene", 371 y=0.0, 372 *, 373 dy=1.0, 374 ax=None, # TODO: select z as well? 375 legend=True, 376 label=None, 377 legend_title="Chemical species", 378): 379 r"""Plot species average relative level in the *x*-direction at a certain approximate y value. 380 `spc` can be ``'all'`` (all will fit in one plot since we are plotting lines). 381 382 Parameters 383 ---------- 384 %(ds_chem_params)s 385 %(spc_params)s 386 y : float 387 *y* value of our "line". 388 dy : float 389 We will include Lagrangian particles within $[-\delta y / 2, \delta y / 2]$. 390 """ 391 p = utils.load_p(ds) 392 X = ds.x.values 393 Y = ds.y.values 394 395 fig, ax = utils.maybe_new_figure(f"horizontal-end-positions-with-conc_{spc}_line", ax=ax) 396 397 spc_to_plot = ds.spc.values if spc == "all" else [spc] 398 399 # Define bins (edges) 400 xe = utils._auto_bins_1d(X, nx_max=100, std_mult=2.0, method="sqrt") 401 ye = np.r_[y - 0.5 * dy, y + 0.5 * dy] 402 # ze = np.r_[z - 0.5*dz, z + 0.5*dz] 403 bins = [xe, ye] 404 405 for spc in spc_to_plot: 406 spc_display_name = str(ds.display_name.sel(spc=spc).values) 407 conc = ds.f_r.sel(spc=spc).values 408 binned = utils.bin_values_xy(X, Y, conc, bins=bins) 409 ax.plot(binned.x, binned.v.squeeze(), label=spc_display_name if label is None else label) 410 411 for xs, ys in p["source_positions"]: 412 if abs(ys - y) <= 5: 413 ax.plot(xs, np.nanmin(binned.v), **_SOURCE_MARKER_PROPS) 414 415 if legend: 416 fig.legend( 417 ncol=2, 418 fontsize="small", 419 title=legend_title, 420 ) 421 ax.set_title(utils.s_t_info(p), loc="left") 422 ax.set( 423 xlabel=f"{ds.x.attrs['long_name']} [{ds.x.attrs['units']}]", 424 ylabel="Relative concentration", 425 ) 426 fig.set_tight_layout(True)
Plot species average relative level in the x-direction at a certain approximate y value.
spc
can be 'all'
(all will fit in one plot since we are plotting lines).
Parameters
- ds (xr.Dataset):
Dataset of Lagrangian particle positions + chemistry, e.g., from
blpd.chem.calc_relative_levels_fix_oxidants
. - spc (str):
ASCII identifier of a chemical species. For example,
'apinene'
. - y (float): y value of our "line".
- dy (float): We will include Lagrangian particles within $[-\delta y / 2, \delta y / 2]$.
474@_add_snippets 475def final_pos_hist( 476 ds, 477 *, 478 bounds=None, 479): 480 """Plot histograms of final position components (*x*, *y*, and *z*). 481 482 Parameters 483 ---------- 484 %(ds_lpd_params)s 485 bounds : 2-tuple(float), optional 486 If provided, used as position component limits for all histograms. 487 """ 488 p = utils.load_p(ds) 489 if "t" in ds.dims: 490 xf = ds.x.isel(t=-1).values 491 yf = ds.y.isel(t=-1).values 492 zf = ds.z.isel(t=-1).values 493 else: 494 xf = ds.x.values 495 yf = ds.y.values 496 zf = ds.z.values 497 498 num = utils.check_fig_num("final-pos-hist") 499 fig, axs = plt.subplots(3, 1, num=num, sharex=True) 500 501 if bounds is None: 502 bins = 100 503 else: 504 bins = np.linspace(bounds[0], bounds[1], 100) 505 506 labels = ["$x$", "$y$", "$z$"] 507 for i, (xi, ax) in enumerate(zip([xf, yf, zf], axs.flat)): 508 ax.hist(xi, bins) 509 ax.text(0.01, 0.98, labels[i], va="top", ha="left", fontsize=13, transform=ax.transAxes) 510 511 if bounds: 512 axs[0].set_xlim(bounds) 513 514 axs[0].set_title(utils.s_t_info(p), loc="left") 515 axs[0].set_title(utils.s_sample_size(p, N_p_only=True), loc="right") 516 axs[-1].set_xlabel(f"[{ds.x.attrs['units']}]") 517 fig.set_tight_layout(True)
Plot histograms of final position components (x, y, and z).
Parameters
- ds (xr.Dataset):
Dataset of Lagrangian particle positions, e.g., from
blpd.model.Model.to_xr
. - bounds (2-tuple(float), optional): If provided, used as position component limits for all histograms.
520@_add_snippets 521def final_pos_hist2d( 522 ds, 523 *, 524 sdim="xy", 525 bins=50, 526 plot_type="pcolor", 527 log_cnorm=False, 528 vmax=None, 529): 530 """Plot 2-d histogram of selected final position components. 531 532 Parameters 533 ---------- 534 %(ds_lpd_params)s 535 %(sdim_params)s 536 %(bins_params)s 537 %(log_cnorm_params)s 538 """ 539 p = utils.load_p(ds) 540 541 dims = utils.dims_from_sdim(sdim) 542 if "t" in ds.dims: 543 x = ds[dims[0]].isel(t=-1).values 544 y = ds[dims[1]].isel(t=-1).values 545 else: 546 x = ds[dims[0]].values 547 y = ds[dims[1]].values 548 549 num = utils.check_fig_num(f"final-pos-hist-{sdim}") 550 fig, ax = plt.subplots(num=num) 551 552 if bins == "auto": 553 bins = utils.auto_bins_xy([x, y]) 554 555 if log_cnorm: 556 norm = mpl.colors.LogNorm(vmin=1.0, vmax=vmax) 557 else: 558 norm = mpl.colors.Normalize(vmin=1.0, vmax=vmax) 559 560 _, _, _, im = ax.hist2d(x, y, bins=bins, norm=norm) 561 # ^ returns h (nx, ny), xedges, yedges, image 562 563 cb = fig.colorbar(im, ax=ax) 564 cb.set_label("Particle count") 565 566 for x, y in p["source_positions"]: 567 ax.plot(x, y, "*", c="gold", ms=11, mec="0.35", mew=1.0) 568 569 ax.set_xlabel(f"${dims[0]}$ [{ds.x.attrs['units']}]") 570 ax.set_ylabel(f"${dims[1]}$ [{ds.x.attrs['units']}]") 571 ax.autoscale(enable=True, axis="both", tight=True) 572 ax.set_title(utils.s_t_info(p), loc="left") 573 ax.set_title(utils.s_sample_size(p, N_p_only=True), loc="right") 574 fig.set_tight_layout(True)
Plot 2-d histogram of selected final position components.
Parameters
- ds (xr.Dataset):
Dataset of Lagrangian particle positions, e.g., from
blpd.model.Model.to_xr
. - sdim (str):
String representation of the set of dimensions.
Valid options:
["xy", "xz", "yz", "xyz", "3d", "3-d"]
. - bins: If
bins='auto'
,blpd.utils.auto_bins
is used to construct the bins. Alternatively, you can pass bin specifications in any of the forms described here. - log_cnorm (bool): Whether to use a logarithmic colormap normalization.
65@_add_snippets 66def final_pos_scatter(ds, sdim="xy"): 67 """Scatter plot of particle end positions. 68 69 Parameters 70 ---------- 71 %(ds_lpd_params)s 72 %(sdim_params)s 73 """ 74 p = utils.load_p(ds) 75 76 dims = utils.dims_from_sdim(sdim) 77 if len(dims) == 3: 78 sdim = "xyz" 79 x = ds.x.values 80 y = ds.y.values 81 z = ds.z.values 82 subplot_kw = {"projection": "3d"} 83 coords = (x, y, z) 84 plot_kw = dict(alpha=0.5, mew=0, ms=7) 85 elif len(dims) == 2: 86 x = ds[dims[0]].values 87 y = ds[dims[1]].values 88 subplot_kw = {} 89 coords = (x, y) 90 plot_kw = dict(alpha=0.5, mfc="none", mew=0.8, ms=5) 91 else: 92 raise ValueError("invalid `sdim`") 93 94 num = utils.check_fig_num(f"final-pos-scatter-{sdim}") 95 fig, ax = plt.subplots(num=num, subplot_kw=subplot_kw) 96 97 ax.plot(*coords, "o", **plot_kw) 98 99 ax.set_xlabel(f"${dims[0]}$") 100 ax.set_ylabel(f"${dims[1]}$") 101 if len(dims) == 3: 102 ax.set_zlabel(f"${dims[2]}$") 103 ax.set_title(utils.s_t_info(p), loc="left") 104 ax.set_title(f"$N_p = {p['Np_tot']}$", loc="right") 105 106 for xs, ys in p["source_positions"]: 107 sp = dict(x=xs, y=ys, z=p["release_height"]) 108 if len(dims) == 3: 109 ax.plot([sp[dims[0]]], [sp[dims[1]]], [sp[dims[2]]], "*", c="gold", ms=10) 110 else: 111 ax.plot(sp[dims[0]], sp[dims[1]], "*", c="gold", ms=10) 112 113 fig.set_tight_layout(True)
Scatter plot of particle end positions.
Parameters
- ds (xr.Dataset):
Dataset of Lagrangian particle positions, e.g., from
blpd.model.Model.to_xr
. - sdim (str):
String representation of the set of dimensions.
Valid options:
["xy", "xz", "yz", "xyz", "3d", "3-d"]
.
120@_add_snippets 121def trajectories(ds, *, smooth=False, smooth_window_size=None, color_sources=False): 122 """Plot particle trajectories. 123 124 Parameters 125 ---------- 126 %(ds_lpd_params)s 127 smooth : bool 128 Whether to smooth the trajectories. 129 smooth_window_size : int, optional 130 How many points to include in the moving average window. 131 color_sources : bool 132 Whether to color particle trajectories by their sources 133 to help in differentiating them. 134 """ 135 if "t" not in ds.dims: 136 raise ValueError("time 't' must be a dimension") 137 138 p = utils.load_p(ds) 139 pos = np.stack((ds.x.values, ds.y.values, ds.z.values), axis=-1) # same shape as hist["pos"] 140 141 t_tot = p["t_tot"] 142 dt = p["dt_out"] # note output timestep not that of the integration 143 # N_t = p["N_t"] 144 Np = p["Np_tot"] 145 # N = Np * N_t 146 assert pos.shape[0] == Np 147 148 ltitle = utils.s_t_info(p) 149 rtitle = utils.s_sample_size(p) 150 151 # allow specifying smooth_window_size only 152 if smooth_window_size is not None and not smooth: 153 smooth = True 154 if smooth: 155 if smooth_window_size is None: 156 n = 100 157 else: 158 n = int(smooth_window_size) 159 160 if n * dt > 0.5 * t_tot: 161 raise ValueError( 162 "can't do any smoothing with the requested window size (not enough points)" 163 ) 164 pos0 = pos[:, 0, :][:, np.newaxis, :] 165 pos = np.swapaxes(utils.moving_average(np.swapaxes(pos, 0, 1), n=n, axis=0), 0, 1) 166 # ^ `np.swapaxes` should return views, not create new arrays (ver >= 1.10.0) 167 168 # preserve starting point 169 pos = np.concatenate((pos0, pos), axis=1) # axis=1 => basically `hstack` 170 171 ltitle = f"$N_{{smooth}} = {n}$ ({n*dt:} s)\n{ltitle}" # add smoothing info to left title 172 173 num = utils.check_fig_num("trajectories") 174 fig, ax = plt.subplots(num=num) 175 176 if color_sources: 177 if isinstance(color_sources, (list, np.ndarray)): 178 colors = color_sources # assume is a list of colors (TODO: should check) 179 else: 180 colors = plt.get_cmap("Dark2").colors 181 # Dark2 is a ListedColormap with 8 colors; `plt.cm.Dark2` same but pylint complains plt.cm `no-member` Dark2 182 183 N_sources = p["N_sources"] 184 for i_source, color in zip(range(N_sources), cycle(colors)): 185 segs = [pos[i, :, :2] for i in range(i_source, Np, N_sources)] 186 187 lc = _LineCollection( 188 segs, 189 linewidths=0.5, 190 colors=color, 191 linestyles="solid", 192 alpha=0.3, 193 ) 194 ax.add_collection(lc) 195 196 else: 197 segs = [pos[i, :, :2] for i in range(Np)] 198 199 lc = _LineCollection( 200 segs, 201 linewidths=0.5, 202 colors="0.6", 203 linestyles="solid", 204 alpha=0.5, 205 ) 206 ax.add_collection(lc) 207 208 for x, y in p["source_positions"]: 209 ax.plot(x, y, **_SOURCE_MARKER_PROPS) 210 211 ax.autoscale() # `ax.add_collection` won't do this automatically 212 ax.set_xlabel("$x$") 213 ax.set_ylabel("$y$") 214 ax.set_title(ltitle, loc="left") 215 ax.set_title(rtitle, loc="right") 216 217 fig.set_tight_layout(True)
Plot particle trajectories.
Parameters
- ds (xr.Dataset):
Dataset of Lagrangian particle positions, e.g., from
blpd.model.Model.to_xr
. - smooth (bool): Whether to smooth the trajectories.
- smooth_window_size (int, optional): How many points to include in the moving average window.
- color_sources (bool): Whether to color particle trajectories by their sources to help in differentiating them.
432@_add_snippets 433def ws_hist( 434 ds, 435 *, 436 bounds=None, 437): 438 """Plot histograms of particle wind speed components (one subplot for each). 439 For a single- or continuous-release run. 440 441 Parameters 442 ---------- 443 %(ds_lpd_params)s 444 bounds : 2-tuple(float), optional 445 If provided, used as wind speed limits for all histograms. 446 """ 447 p = utils.load_p(ds) 448 u_all = np.ravel(ds.u.values) # or can do `.ravel()` 449 v_all = np.ravel(ds.v.values) 450 w_all = np.ravel(ds.w.values) 451 452 num = utils.check_fig_num("ws-hist-all") 453 fig, axs = plt.subplots(3, 1, num=num, sharex=True) 454 455 if bounds is None: 456 bins = 100 457 else: 458 bins = np.linspace(bounds[0], bounds[1], 100) 459 460 labels = ["$u$", "$v$", "$w$"] 461 for i, (ui, ax) in enumerate(zip([u_all, v_all, w_all], axs.flat)): 462 ax.hist(ui, bins) 463 ax.text(0.01, 0.98, labels[i], va="top", ha="left", fontsize=13, transform=ax.transAxes) 464 465 if bounds: 466 axs[0].set_xlim(bounds) 467 468 axs[0].set_title(utils.s_t_info(p), loc="left") 469 axs[0].set_title(utils.s_sample_size(p), loc="right") 470 axs[0].set_xlabel(f"[{ds.u.attrs['units']}]") 471 fig.set_tight_layout(True)
Plot histograms of particle wind speed components (one subplot for each). For a single- or continuous-release run.
Parameters
- ds (xr.Dataset):
Dataset of Lagrangian particle positions, e.g., from
blpd.model.Model.to_xr
. - bounds (2-tuple(float), optional): If provided, used as wind speed limits for all histograms.