gwsource.network#

Global node graph of a simulation: several GWF models, their exchanges, package nodes and MVR.

Nodes (gwsource.flowstep.NodeBlock, in this order): the cells of every GWF model (user node
numbering, model by model as listed in mfsim.nam or given), then one node per reach, lake, well
or UZF cell of every enabled advanced package (open_simulation(package_nodes=...)). A FlowStep
built here has blocks; everything downstream (assembly, solve, metrics, diagnostics, travel
time) works on it unchanged, because it is just a larger graph.

Edges (each water transfer counted once):
- intercell flows of each model (FLOW-JA-FACE, IMETH = 1);
- GWF-GWF exchanges between two models that are both read: from the exchange's FLOW-JA-FACE
  record (IMETH = 6) of the first model that lists it; the second model's record must be its
  exact negative (checked); both models' boundary terms are dropped. An exchange with a model
  that is not read stays a boundary term ('GWF-GWF').
- cell <-> package node: the GWF budget's aquifer-side record of that package (SFR, LAK, MAW,
  UZF-GWRCH), after checking, cell by cell, that the package budget's GWF record is its negative
  to aquifer_rtol (default AQUIFER_RTOL) of the package's largest exchange; the record is then
  an edge, no longer a term;
- package node <-> package node: the package budget's FLOW-JA-FACE (SFR reach to reach, LAK
  outlet to lake, UZF cell to cell);
- MVR (see below).
Every other record is a source or sink term of its node (gwsource.packages lists them). Package
node terms are named '<package>:<record>' (parent = the package name, package_type = its type),
so select(package='MAW') selects all terms of the MAW wells, whatever the package is called, and
select(package='MAW', term='RATE') their pumping.
With several models, every term package name is prefixed '<model>/'; selectors also match the
name without the prefix, so select(package='WEL') selects the wells of every model.

Water mover. With an MVR budget file, each transfer of q from a provider (package, bound) to a
receiver package node becomes an edge, if the receiver package has nodes and the provider's
TO-MVR record(s) for that bound are in the graph: on a cell (DRN-TO-MVR, 'UZF-GWD TO-MVR', ...)
or on an enabled package node (TO-MVR, RATE-TO-MVR, REJ-INF-TO-MVR, ...). When one provider
bound has several such records (a UZF cell's REJ-INF-TO-MVR and its cell's GWD TO-MVR; a MAW
well's RATE-TO-MVR and FW-RATE-TO-MVR), the transfer is shared among them in proportion to their
rates. The routed rate is taken off the provider's TO-MVR sink and the receiver's FROM-MVR
source; a remainder within MOVER_SNAP_RTOL of the original record (MF6 computes the moved share
at the last outer iteration, so the records differ at solver tolerance) is set to 0 and shows up
in the node residual instead. Transfers that cannot be routed stay sink and source terms, as
without package nodes. Movers between models (the simulation-level MVE) are not read.

Residuals: the FLOW-JA-FACE diagonal of a cell keeps MF6's own residual (which includes its
exchange flows); the diagonal of a package node is the sum of all its records in the package
budget, MF6's balance of that node. diagnostics.cell_residual() recomputes both from the graph.

Water volume: package nodes take the VOLUME auxiliary of their STORAGE record (SFR reach, lake,
MAW well, UZF cell) as FlowStep.fixed_volume; a package without it gets zero-volume nodes
(listed in notes['zero_volume_packages']). Cells keep area x saturated thickness x porosity.

Per-model output: model_view() and result_view() cut a network step and a trace result down to
one model's cells (with that model's grid shape and files), so the single-model exports and
reshaping (gwsource.export) apply; package_rows() lists the package-node results as a table.
class gwsource.network.TermTable(node, flow, package, bound, boundname, packages, keep)[source]#

Bases: object

External term entries under assembly: global node, flow, package, bound, boundname.
Parameters:
of_package(index)[source]#
Return the entry indices of one package that are still kept.
Parameters:

index (int)

Return type:

ndarray

Bases: object

CSR triplets (row, column, FLOW-JA-FACE value) of the global graph under assembly.
Parameters:
add(rows, cols, vals)[source]#
Append entries (flowja convention: value = flow INTO row FROM column).
Return type:

None

add_flows(into, from_, q)[source]#
Append a connection both ways for a flow q INTO node `into` FROM node `from_`.
Return type:

None

gwsource.network.block_offsets(sizes)[source]#
Return the first global node of each block.
Parameters:

sizes (list[int])

Return type:

list[int]

gwsource.network.model_blocks(steps)[source]#
Return one block per model, in order.
Parameters:

steps (dict[str, FlowStep])

Return type:

list[NodeBlock]

gwsource.network.package_blocks(pkg_steps, start)[source]#
Return one block per enabled package, after the model blocks.
Parameters:
Return type:

list[NodeBlock]

gwsource.network.model_terms(steps, blocks, multi)[source]#
Return the term table of every model's external terms, on global nodes.
Parameters:
Return type:

TermTable

gwsource.network.package_terms(pkg_steps, blocks, multi, names_of)[source]#
Return the term table of every package node's source and sink records.
Parameters:
Return type:

TermTable

gwsource.network.join_tables(first, second)[source]#
Return the two term tables concatenated (second's packages after first's).
Parameters:
Return type:

TermTable

Add every model's FLOW-JA-FACE (diagonal included) on global nodes.
Parameters:
Return type:

None

Turn GWF-GWF exchange terms between models that are both read into edges; return names.
Parameters:
Return type:

list[str]

Turn each package's cell exchange into edges and drop the aquifer-side GWF records.

Returns {block label: (global package nodes, global cells)} of the aquifer connections.

The edges take the GWF budget's values (so cell residuals stay MF6's); the package budget's
GWF record must have the same connections with opposite flows to aquifer_rtol of the
package's largest exchange (the two sides differ at solver tolerance: MF6 evaluates them
with the head and the package stage of different iterates). A larger tolerance lets a
loosely solved model through: the difference stays in the package node's residual (its
balance in the package budget), which closure and duality include as the residual term.
Parameters:
Return type:

dict[str, tuple[ndarray, ndarray]]

Add package-package edges (SFR reach to reach, LAK outlets, UZF cell to cell).
Parameters:
Return type:

None

gwsource.network.mover_records(table)[source]#
Index the kept mover records once.

Returns ({(model, provider package): {bound: TO-MVR entries}},
{(model, receiver package): {global node: FROM-MVR entries}}).
Parameters:

table (TermTable)

Return type:

tuple[dict, dict]

Route MVR transfers between nodes of the graph; return the total routed rate.
Parameters:
Return type:

float

gwsource.network.package_diagonals(links, pkg_steps, blocks)[source]#
Add each package node's own budget residual as its diagonal entry.
Parameters:
Return type:

None

gwsource.network.build_csr(n, links)[source]#
Return (ia, ja, flowja) sorted by row, diagonal first, then by column.
Parameters:
Return type:

tuple[ndarray, ndarray, ndarray]

gwsource.network.compact_terms(table)[source]#
Return the kept entries; packages whose entries all became edges are removed.

Packages that had no entries in the budget (an empty record) are kept, as in a cells-only
step, so that selecting them still works.
Parameters:

table (TermTable)

Return type:

TermTable

gwsource.network.build_network(steps, pkg_steps, transfers, names_of=None, aquifer_rtol=0.0001)[source]#
Return the global FlowStep of several model steps, package steps and MVR transfers.

aquifer_rtol: tolerance of the GWF and package budgets' aquifer exchange (aquifer_links).
Parameters:
Return type:

FlowStep

gwsource.network.simulation_models(ws)[source]#
Return the names of every GWF6 model in mfsim.nam, in order.
Parameters:

ws (str | Path)

Return type:

list[str]

class gwsource.network.NetworkReader(models, packages, movers, notes=<factory>, aquifer_rtol=0.0001)[source]#

Bases: object

Every model's grid and budget, and the package and MVR budgets, opened once.

read() returns the global FlowStep of one time step; steps and step_length() follow the first
model, so trace_transient() works on a NetworkReader as on a StepReader. aquifer_rtol is the
tolerance of the GWF and package budgets' aquifer exchange (see aquifer_links).
The budget files stay open until close() (or the end of a with block).
Parameters:
close()[source]#
Close every model's files and the package and MVR budget files.
Return type:

None

property steps: list[tuple[int, int]]#
0-based (kstp, kper) of every saved time step (of the first model).
step_length(kstpkper)[source]#
Return the length (DELT) of a saved time step.
Parameters:

kstpkper (tuple[int, int])

Return type:

float

read(kstpkper=(0, 0))[source]#
Read one time step of every model and package into one global FlowStep.
Parameters:

kstpkper (tuple[int, int])

Return type:

FlowStep

gwsource.network.open_simulation(ws, models=None, package_nodes=(), boundnames=False, aquifer_rtol=0.0001)[source]#
Open the models of a simulation (all GWF models by default) and their package budgets.

package_nodes: package types that get nodes, e.g. ("SFR", "MAW"); each needs BUDGET FILEOUT,
and MVR (if present in a model with package nodes) needs BUDGET FILEOUT too.
aquifer_rtol: largest allowed difference, relative to a package's largest aquifer exchange,
between the GWF budget's record of the package and the package budget's GWF record. The two
differ at solver tolerance; a loosely solved model may need more than the default, and the
difference then shows up in the package nodes' residuals.
Parameters:
Return type:

NetworkReader

gwsource.network.read_network(ws, kstpkper=(0, 0), models=None, package_nodes=(), boundnames=False, aquifer_rtol=0.0001)[source]#
Read one time step of a simulation's models (and package nodes) into one global FlowStep.

aquifer_rtol: tolerance of the aquifer exchange check (see open_simulation).
Parameters:
Return type:

FlowStep

gwsource.network.global_values(step, per_model, fill=nan)[source]#
Return a per-node array from {model: per-cell array (any shape)}; fill elsewhere.
Parameters:
gwsource.network.global_nodes(step, label, local)[source]#
Return the global nodes of 0-based local indices (cells or package nodes) of a block.
Parameters:
Return type:

ndarray

gwsource.network.model_block(step, model=None)[source]#
Return a model's block; model may be omitted when the step has only one model.
Parameters:
Return type:

NodeBlock

gwsource.network.model_view(step, model=None)[source]#
Return one model's cells as a single-model FlowStep (for export and reshaping only).

Edges to other models and to package nodes are left out, so its budgets do not close; trace
the network step and cut the result with result_view().
Parameters:
Return type:

FlowStep

gwsource.network.term_index(step, block)[source]#
Return the indices of the term entries located on a block's nodes.
Parameters:
Return type:

ndarray

gwsource.network.result_view(result, step, model=None)[source]#
Return a trace result cut down to one model's cells (pairs with model_view()).
Parameters:
gwsource.network.transient_view(result, model=None)[source]#
Return a TransientResult cut down to one model's cells (statistics and kept series).
Parameters:

model (str | None)

gwsource.network.package_rows(step, result)[source]#
Return one row per package node: block, id and every per-node output of every group.
Parameters:
Return type:

list[dict]

gwsource.network.package_cells(step, block)[source]#
Return, per package node, the global cells of its aquifer connections (not MVR edges).
Parameters:
Return type:

list[ndarray]