Skip to main content

Water utilities — distribution & quality

The problem. A water network flows from sources through trunk mains into district zones and out to meters. When two distant zones report the same quality problem — turbidity, low chlorine — at nearly the same time, the operator must find the common upstream source fast, because that's where to isolate and flush. With a flat sensor list it's guesswork. On the flow graph, it's the point where the two zones' upstream paths converge.

This scenario introduces the multi-source common-cause graph pattern: trace upstream from each affected zone and intersect to find where the trouble enters.

Set up demo data

New workspace? Run this once (Python) to create the flow-network graph (a source feeding a trunk main, which feeds two zones) and a turbidity reading out of range in each zone — so the upstream-convergence trace finds their shared main. Safe to re-run.

import datahub_sdk, pandas as pd

client = datahub_sdk.DataHubClient.from_env()

client.resources.create(
[datahub_sdk.Resource(external_id=x, name=x, labels=[x]) for x in
["source_lake_a", "main_trunk_north", "zone_west_12", "zone_east_07"]],
[datahub_sdk.RelForm.by_external_ids("source_lake_a", "main_trunk_north", "feeds"),
datahub_sdk.RelForm.by_external_ids("main_trunk_north", "zone_west_12", "feeds"),
datahub_sdk.RelForm.by_external_ids("main_trunk_north", "zone_east_07", "feeds")])

for s in ["zone_west_12_turbidity_ntu", "zone_east_07_turbidity_ntu"]:
client.timeseries.create([datahub_sdk.TimeSeries(external_id=s, name=s, unit="ntu", value_type="float")])
client.timeseries.insert_from_lists(timestamps=[pd.Timestamp.now(tz="UTC")], values=[7.5], ts=s)

1. Model the flow network

Each feeds edge points downstream: source → main → zone. Walking against the flow (undirected traversal) recovers a zone's upstream supply.

List<RelForm> flow = List.of(
rel("feeds", "source_lake_a", "main_trunk_north"),
rel("feeds", "main_trunk_north", "zone_west_12"),
rel("feeds", "main_trunk_north", "zone_east_07"));

client.resources().create(assets, flow);

2. Two zones report trouble — find where the flows meet

Trace upstream from each affected zone and intersect the two supply paths. The shared node — here main_trunk_north, or beyond it source_lake_a — is where to isolate.

ResourceNetwork west = client.resources().fetchRelated("zone_west_12", 20);
ResourceNetwork east = client.resources().fetchRelated("zone_east_07", 20);

Set<String> westUpstream = west.nodes().stream()
.map(Resource::getExternalId).collect(Collectors.toSet());
Set<String> common = east.nodes().stream()
.map(Resource::getExternalId).filter(westUpstream::contains).collect(Collectors.toSet());

// common holds main_trunk_north (and source_lake_a) → isolate there

This is the same shared-node reasoning as alarm correlation, applied to a flow network to pinpoint a contamination source.

3. Detect the problem in the first place

Turbidity and chlorine are series per zone (zone_west_12_turbidity_ntu, zone_west_12_chlorine_mg_l); a reading out of range raises a water_quality_alert event that kicks off the trace above.

See the result

The two zones' upstream paths intersect at one node — where to isolate and flush:

main_trunk_north ← the shared supply behind both quality alerts

See also