Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed docs/images/res1d_network_mapping.png
Binary file not shown.
182 changes: 182 additions & 0 deletions docs/images/res1d_network_mapping.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 16 additions & 14 deletions docs/user-guide/network.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ uv add modelskill[networks]

import pandas as pd
import numpy as np
from typing import Any
from modelskill.network import Network, NetworkNode, NetworkReach, ReachBreakPoint


Expand All @@ -46,10 +45,6 @@ class ExampleNode(NetworkNode):
def data(self) -> pd.DataFrame:
return self._data

@property
def boundary(self) -> dict[str, Any]:
return {}


class ExampleReach(NetworkReach):
"""Reach connecting two nodes with a given length."""
Expand Down Expand Up @@ -242,9 +237,21 @@ For the same reason, `resx=` merges node quantities only. Its reach-level quanti
Node timeseries, `to_dataframe()`, `to_dataset()`, `find(node=...)` and `recall()` are unaffected.
:::

A MIKE 1D network contains multiple levels that are unified into a generic network structure as depicted in the image below. The image introduces concepts like _find_, _recall_ and _boundary_ which are explained in the following sections.
A MIKE 1D network contains multiple levels that are unified into a generic network structure as depicted in the image below. The image introduces the _find_ and _recall_ concepts explained in the following sections.

![How a Res1D file maps to a Network object. Reaches and nodes are re-indexed as integers, exposing `find()`/`recall()` round-trip lookups. A reach's own start/end gridpoint sits at the same location as its node but stays a distinct graph node, joined to it by a zero-length boundary edge.](../images/res1d_network_mapping.svg)

::: {.callout-note}
## Zero-length boundary edges

A reach's first and last gridpoint sit at the same location as its start/end node, so each becomes a breakpoint connected to that node by an edge of length `0.0`. That edge carries `boundary=True` in `network.graph`, distinguishing it from an ordinary segment:

![How a Res1D file maps to a Network object. Reaches and nodes are re-indexed as integers; boundary nodes expose `find()`/`recall()` round-trip lookups.](../images/res1d_network_mapping.png)
```{python}
[d for *_, d in network.graph.edges(data=True) if d["boundary"]][:2]
```

If you run a length-weighted `networkx` algorithm on `network.graph` (e.g. shortest path), it will silently treat these as free hops rather than erroring — unlike an edge with `length=None` (see the EPANET callout above), a `0.0` weight is valid input and networkx has no reason to reject it. Filter on `boundary` first if you need distances that only count real reach segments.
:::

#### Selective loading

Expand Down Expand Up @@ -470,7 +477,7 @@ Use `ReachObservation` when your measured quantity is representative of the whol

In case you have your network data in a format that is not included in [Building a Network](#building-a-network), you can assemble a `Network` object by subclassing the abstract base classes `NetworkNode` and `NetworkReach`.

`NetworkNode` requires three properties: `id`, `data`, and `boundary`.
`NetworkNode` requires two properties: `id` and `data`.
`NetworkReach` requires four: `id`, `start`, `end`, and `breakpoints`.

`NetworkReach.length` is optional and defaults to `None`. Reach length matters in some domains (rivers, sewer networks) and not in others (link-node water distribution models), so override it only where a length exists. Where it is left undefined, the reach contributes an edge with `length=None` to `network.graph`, which keeps length-weighted graph algorithms from quietly treating the reach as free. Nothing else in modelskill reads the length — matching and extraction work from break point distances alone.
Expand All @@ -481,7 +488,6 @@ The following is a simple implementation example:
```python
import pandas as pd
import numpy as np
from typing import Any
from modelskill.network import NetworkNode, NetworkReach, Network


Expand All @@ -500,10 +506,6 @@ class ExampleNode(NetworkNode):
def data(self) -> pd.DataFrame:
return self._data

@property
def boundary(self) -> dict[str, Any]:
return {}


class ExampleReach(NetworkReach):
"""Reach connecting two nodes with a given length."""
Expand Down Expand Up @@ -540,7 +542,7 @@ class ExampleReach(NetworkReach):
```

::: {.callout-tip}
The three abstract properties that **every** `NetworkNode` subclass must implement are `id`, `data` and `boundary`. If `boundary` is not relevant for your use case, define the property to return an empty dictionary, as in the example above. Similarly, a `NetworkReach` with no intermediate points can return an empty `breakpoints` list, and one with no meaningful length can leave the `length` property out altogether.
The two abstract properties that **every** `NetworkNode` subclass must implement are `id` and `data`. A `NetworkReach` with no intermediate points can return an empty `breakpoints` list, and one with no meaningful length can leave the `length` property out altogether.
:::


Expand Down
Loading
Loading