Skip to content

Commit a75da74

Browse files
jpalm3rclaude
andcommitted
Update the user guide and notebook for the boundary-edge model
Document the zero-length boundary edges and the two remaining NetworkNode properties, and warn that length-weighted networkx algorithms treat a 0.0 edge as a free hop rather than erroring. Drop the boundary property from the subclassing examples and the notebook. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 25dc7f3 commit a75da74

2 files changed

Lines changed: 1564 additions & 1601 deletions

File tree

docs/user-guide/network.qmd

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ uv add modelskill[networks]
2727
2828
import pandas as pd
2929
import numpy as np
30-
from typing import Any
3130
from modelskill.network import Network, NetworkNode, NetworkReach, ReachBreakPoint
3231
3332
@@ -46,10 +45,6 @@ class ExampleNode(NetworkNode):
4645
def data(self) -> pd.DataFrame:
4746
return self._data
4847
49-
@property
50-
def boundary(self) -> dict[str, Any]:
51-
return {}
52-
5348
5449
class ExampleReach(NetworkReach):
5550
"""Reach connecting two nodes with a given length."""
@@ -242,9 +237,21 @@ For the same reason, `resx=` merges node quantities only. Its reach-level quanti
242237
Node timeseries, `to_dataframe()`, `to_dataset()`, `find(node=...)` and `recall()` are unaffected.
243238
:::
244239

245-
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.
240+
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.
241+
242+
![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)
243+
244+
::: {.callout-note}
245+
## Zero-length boundary edges
246+
247+
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:
246248

247-
![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)
249+
```{python}
250+
[d for *_, d in network.graph.edges(data=True) if d["boundary"]][:2]
251+
```
252+
253+
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.
254+
:::
248255

249256
#### Selective loading
250257

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

471478
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`.
472479

473-
`NetworkNode` requires three properties: `id`, `data`, and `boundary`.
480+
`NetworkNode` requires two properties: `id` and `data`.
474481
`NetworkReach` requires four: `id`, `start`, `end`, and `breakpoints`.
475482

476483
`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.
@@ -481,7 +488,6 @@ The following is a simple implementation example:
481488
```python
482489
import pandas as pd
483490
import numpy as np
484-
from typing import Any
485491
from modelskill.network import NetworkNode, NetworkReach, Network
486492

487493

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

503-
@property
504-
def boundary(self) -> dict[str, Any]:
505-
return {}
506-
507509

508510
class ExampleReach(NetworkReach):
509511
"""Reach connecting two nodes with a given length."""
@@ -540,7 +542,7 @@ class ExampleReach(NetworkReach):
540542
```
541543

542544
::: {.callout-tip}
543-
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.
545+
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.
544546
:::
545547

546548

0 commit comments

Comments
 (0)