Guides

Water quality

Compare water samples with WHO, EU or Brazilian limits, compute ion balance, hydrochemical facies and Piper and Stiff values, and check field QA/QC with @welldot/utils.

water_samples is a ledger of samples, each with its field and laboratory results (water samples). The file records only what was measured. Exceedances, ion balance and facies are derived on demand, and drinking water limits are chosen by the reader, never stored (limit sets).

The outputs below come from two example wells: extremoz-rn-barreiras-raso (nitrate rising in a shallow well) and quixeramobim-ce-cristalino-salobro (brackish water in crystalline rock).

Which samples count

A corrected laboratory report is a new sample whose corrects points to the old one, and a result whose validation.status is rejected is excluded from every derivation (result rules). Start from the effective list:

import { getEffectiveWaterSamples, isResultUsable } from '@welldot/utils';

const samples = getEffectiveWaterSamples(well); // not retracted, sorted by datetime, sequence, file order
const latest = samples.at(-1)!;
const usable = latest.results.filter(isResultUsable);

Latest value of a parameter

import { getLatestResult } from '@welldot/utils';

getLatestResult(well, 'nitrate_as_n');
// { sample: { id: 'am-2025', … }, result: { parameter: {…}, value: 12.6, measured_in: 'lab' } }

It skips blanks and rejected results, and matches CAS numbers to their welldot code.

Exceedances

Pick a limit set from @welldot/core (Water quality vocabulary) and compare:

import { BR_GM_MS_888_2021, WHO_GDWQ_2022 } from '@welldot/core';
import { getExceedances } from '@welldot/utils';

getExceedances(latest, BR_GM_MS_888_2021);
// [
//   { result_index: 0, parameter: { code: 'ph', … }, limit: { min: 6, max: 9, … }, kind: 'below_min' },
//   { result_index: 3, parameter: { code: 'nitrate_as_n', … }, limit: { max: 10, … }, kind: 'above_max' }
// ]

getExceedances(latest, WHO_GDWQ_2022); // [] (WHO limits nitrate as NO₃, not as N)

Each exceedance has the result's index, the parameter, the limit and its kind (above_max, below_min, or a presence where absence is required). Censored values are handled: < 5 cannot exceed a limit of 10. Turbidity in FAU is never compared with nephelometric limits. You can also pass your own Limit[].

The same sample can pass one standard and fail another. That is why limits stay out of the file: the reader chooses the standard (jurisdiction neutrality).

Hydrochemistry

import {
  getEffectiveWaterSamples,
  getHydrochemicalFacies,
  getIonBalance,
  getPiperCoordinates,
  getStiffValues,
} from '@welldot/utils';

const sample = getEffectiveWaterSamples(brackishWell).at(-1)!;

getIonBalance(sample);
// { cations_meq: 44.29, anions_meq: 44.51, error_pct: -0.26 }

getHydrochemicalFacies(sample); // { cation: 'mixed', anion: 'chloride' }
getStiffValues(sample); // meq/L of Na+K, Ca, Mg, Cl, HCO₃+CO₃, SO₄
getPiperCoordinates(sample); // meq % in each triangle and the diamond point

The ion balance error is the usual check on a complete major-ion analysis: within about ±5 % it is considered consistent. Molar masses and charges come from the parameter vocabulary in @welldot/core. getAcidDrainageIndicators(sample) returns net alkalinity and the sulfate/chloride ratio when the analysis has them.

Field QA/QC

FunctionChecks
getRelativePercentDifferences(well, sampleId)Relative percent difference per parameter between a field duplicate or split and its parent_sample_id.
getBlankContamination(well)Substances detected in field, trip and equipment blanks, with their campaign.
getHoldingTimes(sample)Hours from collection to analysis per result, honoring day-resolution dates.
getReceivedTemperatureCompliance(sample, maxC?)Whether the sample arrived at the lab at or below maxC (6 °C by default).
getPurgeStabilization(purge, criteria?, window?)Whether the purge readings stabilized, per parameter, over the last readings.
getSampleDepth(well, sample) / isFormationWater(well, sample)The sampling depth, and whether it lies in a screened interval below the static level.
import { getRelativePercentDifferences } from '@welldot/utils';

getRelativePercentDifferences(well, 'am-2025-dup');
// [{ key: 'nitrate_as_n', original: 2.8, duplicate: 2.7, rpd_pct: 3.64 }, …]

Warnings

getWaterSampleWarnings(well) lists the water quality conditions the spec marks as warnings, such as a field duplicate without parent_sample_id, a sampling depth outside the screens, a result analyzed before it was collected or a duplicated result, with the sample ids and result indexes (validation warnings).

Display

import { formatWaterQualityResult, parameterUnitSymbol } from '@welldot/utils';

const labels = {
  notDetected: 'n.d.',
  present: 'present',
  absent: 'absent',
  estimated: 'est.',
};

for (const r of latest.results) {
  console.log(formatWaterQualityResult(r, labels), parameterUnitSymbol(r));
  // '< 5' 'mg/L', '0.004 (est.)' 'mg/L', 'absent' '', …
}

Labels are passed in, so you keep your own translations.