PFAS fate and transport
pfas.dat activation, per-HRU soil IC and sorption in pfas_hru.ini, in-stream pfas_cha.dat and pfas_calib.dat, and the output and mass-balance files.
Surface-water PFAS fate & transport turns on when the SWAT+ working directory (Scenarios/Default/TxtInOut) contains pfas.dat. Without that file the engine skips the PFAS module entirely. This chapter covers the file schemas and the edit workflow.
pfas.dat / pfas_hru.ini. Those engine inputs are hand-edited, supplied in research/example PFAS packages, or produced offline from soils.sol + hru-data.hru (see below). You need a PFAS-enabled SWAT+ binary from the public fork.Where the files go
Scenarios/Default/TxtInOut/ # run directory for swatplus
├── pfas.dat # REQUIRED to enable PFAS (compounds + chemistry)
├── pfas_hru.ini # REQUIRED for soil IC + sorption (HRU order)
├── pfas_calib.dat # optional: soil_scale koc_scale
├── pfas_cha.dat # optional: in-stream koc/settle/resus/bury + IC
├── pfas_leach.map # research daily MF6 coupling only (with mf6.con)
├── soils.sol # layer thickness, bd, OC, texture (generator input)
├── hru-data.hru # soil + lu_mgt names (generator input)
└── … standard SWAT+ inputs …
<model_root>/ # SWAT_MODEL_Web_Application (sibling of TxtInOut tree)
└── pfas_data/ # OBSERVATIONS only (optional order / add-on snap)
├── pfas_stations_assignment.csv
├── stations.shp
├── <channel>_<site_id>.csv
└── README.mdProduce the soil inputs
pfas_hru.ini is required whenever pfas.dat is present. You can hand-edit it, start from a research/example package that already includes it, or derive a background PFOS initial condition from the soils and HRU tables already in TxtInOut (soils.sol + hru-data.hru). A typical offline derivation writes PFOS only (num_pconta=1, enr=1.0), maps each layer’s OC/clay/sand to the nearest of six reference soils S1–S6 for (kf, n, d50), and seeds sol_pfas from land-use background µg/kg × bulk density × thickness × 1e4 with surface-weighted depth decay 0.5^k. Field-level detail is under pfas_hru.ini below.
Loadings / sources — what you edit
- Initial soil mass: edit sol_pfas columns in
pfas_hru.ini(µg ha⁻¹), or raise land-use / site concentrations and regenerate the background IC fromsoils.sol+ hru-data.hru. - Chemistry / AWI sorption: edit mw, sol, kl, lm, percop in
pfas.dat. - Solid sorption: edit kf / nf per layer in
pfas_hru.ini(or change soils so the S1–S6 match shifts). - In-stream partition / bed exchange:
pfas_cha.dat(or rely on defaults). - Global scale during cal:
pfas_calib.dat. - Point / channel initial concentrations: trailing water_ppt / benthic_ppt on
pfas_cha.datrows.
Observation package (not engine IC)
- Optional order / dashboard add-on places monitoring under pfas_data/ at the model root. Use those CSVs to compare against channel_pfas_day.txt — they are not read as model IC.
- Public PFAS inventory APIs and MCP tools serve monitoring data; they do not write
pfas.dat/pfas_hru.iniinto TxtInOut.
Outputs to check after a run
- pfas_hru_aa.txt — per-HRU budget (kg ha⁻¹): init/final, surq, latq, perc (leach toward GW), sed, resid.
- channel_pfas_day.txt — daily reach mass terms + conc_ngL.
- pfas_balance.out / pfas_cha_balance.out — land-phase and channel mass-balance checks.
- Coupled research path (
mf6.con+ pfas_leach.map): mf6/pfas.ucn, mf6/pfas.lst — see the MODFLOW coupling chapter. Ordinary MODFLOW_250m packages do not include these.
Product vs research scope
- Standard distribution: SWAT+ package download; optional pfas_data observation snap; hand-edited or offline-derived
pfas.dat/pfas_hru.inifor engine IC. - Requires a PFAS-enabled engine binary locally (see Compile the SWAT+ fork), or a research package that already includes the pfas.* inputs. Standard hosted builds do not auto-emit
pfas.dat. - Research scope (not the default order): daily BMI PFAS leach maps, MF6 groundwater transport outputs (pfas.ucn), vadose-zone pathways, multi-analyte setups, and joint SW–GW calibration.
pfas.dat — compound database
PFAS compound database. Its presence in TxtInOut enables the PFAS fate & transport module; absent file is a clean no-op.
Two header lines, then one free-format row per compound; a row with id ≤ 0 terminates the list. The compound order defines the compound index used by every other PFAS file.
pfas.dat: PFAS compound database (PFOS example) id name mw sol kl lm percop 1 PFOS 0.50013 680.0 0.137 2500.0 0.20 0 END 0.0 0.0 0.0 0.0 0.0
| Field | Description | Type | Units |
|---|---|---|---|
| id | Sequential compound index (1…npfas); 0 / negative terminates the list | integer | — |
| name | Compound label, up to 16 characters | character(16) | — |
| mw | Molecular weight | real | kg mol⁻¹ |
| sol | Maximum aqueous solubility | real | mg L⁻¹ |
| kl | Langmuir K_L for air–water interface sorption | real | L nmol⁻¹ |
| lm | Langmuir Γ_max at the air–water interface | real | nmol m⁻² |
| percop | Fraction of mobile PFAS partitioned to percolation vs runoff | real | fraction 0–1 |
pfas_hru.ini — soil initial condition
Per-HRU soil initial condition and Freundlich sorption parameters for every compound in pfas.dat. Required when pfas.dat is present.
One block per HRU in engine HRU order (record k → HRU k). The block declares its own layer count (nly in the header); the reader stores layers 1…min(file nly, soil layer count from soils.sol) and derives the working layer count from the soil, not the file.
hru <id> nly <nlayers> <num_pconta> # contaminated-site count (0 coerced to 1) <d50_L1> … <d50_Lnly> # median grain diameter per layer (mm) <pid> <sol_pfas_L1> … <sol_pfas_Ln> # initial soil PFAS mass per layer <pid> <kf_L1> … <kf_Ln> # Freundlich kf per layer <pid> <nf_L1> … <nf_Ln> # Freundlich n per layer <pid> <enr> # sediment enrichment ratio # repeat the four <pid>… lines once per compound in pfas.dat (ip = 1..npfas) # pid ≤ 0 → no PFAS in this HRU (remaining PFAS rows for that HRU skipped)
| Field | Description | Type | Units |
|---|---|---|---|
| hru <id> nly <n> | Block header: HRU id and file layer count (nly < 1 coerced to 1; stored layers capped at the soils.sol layer count) | integers | — |
| num_pconta | Contaminated-site count for the HRU, used as a mass divisor (≤ 0 coerced to 1) | integer | — |
| d50 | Median grain diameter per layer; sets AWI area A_aw = 6(1−porosity)(1−saturation)/d50 | real per layer | mm |
| pid | Compound index into pfas.dat on the sol_pfas line; ≤ 0 marks the HRU PFAS-free and skips its remaining PFAS rows | integer | — |
| sol_pfas | Initial soil PFAS mass per layer; stored as pfas_soil_scale × 1e-9 × value (kg ha⁻¹ internally) | real per layer | µg ha⁻¹ (file) |
| kf | Freundlich sorption coefficient per layer (leading id token on the line is discarded) | real per layer | (nmol kg⁻¹)/(nM)ⁿ |
| nf | Freundlich exponent per layer | real per layer | dimensionless |
| enr | Enrichment ratio for PFAS on eroded sediment; single value broadcast to all layers | real | dimensionless |
How a background IC is typically produced
- Inputs already in the package:
soils.sol(layer thickness, bulk density, OC, texture) and hru-data.hru(soil + land-use names). - Background template commonly writes PFOS only: num_pconta=1, enr=1.0.
- Sorption (kf, n, d50) per layer: nearest of six reference soils S1–S6 by OC/clay/sand.
- Initial mass: land-use background µg/kg × bulk density × layer thickness × 1e4, with surface-weighted depth decay 0.5^k.
pfas_cha.dat and pfas_calib.dat
Optional in-stream PFAS parameters and initial reach concentrations (pfas_cha.dat) and global calibration multipliers (pfas_calib.dat).
pfas_cha.dat — in-stream parameters
Optional. Absent file → PFOS-like defaults and PFAS-free initial reaches; HRU loads still route through the channel phase. Two header lines, then one row per compound; id ≤ 0 terminates. Compound ids must align with pfas.dat.
| Field | Description | Type | Units | Default |
|---|---|---|---|---|
| id | Compound index (must align with pfas.dat); ≤ 0 ends the list; id > npfas rows are skipped | integer | — | — |
| name | Compound label | character(16) | — | — |
| koc | Water–sediment partition coefficient (multiplied by pfas_koc_scale after read) | real | m³ g⁻¹ | 1e-5 |
| settle | Settling velocity | real | m d⁻¹ | 1.0 |
| resus | Resuspension velocity | real | m d⁻¹ | 0.05 |
| bury | Burial velocity | real | m d⁻¹ | 0.001 |
| act_dep | Active benthic bed depth; values ≤ 1e-6 are ignored (prior value kept) | real | m | 0.1 |
| water_ppt | Optional initial water-column concentration; init pool kg = water_ppt × 1e-9 × reach volume | real (optional) | ng L⁻¹ | 0 |
| benthic_ppt | Optional initial bed concentration; init pool kg = benthic_ppt × 1e-12 × bed mass | real (optional) | ng g⁻¹ | 0 |
pfas_calib.dat — calibration multipliers
Optional single line: soil_scale koc_scale (e.g. 0.11 1.0). Missing file → both 1.0.
| Field | Description | Type | Units | Default |
|---|---|---|---|---|
| soil_scale | Scales the initial soil PFAS pool from pfas_hru.ini | real | dimensionless | 1.0 |
| koc_scale | Scales in-stream sorption (pfas_koc_scale applied to koc) | real | dimensionless | 1.0 |
Output files
The four PFAS output surfaces the engine writes when the PFAS module is active: per-HRU budget, daily channel series, and two mass-balance checks. Written by pfas_output.f90 and cha_pfas_output.f90.
pfas_hru_aa.txt — per-HRU budget (once per run)
Written once at the end of the simulation by pfas_output. Only HRU × compound rows with a nonzero initial pool are emitted.
| Field | Description | Type | Units |
|---|---|---|---|
| hru | HRU index | integer | — |
| pfas | Compound index into pfas.dat | integer | — |
| init_kgha | Initial soil pool (sum of layers at read time) | real | kg ha⁻¹ |
| final_kgha | Final soil pool (sum of layers) | real | kg ha⁻¹ |
| surq_kgha | Run-cumulative loss to surface runoff | real | kg ha⁻¹ |
| latq_kgha | Run-cumulative loss to lateral flow | real | kg ha⁻¹ |
| perc_kgha | Run-cumulative leaching below the profile (toward groundwater) | real | kg ha⁻¹ |
| sed_kgha | Run-cumulative loss on eroded sediment | real | kg ha⁻¹ |
| resid_kgha | (init − final) − losses; mass-balance residual | real | kg ha⁻¹ |
channel_pfas_day.txt — daily reach series
Written per reach per day (on the daily print schedule from print.prt) by cha_pfas_output; a twin CSV is written when csvout = y. Mass terms are daily kg per reach.
| Field | Description | Type | Units |
|---|---|---|---|
| jday mon day yr | Simulation date fields | integers | — |
| unit | Reach (chandeg) index | integer | — |
| gis_id | GIS channel id | integer | — |
| name | Object name | character | — |
| pfas | Compound name from pfas.dat | character(16) | — |
| tot_in_kg | Total mass inflow to the reach | real | kg |
| sol_out_kg / sor_out_kg | Dissolved / sorbed mass outflow | real | kg |
| settle_kg / resus_kg / diffuse_kg / bury_kg | Bed exchange terms (settling, resuspension, diffusion, burial) | real | kg |
| water_kg / benthic_kg | End-of-day water-column and benthic storage | real | kg |
| conc_ngL | Water-column concentration | real | ng L⁻¹ |
Mass-balance checks (once per run)
- pfas_balance.out — area-weighted basin totals per compound: init_kg, final_kg, surq_kg, latq_kg, perc_kg, sed_kg, loss_kg, resid_kg, resid_pct; footer reports the worst per-HRU residual (abs/rel) and its HRU index.
- pfas_cha_balance.out — run-cumulative channel-phase totals (kg): reach-days active, point-source input, cumulative inflow/outflow, cumulative burial, final channel storage.
mf6.con + pfas_leach.map) additionally produce mf6/pfas.ucn (groundwater PFAS concentration) and mf6/pfas.lst (transport mass budget) — see mf6.con and exchange maps in the MODFLOW coupling chapter. Ordinary MODFLOW_250m packages do not include these.