Conversation
There was a problem hiding this comment.
Pull request overview
This PR expands network-based workflows by introducing edge-based observations and alias/breakpoint resolution, enabling extraction/matching at arbitrary breakpoints along edges (not only named nodes). It also adds selective loading options for large Res1D networks and updates the user guide accordingly.
Changes:
- Added
EdgeObservationand extendedNetworkModelResult.extract()to support both node- and edge-based extraction, including alias resolution for string IDs and(edge, distance)breakpoints. - Implemented selective loading in
Network.from_res1d(nodes=..., reaches=...)to reduce memory usage while preserving full topology. - Updated documentation and tests for alias handling and selective loading behavior.
Reviewed changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_network.py | Adds tests for selective loading and alias/breakpoint resolution. |
| tests/test_match.py | Updates error-message expectation for network matching. |
| src/modelskill/timeseries/_timeseries.py | Extends dataset validation to accept edge / edge+distance coords. |
| src/modelskill/timeseries/_point.py | Adds EdgeCoords / breakpoint parsing and adjusts gtype attribution. |
| src/modelskill/timeseries/_coords.py | Introduces NodeCoords and new EdgeCoords. |
| src/modelskill/timeseries/init.py | Exposes _parse_network_breakpoint_input. |
| src/modelskill/obs.py | Adds EdgeObservation, gtype="edge", and node/breakpoint alias docs. |
| src/modelskill/network.py | Adds selective loading for Res1D, safer __repr__, and a copy() helper. |
| src/modelskill/model/network.py | Extends NetworkModelResult to extract by node alias/breakpoint and by edge. |
| src/modelskill/model/adapters/_res1d.py | Refactors Res1D adapters to support optional data loading for nodes/gridpoints. |
| src/modelskill/matching.py | Allows matching EdgeObservation to extracted NodeModelResult. |
| src/modelskill/comparison/_comparison.py | Tightens typing for raw_mod_data. |
| src/modelskill/comparison/_comparer_plotter.py | Adds mypy return-value suppression for populated axes list. |
| src/modelskill/comparison/_collection_plotter.py | Adds mypy return-value suppression for populated axes list. |
| src/modelskill/init.py | Re-exports EdgeObservation. |
| docs/user-guide/network.qmd | Documents selective loading, aliases, breakpoint usage, and edge observations. |
| docs/_quarto.yml | Adds EdgeObservation to docs navigation and quartodoc sections. |
| docs/.gitignore | Ignores generated *.quarto_ipynb notebooks. |
Comments suppressed due to low confidence (1)
src/modelskill/model/network.py:203
_extract_node()resolves string/tuple aliases viaself.network._alias_mapbut does not verify that the resolvednode_idis actually present inself.data.node(which can happen with selective loading). In that caseself.data.sel(node=...)will raise an unhelpfulKeyError. Consider validating membership after_resolve_alias()and raising aValueErrorexplaining that the node exists in the topology but its timeseries was not loaded (and how to load it viaNetwork.from_res1d(nodes=..., reaches=...)).
def _extract_node(self, observation: NodeObservation) -> NodeModelResult:
if observation.at is None and observation.node is None:
raise ValueError("NodeObservation must have either 'node' or 'at' set")
raw_id = observation.at if observation.at is not None else observation.node
assert raw_id is not None # Redundant assertion, included for mypy
node_id = self._resolve_alias(raw_id)
return NodeModelResult(
data=self.data.sel(node=node_id).drop_vars("node"),
node=node_id,
name=self.name,
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Agent-Logs-Url: https://github.com/DHI/modelskill/sessions/27bd2a41-305c-4ba0-a543-f0f064cb98a6 Co-authored-by: jpalm3r <28826351+jpalm3r@users.noreply.github.com>
Agent-Logs-Url: https://github.com/DHI/modelskill/sessions/27bd2a41-305c-4ba0-a543-f0f064cb98a6 Co-authored-by: jpalm3r <28826351+jpalm3r@users.noreply.github.com>
Agent-Logs-Url: https://github.com/DHI/modelskill/sessions/3034caca-12b5-4d02-8b3e-8183de2f2c15 Co-authored-by: jpalm3r <28826351+jpalm3r@users.noreply.github.com>
…into edge_observations
Renames all network-related 'edge' terminology to 'reach' to align with DHI nomenclature as requested in issue #641. Changes: - EdgeBreakPoint -> ReachBreakPoint - NetworkEdge -> NetworkReach - BasicEdge -> BasicReach - EdgeObservation -> ReachObservation - Network.__init__(edges=) -> Network.__init__(reaches=) - Network.find(edge=) -> Network.find(reach=) - Network._edges -> Network._reaches - NetworkModelResult._extract_edge() -> _extract_reach() - xarray coordinate 'edge' -> 'reach' - EdgeCoords -> ReachCoords in timeseries layer - observation() factory: gtype='edge' -> gtype='reach' - recall() return dict: 'edge' key -> 'reach' key - All docs, tests, and error messages updated Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rename edge to reach throughout network extension
ryan-kipawa
left a comment
There was a problem hiding this comment.
I reviewed together with an agent, mostly focused on public facing API. I distilled it down to these, which we can discuss if anything is unclear:
NodeObservation dual-mode parameters
node= xor at=, with both properties returning None when the other is active, is a smell that gets locked in once shipped. Could we collapse to a single polymorphic at= accepting:
str— node aliasint— internal idtuple[str, float]— breakpoint
_resolve_alias already handles all three.
gtype="reach" special-case
The observation() factory short-circuits for gtype="reach" before reaching GeometryType and _obs_class_lookup. Could we:
- Add
REACHto theGeometryTypeenum - Register
ReachObservationin_obs_class_lookup - Drop the early-return branch
Also worth a look: gtype="reach" + distance= returns a NodeObservation — the label and the returned class disagree.
Constructor kw-only convention
PointObservation and TrackObservation make spatial args keyword-only. The new constructors don''t:
NodeObservation(data, node=None, *, ...)
ReachObservation(data, reach, *, ...)Worth moving node and reach behind *, now — flipping positional → keyword-only later is a breaking change.
Two-class taxonomy vs MIKE 1D vocabulary
Topology has three concepts (Node / BreakPoint / Reach); observations have two classes, and NodeObservation(at=...) quietly covers breakpoints. MIKE 1D users distinguish node (junction) from gridpoint sharply, so NodeObservation(at=("r1", 21.5)) may read as a category error.
A sentence or two in the user guide would help — something framed around "do you know where the sensor is, or just which reach?", noting that "node" in the observation API is the broader graph sense (junctions and chainage points).
Also, it should be clear which grid point ReachObservation converges on (e.g. if there's two discharge gridpoints, what's the rule of thumb on which gets chosen).
Follow-ups (not blocking)
model_result()factory doesn''t includeNetworkModelResult(factory.py:16) — network workflow has to construct the class directly, which is asymmetric with the otherModelResulttypes.
Issue
Network data is stored in nodes or breakpoints and, to access the latter, you need the edge name and the distance within the edge (i.e. chainage). There are variables that are constant in all breakpoints from the same edge (e.g. discharge), thus requiring the chainage is redundant. In addition, in some domains, users typically do not use the chainage at all which creates friction.
This PR introduces the following changes:
Enhanced observation and matching capabilities:
EdgeObservationtype and integrated it throughout the codebase, allowing extraction and matching of model results at arbitrary breakpoints along network edges, not just at named nodes. [1] [2] [3] [4] [5] [6]NetworkModelResult.extract()method to support bothNodeObservationandEdgeObservation, with improved error messages and alias resolution for node and edge identifiers. [1] [2]Documentation improvements:
Example use