Skip to content

Commit 4576ba9

Browse files
authored
Merge pull request #243 from iiasa/costs/wat-ssp
Update `water` to use cost projections from `tools.costs`
2 parents a073f64 + 6e2ba3b commit 4576ba9

File tree

7 files changed

+200
-28
lines changed

7 files changed

+200
-28
lines changed

doc/whatsnew.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ What's new
44
Next release
55
============
66

7+
- Connect the water module to the cost module for cooling technologies (:pull:`245`).
78
- Make setup of constraints for cooling technologies flexible and update solar csp tech. name (:pull:`242`).
89
- Fix the nexus/cooling function and add test for checking some input data (:pull:`236`).
910
- Add :doc:`/project/circeular` project code and documentation (:pull:`232`).

message_ix_models/data/costs/cooling/tech_map.csv

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,11 @@ solar_th_ppl__air,energy,solar_th_ppl,220,0,2015
9191
solar_th_ppl__cl_fresh,energy,solar_th_ppl,100,0,2015
9292
solar_th_ppl__ot_fresh,energy,solar_th_ppl,0.4,0,2015
9393
solar_th_ppl__ot_saline,energy,solar_th_ppl,0.3,0,2015
94+
csp_sm1_ppl__air,energy,csp_sm1_ppl,220,0,2015
95+
csp_sm1_ppl__cl_fresh,energy,csp_sm1_ppl,100,0,2015
96+
csp_sm1_ppl__ot_fresh,energy,csp_sm1_ppl,0.4,0,2015
97+
csp_sm1_ppl__ot_saline,energy,csp_sm1_ppl,0.3,0,2015
98+
csp_sm3_ppl__air,energy,csp_sm3_ppl,220,0,2015
99+
csp_sm3_ppl__cl_fresh,energy,csp_sm3_ppl,100,0,2015
100+
csp_sm3_ppl__ot_fresh,energy,csp_sm3_ppl,0.4,0,2015
101+
csp_sm3_ppl__ot_saline,energy,csp_sm3_ppl,0.3,0,2015

message_ix_models/model/water/cli.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44

55
from message_ix_models import Context
66
from message_ix_models.model.structure import get_codes
7-
from message_ix_models.util.click import common_params
7+
from message_ix_models.util.click import common_params, scenario_param
88

99
log = logging.getLogger(__name__)
1010

1111

1212
# allows to activate water module
1313
@click.group("water-ix")
1414
@common_params("regions")
15+
@scenario_param("--ssp", default="SSP2")
1516
@click.option("--time", help="Manually defined time")
1617
@click.pass_obj
1718
def cli(context: "Context", regions, time):
@@ -206,6 +207,7 @@ def nexus(context: "Context", regions, rcps, sdgs, rels, macro=False):
206207

207208
@cli.command("cooling")
208209
@common_params("regions")
210+
@scenario_param("--ssp")
209211
@click.option(
210212
"--rcps",
211213
default="no_climate",

message_ix_models/model/water/data/water_for_ppl.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -718,7 +718,7 @@ def cool_tech(context: "Context") -> dict[str, pd.DataFrame]:
718718
# con4 = cost['technology'].str.endswith("air")
719719
# con5 = cost.technology.isin(input_cool['technology_name'])
720720
# inv_cost = cost[(con3) | (con4)]
721-
inv_cost = cost.copy()
721+
722722
# Manually removing extra technologies not required
723723
# TODO make it automatic to not include the names manually
724724
techs_to_remove = [
@@ -735,23 +735,31 @@ def cool_tech(context: "Context") -> dict[str, pd.DataFrame]:
735735
"nuc_htemp__cl_fresh",
736736
"nuc_htemp__air",
737737
]
738-
inv_cost = inv_cost[~inv_cost["technology"].isin(techs_to_remove)]
739-
# Converting the cost to USD/GW
740-
inv_cost["investment_USD_per_GW_mid"] = (
741-
inv_cost["investment_million_USD_per_MW_mid"] * 1e3
742-
)
743738

744-
inv_cost = (
745-
make_df(
746-
"inv_cost",
747-
technology=inv_cost["technology"],
748-
value=inv_cost["investment_USD_per_GW_mid"],
749-
unit="USD/GWa",
750-
)
751-
.pipe(same_node)
752-
.pipe(broadcast, node_loc=node_region, year_vtg=info.Y)
739+
from message_ix_models.tools.costs.config import Config
740+
from message_ix_models.tools.costs.projections import create_cost_projections
741+
742+
# Set config for cost projections
743+
# Using GDP method for cost projections
744+
cfg = Config(
745+
module="cooling", scenario=context.ssp, method="gdp", node=context.regions
753746
)
754747

748+
# Get projected investment and fixed o&m costs
749+
cost_proj = create_cost_projections(cfg)
750+
751+
# Get only the investment costs for cooling technologies
752+
inv_cost = cost_proj["inv_cost"][
753+
["year_vtg", "node_loc", "technology", "value", "unit"]
754+
]
755+
756+
# Remove technologies that are not required
757+
inv_cost = inv_cost[~inv_cost["technology"].isin(techs_to_remove)]
758+
759+
# Only keep cooling module technologies by filtering for technologies with "__"
760+
inv_cost = inv_cost[inv_cost["technology"].str.contains("__")]
761+
762+
# Add the investment costs to the results
755763
results["inv_cost"] = inv_cost
756764

757765
# Addon conversion

message_ix_models/tests/model/water/data/test_water_for_ppl.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,11 @@ def test_cool_tec(request, test_context, RCP):
9494
test_context.time = "year"
9595
test_context.nexus_set = "nexus"
9696
# TODO add
97-
test_context.RCP = RCP
98-
test_context.REL = "med"
97+
test_context.update(
98+
RCP=RCP,
99+
REL="med",
100+
ssp="SSP2",
101+
)
99102

100103
# TODO: only leaving this in so you can see which data you might want to assert to
101104
# be in the result. Please remove after adapting the assertions below:

message_ix_models/tests/util/test_click.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
"""Basic tests of the command line."""
22

33
import click
4+
import pytest
45

56
from message_ix_models.cli import cli_test_group
6-
from message_ix_models.util.click import common_params, temporary_command
7+
from message_ix_models.util.click import (
8+
common_params,
9+
scenario_param,
10+
temporary_command,
11+
)
712

813

914
def test_default_path_cb(session_context, mix_models_cli):
@@ -59,6 +64,50 @@ def inner(context, regions):
5964
assert "ZMB" == result.output.strip()
6065

6166

67+
@pytest.mark.parametrize(
68+
"args, command, expected",
69+
[
70+
# As a (required, positional) argument
71+
(dict(param_decls="ssp"), ["LED"], "LED"),
72+
(dict(param_decls="ssp"), ["FOO"], "'FOO' is not one of 'LED', 'SSP1', "),
73+
# As an option
74+
# With no default
75+
(dict(param_decls="--ssp"), [], "None"),
76+
# With a limited of values
77+
(
78+
dict(param_decls="--ssp", values=["LED", "SSP2"]),
79+
["--ssp=SSP1"],
80+
"'SSP1' is not one of 'LED', 'SSP2'",
81+
),
82+
# With a default
83+
(dict(param_decls="--ssp", default="SSP2"), [], "SSP2"),
84+
# With a different name
85+
(dict(param_decls=["--scenario", "ssp"]), ["--scenario=SSP5"], "SSP5"),
86+
],
87+
)
88+
def test_scenario_param(capsys, mix_models_cli, args, command, expected):
89+
"""Tests of :func:`scenario_param`."""
90+
91+
# scenario_param() can be used as a decorator with `args`
92+
@click.command
93+
@scenario_param(**args)
94+
@click.pass_obj
95+
def cmd(context):
96+
"""Temporary click Command: print the direct value and Context attribute."""
97+
print(f"{context.ssp}")
98+
99+
with temporary_command(cli_test_group, cmd):
100+
try:
101+
result = mix_models_cli.assert_exit_0(["_test", "cmd"] + command)
102+
except RuntimeError as e:
103+
# `command` raises the expected value or error message
104+
assert expected in capsys.readouterr().out, e
105+
else:
106+
# `command` can be invoked without error, and the function/Context get the
107+
# expected value
108+
assert expected == result.output.strip()
109+
110+
62111
def test_store_context(mix_models_cli):
63112
"""Test :func:`.store_context`."""
64113

message_ix_models/util/click.py

Lines changed: 110 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,28 @@ def common_params(param_names: str):
2828
"""Decorate a click.command with common parameters `param_names`.
2929
3030
`param_names` must be a space-separated string of names appearing in :data:`PARAMS`,
31-
e.g. ``"ssp force output_model"``. The decorated function receives keyword
32-
arguments with these names::
31+
for instance :py:`"ssp force output_model"`. The decorated function receives keyword
32+
arguments with these names; some are also stored on the
3333
34-
@click.command()
35-
@common_params("ssp force output_model")
36-
def mycmd(ssp, force, output_model)
37-
# ...
34+
Example
35+
-------
36+
37+
>>> @click.command
38+
... @common_params("ssp force output_model")
39+
... @click.pass_obj
40+
... def mycmd(context, ssp, force, output_model):
41+
... assert context.force == force
3842
"""
3943

44+
# Create the decorator
4045
# Simplified from click.decorators._param_memo
4146
def decorator(f):
42-
if not hasattr(f, "__click_params__"):
43-
f.__click_params__ = []
44-
f.__click_params__.extend(
47+
# - Ensure f.__click_params__ exists
48+
# - Append each param given in `param_names`
49+
f.__dict__.setdefault("__click_params__", []).extend(
4550
PARAMS[name] for name in reversed(param_names.split())
4651
)
52+
4753
return f
4854

4955
return decorator
@@ -102,6 +108,101 @@ def format_sys_argv() -> str:
102108
return "\n".join(lines)[:-2]
103109

104110

111+
def scenario_param(
112+
param_decls: Union[str, list[str]],
113+
*,
114+
values: list[str] = None,
115+
default: Optional[str] = None,
116+
):
117+
"""Add an SSP or scenario option or argument to a :class:`click.Command`.
118+
119+
The parameter uses :func:`.store_context` to store the given value (if any) on
120+
the :class:`.Context`.
121+
122+
Parameters
123+
----------
124+
param_decls :
125+
:py:`"--ssp"` (or any other name prefixed by ``--``) to generate a
126+
:class:`click.Option`; :py:`"ssp"` to generate a :class:`click.Argument`.
127+
Click-style declarations are also supported; see below.
128+
values :
129+
Allowable values. If not given, the allowable values are
130+
["LED", "SSP1", "SSP2", "SSP3", "SSP4", "SSP5"].
131+
default :
132+
Default value.
133+
134+
Raises
135+
------
136+
ValueError
137+
if `default` is given with `param_decls` that indicate a
138+
:class:`click.Argument`.
139+
140+
Examples
141+
--------
142+
Add a (mandatory, positional) :class:`click.Argument`. This is nearly the same as
143+
using :py:`common_params("ssp")`, except the decorated function does not receive an
144+
:py:`ssp` argument. The value is still stored on :py:`context` automatically.
145+
146+
>>> @click.command
147+
... @scenario_param("ssp")
148+
... @click.pass_obj
149+
... def mycmd(context):
150+
... print(context.ssp)
151+
152+
Add a :class:`click.Option` with certain, limited values and a default:
153+
154+
>>> @click.command
155+
... @scenario_param("--ssp", values=["SSP1", "SSP2", "SSP3"], default="SSP3")
156+
... @click.pass_obj
157+
... def mycmd(context):
158+
... print(context.ssp)
159+
160+
An option given by the user as :command:`--scenario` but stored as
161+
:py:`Context.ssp`:
162+
163+
>>> @click.command
164+
... @scenario_param(["--scenario", "ssp"])
165+
... @click.pass_obj
166+
... def mycmd(context):
167+
... print(context.ssp)
168+
"""
169+
if values is None:
170+
values = ["LED", "SSP1", "SSP2", "SSP3", "SSP4", "SSP5"]
171+
172+
# Handle param_decls; identify the first string element
173+
if isinstance(param_decls, list):
174+
decl0 = param_decls[0]
175+
else:
176+
decl0 = param_decls
177+
param_decls = [param_decls] # Ensure list for use by click
178+
179+
# Choose either click.Option or click.Argument
180+
if decl0.startswith("-"):
181+
cls = Option
182+
else:
183+
cls = Argument
184+
if default is not None:
185+
raise ValueError(f"{default=} given for {cls}")
186+
187+
# Create the decorator
188+
def decorator(f):
189+
# - Ensure f.__click_params__ exists
190+
# - Generate and append the parameter
191+
f.__dict__.setdefault("__click_params__", []).append(
192+
cls(
193+
param_decls,
194+
callback=store_context,
195+
type=Choice(values),
196+
default=default,
197+
expose_value=False,
198+
)
199+
)
200+
201+
return f
202+
203+
return decorator
204+
205+
105206
def store_context(context: Union[click.Context, Context], param, value):
106207
"""Callback that simply stores a value on the :class:`.Context` object.
107208

0 commit comments

Comments
 (0)