Source code for bldfm.cli

"""
Command-line interface for BLDFM.

Usage:
    bldfm run config.yaml
    bldfm run config.yaml --dry-run
    bldfm run config.yaml --plot
"""

import argparse
import os
import sys

from . import initialize
from .config_parser import load_config
from .interface import run_bldfm_single
from abltk.logging import get_logger


[docs] def cmd_run(args): """Execute a BLDFM run from a YAML config file.""" initialize() logger = get_logger("cli") config = load_config(args.config) logger.info(f"Loaded config: {args.config}") logger.info( f" Domain: {config.domain.nx}x{config.domain.ny}, nz={config.domain.nz}" ) logger.info(f" Towers: {len(config.towers)}") logger.info(f" Timesteps: {config.met.n_timesteps}") if args.dry_run: logger.info("Dry run complete. Config is valid.") for tower in config.towers: logger.info( f" Tower '{tower.name}': lat={tower.lat}, lon={tower.lon}, " f"z_m={tower.z_m}, x={tower.x:.1f}, y={tower.y:.1f}" ) return # Apply runtime settings from . import config as runtime_config runtime_config.NUM_THREADS = config.parallel.num_threads runtime_config.MAX_WORKERS = config.parallel.max_workers runtime_config.USE_CACHE = config.parallel.use_cache # Run for each tower and timestep results = [] for tower in config.towers: logger.info(f"Running tower '{tower.name}'...") for t in range(config.met.n_timesteps): result = run_bldfm_single(config, tower, met_index=t) results.append(result) logger.info(f" t={result['timestamp']}: done") logger.info("All runs complete.") if args.plot: _save_plots(results, config, logger)
def _save_plots(results, config, logger): """Save concentration and flux/footprint PNGs for each result.""" import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt os.makedirs("plots", exist_ok=True) is_footprint = config.solver.footprint for result in results: name = result["tower_name"] ts = result["timestamp"] conc = result["conc"] flx = result["flx"] grid = result["grid"] tx, ty = result["tower_xy"] is_3d = conc.ndim == 3 if is_3d: _save_3d_plots(plt, conc, flx, grid, name, ts, logger) else: _save_2d_plots(plt, conc, flx, grid, tx, ty, name, ts, is_footprint, logger) def _save_2d_plots(plt, conc, flx, grid, tx, ty, name, ts, is_footprint, logger): """Save 2D concentration and flux plots.""" from abltk.plotting import plot_footprint_field # Concentration fname = f"plots/concentration_{name}_t{ts}.png" fig, ax = plt.subplots() plot_footprint_field(conc, grid, ax=ax, title=f"Concentration — {name} (t={ts})") ax.plot( tx, ty, "r*", markersize=12, markeredgecolor="black", markeredgewidth=0.8, zorder=5, ) fig.savefig(fname, dpi=150, bbox_inches="tight") plt.close(fig) logger.info(f" Saved {fname}") # Flux / footprint flx_label = "Footprint" if is_footprint else "Kinematic flux" fname = f"plots/flux_{name}_t{ts}.png" fig, ax = plt.subplots() plot_footprint_field( flx, grid, ax=ax, contour_pcts=[0.5, 0.8] if is_footprint else None, title=f"{flx_label}{name} (t={ts})", ) ax.plot( tx, ty, "r*", markersize=12, markeredgecolor="black", markeredgewidth=0.8, zorder=5, ) fig.savefig(fname, dpi=150, bbox_inches="tight") plt.close(fig) logger.info(f" Saved {fname}") def _save_3d_plots(plt, conc, flx, grid, name, ts, logger): """Save horizontal and vertical slice plots for 3D results.""" from abltk.plotting import plot_vertical_slice ny = conc.shape[1] nx = conc.shape[2] # Horizontal slice at z=0 fname = f"plots/{name}_t{ts}_xy_slice_z0.png" fig, axes = plt.subplots(1, 2, figsize=(14, 5)) plot_vertical_slice(conc, grid, "z", 0, ax=axes[0], title="Concentration at z0") plot_vertical_slice(flx, grid, "z", 0, ax=axes[1], title="Flux at z0") fig.savefig(fname, dpi=150, bbox_inches="tight") plt.close(fig) logger.info(f" Saved {fname}") # Vertical xz slice at y midpoint fname = f"plots/{name}_t{ts}_xz_slice.png" fig, axes = plt.subplots(1, 2, figsize=(14, 5)) plot_vertical_slice( conc, grid, "y", ny // 2, ax=axes[0], title="Concentration (xz slice)" ) plot_vertical_slice(flx, grid, "y", ny // 2, ax=axes[1], title="Flux (xz slice)") fig.savefig(fname, dpi=150, bbox_inches="tight") plt.close(fig) logger.info(f" Saved {fname}") # Vertical yz slice at x midpoint fname = f"plots/{name}_t{ts}_yz_slice.png" fig, axes = plt.subplots(1, 2, figsize=(14, 5)) plot_vertical_slice( conc, grid, "x", nx // 2, ax=axes[0], title="Concentration (yz slice)" ) plot_vertical_slice(flx, grid, "x", nx // 2, ax=axes[1], title="Flux (yz slice)") fig.savefig(fname, dpi=150, bbox_inches="tight") plt.close(fig) logger.info(f" Saved {fname}")
[docs] def main(): """Main CLI entry point.""" parser = argparse.ArgumentParser( prog="bldfm", description="BLDFM - Boundary Layer Footprint Dispersion Model", ) subparsers = parser.add_subparsers(dest="command") # 'run' subcommand run_parser = subparsers.add_parser("run", help="Run BLDFM from a YAML config file") run_parser.add_argument("config", help="Path to YAML configuration file") run_parser.add_argument( "--dry-run", action="store_true", help="Validate config without running the solver", ) run_parser.add_argument( "--plot", action="store_true", help="Save footprint plots to plots/" ) args = parser.parse_args() if args.command is None: parser.print_help() sys.exit(1) if args.command == "run": cmd_run(args)
if __name__ == "__main__": main()