network.Network

network.Network(self, reaches, results)

A network of nodes and reaches, addressable by the names it came with.

A result file names a location the way the model did: a node id, or a reach and a position along it, and every member here takes and gives those names. :meth:to_networkx is labelled with integers instead, each node carrying its name as the address attribute, and :meth:resolve gives the integer for a name - see :attr:NetworkLocation.graph_node.

Build one with :meth:open, which reads no timeseries. They stay in the file until :meth:read asks for them. The constructor is internal: it takes what a loader produces, not a file.

Notes

Which locations carry series depends on the format:

  • MIKE 1D (.res1d): nodes and reach gridpoints both carry them.
  • MIKE 11 (.res11): only gridpoints do. A node carries nothing, so an observation at a node is read at a gridpoint beside it, which addresses(reach=...) lists.
  • EPANET (.res): a node carries its own. A pipe’s are read at its start, (pipe_id, 0.0), and at its end, (pipe_id, length); the two give the same series. A pump or valve has no length, so only its start.

Examples

>>> from mikeio1d.network import Network
>>> network = Network.open("tests/testdata/network.res1d")
>>> print(network)
<Network>
Reaches: 118
Nodes: 495
Quantities: ['WaterLevel', 'Discharge']
Time: 1994-08-07 16:35:00 - 1994-08-07 18:35:00

Attributes

Name Description
period First and last timestep of the series this network reads.
quantities Quantities readable somewhere in this network, by their units.
reaches The network’s reaches, by the id the model gave them.

Methods

Name Description
addresses List the addresses this network can be read at.
open Read a network from a result file.
read Read the series named by (address, quantity) pairs, or one quantity everywhere.
resolve Say whether a location is in this network, what it carries, and where.
to_dataframe Read every series in the network, labelled the way :meth:read labels them.
to_dataset Dataset of the timeseries, with each location’s address alongside.
to_networkx Build the network as an undirected networkx graph.

addresses

network.Network.addresses(reach=None, quantity=None)

List the addresses this network can be read at.

Where :meth:resolve goes from an address to what the network knows about the location, this gives the addresses themselves: the names :meth:read takes.

Parameters

Name Type Description Default
reach str Only the breakpoints along this reach, in the order they sit. The reach’s own end nodes are not among them - they are nodes, and a node is named by its own ID. A structure’s reach keeps the type prefix the file gives it: "Weir:119w1", where Res1D.structures says "119w1". None (default) lists everything. None
quantity str Only locations carrying this quantity. None (default) does not filter. None

Returns

Name Type Description
Sequence[str | tuple[str, float]] Addresses, each of which :meth:resolve answers for and :meth:read takes. A sequence: it has a length, can be indexed and can be iterated more than once. Pass it to list() for a list.

Raises

Name Type Description
KeyError If reach is not a reach of this network. The message suggests the reach meant, such as the prefixed id of a structure’s reach.

Examples

>>> network = Network.open("tests/testdata/network.res1d")
>>> network.addresses(reach="100l1")
[('100l1', 0.0), ('100l1', 23.8413574216414), ('100l1', 47.6827148432828)]

Every breakpoint of a reach that carries discharge, which is the batch a reach observation has to be scored against:

>>> network.addresses(reach="100l1", quantity="Discharge")
[('100l1', 23.8413574216414)]

open

network.Network.open(res, *, companions=None)

Read a network from a result file.

Opening reads no timeseries. :meth:read, :meth:to_dataframe and :meth:to_dataset read them when asked.

Parameters

Name Type Description Default
res (str, Path or Res1D) Path to a .res1d, .res11 or .res result file, or an already-opened :class:~mikeio1d.Res1D. Series are read from the file on disk, so edits made to a Res1D in memory with modify() are not seen. A Res1D opened with nodes=, reaches= or quantities= still gives the whole network, but only the series its filter lets through: a location it leaves out is there, carrying nothing. One opened with time= or step_every= is not supported yet. required
companions sequence of str, Path or Res1D, or None Files read alongside the result. The one kind is .resx: extra EPANET results for the same network. Its node quantities (tank Volume and Volume Percentage) are merged onto the matching nodes, and its reach quantities (pump efficiency, energy and energy costs) onto the matching reach’s breakpoints. None (default) looks for one beside the result file, matching its folder and stem; [] reads none; a list reads exactly those. Only EPANET results are looked beside. A companion passed as a Res1D follows the same rules for filters as res. The EPANET .inp input file is not a companion: reach lengths come from the .res itself. None

Returns

Name Type Description
Network

Raises

Name Type Description
NotImplementedError If no network can be built from the file’s extension, if the file holds catchments and no reaches, or if the result or a companion is a Res1D opened with time= or step_every=.
ValueError If a companion has an extension this reader does not know or is an .inp, if two .resx are given, if a .resx covers a different period from the result file, or if the two carry the same quantity at one location.

Notes

A UserWarning is raised if the file holds catchments beside its reaches, which the network leaves out with their quantities, or nodes that end no reach, which it leaves out too.

Examples

>>> from mikeio1d.network import Network
>>> network = Network.open("tests/testdata/network.res1d")
>>> network.reaches["100l1"].start
'100'

Name the companions rather than letting them be found, or pass [] to read the result file on its own:

>>> epanet = Network.open(
...     "tests/testdata/epanet.res",
...     companions=["tests/testdata/epanet.resx"],
... )
>>> "Volume" in epanet.quantities
True
>>> alone = Network.open("tests/testdata/epanet.res", companions=[])
>>> "Volume" in alone.quantities
False

An EPANET reach’s length comes from the .res itself. A pump or valve has none:

>>> alone.reaches["10"].length
3209.544
>>> alone.reaches["9"].length is None
True

A filtered Res1D gives the whole network, carrying only what its filter lets through:

>>> from mikeio1d import Res1D
>>> res = Res1D("tests/testdata/network.res1d", nodes=["1"], reaches=["100l1"])
>>> filtered = Network.open(res)
>>> filtered.resolve("1").quantities, filtered.resolve("101").quantities
(('WaterLevel',), ())

Notes

Which locations carry series depends on the format; see :class:Network.

A .resx and its result file are compared on their periods here, but on their time steps only when a :meth:read uses both: neither header gives the steps, and reading them would read the timeseries.

read

network.Network.read(items=None, *, quantity=None, position_tol=None)

Read the series named by (address, quantity) pairs, or one quantity everywhere.

Pass either items or quantity, not both.

Each call opens the file once, however many items it asks for. So one call with many items is faster than many calls with one item each.

Parameters

Name Type Description Default
items sequence of (address, quantity) What to read. An address is a node ID, or a reach ID and a position along it, as :meth:addresses gives and :meth:resolve confirms. An empty sequence reads nothing at all, and returns an empty frame rather than the whole file. None
quantity str A quantity to read at every location that carries it, in place of items. The same as passing [(a, quantity) for a in addresses(quantity=quantity)]. None
position_tol float How far a position may be from a breakpoint’s own and still mean it. Defaults to 1e-3, enough to absorb a rounded float, and never narrows below it: a position that close to a breakpoint names it, and is read there or refused. Widen it to snap a measured chainage onto the model’s; the nearest breakpoint inside the window carrying the item’s quantity wins. Checked for a node ID too, but not used. Only with items. None

Returns

Name Type Description
pd.DataFrame Time-indexed over :attr:period, one column per element of items, in that order and keeping duplicates. The columns are the items themselves, as asked for rather than as snapped, so df[items[i]] selects the series asked for.

Raises

Name Type Description
KeyError If any item names a location the network does not have, or a quantity that location does not carry. The message says how many items fail and names the first ones. For quantity, if no location in the network carries it.
TypeError If neither or both of items and quantity are given, or position_tol is given with quantity.
ValueError If position_tol is negative or not finite, or if the items span the result file and its .resx companion and the two turn out to have different time axes.

Notes

A node of a MIKE 11 result carries nothing; see :class:Network for which locations carry series in each format.

Examples

>>> network = Network.open("tests/testdata/network.res1d")
>>> df = network.read([("101", "WaterLevel")])
>>> df.shape
(110, 1)
>>> df.columns.tolist()
[('101', 'WaterLevel')]

One quantity at every location that carries it:

>>> network.read(quantity="Discharge").shape
(110, 129)

A measured chainage, snapped onto the model’s nearest breakpoint. The column keeps the address asked for:

>>> df = network.read([(("100l1", 23.8), "Discharge")], position_tol=0.1)
>>> df.columns.tolist()
[(('100l1', 23.8), 'Discharge')]

A reach observation, whose breakpoints have to agree before one of them can stand for the reach:

>>> points = network.addresses(reach="100l1", quantity="Discharge")
>>> network.read([(point, "Discharge") for point in points]).columns.tolist()
[(('100l1', 23.8413574216414), 'Discharge')]

resolve

network.Network.resolve(address, *, position_tol=None, quantity=None)

Say whether a location is in this network, what it carries, and where.

An address that is not here gives None rather than an exception.

Parameters

Name Type Description Default
address str or tuple[str, float] A node ID, or a reach ID and a position along it. A reach’s own end nodes are named by their IDs, which reaches[reach_id].start and .end give. A structure’s reach ID keeps the type prefix the file gives it, as in ("Weir:119w1", 0.5). required
position_tol float How far a position may be from a breakpoint’s own and still mean it. Defaults to 1e-3, enough to absorb a rounded float, and never narrows below it: a position that close to a breakpoint names it. Widen it to snap a measured chainage onto the model’s; the nearest breakpoint inside the window wins. Checked for a node ID too, but not used. None
quantity str Only a location carrying this quantity. A position snaps onto the nearest breakpoint that carries it, so a caller need not know which quantities sit where on a staggered grid. A position naming a breakpoint that lacks it, or a node lacking it, gives None rather than a neighbour. None (default) snaps onto any breakpoint. None

Returns

Name Type Description
NetworkLocation or None None if there is no such location, or none carrying quantity: exactly when :meth:read would refuse the address with that quantity. Otherwise its address as the network spells it, every quantity readable there, and its graph node.

Raises

Name Type Description
ValueError If position_tol is negative or not finite.

Notes

A node of a MIKE 11 result carries nothing, so it resolves with empty quantities; see :class:Network for which locations carry series in each format.

Examples

>>> network = Network.open("tests/testdata/network.res1d")
>>> network.resolve("101")
NetworkLocation(address='101', quantities=('WaterLevel',), graph_node=5)

A measured chainage, snapped onto the breakpoint the file stores:

>>> network.resolve(("100l1", 23.8), position_tol=0.1)
NetworkLocation(address=('100l1', 23.8413574216414), quantities=('Discharge',), graph_node=3)

With a quantity, a position snaps only onto a breakpoint carrying it. On this staggered grid 5.0 is nearest the water level point at 0.0, but discharge sits at 23.84:

>>> network.resolve(("100l1", 5.0), position_tol=30, quantity="Discharge").address
('100l1', 23.8413574216414)

A position naming a breakpoint is not snapped away from it:

>>> network.resolve(("100l1", 0.0), position_tol=30, quantity="Discharge") is None
True
>>> network.resolve("no_such_node") is None
True

A node of a MIKE 11 result is in the network, carrying nothing:

>>> Network.open("tests/testdata/network_cali.res11").resolve("0 CALI")
NetworkLocation(address='0 CALI', quantities=(), graph_node=0)

to_dataframe

network.Network.to_dataframe()

Read every series in the network, labelled the way :meth:read labels them.

Meant for small networks: it holds every series in memory at once, and each call reads the file again. To read only some locations, or one quantity, use :meth:read::

network.read(quantity="Discharge")

Returns

Name Type Description
pd.DataFrame Time-indexed. Columns are (address, quantity) pairs, as :meth:read gives them.

Examples

>>> network = Network.open("tests/testdata/network.res1d")
>>> df = network.to_dataframe()
>>> df.shape
(110, 495)
>>> df.columns[:2].tolist()
[('100', 'WaterLevel'), ('99', 'WaterLevel')]

to_dataset

network.Network.to_dataset()

Dataset of the timeseries, with each location’s address alongside.

Meant for small networks: it reads every series, as :meth:to_dataframe does.

Returns

Name Type Description
xr.Dataset One variable per quantity over (time, graph_node). All of them share one graph_node axis, holding every location that carries any quantity; a variable is NaN where its location does not carry it. graph_node is the graph’s integer, as :attr:NetworkLocation.graph_node gives it, and the node_id, reach and position coordinates carry the address, so a consumer never has to hold on to the network to know what a column is. A node fills in node_id, a breakpoint reach and position, and the empty half says which it is:: Coordinates: * time datetime64 * graph_node int64 0 1 2 3 … node_id <U16 ‘J1’ ‘J2’ ’’ ’’ reach <U16 ’’ ’’ ‘r1’ ‘r1’ position float64 nan nan 0.0 24.5 Empty when no location carries data.

Examples

>>> network = Network.open("tests/testdata/network.res1d")
>>> ds = network.to_dataset()
>>> list(ds.data_vars), dict(ds.sizes)
(['WaterLevel', 'Discharge'], {'time': 110, 'graph_node': 495})

A column’s address, from its coordinates:

>>> graph_node = network.resolve(("100l1", 23.8), position_tol=0.1).graph_node
>>> column = ds.sel(graph_node=graph_node)
>>> column.node_id.item(), column.reach.item(), column.position.item()
('', '100l1', 23.8413574216414)

to_networkx

network.Network.to_networkx()

Build the network as an undirected networkx graph.

Each call builds a new graph, which the caller is free to edit. Hold on to it rather than calling this again.

A reach becomes a chain of edges: its start node, its breakpoints in order, its end node. The graph is undirected; which way a reach runs is in reaches[reach_id].start and .end.

Returns

Name Type Description
nx.Graph Nodes are graph nodes, the integers :attr:NetworkLocation.graph_node gives. Nodes and edges carry at least these attributes: * node address – the location’s address: a model node’s id, or a (reach_id, position) breakpoint. * edge length – the distance between the edge’s two ends, in the reach’s own units. None where the reach’s length is not known, which is only on the edge into its end node: an EPANET pump or valve has no length. networkx treats an edge whose weight is None as absent, so a route weighted by length goes around it. * edge boundary – True where both ends are the same place, a breakpoint sitting on its reach’s end node. Its length is 0.0.

Examples

>>> import networkx as nx
>>> network = Network.open("tests/testdata/network.res1d")
>>> graph = network.to_networkx()

From a graph node back to its address:

>>> graph.nodes[network.resolve("101").graph_node]["address"]
'101'

The shortest route between two nodes by length:

>>> start = network.resolve("100").graph_node
>>> end = network.resolve("99").graph_node
>>> route = nx.shortest_path(graph, start, end, weight="length")
>>> [graph.nodes[n]["address"] for n in route]
['100', ('100l1', 0.0), ('100l1', 23.8413574216414), ('100l1', 47.6827148432828), '99']

Its first edge is a boundary edge, since the reach’s first breakpoint sits on its start node:

>>> graph.edges[route[0], route[1]]
{'length': 0.0, 'boundary': True}

A route by length skips an edge whose length is None, as for an EPANET pump, and finds none where that edge was the only way:

>>> epanet = Network.open("tests/testdata/epanet.res")
>>> pump = epanet.reaches["9"]
>>> start, end = epanet.resolve(pump.start).graph_node, epanet.resolve(pump.end).graph_node
>>> nx.shortest_path(epanet.to_networkx(), start, end, weight="length")
Traceback (most recent call last):
...
networkx.exception.NetworkXNoPath: No path between 34 and 0.