Skip to content
WaterMirror
Language: English
GitHub

Read a river's water quality from five measurements.

WaterMirror turns dissolved oxygen, BOD, ammonia nitrogen, conductivity and suspended solids into a WQI5 score, a quality band and a status for each indicator. Use the Android app, call the open API, or run the whole service on a Jetson Nano.

  • Android app 2.3.7
  • API 2.3.0 on PyPI
  • Runs on x86 and ARM64
WQI5 gauge showing a score of 61.48, in the Fair band10085705030150ExcellentGoodPoorBadTerrible61.48Fair
WaterMirror report for a sample reading: overall score 61.48, Fair, from the direct WQI5 formula, with a percentile chart and the share of each band in the reference data.
DO
Dissolved oxygen
% saturation
BOD
Biochemical oxygen demand
mg/L
NH₃-N
Ammonia nitrogen
mg/L
EC
Electrical conductivity
µS/cm
SS
Suspended solids
mg/L

What you can do with the app

  • Enter a sample or upload a CSV

    Type five values by hand, or upload a CSV with the header DO,BOD,NH3N,EC,SS to assess a batch.

  • Choose how the score is computed

    Use the direct WQI5 formula, or compare it with one of six trained models: linear, polynomial, SVM, random forest, XGBoost or LightGBM.

  • See what the score means

    The report shows the score, its band and range, how each indicator rates, where the sample sits in the reference data, and any input warnings.

  • Connect to your own server

    Enter a backend address in Backend settings and test its health and readiness before you send data. The address stays saved on the phone.

  • Use it in your language

    English, Traditional Chinese, Simplified Chinese and Japanese.

Data entry with the model list open
Backend settings with a server address

How a reading becomes a score

  1. 1

    Measure

    Collect DO, BOD, NH₃-N, EC and SS at a sampling point.

  2. 2

    Send

    The app posts the values to a WQSurrogateModels API at /api/v2/assessment.

  3. 3

    Assess

    The API validates the input, then computes WQI5 with the formula or the model you picked.

  4. 4

    Read

    The app shows the score, band, range, per-indicator status and warnings that the API returns.

The WQI5 formula

Each indicator is converted to a sub-index between 0 and 100. WQI5 is the equal-weighted mean of the five, rounded to three decimals.

WQI5 = (QDO + QBOD + QNH₃-N + QEC + QSS) ÷ 5

Read the sub-index equations

The app never scores data on its own. The API is the single source of truth, so every client gets the same answer.

System overview

WaterMirror app

Android app built with Expo and React Native

HTTP, /api/v2

WQSurrogateModels API

FastAPI service with the formula and the trained models

pip or Docker

Where the API runs

Any x86 server or PC

Jetson Nano 2GB in Docker (ARM64)

Six bands, each with a use

The API places every score in one of six bands. The app then explains what water in that band is suitable for.

WQI5 bands, score ranges and suitable uses
Excellent85 < WQI5 ≤ 100Suitable for all uses. Drinkable after chlorine disinfection.
Good70 < WQI5 ≤ 85Suitable as a tap-water source and for aquaculture, industry, recreation and irrigation.
Fair50 < WQI5 ≤ 70A tap-water source only after special treatment. Suitable for rough-fish farming, industry, recreation and irrigation.
Poor30 < WQI5 ≤ 50Suitable only for irrigation or industrial cooling.
Bad15 < WQI5 ≤ 30Not offensive, and usable for environmental conservation.
Terrible0 ≤ WQI5 ≤ 15May produce odours.

The formula is the reference. The models are for comparison.

With all five readings, the direct WQI5 formula is the default and the reference. Six regression models were trained to approximate it, so their accuracy and running cost can be compared, including in experiments where indicators are missing.

Mean absolute error with all five inputs (lower is better)
ModelMAER²
XGBoost0.2640.9996
LightGBM0.2880.9995
Random Forest0.3380.9990
SVM1.9160.9813
Polynomial regression5.1600.8995
Linear regression7.5560.7803

Averaged over five repeated splits. Error is measured against the dataset's Score column. The formula itself differs from that column (MAE 6.07) and the cause is still under investigation, so a lower error does not make a model more correct than the formula.

The research dataset and trained model files are not distributed. The formula works out of the box; trained models must be supplied locally.

Read the model card

When a reading is missing

The formula needs all five readings, but BOD is a five-day lab test and NH₃-N is usually analysed in a lab too, so field data often arrives incomplete. The research tested two ways to keep scoring: retrain a model on the indicators you have, or fill in the missing one from the others first. Both were checked on 10,714 records the models never saw during training.

  • Missing NH₃-N

    Uses DO, BOD, EC, SS

    Usable as an estimate

    Retrain without it
    MAE 2.37R² 0.95
    Fill it in from the others first
    MAE 2.40R² 0.95
  • Missing BOD

    Uses DO, NH₃-N, EC, SS

    Not reliable

    Retrain without it
    MAE 14.12R² −0.02
    Fill it in from the others first
    MAE 12.14R² 0.10
  • Missing BOD and NH₃-N

    Uses DO, EC, SS

    Not reliable

    Retrain without it
    MAE 14.72R² −0.14
    Fill it in from the others first
    MAE 12.89R² −0.01

MAE is in WQI5 points; lower is better. An R² below 0 means the estimate does worse than always guessing the average score.

NH₃-N can be estimated from the other four indicators to within about 2.4 points. BOD cannot: without it, the error rises to 12–15 points, so BOD is the indicator that has to be measured.

These are research results. The app and API currently score complete five-indicator records only.

Read the missing-indicator experiments

Tested on a 2 GB Jetson Nano

The same API runs in Docker on a Jetson Nano 2GB with four Cortex-A57 cores, using the CPU only. Training stays off the device.

Response time per request on the Nano
Scoring method95th percentileMedian
Direct WQI57.42 ms6.41 ms
XGBoost17.42 ms15.52 ms
LightGBM17.57 ms15.67 ms
Random Forest163.81 ms146.90 ms

Warm one-row HTTP requests over the device's loopback, MAXN power mode, 300 requests per method. Cold model loading is measured separately.

597 of 610

Replay jobs that match x86

The full experiment set was rerun on the Nano and compared with x86 within an absolute tolerance of 1e-6. The 13 that differ are missing-indicator reconstructions in linear and polynomial models, where ill-conditioned coefficients amplify tiny floating-point differences.

2.1.4 → 3.2.0

A wrong-score bug, caught

The first Nano deployment used XGBoost 2.1.4. It loaded the model without an error but returned wrong scores. Comparing against the x86 reference exposed it, and pinning 3.2.0 fixed it.

1.05 GB

Random forest is the heavy one

That is the size of the production random forest file. It takes 168 s to load cold and peaks at 1.2 GiB of memory on a device with 1.9 GiB. A 200-tree version is a third smaller for 0.33% more error; it stays a candidate until a full confirmation study.

Read the benchmark report

Get started

Install the app

Download the APK, open Backend settings and enter the address of a running API, for example http://192.168.1.20:8001.

Run the API

Needs Python 3.10 or newer. The direct formula needs no model files.

git clone https://github.com/KageRyo/WQSurrogateModels.git
cd WQSurrogateModels
uv sync
uv run python main.py

The runtime is also on PyPI as wqsurrogatemodels.

Call it from anything

Any HTTP client can use the endpoint the app uses.

curl -X POST http://localhost:8001/api/v2/assessment \
  -H "Content-Type: application/json" \
  -d '{"DO":70,"BOD":2,"NH3N":0.3,"EC":400,"SS":10,
       "model_type":"direct_wqi5"}'

{"score": 75.685, "category": "Good",
 "rating_range": "70 < WQI5 ≤ 85", ...}

Read the API reference