Conversation
| on_missing: Literal["raise", "skip"] = "raise", | ||
| aux_items: list[int | str] | None = None, | ||
| attrs: dict | None = None, | ||
| ) -> list[NodeObservation]: ... |
| quantity: Quantity | None = None, | ||
| aux_items: list[int | str] | None = None, | ||
| attrs: dict | None = None, | ||
| ) -> list[ReachObservation]: ... |
| on_missing: Literal["raise", "skip"] = "raise", | ||
| aux_items: list[int | str] | None = None, | ||
| attrs: dict | None = None, | ||
| ) -> list[ReachObservation]: ... |
db99d3c to
29aa481
Compare
The MIKE+ database reader lands next and does not belong in the same file as the Network class. Move network.py to network/__init__.py unchanged so the split that follows is a pure addition. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A network result file identifies nodes and reaches by ID, but observations are usually recorded against real-world station names held in the MIKE+ setup database. Read that sqlite database to resolve a station to the node or reach it sits on, so observations can be placed without hand-mapping every ID. Dataset variables also gain a long_name attribute, so a quantity keeps its label once it reaches xarray. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Closing unmerged. This branch splits in two, and both halves survive. The MIKE+ lookup is on branch One design decision from this PR did not survive the extraction, and is recorded on #706: here the lookup is Everything else is in #702:
The three review comments here are lint nits about #697 stays open; it was only referenced, not closed, here. |
Building a
NodeObservationtoday means knowing which node each sensor sits on, which usually ends up as a hand-written SQL query in a notebook. A MIKE+ project already records that:m_Measurementsays which file and item each measured timeseries lives in,m_Stationsays where in the network it belongs.This adds a
dbargument toNodeObservation.from_multipleandReachObservation.from_multiplethat does the lookup.The mapping is keyed by item, not by location
One dfs0 item maps to exactly one location, but a location can carry several items — two pressure transmitters either side of a check valve, three flow meters on one pipe. So the lookup returns one row per item and one observation is created per row. The existing
nodes={location: item}dict cannot express this, since a location can only appear once as a key; it still works, and its docstring now says what it cannot do.Which class you get is decided by the database
m_Station.locationtypesays whether a station sits on a node or a link, and it is load-bearing: somelocationidvalues match both a node alias and a reach id, so trying node-first-then-reach would silently resolve the wrong one. Junctions and tanks becomeNodeObservation, links becomeReachObservation, and a link carrying a chainage becomes aNodeObservationat that breakpoint. Asking one class for a quantity the database places on the other raises and names the class you want.Contained behind a fixed contract
network/_mikeplus.pyreturns a fixed set of columns —item_name,name,location,kind,quantity. Table names, the join, thelocationtypecodes and theresitemnameencoding stay inside that module, so a change to the database layout is a change to one file.obs.pynever sees a MIKE+ concept.network.pybecomesnetwork/__init__.pyto make room for it. Pure move — every import in the repo isfrom modelskill.network import ...and is unaffected.Other changes
ReachObservation.from_multiple, mirroring theNodeObservationclassmethod, which did not exist before.from_multiple(nodes=...)keys now accept aliases and(reach_id, distance)breakpoints.NodeObservation.atalready took all three; onlyfrom_multipleadvertised integers.Network.to_dataset()keyed its data variables by quantity name but never wrote it to the DataArray attrs, soNetworkModelResultreportedQuantity.undefined()and skill tables labelled the model column with the observation's quantity. res1d and EPANET files carry a name without a unit, andQuantity.from_cf_attrsneeds both, so it falls back to the name alone.Notes for review
Undefinedfor every item, so the database is the only reliable source for the name; the unit still comes from the data.on_missing="skip"builds from the rest.match()still does not compare an observation's quantity with the model's; that is a wider change, tracked in Quantity.is_compatible reports undefined quantities as incompatible #697.Verified end to end against a real EPANET model and its MIKE+ database: 30 observations, 30 comparers.