Skip to content

Repository files navigation

Standalone LSTM Water-Quality Forecasting

A standalone PyTorch LSTM system for direct, multi-horizon forecasting of water temperature and dissolved oxygen at USGS station 02334500. The model uses 48 hours of 15-minute observations to forecast the next 24 hours, or 96 steps, in one forward pass.

Unlike the main spatiotemporal model, this experiment is a single-site deterministic LSTM. It does not use PatchTST, graph attention, Koopman dynamics, physics-informed losses, or diffusion sampling.

Table of Contents

Problem Statement

Dissolved oxygen and water temperature are important indicators of river health. The LSTM forecasts both variables from historical observations and calendar-derived features at USGS station 02334500:

  • Water temperature in degrees Celsius.
  • Dissolved oxygen in mg/L.

The validation experiment used 17,472 observations from January 1 through June 30, 2024. Sliding windows contained 192 historical steps and a 96-step forecast horizon. Scalers were fitted on training rows only, and forecast targets were kept within chronological split boundaries.

Split Forecast windows Purpose
Training 11,943 Fit model parameters
Validation 2,525 Select the best checkpoint
Test 2,527 Report held-out performance
Total 16,995

Results

Deterministic Forecasting Performance

The table below reports the saved held-out test metrics for the best validation checkpoint. Persistence skill compares the LSTM with repeating the last observed value:

skill = 1 - model MSE / persistence MSE
Target RMSE MAE R-squared Persistence skill
Temperature 0.776 C 0.604 C 0.227 +0.603
Dissolved oxygen 1.408 mg/L 0.990 mg/L 0.263 -0.021

The LSTM reduced temperature mean-squared error by about 60% relative to persistence. Dissolved-oxygen mean-squared error was about 2.1% worse than persistence in this bounded run.

LSTM test metrics

Hypoxia evaluation

The test split contained no dissolved-oxygen values below the configured 2 mg/L threshold. Hypoxia precision, recall, and F1 are therefore not measurable for this run and are not interpreted as model skill.

Example Forecast

The inference script exported a complete 96-step forecast for a historical 24-hour window.

LSTM 24-hour forecast profile

This figure is a forecast profile, not a forecast-versus-observed accuracy plot, because the exported inference CSV contains predictions and timestamps but no paired observations for that window. Aggregate accuracy is reported above.

Training Behavior

The bounded validation run used a one-layer LSTM with hidden size 64. The best checkpoint was selected at epoch 4 using validation physical-unit mean RMSE.

Setting Value
LSTM layers 1
Hidden size 64
Trainable parameters 38,114
Epoch budget 8
Batch size 256
Optimizer AdamW
Loss Weighted Huber
Temperature loss weight 1.0
Dissolved-oxygen loss weight 2.0
Gradient clipping 1.0

LSTM training curves

Training loss continued to decrease after epoch 4 while validation error worsened, indicating early overfitting. The saved best checkpoint prevents later epochs from replacing the better validation model.

Forecast Performance by Lead Time

The following figure reports target-specific RMSE at each of the 96 forecast leads. The panels use separate physical units.

LSTM horizon RMSE

The dissolved-oxygen error is larger than the temperature error and rises substantially across the horizon. The non-monotonic temperature curve reflects the daily cycle and the specific held-out test period.

Architecture Overview

Input: [batch, 192, 15]
             |
             v
      One-layer LSTM
       hidden size 64
             |
             v
Final hidden state: [batch, 64]
             |
             +-----------------------------+
             |                             |
             v                             v
96 learned horizon embeddings      Future calendar features
             |                             |
             +-------------+---------------+
                           |
                           v
             Horizon states: [batch, 96, 64]
                           |
                           v
                    Shared MLP head
                      /          \
                     v            v
          Temperature head   DO head conditioned
                             on temperature
                      \          /
                       v        v
              Direct output: [batch, 96, 2]
                           |
                           v
             Add last observed target values

The model predicts all 96 future steps directly and is not autoregressive. By default, each target is predicted as a residual from its last observed scaled value, providing a persistence-like starting point.

Feature Engineering

The validation run used 15 historical features:

  • Historical temperature and dissolved oxygen.
  • Hour-of-day, weekday, and annual-cycle sine/cosine features.
  • Six-hour temperature mean and 24-hour temperature mean.
  • Six-hour dissolved-oxygen mean.
  • One-hour temperature and dissolved-oxygen changes.
  • 24-hour temperature range.
  • Gage height.

Six known future calendar features were supplied over the forecast horizon. No future meteorological file was available during this run, so future air temperature, radiation, wind, cloud cover, and humidity were not included.

Repository Structure

.
├── data/raw/02334500/iv.parquet     # Included USGS observations
├── figures/lstm/                    # README figures
├── lstm_model/
│   ├── model.py                     # Direct multi-horizon LSTM
│   ├── data.py                      # Loading, features, scaling, windows
│   ├── train.py                     # Training and validation
│   ├── evaluate.py                  # Test and lead-time metrics
│   ├── predict.py                   # Timestamped forecast export
│   ├── config.yaml                  # Main experiment configuration
│   ├── config_validation.yaml        # Bounded validation configuration
│   ├── checkpoints_validation/best.pt
│   ├── evaluation_metrics_validation.json
│   ├── evaluation_metrics_validation_by_lead.csv
│   ├── training_log_validation.csv
│   └── create_readme_figures.py      # Rebuilds all four figures
├── output/pdf/                      # Validation and explainer reports
├── DATASET.md
├── requirements.txt
├── verify.py
└── README.md

Installation

Python 3.10 or newer is recommended.

python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install -r requirements.txt

On macOS or Linux:

source .venv/bin/activate

Usage

Verify the included data, model, checkpoint, and forecast path:

python verify.py

Run the LSTM smoke test:

python -m lstm_model.smoke_test --config lstm_model/config.yaml

Train the bounded validation configuration:

python -m lstm_model.train --config lstm_model/config_validation.yaml

Train the full configuration:

python -m lstm_model.train --config lstm_model/config.yaml

Evaluate a checkpoint:

python -m lstm_model.evaluate `
  --checkpoint lstm_model/checkpoints_validation/best.pt

Generate a 24-hour forecast:

python -m lstm_model.predict `
  --checkpoint lstm_model/checkpoints_validation/best.pt `
  --output lstm_model/latest_forecast.csv

Regenerate all README figures from the saved metrics and logs:

python lstm_model/create_readme_figures.py

Limitations

This is a bounded CPU verification experiment using six months from one station and a smaller one-layer LSTM. The test period contains no hypoxic events, dissolved-oxygen performance does not beat persistence, and the exported forecast profile has no paired observations. A publication-quality comparison should use longer multi-season data, multiple stations, genuine future weather forecasts, hypoxia-containing test periods, identical leakage-safe splits, and multiple random seeds.

For the full saved artifact details, see lstm_model/RESULTS.md, the validation report, and the beginner LSTM explanation.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages