Skip to content

Commit 8dd0465

Browse files
committed
Refactor .transport.freight
- Move freight activity/demand calculation from .transport.demand.TASKS to .freight.demand(). - Split groups of tasks to .freight.{tech_econ,usage}() functions. - Use Collector. - Replace use of genno.KeySeq with usage of Key. - Simplify invocation of broadcast_wildcard. - Edit comments, formatting, and docstrings for consistency.
1 parent 1e0946e commit 8dd0465

File tree

3 files changed

+148
-133
lines changed

3 files changed

+148
-133
lines changed

message_ix_models/model/transport/demand.py

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@
1717
cg,
1818
cost,
1919
exo,
20-
fv,
21-
fv_cny,
2220
gdp_cap,
23-
gdp_index,
2421
gdp_ppp,
2522
ldv_cny,
2623
ldv_ny,
@@ -165,24 +162,6 @@ def dummy(
165162
(ldv_ny + "total", "mul", ldv_ny + "ref", "ldv pdt factor:n-y"),
166163
# LDV PDT shared out by consumer group (cg, n, y)
167164
(ldv_nycg, "mul", ldv_ny + "total", cg),
168-
#
169-
# # Base freight activity from IEA EEI
170-
# ("iea_eei_fv", "fv:n-y:historical", quote("tonne-kilometres"), "config"),
171-
# Base year freight activity from file (n, t), with modes for the 't' dimension
172-
("fv:n-t:historical", "mul", exo.mode_share_freight, exo.activity_freight),
173-
# …indexed to base-year values
174-
(gdp_index, "index_to", gdp_ppp, literal("y"), "y0"),
175-
(fv + "0", "mul", "fv:n-t:historical", gdp_index),
176-
# Scenario-specific adjustment factor for freight activity
177-
("fv factor:n-t-y", "factor_fv", n, y, "config"),
178-
# Apply the adjustment factor
179-
(fv + "1", "mul", fv + "0", "fv factor:n-t-y"),
180-
# Select only the ROAD data. NB Do not drop so 't' labels can be used for 'c', next.
181-
((fv + "2", "select", fv + "1"), dict(indexers=dict(t=["RAIL", "ROAD"]))),
182-
# Relabel
183-
((fv_cny, "relabel2", fv + "2"), dict(new_dims={"c": "transport F {t}"})),
184-
# Convert to ixmp format
185-
(("demand::F+ixmp", "as_message_df", fv_cny), _DEMAND_KW),
186165
# Select only non-LDV PDT
187166
((pdt_nyt + "1", "select", pdt_nyt), dict(indexers=dict(t=["LDV"]), inverse=True)),
188167
# Relabel PDT
@@ -205,7 +184,6 @@ def dummy(
205184
"merge_data",
206185
"demand::LDV+ixmp",
207186
"demand::P+ixmp",
208-
"demand::F+ixmp",
209187
"demand::dummy+ixmp",
210188
),
211189
]

message_ix_models/model/transport/freight.py

Lines changed: 144 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
from collections import defaultdict
44
from functools import partial
5+
from operator import itemgetter
56
from typing import TYPE_CHECKING
67

7-
import genno
88
import numpy as np
99
import pandas as pd
10+
from genno import Key, literal
1011
from iam_units import registry
1112
from message_ix import make_df
1213

@@ -17,20 +18,28 @@
1718
same_node,
1819
same_time,
1920
)
21+
from message_ix_models.util.genno import Collector
2022

23+
from .demand import _DEMAND_KW
24+
from .key import bcast_tcl, bcast_y, exo, fv, fv_cny, gdp_index, gdp_ppp, n, y
2125
from .util import has_input_commodity, wildcard
2226

2327
if TYPE_CHECKING:
28+
from genno import Computer
2429
from sdmx.model.common import Code
2530

2631
from message_ix_models.model.transport import Config
2732

33+
#: Fixed values for some dimensions of some :mod:`message_ix` parameters.
2834
COMMON = dict(
2935
mode="all",
3036
time="year",
3137
time_dest="year",
3238
time_origin="year",
3339
)
40+
41+
#: Mapping from :mod:`message_ix` parameter dimensions to source dimensions in some
42+
#: quantities.
3443
DIMS = dict(
3544
node_loc="n",
3645
node_dest="n",
@@ -42,119 +51,13 @@
4251
level="l",
4352
)
4453

45-
#: Shorthand for tags on keys
46-
Fi = "::F+ixmp"
47-
54+
NTY = tuple("nty")
4855

49-
def prepare_computer(c: genno.Computer):
50-
from genno.core.attrseries import AttrSeries
56+
#: Target key that collects all data generated in this module.
57+
TARGET = "transport::F+ixmp"
5158

52-
from .key import bcast_tcl, bcast_y, n, y
5359

54-
to_add = [] # Keys for ixmp-structured data to add to the target scenario
55-
k = genno.KeySeq("F") # Sequence of temporary keys for the present function
56-
57-
### Produce the full quantity for input efficiency
58-
59-
# Add a technology dimension with certain labels to the energy intensity of VDT
60-
# NB "energy intensity of VDT" actually has dimension (n,) only
61-
t_F_ROAD = "t::transport F ROAD"
62-
c.add(k[0], AttrSeries.expand_dims, "energy intensity of VDT:n-y", t_F_ROAD)
63-
# Broadcast over dimensions (c, l, y, yv, ya)
64-
prev = c.add(k[1], "mul", k[0], bcast_tcl.input, bcast_y.model)
65-
# Convert input to MESSAGE data structure
66-
c.add(k[2], "as_message_df", prev, name="input", dims=DIMS, common=COMMON)
67-
68-
# Convert units
69-
to_add.append(f"input{Fi}")
70-
c.add(to_add[-1], convert_units, k[2], "transport info")
71-
72-
# Create base quantity for "output" parameter
73-
# TODO Combine in a loop with "input", above—similar to .ldv
74-
k_output = genno.KeySeq("F output")
75-
nty = tuple("nty")
76-
c.add(k_output[0] * nty, wildcard(1.0, "dimensionless", nty))
77-
for i, coords in enumerate(["n::ex world", "t::F", "y::model"]):
78-
c.add(
79-
k_output[i + 1] * nty,
80-
"broadcast_wildcard",
81-
k_output[i] * nty,
82-
coords,
83-
dim=coords[0],
84-
)
85-
86-
for par_name, base, ks, i in (("output", k_output[3] * nty, k_output, 3),):
87-
# Produce the full quantity for input/output efficiency
88-
prev = c.add(ks[i + 1], "mul", ks[i], getattr(bcast_tcl, par_name), bcast_y.all)
89-
90-
# Convert to ixmp/MESSAGEix-structured pd.DataFrame
91-
# NB quote() is necessary with dask 2024.11.0, not with earlier versions
92-
c.add(ks[i + 2], "as_message_df", prev, name=par_name, dims=DIMS, common=COMMON)
93-
94-
# Convert to target units
95-
to_add.append(f"output{Fi}")
96-
c.add(to_add[-1], convert_units, ks[i + 2], "transport info")
97-
98-
# Extract the 'output' data frame
99-
c.add(k[3], lambda d: d["output"], to_add[-1])
100-
101-
# Produce corresponding capacity_factor and technical_lifetime
102-
c.add(
103-
k[4],
104-
partial(
105-
make_matched_dfs,
106-
capacity_factor=registry.Quantity("1"),
107-
technical_lifetime=registry("10 year"),
108-
),
109-
k[3],
110-
)
111-
112-
# Convert to target units
113-
c.add(k[5], convert_units, k[4], "transport info")
114-
115-
# Fill values
116-
to_add.append(f"other{Fi}")
117-
c.add(to_add[-1], same_node, k[5])
118-
119-
# Base values for conversion technologies
120-
prev = c.add("F usage output:t:base", "freight_usage_output", "context")
121-
# Broadcast from (t,) to (t, c, l) dimensions
122-
prev = c.add(k[6], "mul", prev, bcast_tcl.output)
123-
124-
# Broadcast over the (n, yv, ya) dimensions
125-
d = tuple("tcl") + tuple("ny")
126-
prev = c.add(k[7] * d, "expand_dims", prev, dim=dict(n=["*"], y=["*"]))
127-
prev = c.add(k[8] * d, "broadcast_wildcard", prev, "n::ex world", dim="n")
128-
prev = c.add(k[9] * d, "broadcast_wildcard", prev, "y::model", dim="y")
129-
prev = c.add(k[10] * (d + ("ya", "yv")), "mul", prev, bcast_y.no_vintage)
130-
131-
# Convert output to MESSAGE data structure
132-
c.add(k[11], "as_message_df", prev, name="output", dims=DIMS, common=COMMON)
133-
to_add.append(f"usage output{Fi}")
134-
c.add(to_add[-1], lambda v: same_time(same_node(v)), k[11])
135-
136-
# Create corresponding input values in Gv km
137-
prev = c.add(k[12], wildcard(1.0, "gigavehicle km", tuple("nty")))
138-
for i, coords in enumerate(["n::ex world", "t::F usage", "y::model"], start=12):
139-
prev = c.add(k[i + 1], "broadcast_wildcard", k[i], coords, dim=coords[0])
140-
prev = c.add(k[i + 2], "mul", prev, bcast_tcl.input, bcast_y.no_vintage)
141-
prev = c.add(
142-
k[i + 3], "as_message_df", prev, name="input", dims=DIMS, common=COMMON
143-
)
144-
to_add.append(f"usage input{Fi}")
145-
c.add(to_add[-1], prev)
146-
147-
# Constraint data
148-
k_constraint = f"constraints{Fi}"
149-
to_add.append(k_constraint)
150-
c.add(k_constraint, constraint_data, "t::transport", n, y, "config")
151-
152-
# Merge data to one collection
153-
k_all = f"transport{Fi}"
154-
c.add(k_all, "merge_data", *to_add)
155-
156-
# Append to the "add transport data" key
157-
c.add("transport_data", __name__, key=k_all)
60+
collect = Collector(TARGET, "{}::F+ixmp".format)
15861

15962

16063
def constraint_data(
@@ -165,6 +68,7 @@ def constraint_data(
16568
Responds to the :attr:`.Config.constraint` keys :py:`"non-LDV *"`; see description
16669
there.
16770
"""
71+
# Retrieve transport configuration
16872
config: "Config" = genno_config["transport"]
16973

17074
# Freight modes
@@ -225,3 +129,132 @@ def constraint_data(
225129
assert not any(v.isna().any(axis=None) for v in result.values()), "Missing labels"
226130

227131
return result
132+
133+
134+
def demand(c: "Computer") -> None:
135+
"""Prepare calculation of freight activity/``demand``."""
136+
# commented: Base freight activity from IEA EEI
137+
# c.add("iea_eei_fv", "fv:n-y:historical", quote("tonne-kilometres"), "config")
138+
139+
# Base year freight activity from file (n, t), with modes for the 't' dimension
140+
c.add("fv:n-t:historical", "mul", exo.mode_share_freight, exo.activity_freight)
141+
142+
# …indexed to base-year values
143+
c.add(gdp_index, "index_to", gdp_ppp, literal("y"), "y0")
144+
c.add(fv[0], "mul", "fv:n-t:historical", gdp_index)
145+
146+
# (NAVIGATE) Scenario-specific adjustment factor for freight activity
147+
c.add("fv factor:n-t-y", "factor_fv", n, y, "config")
148+
149+
# Apply the adjustment factor
150+
c.add(fv[1], "mul", fv[0], "fv factor:n-t-y")
151+
152+
# Select only the ROAD data. NB Do not drop so 't' labels can be used for 'c', next.
153+
c.add(fv[2], "select", fv[1], indexers=dict(t=["RAIL", "ROAD"]))
154+
155+
# Relabel
156+
c.add(fv_cny, "relabel2", fv[2], new_dims={"c": "transport F {t}"})
157+
158+
# Convert to ixmp format
159+
collect("demand", "as_message_df", fv_cny, **_DEMAND_KW)
160+
161+
162+
def prepare_computer(c: "Computer") -> None:
163+
"""Prepare `c` to calculate and add data for freight transport."""
164+
# Collect data in `TARGET` and connect to the "add transport data" key
165+
collect.computer = c
166+
c.add("transport_data", __name__, key=TARGET)
167+
168+
# Call further functions to set up tasks for categories of freight data
169+
tech_econ(c)
170+
usage(c)
171+
demand(c)
172+
173+
# Add a task to call constraint_data()
174+
collect("constraints", constraint_data, "t::transport", n, y, "config")
175+
176+
177+
def tech_econ(c: "Computer") -> None:
178+
"""Prepare calculation of technoeconomic parameters for freight technologies."""
179+
180+
### `input`
181+
k = Key("input", NTY, "F")
182+
183+
# Add a technology dimension with certain labels to the energy intensity of VDT
184+
# NB "energy intensity of VDT" actually has dimension (n,) only
185+
t_F_ROAD = "t::transport F ROAD"
186+
c.add(k[0], "expand_dims", "energy intensity of VDT:n-y", t_F_ROAD)
187+
# Broadcast over dimensions (c, l, y, yv, ya)
188+
prev = c.add(k[1], "mul", k[0], bcast_tcl.input, bcast_y.model)
189+
# Convert MESSAGE data structure
190+
c.add(k[2], "as_message_df", prev, name="input", dims=DIMS, common=COMMON)
191+
# Convert units; add to `TARGET`
192+
collect(k.name, convert_units, k[2], "transport info")
193+
194+
### `output`
195+
k = Key("output", NTY, "F")
196+
197+
# Create base quantity
198+
c.add(k[0], wildcard(1.0, "dimensionless", NTY))
199+
coords = ["n::ex world", "t::F", "y::model"]
200+
c.add(k[1], "broadcast_wildcard", k[0], *coords, dim=NTY)
201+
# Broadcast over dimensions (c, l, y, yv, ya)
202+
prev = c.add(k[2], "mul", k[1], bcast_tcl.output, bcast_y.all)
203+
# Convert to MESSAGE data structure
204+
prev = c.add(k[3], "as_message_df", prev, name="output", dims=DIMS, common=COMMON)
205+
# Convert units; add to `TARGET`
206+
k_output = collect(k.name, convert_units, prev, "transport info")
207+
208+
### `capacity_factor` and `technical_lifetime`
209+
k = Key("other::F")
210+
211+
# Extract the 'output' data frame
212+
c.add(k[0], itemgetter("output"), k_output)
213+
214+
# Produce corresponding capacity_factor and technical_lifetime
215+
c.add(
216+
k[1],
217+
partial(
218+
make_matched_dfs,
219+
capacity_factor=registry.Quantity("1"),
220+
technical_lifetime=registry("10 year"),
221+
),
222+
k[0],
223+
)
224+
# Convert units
225+
collect(k.name, convert_units, k[1], "transport info")
226+
227+
228+
def usage(c: "Computer") -> None:
229+
"""Prepare calculation of 'usage' pseudo-technologies for freight activity."""
230+
### `output`
231+
k = Key("F usage output:t")
232+
233+
# Base values
234+
c.add(k[0], "freight_usage_output", "context")
235+
# Broadcast from (t,) → (t, c, l) dimensions
236+
prev = c.add(k[1], "mul", k[0], bcast_tcl.output)
237+
238+
# Broadcast over the (n, yv, ya) dimensions
239+
d = bcast_tcl.output.dims + tuple("ny")
240+
prev = c.add(k[2] * d, "expand_dims", prev, dim=dict(n=["*"], y=["*"]))
241+
coords = ["n::ex world", "y::model"]
242+
prev = c.add(k[3] * d, "broadcast_wildcard", prev, *coords, dim=tuple("ny"))
243+
prev = c.add(k[4], "mul", prev, bcast_y.no_vintage)
244+
# Convert to MESSAGE data structure
245+
c.add(k[5], "as_message_df", prev, name="output", dims=DIMS, common=COMMON)
246+
# Fill node_dest, time_dest key values
247+
collect("usage output", lambda v: same_time(same_node(v)), k[5])
248+
249+
### `input`
250+
k = Key("F usage input", NTY)
251+
252+
c.add(k[0], wildcard(1.0, "gigavehicle km", NTY))
253+
coords = ["n::ex world", "t::F usage", "y::model"]
254+
c.add(k[1], "broadcast_wildcard", k[0], *coords, dim=NTY)
255+
# Broadcast (t,) → (t, c, l) and (y,) → (yv, ya) dimensions
256+
prev = c.add(k[2], "mul", k[1], bcast_tcl.input, bcast_y.no_vintage)
257+
# Convert to MESSAGE data structure
258+
collect(
259+
"usage input", "as_message_df", prev, name="input", dims=DIMS, common=COMMON
260+
)

message_ix_models/tests/model/transport/test_build.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ def _join_levels(df):
257257
ContainsDataForParameters(
258258
{
259259
"capacity_factor",
260+
"demand",
260261
"growth_activity_lo",
261262
"growth_activity_up",
262263
"growth_new_capacity_up",
@@ -269,6 +270,9 @@ def _join_levels(df):
269270
),
270271
# HasCoords({"technology": ["f rail electr"]}),
271272
),
273+
"transport::O+ixmp": (
274+
ContainsDataForParameters({"bound_activity_lo", "bound_activity_up", "input"}),
275+
),
272276
#
273277
# The following are intermediate checks formerly in .test_demand.test_exo
274278
"mode share:n-t-y:base": (HasUnits(""),),

0 commit comments

Comments
 (0)