Repository for NIST WUI manufactured home indoor air quality (IAQ) study Python tools. Includes scripts for particulate matter concentration analysis, Clean Air Delivery Rate (CADR) calculations, spatial variation quantification, instrument validation, and statistical modeling of wildfire smoke infiltration events in a manufactured home test structure.
This project analyzes indoor air quality during simulated wildfire smoke infiltration events in the NIST Indoor Air Quality (IAQ) Test House. The research investigates the effectiveness of various mitigation strategies for protecting indoor environments from wildfire smoke:
- Particulate matter (PM) concentration dynamics during smoke infiltration events
- Clean Air Delivery Rate (CADR) calculations for air cleaning devices
- Spatial variation in PM concentrations across different rooms
- Particle size distribution analysis (0.3 μm to 25 μm)
- Decay rate characterization for different mitigation strategies
- Filter performance evaluation (MERV 12A and MERV 13)
- Compartmentalization strategy comparison
Certain commercial equipment, instruments, software, or materials are identified in this repository in order to specify the experimental and analytical procedures adequately. Such identification is not intended to imply recommendation or endorsement of any product or service by NIST, nor is it intended to imply that the materials or equipment identified are necessarily the best available for the purpose.
NIST_wui_mh_iaq/
├── .vscode/ # VS Code configuration
│ └── settings.json
├── .gitignore
├── CODEMETA.yaml # NIST Software Portal metadata
├── CODEOWNERS # Repository ownership for PR reviews
├── LICENSE.md # NIST software licensing statement
├── README.md
├── data_config.json # Active configuration (gitignored)
├── data_config.template.json # Configuration template
├── wui.yml # Conda environment specification
│
├── src/ # Core analysis scripts
│ ├── __init__.py # Package initialization
│ ├── data_paths.py # Portable data access via data_config.json
│ ├── fig_style.py # Shared ES&T Air figure styling (Section 3.2)
│ │
│ │ # CADR (Clean Air Delivery Rate) Analysis
│ ├── clean_air_delivery_rates_update.py
│ ├── clean_air_delivery_rates_barchart.py
│ ├── clean_air_delivery_rates_pmsizes.py
│ ├── clean_air_delivery_rates_pmsizes_SIUniformaty.py
│ ├── clean_air_delivery_rates_vs_total_surface_area.py
│ ├── cadr_comparison_statistical_analysis.py
│ │
│ │ # Compartmentalization & Mitigation Strategy Analysis
│ ├── compartmentalization_strategy_comparison.py
│ ├── decay_rate_barchart.py
│ │
│ │ # Concentration Dynamics
│ ├── conc_increase_to_decrease.py
│ ├── peak_concentration_script.py
│ ├── bracketing.py
│ ├── co2_decay_comparison.py
│ │
│ │ # Instrument Behavior Characterization
│ ├── aerotrak_coincidence.py
│ ├── modulair_5sec_io.py
│ ├── modulair_5sec_peak_window.py
│ ├── modulair_5sec_post_peak.py
│ │
│ │ # Spatial Variation Analysis
│ ├── spatial_variation_analysis.py
│ ├── spatial_variation_analysis_plot.py
│ ├── spatial_variation_analysis_plot_timeseries.py
│ │
│ │ # Instrument Comparison & Validation
│ ├── dusttrak-rh_comparison.py
│ ├── purpleair_comparison.py
│ ├── quantaq_pm2.5_burn8.py
│ ├── general_particle_count_comparison.py
│ ├── aham_ac1_comparison.py
│ ├── temp-rh_comparison.py
│ │
│ │ # SMPS (Scanning Mobility Particle Sizer) Analysis
│ ├── smps_filterperformance.py
│ ├── smps_finepm_comparison.py
│ ├── smps_heatmap.py
│ ├── smps_mass_vs_conc.py
│ ├── smps_size_bin_barchart.py
│ ├── smps_size_distribution_comparison.py
│ ├── smps_decay_report.py
│ ├── smps_reversal_crosscheck.py
│ │
│ │ # Data Processing & Utilities
│ ├── remove_aerotrak_dup_data.py
│ ├── mh_relay_control_log.py
│ ├── toc_figure_script.py # Publication TOC figure generator
│ │
│ │ # Data Release (MIDAS)
│ └── data_release/ # Builds the public tidy-CSV PM data release
│ ├── __init__.py
│ ├── build_release.py # Orchestrator: run exporters, write CSVs + manifest
│ ├── release_config.py # Instrument registry (single source of truth)
│ ├── exporters.py # Per-instrument tidy-schema exporters
│ ├── burns.py # Burn-date bookkeeping and per-row burn tagging
│ ├── writers.py # Standard-column CSV writer (no silent overwrite)
│ └── readme.py # README_dataset.md generator
│
├── scripts/ # Reusable utility modules
│ ├── __init__.py
│ ├── datetime_utils.py # DateTime handling and time synchronization
│ ├── data_filters.py # Data filtering and quality control
│ ├── statistical_utils.py # Curve fitting and statistical analysis
│ ├── plotting_utils.py # Standardized Bokeh figure creation
│ ├── instrument_config.py # Instrument configs and bin definitions
│ ├── data_loaders.py # Instrument-specific data loading
│ ├── spatial_analysis_utils.py # Spatial variability calculations
│ ├── metadata_utils.py # Script metadata and provenance strings
│ ├── export_smps_total_concentration.py
│ ├── test_utilities.py # Utility module tests
│ └── README.md # Detailed module documentation
│
└── testing/ # Diagnostic and testing scripts
├── diagnostic_hourly_ratios.py
├── diagnostic_spatial_variation.py
├── diagnostic_timestamp_alignment.py
└── quantaq_5sec_timeseries.py
The study conducted multiple controlled burn experiments (Burn 1–10) in a manufactured home test structure with the following instrumentation:
Monitoring Instruments:
- AeroTrak 9510 — Optical particle counters in bedroom and kitchen/morning room
- QuantAQ MODULAIR-PM — Low-cost PM sensors in multiple locations
- TSI DustTrak DRX 8533 — Real-time aerosol monitor
- TSI SMPS 3938 (Scanning Mobility Particle Sizer) — Ultrafine particle size distribution (9–437 nm)
- PurpleAir PA-II-SD — Community-grade PM sensors
- Vaisala — Temperature and relative humidity sensors
- Aranet4 — CO₂ sensors in Bedroom, Entry, and MH Outside locations
Measured Pollutants:
- PM₀.₅, PM₁, PM₂.₅, PM₃, PM₄, PM₅, PM₁₀, PM₁₅, PM₂₅ (μg/m³)
- Particle number concentrations (#/cm³)
- Particle size distributions
Mitigation Strategies Tested:
- Portable air cleaners with MERV filtration (various CADR ratings)
- Central HVAC systems with MERV 12A and MERV 13 filters
- Room compartmentalization (closed bedroom with/without filtration)
- Multiple filter configurations (1 and 2 filters analyzed; 4-filter configuration excluded due to data quality issues)
- Python 3.13+
- Conda (recommended for environment management)
-
Clone the repository:
git clone https://github.com/usnistgov/WUI_MH_IAQ.git cd WUI_MH_IAQ -
Create the conda environment:
conda env create -f wui.yml conda activate wui
-
Configure data paths:
cp data_config.template.json data_config.json
Edit
data_config.jsonto set the correctdata_rootpath for your machine:{ "machine_name": "YourMachineName", "data_root": "C:/path/to/your/WUI_smoke", "instruments": { ... } }See
data_config.template.jsonfor the complete structure with all instruments. -
Verify configuration:
python -c "from src.data_paths import resolver; resolver.list_instruments()"
Note: data_config.json is excluded from version control (.gitignore). Each user maintains their own local configuration.
Scripts in src/ can be run from the command line or interactively in Jupyter/VS Code:
# Run CADR analysis
python src/clean_air_delivery_rates_update.py
# Run spatial variation analysis
python src/spatial_variation_analysis.py
python src/spatial_variation_analysis_plot.pyEnsure your data files are organized as follows:
WUI_smoke/
├── burn_log.xlsx
├── burn_data/
│ ├── aerotraks/
│ │ ├── bedroom2/
│ │ │ └── all_data.xlsx
│ │ └── kitchen/
│ │ └── all_data.xlsx
│ ├── quantaq/
│ │ ├── MOD-PM-00194-*.csv # QuantAQ bedroom
│ │ └── MOD-PM-00197-*.csv # QuantAQ kitchen
│ ├── dusttrak/
│ │ └── *.xlsx
│ ├── smps/
│ │ └── *.txt
│ ├── purpleair/
│ │ └── *.csv
│ ├── miniams/
│ │ └── *.csv
│ ├── vaisala_th/
│ │ └── *.xlsx
│ ├── co2/
│ │ └── *.csv
│ └── relaycontrol/
│ └── *.txt
├── burn_dates_decay_aerotraks_bedroom.xlsx
├── burn_dates_decay_aerotraks_kitchen.xlsx
├── burn_dates_decay_smps.xlsx
├── peak_concentrations_all_instruments.xlsx
├── spatial_variation_analysis.xlsx
└── Paper_figures/ # Output directory for plots
clean_air_delivery_rates_update.py— Primary CADR calculation with exponential decay fitting for all instrumentsclean_air_delivery_rates_barchart.py— CADR visualization across burn experimentsclean_air_delivery_rates_pmsizes.py— Size-resolved CADR and exponential decay fitting across all instruments (AeroTrak, DustTrak, QuantAQ, SMPS, MiniAMS, PurpleAir); exports decay rates and 95% confidence intervals to Excel for downstream barchart scriptsclean_air_delivery_rates_pmsizes_SIUniformaty.py— CADR analysis with SI unit formatting and uniformity checksclean_air_delivery_rates_vs_total_surface_area.py— CADR vs. particle surface area correlationcadr_comparison_statistical_analysis.py— Statistical comparison of CADR values across experimental conditions
compartmentalization_strategy_comparison.py— Comparison of compartmentalization approaches (open house vs. closed bedroom vs. closed bedroom with filtration)decay_rate_barchart.py— Decay rate visualization and comparison
conc_increase_to_decrease.py— Analysis of concentration increase vs. decay phasepeak_concentration_script.py— Peak PM concentration identification and characterizationbracketing.py— Brackets the true peak PM mass concentration between a DustTrak photometer upper bound (biomass-smoke correction factors) and an AeroTrak OPC lower bound (unit-density correction), with MODULAIR-PM and PurpleAir cross-checksco2_decay_comparison.py— Plots CO₂ traces (Bedroom, Entry, MH Outside) and fits exponential decay to bedroom CO₂ over user-defined intervals to quantify decay rates with uncertainty
aerotrak_coincidence.py— Validates the transient optical-coincidence claim: detects bin reversals in the AeroTrak channels, estimates coincidence losses via a Poisson dead-time model, checks counts conservation, and cross-checks against co-located SMPS number concentrationmodulair_5sec_io.py— Shared I/O for the MODULAIR-PM analyses: loads the raw on-instrument 5 s SD-card record (no QA/QC) and the QA/QC-filtered 1-minute portal product, with UTC-to-local and per-unit clock-correction handlingmodulair_5sec_peak_window.py— Characterizes the raw 5 s MODULAIR-PM record during the smoke-peak window: PMS5003 nephelometer saturation, OPC-N3 small-bin suppression, and alignment with the portal QA/QC removal windowmodulair_5sec_post_peak.py— Quantifies the post-peak OPC-N3 small-bin count inversion and contrasts it against the monotonic decay of co-located AeroTrak and SMPS measurements
spatial_variation_analysis.py— Spatial variability quantification (Peak Ratio Index, Average Ratio, RSD)spatial_variation_analysis_plot.py— Interactive Bokeh visualizations comparing bedroom vs. morning roomspatial_variation_analysis_plot_timeseries.py— Time-series spatial analysis plots
dusttrak-rh_comparison.py— DustTrak performance and relative humidity effectspurpleair_comparison.py— PurpleAir sensor validation against reference instrumentsquantaq_pm2.5_burn8.py— QuantAQ sensor analysis for specific burn experimentgeneral_particle_count_comparison.py— Cross-instrument particle count comparisonaham_ac1_comparison.py— AHAM AC-1 smoke concentration standard comparison with WUI measurementstemp-rh_comparison.py— Temperature and relative humidity correlation with PM measurements
smps_filterperformance.py— Filter performance evaluation using ultrafine particle measurementssmps_finepm_comparison.py— Fine PM behavior comparison across instrumentssmps_heatmap.py— Particle size distribution evolution heatmapsmps_mass_vs_conc.py— Mass concentration vs. number concentration analysissmps_size_bin_barchart.py— CADR-per-CR-box barcharts grouped by SMPS size bins (9–100 nm, 100–200 nm, 200–300 nm, 300–437 nm) for filter count, new vs. used filter, and MERV grade comparisonssmps_size_distribution_comparison.py— Compares normalized dN/dlogDp distributions of WUI mixed-fuel smoke against the KCl challenge aerosol used for ASTM CADR derivationsmps_decay_report.py— Extracts the four coarse-band SMPS decay rates for Burn 01 (the no-filtration whole-house baseline) from the fittedSMPS_decay_and_CADR.xlsx, converts each first-order rate to a half-life (ln(2)/k, minutes), and writes a compact CSV for the Section 3.4 natural-decay analysis; performs no fitting itselfsmps_reversal_crosscheck.py— Cross-checks the AeroTrak Ch1 (0.3–0.5 μm) coincidence-overload claim against the SMPS, an independent non-optical (electrical-mobility) reference; for each Bedroom 2 reversal window it compares the reported Ch1 count, the SMPS 300–437 nm number concentration, and the Poisson rollover prediction of the true concentration
data_paths.py— Portable path resolver; readsdata_config.jsonto provide machine-independent access to instrument data folders and common files without hardcoded pathsfig_style.py— Shared ES&T Air figure styling for the Section 3.2 instrument figures: single source of truth for matplotlib settings, column widths (single, 1.5, and double), and the colorblind-safe Okabe-Ito categorical palette used by the bracketing, MODULAIR-PM peak-window, MODULAIR-PM post-peak, AeroTrak coincidence, and SMPS reversal figuresremove_aerotrak_dup_data.py— Duplicate AeroTrak timestamp removalmh_relay_control_log.py— HVAC relay control log processingtoc_figure_script.py— Publication table of contents figure generator
Builds the public PM data release: one tidy CSV per instrument product plus a manifest and a generated dataset README, written to the MIDAS release directory. No raw data are written or committed, no artifact corrections are applied, and timestamps keep each instrument's native clock. Existing outputs are never overwritten silently; a name collision is versioned with an ISO date suffix. Run under the project environment:
CONDA_NO_PLUGINS=true conda run -n wui python -m src.data_release.build_release
# subset with --only smps_bedroom2_number,dusttrakbuild_release.py— Orchestrator; runs the per-instrument exporters, writes each product CSV, and recordsmanifest.csv(file, rows, date range, source folder)release_config.py— Instrument registry (model, serial number, location, cadence, available burns, output filename); the single source of truth consumed by the exporters, manifest, and README generatorexporters.py— Per-instrumentprepare_<instrument>functions that reshape each raw record to the shared tidy schema (datetime, burn_id, location, instrument, serial_number, then measurement columns with units in the name), reusing existing repository parsersburns.py— Reads the burn log once and tags each record row with the burn it falls on; rows outside any burn day carry an emptyburn_idso the released file keeps the full continuous recordwriters.py— Writes each prepared DataFrame with the standard leading column order and UTF-8-with-BOM encoding (so Excel renders the micro sign), versioning any name collision with an ISO date suffixreadme.py— GeneratesREADME_dataset.mdfrom the registry and manifest, documenting the known instrument artifacts rather than correcting them in the data
diagnostic_timestamp_alignment.py— Examines raw AeroTrak timestamps and resampling behavior to diagnose merge failures in hourly spatial variation binsdiagnostic_hourly_ratios.py— Diagnostic analysis of hourly concentration ratios between instrumentsdiagnostic_spatial_variation.py— Diagnostic checks for spatial variation calculation inputs and outputsquantaq_5sec_timeseries.py— Plots raw MODULAIR-PM 5 s time series with QA/QC flag rendering; establishes the UTC-to-local and per-unit clock-correction conventions reused by themodulair_5sec_*analyses
The scripts/ directory contains shared utility modules used across analysis scripts. See scripts/README.md for full documentation.
| Module | Purpose |
|---|---|
datetime_utils.py |
DateTime handling, time shifts, synchronization |
data_filters.py |
Data filtering, quality control, transformations |
statistical_utils.py |
Curve fitting, exponential decay, regression |
plotting_utils.py |
Standardized Bokeh figure creation |
instrument_config.py |
Instrument configs, bin definitions, constants |
data_loaders.py |
Instrument-specific data loading functions |
spatial_analysis_utils.py |
Spatial variability calculations |
metadata_utils.py |
Script metadata and provenance strings |
export_smps_total_concentration.py |
Reads raw SMPS files and exports a combined datetime/total-concentration CSV for sharing; configurable for mass or number concentration; also runnable as a standalone script |
test_utilities.py |
Validates all shared utility modules (datetime_utils, data_filters, statistical_utils, plotting_utils, instrument_config) with assertions; run to verify environment setup after installation |
Most analysis scripts generate interactive Bokeh HTML plots saved to the configured Paper_figures/ output directory. HTML files can be opened in any web browser for interactive data exploration.
Clean Air Delivery Rate is calculated using first-order exponential decay analysis:
C(t) = C₀ × exp(-kt)
CADR = k × V
where:
C(t) = PM concentration at time t
C₀ = Initial (peak) concentration
k = Decay rate constant (min⁻¹)
V = Room volume (m³)
- Peak Ratio Index (PRI): Ratio of peak concentrations between locations
- Average Ratio: Time-averaged concentration ratio during the decay period
- Relative Standard Deviation (RSD): Coefficient of variation across locations
Decay rate and CADR uncertainties are propagated through two stages.
Stage 1 — Decay rate uncertainty
Exponential decay curves are fitted with scipy.optimize.curve_fit (non-linear least squares). The 95% confidence interval on the decay rate k is derived from the covariance matrix returned by the solver:
σ_k = 1.96 × √pcov[1, 1]
Fits where the relative standard deviation exceeds 10% (σ_k / k > 0.10) are flagged and excluded from further analysis.
Stage 2 — CADR uncertainty
CADR is computed as V × (k − k_baseline). Uncertainty is propagated using the range method:
σ_CADR = V × [(k + σ_k) − (k_baseline − σ_k_baseline)]
− V × [(k − σ_k) − (k_baseline + σ_k_baseline)]
= 2V × (σ_k + σ_k_baseline)
where V is the effective room volume (m³). When two baseline burns are available, k_baseline and σ_k_baseline are calculated as an inverse-variance weighted average. CADR-per-CR-box uncertainty is obtained by dividing σ_CADR by the number of CR boxes.
- Exponential curve fitting with
scipy.optimize.curve_fit - Uncertainty propagation for CADR calculations
- Multi-instrument data synchronization
- Baseline subtraction
| Instrument | Model | Purpose | Burns Available |
|---|---|---|---|
| AeroTrak Bedroom | TSI AeroTrak 9510 | Optical particle counting | 1–10 |
| AeroTrak Kitchen | TSI AeroTrak 9510 | Optical particle counting | 1–10 |
| DustTrak | TSI DRX 8533 | Real-time aerosol monitoring | 1–10 |
| MiniAMS | Mini Aerosol Mass Spectrometer | Chemical species analysis | 1–10 |
| PurpleAir | PA-II-SD | Low-cost PM monitoring | 6–10 |
| QuantAQ Bedroom | MODULAIR-PM (MOD-PM-00194) | PM monitoring | 4–10 |
| QuantAQ Kitchen | MODULAIR-PM (MOD-PM-00197) | PM monitoring | 4–10 |
| SMPS | TSI SMPS 3938 | Ultrafine size distribution (9–437 nm) | 1–10 |
| Vaisala T/RH | Vaisala sensors | Temperature and relative humidity | 1–10 |
| CO₂ | Aranet4 | CO₂ concentration (Bedroom, Entry, MH Outside) | 1–10 |
The experimental data associated with this project are not included in this repository. Data can be made available upon request by contacting the PI.
If you use this software, please cite it as:
@software{lima_wui_mh_iaq_2025,
author = {Lima, Nathan M. and Poppendieck, Dustin G.},
title = {Manufactured Housing Wildland-Urban Interface ({WUI}) Fire Smoke Study: Indoor Air Quality Analysis Tools},
year = {2025},
publisher = {National Institute of Standards and Technology},
doi = {10.18434/mds2-4025},
url = {https://github.com/usnistgov/WUI_MH_IAQ}
}Two journal articles based on this dataset are currently in progress. Citations will be added here upon acceptance.
- PI: Dustin G. Poppendieck
- Researcher / Code Developer: Nathan M. Lima
- NIST Organizational Unit: Engineering Laboratory
- Division: Building Energy and Environment Division
- Group: Indoor Air Quality and Ventilation Group
- Email: nathan.lima@nist.gov
This research was conducted at the National Institute of Standards and Technology (NIST) as part of the Indoor Air Quality and Ventilation Group and Wildland-Urban Interface Fire Group.
Data collection period: April – June 2024
This software was developed by employees of the National Institute of Standards and Technology (NIST), an agency of the Federal Government and is being made available as a public service. Pursuant to Title 17 United States Code Section 105, works of NIST employees are not subject to copyright protection in the United States. This software may be subject to foreign copyright. Permission in the United States and in foreign countries, to the extent that NIST may hold copyright, to use, copy, modify, create derivative works, and distribute this software and its documentation without fee is hereby granted on a non-exclusive basis, provided that this notice and disclaimer of warranty appears in all copies.
See LICENSE.md for the full NIST licensing statement.