Skip to content

Reach EPANET link quantities via duplicated end breakpoints - #695

Open
jpalm3r wants to merge 2 commits into
expose-reach-quantitiesfrom
epanet-link-quantities
Open

Reach EPANET link quantities via duplicated end breakpoints#695
jpalm3r wants to merge 2 commits into
expose-reach-quantitiesfrom
epanet-link-quantities

Conversation

@jpalm3r

@jpalm3r jpalm3r commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Important

Stacked on #694 (expose-reach-quantities) — merge that first. This diff will include #694's changes until then; the description below covers only what this branch adds on top of it.

Summary

Fixes #680: from_res1d couldn't reach an EPANET link's own quantities (Flow, Velocity, ...), because a link-node reach has only one gridpoint and the existing gate (len(reach.gridpoints) > 2) excluded it from getting any breakpoints at all.

  • A reach with 2 or fewer gridpoints now gets its single synthetic gridpoint duplicated into two breakpoints, one at each end (distance 0.0, and distance reach.length if known, otherwise None). Its quantities become reachable through find()/recall()/ReachObservation, the same way MIKE's reach-end data already is.
  • ReachBreakPoint.distance is widened to float | None to allow this, with guards added everywhere a breakpoint's distance was assumed to be a real number (_generate_graph's edge-length math, find()'s tolerance matching).
  • Reach breakpoint construction moves out of Res1DReach.__init__ into a standalone _build_reach_breakpoints, mirroring how node loading already resolves merged data before constructing Res1DNode. This keeps Res1DReach free of Res1D/companion-file knowledge.
  • _build_reach_breakpoints also merges the companion resx file's reach-level quantities (pump efficiency, energy, energy cost) onto each breakpoint, using the same _merge_extra_quantities mechanism node loading already uses. _merge_extra_quantities's node_id keyword is renamed to location_id since it now serves both nodes and reaches.
  • Docs (from_epanet docstring, network.qmd) updated to describe the new breakpoint shape and drop the closed-issue callout.

Test plan

  • uv run pytest tests/ passing
  • uv run ruff check / uv run mypy on all touched files

@jpalm3r
jpalm3r changed the base branch from main to expose-reach-quantities August 6, 2026 13:34
@jpalm3r
jpalm3r force-pushed the epanet-link-quantities branch from 460396c to d842f9a Compare August 10, 2026 13:25
@jpalm3r
jpalm3r requested a lite review from Copilot August 10, 2026 14:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the network/Res1D adapter pipeline so EPANET “link-node” reaches expose their link quantities (e.g. Flow/Velocity) via synthetic duplicated reach-end breakpoints, including support for merging reach-level .resx quantities onto those breakpoints. This aligns EPANET reach data access with the existing breakpoint-based find()/recall()/ReachObservation workflow used elsewhere in ModelSkill’s network subsystem.

Changes:

  • Build reach breakpoints centrally (including “duplicate to both ends” behavior for short/synthetic-gridpoint reaches) and widen breakpoint distance typing to allow None when the along-reach position is unknown.
  • Merge companion .resx reach-level quantities onto reach breakpoints using the same “extra quantities” merge mechanism previously used for nodes.
  • Update EPANET-related docs and add/extend tests to validate breakpoint shape, graph edge lengths, and reach-observation extraction behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/test_network.py Adds coverage for duplicated EPANET reach breakpoints, edge length semantics, reach-observation extraction, and _merge_extra_quantities error messaging.
src/modelskill/network.py Allows ReachBreakPoint.id distances to be float | None and adds guards in graph-building / find() tolerance matching for unknown distances.
src/modelskill/model/network.py Skips None-distance breakpoints during reach extraction to avoid calling find() with an unknown distance.
src/modelskill/model/adapters/_res1d.py Moves reach breakpoint construction into _build_reach_breakpoints, adds reach-length resolution helper, and merges .resx reach quantities onto breakpoint data.
docs/user-guide/network.qmd Updates EPANET network documentation to describe duplicated breakpoints and new reach-quantity reachability/limitations.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/modelskill/network.py
Comment on lines 144 to 149
* :attr:`id` - a ``(reach_id, distance)`` tuple that uniquely locates the
break point within the network.
break point within the network. ``distance`` may be ``None`` when the
break point's position along the reach is genuinely unknown (e.g. a
link-node reach with no known length).
* :attr:`data` - a time-indexed :class:`pandas.DataFrame` whose columns
are quantity names.
Comment on lines +241 to +245
# distance - find() treats distance=None the same as "not
# provided" and would raise. Such a breakpoint is unreachable
# this way, not missing; skip it rather than error, since
# another breakpoint on the same reach may still resolve.
if breakpoint.distance is None:
@jpalm3r
jpalm3r marked this pull request as ready for review August 10, 2026 14:36
@jpalm3r
jpalm3r requested a review from ecomodeller as a code owner August 10, 2026 14:36
jpalm3r and others added 2 commits August 11, 2026 10:36
A link-node reach (EPANET) has one synthetic gridpoint belonging to
neither end, so its own quantities — Flow on a pipe, energy on a pump —
were unreachable through find() or ReachObservation.

Duplicate that gridpoint into a breakpoint at each end: distance 0.0, and
the reach length where known. Reaches with real gridpoints are unaffected.
A companion .resx now contributes reach-level quantities the same way it
already did for nodes, matched by gridpoint index, so _merge_extra_quantities
takes a location_id rather than a node_id.

Where the length is unknown the trailing breakpoint's distance is None, so
ReachBreakPoint.distance widens to float | None and the graph's
distance arithmetic guards against it. Such a breakpoint is not addressable
by find(reach=..., distance=...), but stays reachable via ReachObservation
and recall().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_resolve_alias walks the alias map comparing each key's distance against
the requested one. A breakpoint with distance None — now possible on a
link-node reach of unknown length — made that arithmetic raise a
TypeError, even when a sibling breakpoint on the same reach would have
resolved cleanly.

Skip those keys instead: such a breakpoint is unreachable by distance,
not missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jpalm3r
jpalm3r force-pushed the epanet-link-quantities branch from 98c3c98 to 0a4a007 Compare August 11, 2026 09:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

from_res1d cannot reach EPANET link quantities — no breakpoint for single-gridpoint reaches

2 participants