SWATGenXSWATGenX
Watershed ExplorerPricing
ServicesWatershed ExplorerExample modelsCloud calibrationDocsPricing

Build guide · open-source engine

How to compile SWAT+ with NetCDF output and OpenMP parallelism

SWATGenX runs a pinned release of our fork, currently ship-v1.0-51.3-61-g3a07e3b. This page documents the exact compile recipe for that production engine: NetCDF-4 output instead of bulk text, and multi-core OpenMP that, with routing serial, stays byte-identical to a one-thread run of the same binary in our tests.

Intel ifx or gfortran, CMake switches, the netcdf-fortran module trap, and the three run modes. Source on our public fork of swat-model/swatplus.

  • Intel ifx + CMake
  • NetCDF-4 output
  • OpenMP multi-core
  • Open fork, MIT-compatible

Stock SWAT+ writes large formatted text files and runs on a single core. The engine SWATGenX runs in production is the same science with two build-time capabilities added: a NetCDF-4 output backend and OpenMP shared-memory parallelism. This page is the build recipe for that engine — the compiler, the dependency that trips everyone up, the CMake switches, and how to turn each capability on at run time. The source is public on our fork, github.com/rafiei-vahid/swatplus, distributed under the GNU Lesser General Public License v2.1 — the branch built into the engine now in production is ship/v1.0-50-1-g9482c07-deployed.

What the two capabilities buy you

NetCDF output replaces per-timestep formatted text with compressed NetCDF-4. On a 3-month Peace River run (94,303 HRUs), the channel_sd output goes from a 695 MB text file to a 2.0 MB NetCDF file — about 341× smaller — and wall time drops from 462 s to 227 s, because for large models writing text is a large share of the runtime. Results are byte-identical.

OpenMP parallelism runs the independent parts of a watershed at once: the HRU land phase, and a wavefront over the channel-routing DAG. On a 32-core AWS c8a node with the 57,998-HRU Peace River model, the full wavefront of the shipped engine (v1.0-49) reaches 5.47× at 24 threads, 6.91× end to end versus our original single-core engine — which already carries the NetCDF backend and print filter, so against unmodified upstream SWAT+ the figure would be larger; we quote the one we measured directly. With routing serial it matched a one-thread run byte for byte in every file in our tests; in the full wavefront, the channel pesticide output and the PFAS channel mass-balance diagnostic can still differ between runs until a fix ships.

Prerequisites

You need a Fortran compiler, CMake ≥ 3.16, and a Fortran NetCDF library. We build production with the Intel Fortran compiler (ifx) from the free oneAPI HPC Toolkit. Which compiler runs faster depends on the host: in a 20-day test of one model (09471300) on 2026-05-28, ifx -O2 ran about 23% faster than gfortran 11.4, while on 2026-10-05, on an Intel Xeon c7i.4xlarge, every gfortran build ran 17 to 28% faster than ifx -O2 on all three benchmark models (see theruntime benchmark). Our 2026-10-05 compiler runs used one change to the source (fork commit 67757ffb, a longer output record for area_calc.out); with the stock record length, gfortran builds stop on the large model at hyd_connect.f90:540 while ifx completes. Time both on your own hardware. gfortran uses the same CMake switches, and you can install its NetCDF from your distro:

# gfortran route (simplest install)
sudo apt-get install -y gfortran cmake libnetcdff-dev libnetcdf-dev

For the ifx route, install the oneAPI HPC Toolkit, then load the compiler environment. Note the shell caveat below — it bites everyone once.

source /opt/intel/oneapi/setvars.sh

Do not run setvars.sh under set -u

The oneAPI environment script dereferences unset variables. If your build script uses set -eu, sourcing it aborts with a confusing unbound-variable error. Source it before enabling -u, or temporarily disable it.

The NetCDF dependency (the real trap)

Fortran module files (.mod) are compiler-specific and not portable between compilers. Your distro's libnetcdff-dev is built with gfortran, so an ifx build cannot use it — you get module-format errors, or worse, a configure that silently picks gfortran. If you build with ifx you must also build netcdf-fortran with ifx. Only the Fortran layer needs rebuilding; the C library and HDF5 can come from the system.

# build netcdf-fortran 4.5.4 against the system NetCDF-C, using ifx
export FC=ifx F77=ifx CC=gcc CXX=g++
./configure --prefix=$PREFIX/deps/netcdf-ifx \
            --enable-shared --disable-fortran-type-check \
            CPPFLAGS="$(pkg-config --cflags netcdf)" \
            LDFLAGS="$(pkg-config --libs netcdf)"
make -j"$(nproc)" && make install

Then point pkg-config at it before configuring SWAT+, so CMake finds your ifx build rather than the system one:

export PKG_CONFIG_PATH="$PREFIX/deps/netcdf-ifx/lib/pkgconfig:$PKG_CONFIG_PATH"

Configure and build

Clone the fork and configure with both capabilities on. These are the exact flags behind the production binary:

git clone https://github.com/rafiei-vahid/swatplus.git
cd swatplus

cmake -S . -B build \
      -DCMAKE_BUILD_TYPE=Release \
      -DCMAKE_Fortran_COMPILER=ifx \
      -DCMAKE_Fortran_FLAGS="-O3 -ipo -recursive -init=zero -init=arrays -free -fpe0" \
      -DSWATPLUS_NETCDF=ON \
      -DSWATPLUS_OPENMP=ON

cmake --build build -j"$(nproc)"

What the switches do: SWATPLUS_NETCDF=ON resolves netcdf-fortran through pkg-config and compiles the NetCDF output backend in; SWATPLUS_OPENMP=ON adds -fiopenmp and links the OpenMP runtime. Of the compiler flags, -init=zero -init=arrays matters for reproducibility — it guarantees uninitialized locals start at zero rather than whatever the stack held, which is what makes thread-count invariance testable.

Pin the compiler explicitly, on a fresh configure

Always pass -DCMAKE_Fortran_COMPILER=ifx together with the PKG_CONFIG_PATH above. If you omit either on a clean build directory, CMake may select gfortran and the system NetCDF, and the two sets of .mod files collide in ways whose error messages point nowhere near the actual cause. Also note that a NetCDF build cannot be statically linked — -static and a dynamic NetCDF are mutually exclusive.

Turning NetCDF output on at run time

Compiling the backend in does not switch it on. The flag lives in print.prt, on the value line under the csvout dbout cdfout header — set cdfout to y:

         csvout       dbout      cdfout
              n           n           y

Each enabled output stream is then written as NetCDF-4 — channel_sd_day.nc, hru_wb_day.nc, basin_wb_day.nc — instead of formatted text. CSV output is skipped in NetCDF mode. Compression is tunable through an environment variable; deflate level 4 is the default, and 0 trades file size for speed on fast local disks:

export SWATPLUS_NC_DEFLATE=0   # 0 = no compression, 4 = default

Running it multi-core

Parallelism is controlled by two environment variables, giving three modes. They differ inspeed, and in what our byte-identity tests cover:

# 1. serial — the reference
OMP_NUM_THREADS=1 ./swatplus

# 2. HRU-parallel — land phase parallel, routing in command order
OMP_NUM_THREADS=8 SWATPLUS_ROUTING_SERIAL=1 ./swatplus

# 3. full wavefront — HRUs and channel routing both parallel; fastest
OMP_NUM_THREADS=8 SWATPLUS_ROUTING_SERIAL=0 ./swatplus

Mode 2 parallelizes the HRU land phase while routing stays in command order. Mode 3 adds the routing wavefront and is meaningfully faster. Mode 2 matched the serial result bit for bit in every output file in our tests (seven models, one simulated year each, October 2026). Mode 3 matched it except the channel pesticide output and the PFAS channel mass-balance diagnostic, which can differ between runs until a fix ships, so mode 2 is the one to use when those outputs matter. Reproducibility used to cost about 37% of the available scaling. It now costs 0.05%, because the disagreement turned out to be three fixable order dependencies rather than an inherent property of parallel routing.

How to verify your own build

Run the same model at 1 and at N threads and compare every output file, not only NetCDF. With routing serial they should match exactly. If they do not, the build is not thread-safe on your compiler and you should stay serial until it is. Thread-count invariance is the correctness test, it is cheap to run, and it is strictly stronger than a race detector: it catches state that leaks between simulated objects, which no race detector reports.

Relationship to official SWAT+

This is a fork of swat-model/swatplus, not a replacement for it, and the hydrologic science is unchanged. Four of the contributions on this page went upstream as independent pull requests — the NetCDF backend (#213), channel_sd print filtering (#214), and two engine performance fixes (#219, #220). Upstream merged #220 on 2026-08-20 and closed the other three without merging, which is why the build above uses the fork. The OpenMP parallelism, PFAS fate-and-transport, and MODFLOW 6 coupling layers live on the fork's main branch; all three are inert unless you configure them, so a plain SWAT+ model built from this source runs as an ordinary SWAT+ model.

FAQ

Which SWAT+ engine does SWATGenX run in production?

SWATGenX runs a pinned release of our fork, currently ship-v1.0-51.3-61-g3a07e3b (commit rafiei-vahid/swatplus@3a07e3b). This build guide documents how to compile that fork with NetCDF output and OpenMP parallelism.

Do I need the Intel compiler to build SWAT+ with NetCDF and OpenMP?

No — gfortran builds both capabilities with the same CMake switches, and for most models it is the simpler route because your distribution already ships a matching netcdf-fortran (libnetcdff-dev). We build production with Intel ifx. Which compiler runs faster depends on the host: in a 20-day test of one model (09471300) on 2026-05-28, ifx -O2 ran about 23% faster than gfortran 11.4, while on 2026-10-05, on an Intel Xeon c7i.4xlarge, every gfortran build ran 17 to 28% faster than ifx -O2 on all three benchmark models. Our 2026-10-05 compiler runs used one change to the source (fork commit 67757ffb, a longer output record for area_calc.out); with the stock record length, gfortran builds stop on the large model at hyd_connect.f90:540 while ifx completes. Time both on your own hardware. If you use ifx you must also build netcdf-fortran with ifx, since Fortran .mod files are not portable between compilers.

Why does my ifx build fail to find NetCDF, or silently use gfortran?

Two causes, and they look alike. First, CMake picks a compiler at the first configure of a build directory: pass -DCMAKE_Fortran_COMPILER=ifx explicitly, and if you changed it, delete the build directory rather than reconfiguring in place. Second, pkg-config finds the distro netcdf-fortran (built with gfortran) before your ifx build: export PKG_CONFIG_PATH=<prefix>/deps/netcdf-ifx/lib/pkgconfig before configuring. The resulting .mod-format errors do not name either cause, which is why both are worth checking first.

How do I turn NetCDF output on once the engine is built?

Set cdfout to y in print.prt, on the value line beneath the "csvout dbout cdfout" header. Every enabled output stream is then written as compressed NetCDF-4 (channel_sd_day.nc, hru_wb_day.nc, and so on) rather than formatted text, and CSV output is skipped. Compression level is set by the SWATPLUS_NC_DEFLATE environment variable — 4 by default, or 0 to trade file size for write speed on fast disks.

Is the multi-core engine reproducible?

With routing serial, yes. Serial (OMP_NUM_THREADS=1) is the reference. HRU-parallel (OMP_NUM_THREADS=N with SWATPLUS_ROUTING_SERIAL=1) matched it byte for byte in every file in our tests (seven models, one simulated year each, October 2026). The full wavefront (SWATPLUS_ROUTING_SERIAL=0), which parallelizes channel routing as well and is the fastest of the three, matched it except the channel pesticide output and the PFAS channel mass-balance diagnostic, which can differ between runs; a fix is in progress. Reproducibility used to cost about 37% of the available scaling; it now costs 0.05%, because the disagreement turned out to be three fixable order dependencies rather than an inherent property of parallel routing.

How fast is it, really?

Two independent gains. On output and serial performance: a 3-month run of an earlier Peace River build (94,303 HRUs) goes from 462 s and a 695 MB text file to 227 s and a 2.0 MB NetCDF file — about 2.0× faster with 341× smaller output and the same values. On parallelism: the routing wavefront of the shipped engine (v1.0-49) reaches 5.47× at 24 threads on a dedicated 32-core AWS c8a node with a later build of the same Peace River basin (57,998 HRUs), 6.91× end to end against our original engine on one core. In an earlier run on the same node, before the correctness corrections, the routing-serial mode reached roughly 2.5× at 8 threads; it is the mode that matched a one-thread run in every file in our tests.

Can I use this engine with models I did not build in SWATGenX?

Yes. It is a fork of swat-model/swatplus with the hydrologic science unchanged, so it runs any standard SWAT+ project directory (TxtInOut). The added layers — OpenMP, PFAS fate-and-transport, and the MODFLOW 6 coupling — are inert unless configured, so an ordinary SWAT+ model behaves as it would on the stock engine, and at one thread produces identical output.

Next steps

Build a model of your watershed
SWAT+ production engine
Hydrology calibration methods

Last updated 2026-07-18.