Skip to content

transport

A network: generators sit on buses, lines connect buses, and power balances at every bus.

✔ Agrees with hand-written linopy 0.9.0 — objective 4400, matched to rtol=1e-09.

The problem

\[\sum_{g \thinspace:\thinspace \mathrm{bus}(g) = b} p_{s,g} \quad+\quad \sum_{\ell \thinspace:\thinspace \mathrm{to}(\ell) = b} f_{s,\ell} \quad-\quad \sum_{\ell \thinspace:\thinspace \mathrm{from}(\ell) = b} f_{s,\ell} \quad=\quad d_{s,b}\]

Each sum runs over the generators or lines a relation sends to bus \(b\). \(\mathrm{bus}\), \(\mathrm{to}\) and \(\mathrm{from}\) are relations the dimensions declare, not sets in their own right. Load is \(d\) here, because \(\ell\) is already the line index.

The model

The same model, as math

Least-cost dispatch over a network, where a generator sits on a bus, a line joins two of them, and what is generated has to reach the load over the lines.

Sets

Symbol Meaning
\(\mathcal{S}\) index \(s\) — snapshot — dispatch periods
\(\mathcal{G}\) index \(g\) — generator with \(\mathrm{gen\_bus}: \mathcal{G} \to \mathcal{B}\) — generating units, each sitting on one bus
\(\mathcal{B}\) index \(b\) — bus with \(\mathrm{gen\_bus}: \mathcal{G} \to \mathcal{B},\ \mathrm{line\_from}: \mathcal{L} \to \mathcal{B},\ \mathrm{line\_to}: \mathcal{L} \to \mathcal{B}\) — network nodes
\(\mathcal{L}\) index \(\ell\) — line with \(\mathrm{line\_from}: \mathcal{L} \to \mathcal{B},\ \mathrm{line\_to}: \mathcal{L} \to \mathcal{B}\) — transmission lines, each joining two buses

Parameters

Symbol Meaning
\(\bar p\) p_max over \(\mathcal{G}\) — installed capacity
\(c\) cost over \(\mathcal{G}\) — marginal cost
\(\bar f\) cap over \(\mathcal{L}\) — forward transmission limit
\(\underline{f}\) neg_cap over \(\mathcal{L}\) — reverse transmission limit
\(d\) load over \(\mathcal{S} \times \mathcal{B}\) — demand at each bus

Variables

Symbol Meaning
\(p\) p over \(\mathcal{S} \times \mathcal{G}\) — output of a generator in a snapshot
\(f\) f over \(\mathcal{S} \times \mathcal{L}\) — flow on a line, signed towards its line_to bus

Definitions

Symbol Meaning
\(\mathit{gen\_at\_bus}\) gen_at_bus over \(\mathcal{S} \times \mathcal{B}\) — what the generators sitting on a bus produce there
\(\mathit{net\_inflow}\) net_inflow over \(\mathcal{S} \times \mathcal{B}\) — flow arriving at a bus minus flow leaving it, so a negative value is a net export

Objective

\[ \min \sum_{s \in \mathcal{S},\ g \in \mathcal{G}} p_{s,g} \cdot c_{g} \]

Subject to

balance

\[ \mathit{gen\_at\_bus}_{s,b} + \mathit{net\_inflow}_{s,b} = d_{s,b} \qquad \forall\, s \in \mathcal{S},\ b \in \mathcal{B} \]

Definitions

gen_at_bus

\[ \mathit{gen\_at\_bus}_{s,b} = \sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b} p_{s,g} \qquad \forall\, s \in \mathcal{S},\ b \in \mathcal{B} \]

net_inflow

\[ \mathit{net\_inflow}_{s,b} = \sum_{\ell \in \mathcal{L} \,:\, \mathrm{line\_to}(\ell) = b} f_{s,\ell} - \left( \sum_{\ell \in \mathcal{L} \,:\, \mathrm{line\_from}(\ell) = b} f_{s,\ell} \right) \qquad \forall\, s \in \mathcal{S},\ b \in \mathcal{B} \]

Variable domains

p

\[ 0 \le p_{s,g} \le \bar p_{g} \qquad \forall\, s \in \mathcal{S},\ g \in \mathcal{G} \]

f

\[ \underline{f}_{\ell} \le f_{s,\ell} \le \bar f_{\ell} \qquad \forall\, s \in \mathcal{S},\ \ell \in \mathcal{L} \]

The tabs start from the instance's tables — one frame per parameter.

description: >-
  Least-cost dispatch over a network, where a generator sits on a bus, a line
  joins two of them, and what is generated has to reach the load over the
  lines.

dimensions:
  snapshot:
    description: dispatch periods
    dtype: int
  generator:
    description: generating units, each sitting on one bus
    dtype: str
  bus:
    description: network nodes
    dtype: str
  line:
    description: transmission lines, each joining two buses
    dtype: str

relations:
  gen_bus:
    description: the bus a generator sits on
    key: generator
    values: bus
  line_from:
    description: the bus a line leaves
    key: line
    values: bus
  line_to:
    description: the bus a line arrives at
    key: line
    values: bus

parameters:
  p_max:
    description: installed capacity
    dims: [generator]
  cost:
    description: marginal cost
    dims: [generator]
  cap:
    description: forward transmission limit
    dims: [line]
  neg_cap:
    description: reverse transmission limit
    dims: [line]
  load:
    description: demand at each bus
    dims: [snapshot, bus]

variables:
  p:
    description: output of a generator in a snapshot
    dims: [snapshot, generator]
    bounds:
      lower: 0
      upper: p_max
  f:
    description: flow on a line, signed towards its `line_to` bus
    dims: [snapshot, line]
    bounds:
      lower: neg_cap
      upper: cap

expressions:
  gen_at_bus:
    expression: sum(p, by=gen_bus, over=generator, into=bus)
    description: what the generators sitting on a bus produce there
  net_inflow:
    expression: sum(f, by=line_to, over=line, into=bus) - sum(f, by=line_from, over=line, into=bus)
    description: flow arriving at a bus minus flow leaving it, so a negative value is a net export

constraints:
  balance:
    description: what is generated at a bus plus what arrives over the lines meets the load there
    dims: [snapshot, bus]
    expression: gen_at_bus + net_inflow == load

objective:
  sense: minimize
  description: total cost of generation; moving power over a line is free here
  expression: sum(p * cost)
# sources: parameter name -> frame or parquet path
with sps.solve('examples/transport.yaml', sources) as solution:
    solution.objective  # 4400.0
    solution.dual('balance')

The model-building half of examples/ports/references/linopy/transport.py:

def build(tables: dict[str, pd.DataFrame]) -> linopy.Model:
    """The instance's tables as a linopy model, row for row.

    ``tables`` is the same mapping the specsolve call attaches as ``sources``.
    """
    p_max: pd.Series = tables['p_max'].set_index('generator')['value']
    cost: pd.Series = tables['cost'].set_index('generator')['value']
    cap: pd.Series = tables['cap'].set_index('line')['value']
    neg_cap: pd.Series = tables['neg_cap'].set_index('line')['value']
    load = xr.DataArray(tables['load'].pivot(index='snapshot', columns='bus', values='value'))
    snapshots, buses = load.indexes['snapshot'], load.indexes['bus']

    gen_at = pd.DataFrame(0.0, index=buses, columns=p_max.index)
    for gen, bus in zip(tables['gen_bus']['generator'], tables['gen_bus']['bus'], strict=True):
        gen_at.loc[bus, gen] = 1.0
    flow_in = pd.DataFrame(0.0, index=buses, columns=cap.index)
    for line, src, dst in zip(
        tables['line_from']['line'], tables['line_from']['bus'], tables['line_to']['bus'], strict=True
    ):
        flow_in.loc[dst, line] += 1.0
        flow_in.loc[src, line] -= 1.0

    m = linopy.Model()
    p = m.add_variables(lower=0, upper=p_max, coords=[snapshots, p_max.index], name='p')
    f = m.add_variables(lower=neg_cap, upper=cap, coords=[snapshots, cap.index], name='f')
    m.add_constraints(
        (p * xr.DataArray(gen_at)).sum('generator') + (f * xr.DataArray(flow_in)).sum('line') == load,
        name='balance',
    )
    m.add_objective((p * cost).sum())
    return m

What it exercises

Three sum(by=) calls are what a network is in this language. The model declares three relations: gen_bus maps generator onto bus, and line_from and line_to map line onto bus. sum(f, by=line_to, over=line, into=bus) sums each line's flow onto its line_to bus, so the result lands on bus. The same f is summed twice through two relations, once as an inflow and once as an outflow.

There is no adjacency matrix and no hand-written join: the topology is data on the dimension.

The two halves of the balance are named expressions. Each stands in the constraint as its name with its body under it, and both backends build the body where the name stands, so naming them costs nothing at build or solve. What it buys is a constraint that reads as a sentence, and a quantity the solution hands back: expression('net_inflow') is the net flow at each bus that the balance constrained. One definition serves the constraint and the report.


examples/transport.yaml · back to all models