Metadata-Version: 2.4
Name: pipetransport
Version: 0.3.3
Summary: Timeseries analysis of water quality in branched drinking water distribution networks
Project-URL: Homepage, https://github.com/pipetransport/pipetransport
Project-URL: Documentation, https://pipetransport.github.io/pipetransport/
Project-URL: Repository, https://github.com/pipetransport/pipetransport
Project-URL: Issues, https://github.com/pipetransport/pipetransport/issues
Project-URL: Changelog, https://github.com/pipetransport/pipetransport/releases
Project-URL: Sponsor, https://www.savethechildren.net/what-we-do/emergencies/war-gaza-escalations-west-bank
Author: Bas des Tombe
License-Expression: AGPL-3.0-only
License-File: LICENSE
Keywords: chlorine-decay,distribution-network,drinking-water,hydraulics,pipe-network,residence-time,time-series,transport,water-age,water-quality
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Scientific/Engineering :: Hydrology
Classifier: Topic :: Scientific/Engineering :: Physics
Requires-Python: >=3.12
Requires-Dist: ipykernel>=4.0.1
Requires-Dist: matplotlib>=3.8.4
Requires-Dist: numpy>=2.0.0
Requires-Dist: pandas>=2.2.2
Requires-Dist: scipy>=1.13.0
Provides-Extra: docs
Requires-Dist: jupyterlite-core>=0.6; extra == 'docs'
Requires-Dist: jupyterlite-pyodide-kernel>=0.6; extra == 'docs'
Requires-Dist: jupyterlite-sphinx>=0.22; extra == 'docs'
Requires-Dist: nbformat>=5.0.2; extra == 'docs'
Requires-Dist: nbsphinx; extra == 'docs'
Requires-Dist: nbsphinx-link; extra == 'docs'
Requires-Dist: sphinx-autodoc-typehints; extra == 'docs'
Requires-Dist: sphinx-book-theme>=1.2.0; extra == 'docs'
Requires-Dist: sphinx-copybutton; extra == 'docs'
Requires-Dist: sphinx<10.0,>=9.0; extra == 'docs'
Requires-Dist: sphinxext-opengraph[social-cards]; extra == 'docs'
Provides-Extra: test
Requires-Dist: nbconvert>=7.1.0; extra == 'test'
Requires-Dist: nbformat>=5.0.2; extra == 'test'
Requires-Dist: pytest-cov>=2.3.1; extra == 'test'
Requires-Dist: pytest-xdist>=3.0.0; extra == 'test'
Requires-Dist: pytest>=6.2.5; extra == 'test'
Requires-Dist: ruff==0.15.6; extra == 'test'
Requires-Dist: ty; extra == 'test'
Requires-Dist: validate-pyproject-schema-store!=2026.7.20; extra == 'test'
Requires-Dist: validate-pyproject[all,store]; extra == 'test'
Description-Content-Type: text/markdown

# pipetransport

`pipetransport` computes the water quality delivered at the far end of a branched pipe network — a drinking water distribution system — from the quality of the produced water, the pipe dimensions, and the demand metered at each delivery point. It also runs the other way: from quality measured at a few delivery points, back to the quality that must have left the treatment plant. Water age and chlorine residual come out of the same machinery. All without a hydraulic solver!

|                        |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Testing of source code | <a href="https://github.com/pipetransport/pipetransport/actions/workflows/site.yml"><img alt="CI" src="https://github.com/pipetransport/pipetransport/actions/workflows/site.yml/badge.svg?branch=main" width="66" height="20"></a> <a href="https://pipetransport.github.io/pipetransport/htmlcov/"><img alt="Test Coverage" src="https://pipetransport.github.io/pipetransport/coverage-badge.svg" width="114" height="20"></a> <a href="https://github.com/pipetransport/pipetransport/actions/workflows/linting.yml"><img alt="Linting" src="https://github.com/pipetransport/pipetransport/actions/workflows/linting.yml/badge.svg?branch=main" width="115" height="20"></a> <a href="https://github.com/pipetransport/pipetransport/actions/workflows/release.yml"><img alt="Build and release package" src="https://github.com/pipetransport/pipetransport/actions/workflows/release.yml/badge.svg?branch=main" width="222" height="20"></a> |
| Testing of examples    | <a href="https://pipetransport.github.io/pipetransport/htmlcov_examples/"><img alt="Example Coverage" src="https://pipetransport.github.io/pipetransport/coverage_examples-badge.svg" width="114" height="20"></a>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Package                | <a href="https://pypi.org/project/pipetransport/"><img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/pipetransport.svg?logo=python&label=Python&logoColor=gold" width="215" height="20"></a> <a href="https://pypi.org/project/pipetransport/"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/pipetransport.svg?logo=pypi&label=PyPI&logoColor=gold" width="105" height="20"></a> <a href="https://github.com/pipetransport/pipetransport/compare/"><img alt="GitHub commits since latest release" src="https://img.shields.io/github/commits-since/pipetransport/pipetransport/latest?logo=github&logoColor=lightgrey" width="163" height="20"></a>                                                                                                                                                                                                                                                        |

## What you can do

- **Trace a contamination event** from the plant to every delivery point, with arrival times that follow the real demand pattern
- **Model chlorine residual** with separate bulk and wall decay, so thin service lines lose residual faster than trunk mains — as they do
- **Map water age** across the network and watch it swing over the day
- **Reconstruct the produced water quality** from measurements at a handful of delivery points
- **Size a monitoring campaign** by asking which part of the production history each sampling point actually constrains

## The idea

A branched network has one path from the plant to each delivery point, and a split does not change concentration. So the delivered quality is the produced quality, delayed — and the delay is set by how much water has to be pushed through each pipe on the way.

Inside a pipe of water volume `V` carrying flow `Q(t)`, a parcel entering at time `s` leaves when it has displaced exactly `V`. Chain that pipe by pipe and you have the arrival time at any node, exactly, for any demand pattern. Nothing has to be steady.

When the demands _do_ move in proportion, the chain collapses to a single number per path: `sum(V_i / f_i)`, with `f_i` the fraction of production that segment `i` carries. A shared trunk main enters every downstream path in full rather than being divided among them. `pipetransport` does not need that assumption, but it reproduces it exactly when it holds.

## Installation

```bash
pip install pipetransport
```

## Forward: what arrives at the taps

```python
import numpy as np
import pandas as pd

from pipetransport.examples import example_demand, example_network
from pipetransport.transport import source_to_endmember

network = example_network()  # plant -> trunk main -> two district mains -> four taps
tedges = pd.date_range("2025-06-01", "2025-06-08", freq="h")  # n+1 edges for n bins
demand = example_demand(tedges=tedges, network=network)  # m3/day, one array per tap

# A three-hour contamination event leaves the plant on 2 June, 06:00-09:00
cin = np.zeros(len(tedges) - 1)
cin[30:33] = 1.0

cout = source_to_endmember(
    cin=cin,
    flow=demand,
    tedges=tedges,
    cout_tedges=tedges,
    network=network,
)  # one series per tap, same units as cin

for tap, series in cout.items():
    peak = int(np.nanargmax(series))
    print(f"{tap}: peak {series[peak]:.2f} at {tedges[peak]}")
```

Only the demand at the taps is needed. Every internal pipe flow follows from mass conservation, and the split is recomputed at every time step — no proportional-demand assumption.

## Reverse: what left the plant

```python
import numpy as np
import pandas as pd

from pipetransport.examples import example_demand, example_network
from pipetransport.transport import endmember_to_source

network = example_network()
tedges = pd.date_range("2025-06-01", "2025-06-15", freq="h")
demand = example_demand(tedges=tedges, network=network)

# Hourly grab samples at two taps; NaN wherever nothing was sampled. The keys say which
# taps were sampled, so there is no separate node list to keep in step with the rows.
measured = {
    "T1": np.full(len(tedges) - 1, 0.6),
    "T4": np.full(len(tedges) - 1, 0.4),
}

cin = endmember_to_source(
    cout=measured,
    flow=demand,
    tedges=tedges,
    cout_tedges=tedges,
    network=network,
    regularization_strength=1e-4,  # ~ (noise / signal)^2
)  # (336,), NaN where no measurement constrains the bin
print(f"reconstructed {np.isfinite(cin).sum()} of {len(cin)} production bins")
```

Each sampling point constrains a different — and moving — window of the production history, so several of them together pin down more than any one alone.

## Chlorine residual and water age

```python
import numpy as np
import pandas as pd

from pipetransport.examples import example_demand, example_network
from pipetransport.logremoval import segment_decay_rate
from pipetransport.residence_time import endmember_to_source as water_age
from pipetransport.transport import source_to_endmember

network = example_network()
tedges = pd.date_range("2025-06-01", "2025-06-08", freq="h")
demand = example_demand(tedges=tedges, network=network)

# Bulk decay plus a wall reaction that scales with 4 / diameter
decay = segment_decay_rate(network=network, bulk_decay_rate=0.3, wall_decay_rate=0.02)

residual = source_to_endmember(
    cin=np.full(len(tedges) - 1, 1.0),  # dosed to 1 mg/L at the plant
    flow=demand,
    tedges=tedges,
    cout_tedges=tedges,
    network=network,
    decay_rate=decay,
)
age = water_age(flow=demand, tedges=tedges, cout_tedges=tedges, network=network)  # days

for tap, days in age.items():
    res = residual[tap]
    print(
        f"{tap}: residual {np.nanmin(res):.2f}-{np.nanmax(res):.2f} mg/L, "
        f"age up to {np.nanmax(days) * 24:.1f} h"
    )
```

## Scope

`pipetransport` models a **single source feeding a tree**: flow splits, never merges, and never reverses. That is what makes one source signal enough to describe the whole network. Loops, a second plant, storage tanks and flow reversals are outside it. Transport inside a pipe is plug flow, which is a good approximation for turbulent mains and an optimistic one for laminar service lines.

See [Assumptions](https://pipetransport.github.io/pipetransport/user_guide/assumptions.html) for the full list and what each one costs you.

## Examples and Documentation

- [Core concepts](https://pipetransport.github.io/pipetransport/user_guide/concepts.html) — the label coordinate, the arrival map, and why the flow-weighted average is exact
- [Assumptions](https://pipetransport.github.io/pipetransport/user_guide/assumptions.html) — when this package fits your network
- [Water quality in a distribution network](https://pipetransport.github.io/pipetransport/examples/01_Distribution_Network_Water_Quality.html) — the worked example notebook

Full documentation: [pipetransport.github.io/pipetransport](https://pipetransport.github.io/pipetransport/)

`pipetransport` is the distribution-network sibling of [gwtransport](https://github.com/gwtransport/gwtransport), which does the same kind of timeseries transport for groundwater.

## License

This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0), a strong copyleft license that requires making source code of any modifications available. This ensures improvements remain available to the community.

| Permissions      | Conditions                     | Limitations |
| ---------------- | ------------------------------ | ----------- |
| ✓ Commercial use | ℹ Disclose source              | ✗ Liability |
| ✓ Distribution   | ℹ License and copyright notice | ✗ Warranty  |
| ✓ Modification   | ℹ Network use is distribution  |             |
| ✓ Patent use     | ℹ Same license                 |             |
| ✓ Private use    | ℹ State changes                |             |

For more details about this license, see the [full AGPL-3.0 license text](https://choosealicense.com/licenses/agpl-3.0/).

All contributors are required to agree with the [Contibutor's License Agreement](CLA.md).
