Skip to content

Commit d842f9a

Browse files
committed
Update from_epanet docstring and network.qmd for the new breakpoints
Rewrites the from_epanet docstring's Notes section and the qmd's "EPANET reach geometry is limited" callout to describe the new behavior: a reach's single synthetic gridpoint duplicated into two breakpoints (one at each end), addressable by distance where reach.length is known and otherwise only via ReachObservation/recall(), plus the resx reach-quantity merge. Drops the closed #680 reference now that it describes the fix rather than the gap.
1 parent c23f900 commit d842f9a

2 files changed

Lines changed: 35 additions & 26 deletions

File tree

docs/user-guide/network.qmd

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,13 +226,12 @@ sorted(
226226
::: {.callout-warning}
227227
## EPANET reach geometry is limited
228228

229-
EPANET is a link-node model, and mikeio1d reports no length and a single synthetic gridpoint for each reach. So for an EPANET network:
229+
EPANET is a link-node model, and mikeio1d reports a single synthetic gridpoint for each reach, not tied to either end. That gridpoint is duplicated into two breakpoints, one at each end of the reach, so its own quantities (`Flow`, `Velocity`, ...) reach `find()`/`recall()`/`ReachObservation` the same way a MIKE reach's end data already does. So for an EPANET network:
230230

231-
* without `inp=`, every edge of `network.graph` has `length=None`. A length-weighted `networkx` call then fails rather than returning a meaningless number — shortest-path treats the edge as unreachable, and anything that sums the weights raises `TypeError`. The attribute is always present, since `networkx` defaults a missing weight to `1`. With `inp=`, only pumps and valves stay `None`, since `[PIPES]` is the one section carrying lengths
232-
* reaches have no breakpoints, so a `ReachObservation` cannot be matched — use `NodeObservation` instead
233-
* `find(reach=..., distance=<number>)` never resolves; only `distance="start"` and `distance="end"` work
231+
* without `inp=`, a reach's length is unknown, so only its first breakpoint (`distance=0.0`) is real; the second isn't addressable by a number at all — `find(reach=..., distance=<number>)` only resolves it via `distance="start"`/`"end"` (which return the node, not the breakpoint). The corresponding edges of `network.graph` are `length=None` — a length-weighted `networkx` call then fails rather than returning a meaningless number, since shortest-path treats a `None`-weight edge as unreachable and anything that sums the weights raises `TypeError`
232+
* with `inp=`, a pipe's second breakpoint sits at its full length, and the edge between the two breakpoints carries that real length. Pumps and valves keep an unaddressable second breakpoint even with `inp=`, since `[PIPES]` is the only section carrying lengths
234233

235-
For the same reason, `resx=` merges node quantities only. Its reach-level quantities — pump energy, efficiency and costs — have no breakpoint to live on, which is tracked in [#680](https://github.com/DHI/modelskill/issues/680).
234+
`resx=`'s reach-level quantities — pump energy, efficiency and costs — merge onto the matching reach's breakpoints the same way its node quantities merge onto nodes.
236235

237236
Node timeseries, `to_dataframe()`, `to_dataset()`, `find(node=...)` and `recall()` are unaffected.
238237
:::

src/modelskill/network.py

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -541,18 +541,21 @@ def from_epanet(
541541
resx : str, Path, Res1D or None, optional
542542
Companion ``.resx`` file from the same run. Its extra node
543543
quantities (tank ``Volume`` and ``Volume Percentage``) are merged
544-
onto the matching nodes. By default None, and those quantities are
545-
simply absent.
544+
onto the matching nodes, and its extra reach quantities (e.g. pump
545+
``efficiency``, ``energy`` and ``energy costs``) are merged onto
546+
the matching reach's breakpoints. By default None, and those
547+
quantities are simply absent.
546548
inp : str, Path or None, optional
547549
EPANET ``.inp`` input file for the same model, read for its
548550
``[PIPES]`` lengths. By default None, and reach lengths are
549551
undefined.
550552
nodes : str, list of str, or None, optional
551553
Which nodes get their timeseries loaded. See :meth:`from_mike`.
552554
reaches : str, list of str, or None, optional
553-
Which reaches get their gridpoint data loaded. See
554-
:meth:`from_mike`. EPANET results have no intermediate gridpoints,
555-
so this argument has no effect.
555+
Which reaches get their breakpoint data loaded. See
556+
:meth:`from_mike`. EPANET reaches have at most one gridpoint (see
557+
Notes), but this argument still governs whether its data - and
558+
any matching ``resx`` reach quantities - are populated.
556559
quantities : str, list of str, or None, optional
557560
Which quantities are read at each selected location. See
558561
:meth:`from_mike`.
@@ -585,22 +588,29 @@ def from_epanet(
585588
586589
Notes
587590
-----
588-
EPANET is a link-node model, and mikeio1d reports no length and a
589-
single synthetic gridpoint for each of its reaches. As a result:
590-
591-
* without ``inp``, every edge of :attr:`graph` has ``length=None``, so a
592-
length-weighted graph algorithm fails rather than returning a
593-
meaningless number. Pumps and valves keep ``length=None`` even with
594-
``inp``, since ``[PIPES]`` is the only section carrying lengths
595-
* reaches have no breakpoints, so
596-
:class:`~modelskill.obs.ReachObservation` cannot be matched against
597-
an EPANET network — use :class:`~modelskill.obs.NodeObservation`
598-
* ``find(reach=..., distance=<number>)`` never resolves; only
599-
``distance="start"`` and ``distance="end"`` work
600-
601-
For the same reason, ``resx`` merges node quantities only. Its
602-
reach-level quantities (pump energy, efficiency and costs) have no
603-
breakpoint to live on, which is tracked in issue #680.
591+
EPANET is a link-node model, and mikeio1d reports a single synthetic
592+
gridpoint for each reach, not tied to either end. That gridpoint is
593+
duplicated into two breakpoints, one at each end of the reach, so its
594+
own quantities (``Flow``, ``Velocity``, ...) are reachable through
595+
:meth:`find`, :meth:`recall` and
596+
:class:`~modelskill.obs.ReachObservation` the same way a MIKE reach's
597+
end data already is. As a result:
598+
599+
* without ``inp``, a reach's length is unknown, so only its first
600+
breakpoint (``distance=0.0``) is real; the second is not
601+
addressable by distance at all — ``find(reach=..., distance=...)``
602+
resolves it only via ``distance="start"``/``"end"`` (which return
603+
the node, not the breakpoint), or not at all by a number. The
604+
corresponding edges of :attr:`graph` are ``length=None``
605+
* with ``inp``, a pipe's second breakpoint sits at its full length —
606+
both breakpoints are then addressable by distance, and the edge
607+
between them carries the pipe's real length. Pumps and valves keep
608+
an unaddressable second breakpoint even with ``inp``, since
609+
``[PIPES]`` is the only section carrying lengths
610+
611+
``resx``'s reach-level quantities (pump ``efficiency``, ``energy`` and
612+
``energy costs``) merge onto the matching reach's breakpoints the same
613+
way its node quantities merge onto nodes.
604614
605615
Node timeseries, :meth:`to_dataframe`, :meth:`to_dataset`,
606616
``find(node=...)`` and :meth:`recall` are unaffected.

0 commit comments

Comments
 (0)