The tests subpackage

All tests can be run with:

$ pytest -v tests/

Tests are organised into five categories using pytest marks:

$ pytest -v -m unit tests/          # fast, no solver
$ pytest -v -m integration tests/   # solver-dependent
$ pytest -v -m parallel tests/      # multiprocessing
$ pytest -v -m plotting tests/      # matplotlib / plotly
$ pytest -v -m regression tests/    # compare against saved references

Unit tests (unit)

tests.test_pbl_model module

This module contains unit tests for the src.pbl_model.vertical_profiles() function in the src.pbl_model module. The tests validate the behavior of the function under different closure schemes, including:

  • CONSTANT Closure: Ensures the profiles for velocity and eddy diffusivity are constant and match expected values.

  • MOST (Monin-Obukhov Similarity Theory) Closure: Verifies that the profiles vary with height and are physically reasonable.

The tests use parameterized inputs to check the function’s output shapes, values, and physical consistency across various scenarios.

tests.test_pbl_model.test_both_z0_and_ustar_raises()[source]

Test that providing both z0 and ustar raises ValueError.

tests.test_pbl_model.test_constant_closure(n, meas_height, wind, ustar, prsc)[source]

Test the CONSTANT closure for scalar inputs.

Raises:

AssertionError – If the output shapes do not match the expected dimensions or if the profiles are not constant.

tests.test_pbl_model.test_invalid_closure_raises()[source]

Test that an invalid closure type raises ValueError.

tests.test_pbl_model.test_most_closure(n, meas_height, wind, ustar, mol)[source]

Test the MOST closure for scalar inputs.

Raises:

AssertionError – If the output shapes do not match the expected dimensions or if the profiles are not physically reasonable.

tests.test_pbl_model.test_mostm_closure()[source]

Test the MOSTM (modified MOST) closure has anisotropic diffusion.

tests.test_pbl_model.test_oaahoc_closure()[source]

Test the OAAHOC (one-and-a-half order) closure with explicit TKE.

tests.test_pbl_model.test_oaahoc_closure_default_tke()[source]

Test that OAAHOC defaults TKE to 1.0 when not provided.

tests.test_pbl_model.test_stretch_and_domain_height()[source]

Test custom stretch and domain_height parameters.

tests.test_pbl_model.test_z0_only_path()[source]

Test that providing z0 without ustar derives ustar from z0.

tests.test_config module

Tests for config_parser module.

tests.test_config.test_domain_full_output()[source]

Test that full_output in domain config round-trips correctly.

tests.test_config.test_domain_full_output_default()[source]

Test that full_output defaults to False when not specified.

tests.test_config.test_domain_output_levels()[source]

Test that output_levels in domain config round-trips correctly.

tests.test_config.test_domain_output_levels_default()[source]

Test that output_levels defaults to None when not specified.

tests.test_config.test_latlon_to_xy(lat, lon, ref_lat, ref_lon, exp_x, exp_y, tol)[source]
tests.test_config.test_load_config_file_not_found()[source]
tests.test_config.test_load_config_from_yaml()[source]
tests.test_config.test_met_config_get_step_scalar()[source]
tests.test_config.test_met_config_get_step_timeseries()[source]
tests.test_config.test_met_config_mixed_lengths_raises()[source]
tests.test_config.test_met_config_scalar()[source]
tests.test_config.test_met_config_timeseries()[source]
tests.test_config.test_met_z0()[source]

Test that z0 in met config appears in get_step() result.

tests.test_config.test_met_z0_default()[source]

Test that z0 is not in get_step() when not set.

tests.test_config.test_parse_config_missing_domain()[source]
tests.test_config.test_parse_config_missing_towers()[source]
tests.test_config.test_parse_minimal_config()[source]
tests.test_config.test_solver_analytic()[source]

Test that analytic field in solver config round-trips correctly.

tests.test_config.test_solver_defaults_new_fields()[source]

Test that new solver fields default correctly when not specified.

tests.test_config.test_solver_src_loc()[source]

Test that src_loc in solver config is converted to tuple.

tests.test_config.test_tower_compute_local_xy()[source]

tests.test_synthetic module

Tests for synthetic data generators.

class tests.test_synthetic.TestSyntheticTimeseries[source]

Bases: object

test_compatible_with_met_config()[source]

Output dict should be valid input to MetConfig.

test_reproducibility()[source]
test_timestamps_format()[source]
test_value_ranges()[source]
class tests.test_synthetic.TestTowersGrid[source]

Bases: object

test_compatible_with_tower_config()[source]

Output dicts should be valid input to TowerConfig.

test_invalid_layout_raises()[source]
test_reproducibility()[source]
test_tower_output_structure()[source]

Test tower count, dict keys, and name uniqueness.

tests.test_cache module

Tests for Green’s function caching.

tests.test_cache.cache(tmp_path)[source]
tests.test_cache.solver_inputs()[source]

Minimal inputs matching what the cache hashes.

tests.test_cache.test_cache_clear(cache, solver_inputs)[source]
tests.test_cache.test_cache_different_inputs_miss(cache, solver_inputs)[source]
tests.test_cache.test_cache_integration()[source]

Test caching through the full solver with footprint=True.

tests.test_cache.test_cache_miss(cache, solver_inputs)[source]
tests.test_cache.test_cache_put_and_hit(cache, solver_inputs)[source]

Integration tests (integration)

tests.test_integration module

This module contains an integration test for the interaction between the pbl_model, utils, and solver components.

The test validates the combined behavior of these components by comparing the numerical and analytical solutions for the steady-state advection-diffusion equation. Currently, this test is similar to combining the pbl_model and solver tests, but it is designed to serve as a foundation for future extensions.

Functions:

  • test_integration: Tests the combined behavior of the pbl_model, utils, and solver components.

tests.test_integration.test_3d_plume_structure(plume_3d_result_session)[source]

Test 3D plume solver output: shapes, finite values, plume structure.

tests.test_integration.test_comparison_neutral_bldfm_broader()[source]

Verify BLDFM footprint is spatially broader than KM01.

Breadth is measured by counting grid cells above 10% of the respective model’s peak value. BLDFM is expected to be broader because its PBL diffusion scheme accounts for along-wind diffusion that KM01 neglects. Saves a side-by-side contour plot for visual verification.

tests.test_integration.test_comparison_neutral_both_nonnegative()[source]

Verify both BLDFM and KM01 produce nearly non-negative footprint values.

KM01 is analytical and strictly non-negative by construction (only upwind cells with x > 0 are filled). BLDFM uses an FFT-based solver whose MOST diffusivity profile introduces Gibbs ringing near the measurement-point singularity, producing negatives up to ~1e-4. The tolerance -1e-4 catches genuine solver divergence while accepting this known artefact. Prints minimum values of both for inspection.

tests.test_integration.test_comparison_neutral_mass_agreement()[source]

Verify both models integrate to within a factor of 3 of each other.

Each integral is expected to lie in [0.3, 1.2] — a wide window that accounts for the finite domain capturing only part of the footprint. The ratio check ensures neither model wildly over- or under-estimates the total footprint weight relative to the other. Note: KM01 already includes grid_res**2 per cell (see ffm_kormann_meixner line 321), so np.sum(km01_ffm) is directly the dimensionless integral.

tests.test_integration.test_comparison_neutral_peak_proximity()[source]

Verify BLDFM and KM01 peak locations agree within 50 m.

Both peaks must also be upwind of the measurement point (y > meas_y) for wind=(0, -5). The 50 m tolerance is generous given the different physical assumptions of the two models. Prints both peak coordinates and the Euclidean distance between them.

tests.test_integration.test_convergence_trend()[source]

Verify that numerical error decreases monotonically with resolution.

Runs the solver at 3 resolution levels against the analytic solution (CONSTANT closure) and asserts that relative MSE decreases at each refinement step.

tests.test_integration.test_footprint_finite_values()[source]

Verify that all footprint flux and concentration values are finite.

Runs the solver in footprint mode with default parameters and asserts that neither NaN nor Inf appear anywhere in the output arrays. Prints shape, dtype, and value range for quick sanity inspection.

tests.test_integration.test_footprint_mass_conservation_constant()[source]

Verify footprint integrates to a reasonable fraction with CONSTANT closure.

Uses a larger domain and halo so that a substantial fraction of the footprint falls within the window. The acceptable range (0.25, 1.05] accounts for the finite domain capturing only a partial footprint — the true integral over an infinite domain is 1, but at this resolution and domain size a significant portion escapes through the upwind boundary. Prints the raw integral for inspection.

tests.test_integration.test_footprint_mass_conservation_most()[source]

Verify footprint integrates to a reasonable fraction with MOST closure (neutral).

Same domain and halo as the CONSTANT test, but uses a roughness length (z0=0.5) and neutral Monin-Obukhov length (mol=1e9) to exercise the MOST code path. The acceptable range (0.25, 1.05] is identical to the CONSTANT test — the finite domain captures a substantial but incomplete portion of the footprint.

tests.test_integration.test_footprint_peak_upwind_southward()[source]

Verify footprint peak is upwind when wind blows southward (v < 0).

With wind=(0, -5) the flow moves in the -y direction, so the upwind surface contributing to the receptor is at y > meas_y. Saves a plot showing the footprint field with the measurement point (red star) and detected peak (blue cross) annotated for visual verification.

tests.test_integration.test_footprint_peak_upwind_westward()[source]

Verify footprint peak is upwind when wind blows westward (u < 0).

With wind=(-5, 0) the flow moves in the -x direction, so the upwind surface is at x > meas_x. The domain is rotated (elongated in x) so that the footprint has room to develop along the wind axis. Saves a plot with measurement point and peak annotated.

tests.test_integration.test_footprint_positivity_constant()[source]

Verify footprint flux is non-negative everywhere with CONSTANT closure.

FFT roundoff can produce tiny negative values; the tolerance -1e-15 accounts for this without masking genuine negativity bugs. Prints the minimum value and fraction of cells with positive weight.

tests.test_integration.test_footprint_positivity_most()[source]

Verify footprint flux has only small negatives with MOST closure (neutral).

The MOST closure uses a non-constant diffusivity profile which can cause Gibbs/FFT ringing at the measurement-point singularity, producing negative values of order ~1e-5. The tolerance -1e-4 accepts this known numerical artefact while still rejecting physically meaningless large negatives. Neutral conditions (mol=1e9) are used as the baseline case.

tests.test_integration.test_integration()[source]

Tests the combined behavior of the pbl_model, utils, and solver components.

The test validates:
  • The interaction between the components.

  • The numerical accuracy of the solver by comparing its results to the analytical solution within a specified tolerance.

Raises:

AssertionError – If the numerical and analytical solutions differ beyond the specified tolerance.

tests.test_integration.test_solver_double_precision()[source]

Verify double precision path produces float64 output.

tests.test_integration.test_solver_halo_overflow()[source]

Verify solver handles modes exceeding grid+halo gracefully.

tests.test_integration.test_solver_invalid_precision_raises()[source]

Verify invalid precision raises ValueError.

tests.test_integration.test_solver_non_footprint_shift()[source]

Verify non-footprint mode with non-zero meas_pt (shift path).

tests.test_integration.test_solver_odd_modes_raises()[source]

Verify odd modes raise ValueError.

tests.test_integration.test_solver_single_precision()[source]

Verify single precision path produces float32 output.

tests.test_interface module

Tests for the high-level interface module.

tests.test_interface.test_multitower_structure(multitower_results_session)[source]

Test multitower output: dict keyed by tower names, all timesteps, different footprints.

tests.test_interface.test_single_run_structure(single_run_result, simple_config_session, source_area_result_session)[source]

Test output structure, shapes, types, and metadata from a single run.

tests.test_interface.test_single_run_with_synthetic_data()[source]

Integration test: synthetic data -> config -> run.

tests.test_interface.test_timeseries_aggregated_footprint(timeseries_results_session)[source]

Test aggregated mean footprint with 50% and 70% flux contribution contours.

tests.test_interface.test_timeseries_footprints_evolve(timeseries_results_session, timeseries_config_session)[source]

Test that changing met conditions produce different footprints at each timestep.

tests.test_interface.test_timeseries_met_params_vary(timeseries_results_session, timeseries_config_session)[source]

Test that time-varying ustar, mol, wind_dir, wind_speed propagate to each result.

tests.test_interface.test_timeseries_structure(timeseries_results_session, timeseries_config_session)[source]

Test timeseries output: list structure, keys, unique timestamps, finite values.

tests.test_io module

Tests for NetCDF I/O.

tests.test_io.test_load_nonexistent_raises()[source]
tests.test_io.test_netcdf_3d_roundtrip()[source]

Test save/load roundtrip with 3D output fields (z dimension).

tests.test_io.test_netcdf_global_attrs(multitower_results_session)[source]

Test that global attributes capture domain and solver config.

tests.test_io.test_netcdf_met_values_match(multitower_results_session)[source]

Test that saved met values match the original result params exactly.

tests.test_io.test_netcdf_metadata(multitower_results_session)[source]

Test CF attributes, met variables, and tower metadata.

tests.test_io.test_netcdf_multitower_select_by_name(multitower_results_session)[source]

Test that individual tower data can be selected by name and matches.

tests.test_io.test_netcdf_roundtrip_and_values(multitower_results_session)[source]

Test save/load roundtrip and verify values match original.

tests.test_io.test_netcdf_single_tower_timeseries(timeseries_results_session, timeseries_config_session)[source]

Test save/load roundtrip for a single tower timeseries.

tests.test_io.test_netcdf_timeseries_timestamps(multitower_results_session)[source]

Test that timestamps are stored correctly and data varies across time.

tests.test_io.test_netcdf_tower_metadata_match(multitower_results_session)[source]

Test that tower lat/lon/z and names match config exactly.

Parallel tests (parallel)

tests.test_parallel module

Tests for parallel execution.

tests.test_parallel.parallel_config()[source]

Config with 2 towers x 2 timesteps for parallel tests.

tests.test_parallel.test_parallel_invalid_strategy(parallel_config)[source]
tests.test_parallel.test_parallel_matches_serial(parallel_config, strategy)[source]
tests.test_parallel.test_parallel_result_structure(parallel_config)[source]
tests.test_parallel.track_pool()[source]

Patch ProcessPoolExecutor to print diagnostics after each test.

Plotting tests (plotting)

tests.test_plotting module

Tests for the plotting module.

tests.test_plotting.test_extract_percentile_contour_3d_input(plume_3d_result_session)[source]

extract_percentile_contour auto-slices 3D input.

tests.test_plotting.test_get_source_area_basic()[source]

Test that get_source_area returns correct shape and value range.

tests.test_plotting.test_get_source_area_monotone()[source]

Test that higher g values map to lower rescaled values.

tests.test_plotting.test_maybe_slice_level_2d_passthrough()[source]

2D field and grid pass through unchanged.

tests.test_plotting.test_maybe_slice_level_3d_slicing()[source]

3D field is sliced correctly at the given level.

tests.test_plotting.test_percentile_contour_properties(footprint_result_session)[source]

Test that percentile contours return valid floats with monotonic area.

tests.test_plotting.test_plot_convergence()[source]

Test log-log convergence plot with and without fits.

tests.test_plotting.test_plot_field_comparison()[source]

Test 2x2 field comparison with synthetic data.

tests.test_plotting.test_plot_footprint_comparison(source_area_result_session, footprint_result_session)[source]

Test multi-panel comparison: source area footprint vs concentration.

tests.test_plotting.test_plot_footprint_field_3d_input(plume_3d_result_session)[source]

plot_footprint_field auto-slices 3D input at requested level.

tests.test_plotting.test_plot_footprint_field_variants(footprint_result_session)[source]

Test footprint field plot: basic, with contours, and on custom axes.

tests.test_plotting.test_plot_footprint_interactive(footprint_result_session)[source]
tests.test_plotting.test_plot_footprint_on_map_happy_path(footprint_result_session)[source]

Test map plot with contextily tiles (requires network + contextily).

tests.test_plotting.test_plot_footprint_on_map_import_error(footprint_result_session)[source]

Should raise ImportError with helpful message if contextily missing.

tests.test_plotting.test_plot_footprint_on_map_land_cover(footprint_result_session)[source]

Test land cover overlay using the real ESA Terrascope WMS.

tests.test_plotting.test_plot_footprint_on_map_land_cover_import_error(footprint_result_session)[source]

Should raise ImportError with helpful message if owslib missing.

tests.test_plotting.test_plot_footprint_timeseries(timeseries_results_session)[source]

Test temporal evolution plot using the shared timeseries fixture.

tests.test_plotting.test_plot_source_area_contours(source_area_result_session)[source]

Test source area contour plotting returns axes.

tests.test_plotting.test_plot_source_area_contours_custom_ax(source_area_result_session)[source]

Test source area contour plotting on provided axes.

Test gallery plot creates 2x3 grid with 5 visible panels.

Uses an elongated domain (100x700m) matching the high-res example in runs/low_level/source_area_example.py but at 128x64 resolution.

tests.test_plotting.test_plot_vertical_profiles()[source]

Test vertical profile plot using real PBL profiles.

tests.test_plotting.test_plot_vertical_slice(plume_3d_result_session)[source]

Test 2D slice from a real 3D plume: concentration and flux side by side.

tests.test_plotting.test_plot_wind_rose_happy_path()[source]

Test wind rose plot with synthetic data (requires windrose).

tests.test_plotting.test_plot_wind_rose_import_error()[source]

Should raise ImportError with helpful message if windrose missing.

tests.test_plotting.test_source_area_base_functions_shapes(source_area_result_session)[source]

Test that all 5 base function constructors return correct shapes.

Regression tests (regression)

Regression tests compare solver outputs against saved .npz reference files in tests/references/. They catch silent numerical drift that property-based tests (positivity, mass conservation) would miss.

Five scenarios are checked: single_footprint, source_area, plume_3d, timeseries_step0, and timeseries_step2. All use tolerances of atol=1e-6, rtol=1e-5 (MOST closure).

To regenerate baselines after intentional solver changes:

$ pytest --update-references tests/test_regression.py \
      tests/test_integration.py tests/test_interface.py
$ git add tests/references/

Note

The integration and interface test files must be included so that the session-scoped solver fixtures are triggered.

tests.test_regression module

Regression tests: compare solver outputs against saved reference data.

Run with:

pytest -m regression # check against references pytest –update-references -m regression # regenerate references

tests.test_regression.test_regression_plume_3d(plume_3d_result_session, update_references)[source]
tests.test_regression.test_regression_single_footprint(single_run_result, update_references)[source]
tests.test_regression.test_regression_source_area(source_area_result_session, update_references)[source]
tests.test_regression.test_regression_timeseries_step0(timeseries_results_session, update_references)[source]
tests.test_regression.test_regression_timeseries_step2(timeseries_results_session, update_references)[source]