diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index 7052763ab..35c085bc5 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -42,6 +42,7 @@ Migration notes All changes ----------- +- A tutorial for adding an upper bound on investment (:pull:`602`). - Add additional oscillation detection mechanism for macro iterations (:pull:`645`, :pull:`676`) - Adjust default `lpmethod` from "Dual Simplex" (2) to "Barrier" (4); do NOT remove `cplex.opt` file(s) after solving workflow completes (:pull:`657`). - Adjust :meth:`.Scenario.add_macro` calculations for pandas 1.5.0 (:pull:`656`). diff --git a/message_ix/model/MESSAGE/data_load.gms b/message_ix/model/MESSAGE/data_load.gms index f5848720d..f338363bc 100644 --- a/message_ix/model/MESSAGE/data_load.gms +++ b/message_ix/model/MESSAGE/data_load.gms @@ -29,7 +29,7 @@ duration_period, duration_time, interestrate, resource_volume, resource_cost, is_bound_extraction_up, bound_extraction_up, resource_remaining, * technology technical-engineering parameters and economic costs input, output, construction_time, technical_lifetime -capacity_factor, operation_factor, min_utilization_factor, inv_cost, fix_cost, var_cost, +capacity_factor, operation_factor, min_utilization_factor, inv_cost, fix_cost, var_cost, bound_investment_up * upper and lower bounds on new capacity investment, total installed capacity and activity (including mapping sets) is_bound_new_capacity_up, is_bound_new_capacity_lo, bound_new_capacity_up, bound_new_capacity_lo, is_bound_total_capacity_up, is_bound_total_capacity_lo, bound_total_capacity_up, bound_total_capacity_lo, diff --git a/message_ix/model/MESSAGE/model_core.gms b/message_ix/model/MESSAGE/model_core.gms index 1bd9afcc4..f87e253ea 100644 --- a/message_ix/model/MESSAGE/model_core.gms +++ b/message_ix/model/MESSAGE/model_core.gms @@ -53,6 +53,7 @@ * :math:`COMMODITY\_BALANCE_{n,c,l,y,h} \in \mathbb{R}` Auxiliary variable for right-hand side of :ref:`commodity_balance` * :math:`STORAGE_{n,t,m,l,c,y,h} \in \mathbb{R}` State of charge or content of storage at each sub-annual time slice * :math:`STORAGE\_CHARGE_{n,t,m,l,c,y,h} \in \mathbb{R}` Charging of storage in each sub-annual time slice (negative for discharging) +* :math:`INVEST_{n,y} \in \mathbb{R}` Investment per region over time * ======================================================== ==================================================================================== * * The index :math:`y^V` is the year of construction (vintage) wherever it is necessary to @@ -107,6 +108,8 @@ Positive Variables LAND(node,land_scenario,year_all) relative share of land-use scenario * content of storage STORAGE(node,tec,mode,level,commodity,year_all,time) state of charge (SoC) of storage at each sub-annual time slice (positive) +* investment per region and year + INVEST(node,year_all) investment per model region and year ; Variables @@ -290,6 +293,8 @@ Equations STORAGE_BALANCE balance of the state of charge of storage STORAGE_BALANCE_INIT balance of the state of charge of storage at sub-annual time slices with initial storage content STORAGE_INPUT connecting an input commodity to maintain the activity of storage container (not stored commodity) + INVESTMENT_EQUIVALENCE investment per node and year + INVESTMENT_CONSTRAINT upper bound on investment per node and year ; *----------------------------------------------------------------------------------------------------------------------* * equation statements * @@ -337,10 +342,8 @@ OBJECTIVE.. * * .. math:: * COST\_NODAL_{n,y} & = \sum_{c,g} \ resource\_cost_{n,c,g,y} \cdot EXT_{n,c,g,y} \\ -* & + \sum_{t} \ -* \bigg( inv\_cost_{n,t,y} \cdot construction\_time\_factor_{n,t,y} \\ -* & \quad \quad \quad \cdot end\_of\_horizon\_factor_{n,t,y} \cdot CAP\_NEW_{n,t,y} \\[4 pt] -* & \quad \quad + \sum_{y^V \leq y} \ fix\_cost_{n,t,y^V,y} \cdot CAP_{n,t,y^V,y} \\ +* & + INVEST_{n, y} + \sum_{t} \ +* \bigg( \sum_{y^V \leq y} \ fix\_cost_{n,t,y^V,y} \cdot CAP_{n,t,y^V,y} \\ * & \quad \quad + \sum_{\substack{y^V \leq y \\ m,h}} \ var\_cost_{n,t,y^V,y,m,h} \cdot ACT_{n,t,y^V,y,m,h} \\ * & \quad \quad + \Big( abs\_cost\_new\_capacity\_soft\_up_{n,t,y} \\ * & \quad \quad \quad @@ -371,10 +374,9 @@ COST_ACCOUNTING_NODAL(node, year).. SUM((commodity,grade)$( map_resource(node,commodity,grade,year) ), resource_cost(node,commodity,grade,year) * EXT(node,commodity,grade,year) ) * technology capacity investment, maintainance, operational cost + + INVEST(node, year) + SUM((tec)$( map_tec(node,tec,year) ), - ( inv_cost(node,tec,year) * construction_time_factor(node,tec,year) - * end_of_horizon_factor(node,tec,year) * CAP_NEW(node,tec,year) - + SUM(vintage$( map_tec_lifetime(node,tec,vintage,year) ), + (SUM(vintage$( map_tec_lifetime(node,tec,vintage,year) ), fix_cost(node,tec,vintage,year) * CAP(node,tec,vintage,year) ) )$( inv_tec(tec) ) + SUM((vintage,mode,time)$( map_tec_lifetime(node,tec,vintage,year) AND map_tec_act(node,tec,year,mode,time) ), var_cost(node,tec,vintage,year,mode,time) * ACT(node,tec,vintage,year,mode,time) ) @@ -447,13 +449,54 @@ COST_ACCOUNTING_NODAL(node, year).. %SLACK_RELATION_BOUND_LO% + 1e6 * SLACK_RELATION_BOUND_LO(relation,node,year)$( is_relation_lower(relation,node,year) ) ) ; - *** * Here, :math:`n^L \in N(n)` are all nodes :math:`n^L` that are sub-nodes of node :math:`n`. * The subset of technologies :math:`t \in T(\widehat{t})` are all tecs that belong to category :math:`\widehat{t}`, * and similar notation is used for emissions :math:`e \in E`. *** +*** +* Regional investment cost accounting +* ---------------------------------------- +* +* Accounting of regional investment costs over time +* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* +* .. _equation_investment_equivalence: +* +* Equation INVESTMENT_EQUIVALENCE +* """""""""""""""""""""""""""""" +* +* This equation calculates the regional investment costs over time. +* +* .. math:: +* INVEST_{n,y} & = \sum_{t} \ +* \bigg( inv\_cost_{n,t,y} \cdot construction\_time\_factor_{n,t,y} \\ +* & \quad \quad \quad \cdot end\_of\_horizon\_factor_{n,t,y} \cdot CAP\_NEW_{n,t,y}) \\ +*** + +INVESTMENT_EQUIVALENCE(node, year) .. + INVEST(node, year) =E= SUM(tec$( map_tec(node,tec,year) ), + ( inv_cost(node,tec,year) * construction_time_factor(node,tec,year) + * end_of_horizon_factor(node,tec,year) * CAP_NEW(node,tec,year) )$( inv_tec(tec) ) +) +; + +*** +* +* .. _equation_investment_constraint: +* +* Equation INVESTMENT_CONSTRAINT +* """""""""""""""""""""""""""""" +* +* This equation puts an upper bound on the regional investment costs over time. +* +* .. math:: +* INVEST_{n,y} \leq bound\_investment\_up_{n,y} \\ +*** +INVESTMENT_CONSTRAINT(node, year)$(bound_investment_up(node, year) ) .. + INVEST(node, year) =L= bound_investment_up(node, year) +; *----------------------------------------------------------------------------------------------------------------------* *** * .. _section_resource_commodity: diff --git a/message_ix/model/MESSAGE/parameter_def.gms b/message_ix/model/MESSAGE/parameter_def.gms index 4cd02cc5b..e9be21976 100644 --- a/message_ix/model/MESSAGE/parameter_def.gms +++ b/message_ix/model/MESSAGE/parameter_def.gms @@ -573,6 +573,8 @@ Parameters * - ``node`` | ``tec`` | ``year`` * * - beyond_horizon_factor * - ``node`` | ``tec`` | ``year`` +* * - bound_investment_up +* - ``node`` | ``year`` * * *** @@ -583,6 +585,7 @@ Parameters end_of_horizon_factor(node,tec,year_all) multiplier for value of investment at end of model horizon beyond_horizon_lifetime(node,tec,year_all) remaining technical lifetime at the end of model horizon beyond_horizon_factor(node,tec,year_all) discount factor of remaining lifetime beyond model horizon + bound_investment_up(node, year_all) upper bound on investment per node and year ; *----------------------------------------------------------------------------------------------------------------------* diff --git a/message_ix/models.py b/message_ix/models.py index 5de64417e..19fe4243e 100644 --- a/message_ix/models.py +++ b/message_ix/models.py @@ -161,6 +161,7 @@ def item(ix_type, expr): "bound_activity_up": item("par", "nl t ya m h"), "bound_emission": item("par", "n type_emission type_tec type_year"), "bound_extraction_up": item("par", "n c g y"), + "bound_investment_up": item("par", "n y"), "bound_new_capacity_lo": item("par", "nl t yv"), "bound_new_capacity_up": item("par", "nl t yv"), "bound_total_capacity_lo": item("par", "nl t ya"), @@ -269,6 +270,8 @@ def item(ix_type, expr): # # Variables # + # # Investment + "INVEST": item("var", "n y"), # # Activity # "ACT": item("var", "nl t yv ya m h"), # # Maintained capacity diff --git a/message_ix/tests/test_reporting.py b/message_ix/tests/test_reporting.py index b157bac6a..edfdac810 100644 --- a/message_ix/tests/test_reporting.py +++ b/message_ix/tests/test_reporting.py @@ -51,7 +51,7 @@ def test_reporter_from_scenario(message_test_mp): rep = Reporter.from_scenario(scen) # Number of quantities available in a rudimentary MESSAGEix Scenario - assert len(rep.graph["all"]) == 125 + assert len(rep.graph["all"]) == 127 # Quantities have short dimension names assert "demand:n-c-l-y-h" in rep @@ -69,11 +69,11 @@ def test_reporter_from_scenario(message_test_mp): assert_qty_equal(obs, demand, check_attrs=False) # ixmp.Reporter pre-populated with only model quantities and aggregates - assert len(rep_ix.graph) == 5609 + assert len(rep_ix.graph) == 5617 # message_ix.Reporter pre-populated with additional, derived quantities # This is the same value as in test_tutorials.py - assert len(rep.graph) == 13074 + assert len(rep.graph) == 13082 # Derived quantities have expected dimensions vom_key = rep.full_key("vom") diff --git a/message_ix/tests/test_tutorials.py b/message_ix/tests/test_tutorials.py index c5d5f90e4..9870f2acf 100644 --- a/message_ix/tests/test_tutorials.py +++ b/message_ix/tests/test_tutorials.py @@ -55,8 +55,9 @@ (("westeros", "westeros_soft_constraints"), [], {}), (("westeros", "westeros_addon_technologies"), [], {}), (("westeros", "westeros_historical_new_capacity"), [], {}), + (("westeros", "westeros_investment"), [], {}), # NB this is the same value as in test_reporter() - (("westeros", "westeros_report"), [("len-rep-graph", 13074)], {}), + (("westeros", "westeros_report"), [("len-rep-graph", 13082)], {}), ((AT, "austria"), [("solve-objective-value", 206321.90625)], {}), ( (AT, "austria_single_policy"), diff --git a/tutorial/README.rst b/tutorial/README.rst index 2f7d7ba96..65e755a00 100644 --- a/tutorial/README.rst +++ b/tutorial/README.rst @@ -169,6 +169,12 @@ framework, such as used in global research applications of |MESSAGEix|. #. Import and combine data from multiple files to create a new scenario (:tut:`westeros/westeros_baseline_using_xlsx_import_part2.ipynb`). +#. Extend the features of :mod:`message_ix` and :mod:`ixmp`: + + #. Extend the GAMS formulation by adding new sets, parameters, variables, + and equations, and change the :mod:`message_ix` code accordingly + (:tut:`westeros/westeros_investment.ipynb`). + .. _austria-tutorials: Austrian energy system diff --git a/tutorial/westeros/westeros_investment.ipynb b/tutorial/westeros/westeros_investment.ipynb new file mode 100644 index 000000000..6610ed46d --- /dev/null +++ b/tutorial/westeros/westeros_investment.ipynb @@ -0,0 +1,882 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Westeros Tutorial - Extension of GAMS Formulation\n", + "\n", + "This tutorial demonstrates the procedure for changing the GAMS formulation in MESSAGEix. This will be useful for adding new sets, parameters, variables, and equations, to generate a different output or to represent new phenomena, sectors, etc.\n", + "\n", + "**Pre-requisites**\n", + "- You have the *MESSAGEix* framework installed and working\n", + "- You have run Westeros baseline scenario (``westeros_baseline.ipynb``) and solved it successfully\n", + "\n", + "_This tutorial was presented by [Behnam Zakeri](https://iiasa.ac.at/staff/behnam-zakeri) at the **MESSAGEix Community Meeting** May 2022. Please feel free to suggest improvements through issues and pull-requests_." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "application/javascript": [ + "if (typeof IPython !== 'undefined') { IPython.OutputArea.prototype._should_scroll = function(lines){ return false; }}" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import pandas as pd\n", + "import ixmp\n", + "import message_ix\n", + "\n", + "from message_ix.util import make_df\n", + "\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "mp = ixmp.Platform()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Part 1: Adding a new variable `INVEST`\n", + "In the first part, we change the GAMS code by adding a new equation called `INVESTMENT_EQUIVALENCE`, which calculates the investment per region in each model period as a new variable called `INVEST`.\n", + "\n", + "### 1.1. Modifying GAMS files\n", + "For representing the new equation, we do the following modifications in the GAMS file `model_core.gms`:\n", + "1. Introduce a new positive variable for \"investment per node and year\" as: `INVEST(node, year_all)`\n", + "2. Introduce a new equation for \"investment per node and year\" as: `INVESTMENT_EQUIVALENCE`\n", + "3. Specify the new equation as:\n", + "\n", + "$$\\text{INVEST}_{n,y}\n", + " = \\sum_{t} (\\text{inv_cost}_{n,t,y} \\cdot \\text{construction_time_factor}_{n,t,y} \\cdot \\text{CAP_NEW}_{n,t,y})$$\n", + "\n", + "You can check these changes in `model_core.gms` [here](https://github.com/iiasa/message_ix/pull/602/files)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1.2. Solving the scenario after modifications in GAMS:\n", + "We clone a scenario from Westeros \"baseline\" and solve it again with the new changes in the GAMS side and check the outcome." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# Loading the baseline scenario and cloning to a new scenario called \"investment\"\n", + "model = \"Westeros Electrified\"\n", + "base = message_ix.Scenario(mp, model=model, scenario=\"baseline\")\n", + "scen = base.clone(model, \"investment\", keep_solution=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# Solving the new scenario again (for testing if the changes in the GAMS code have done anything)\n", + "scen.solve()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1.3. Checking the outcome\n", + "Let's compare the objective value of the \"baseline\" and \"investment\" scenarios. We use the python method `assert` for doing this assertion. If this equality does not hold true, the assertion method will throw an error. " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# We assert that the objective values of the two scenarios are the same\n", + "assert base.var(\"OBJ\")[\"lvl\"] == scen.var(\"OBJ\")[\"lvl\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# We can also assert that the activity of one technology is equal in both scenarios\n", + "assert base.var(\"ACT\", {\"technology\": \"coal_ppl\"})[\"lvl\"].sum() == scen.var(\"ACT\", {\"technology\": \"coal_ppl\"})[\"lvl\"].sum()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Observation 1: The results are the same\n", + "As asserted above, the objective value, i.e., the total cost of the system, as well as the level of activity of \"coal_ppl\" is the same in both scenarios. This means the modifications we introduced in the GAMS side have not changed the output of the scenario." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Observation 2: variable `INVEST` in the output GDX file\n", + "If we check the output GDX file of the new scenario, we should be able to see the new variable `INVEST`. This means that the new variable has been calculated as part of the GAMS mathematical model." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's check if we can fetch the information about the new variable `INVEST`:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
nodeyearlvlmrg
\n", + "
" + ], + "text/plain": [ + "Empty DataFrame\n", + "Columns: [node, year, lvl, mrg]\n", + "Index: []" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Reading the content variable \"INVEST\"\n", + "scen.var(\"INVEST\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Observation (3): Python side does not know about the changes in GAMS\n", + "As the empty dataframe above shows, the python side, i.e., `message_ix` code is not aware of the changes we have done in the GAMS side. Therefore, we cannot retrieve the content of variable `INVEST` from the GDX file, even though this variable exists there." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1.4. Updating the python code based on the changes in GAMS\n", + "In the `message_ix` python package, there is a configuration file for specifying and initializing the model items, i.e., sets, parameters, and variables. This file is called [`models.py`](https://github.com/iiasa/message_ix/blob/main/message_ix/models.py).\n", + "\n", + "1. For updating the file for our changes, we introduce the new variable `INVEST` consistent with the format shown in the file, i.e., adding the line below under \"MESSAGE_ITEMS\"/\"Variables\":\n", + "\n", + "`\"INVEST\": item(\"var\", \"n y\"),`\n", + "\n", + "This notation informs the `message_ix` package that there exists a variable called `INVEST` with the index sets of \"n\" for \"node\" and \"y\" for \"year\". After adding this information, the python side will initialize this variable for any new or cloned scenario.\n", + "\n", + "2. For being able to retrieve the content of this new variable, we need to explicitly pass it through the `var_list` option when calling `solve()`. This has to be done because this variable is not among the default variables of `message_ix`. So, the notation for solving will be:\n", + "\n", + "`solve(var_list=[\"INVEST\"])`" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# Now, let's solve the scenario again\n", + "scen = scen.clone(keep_solution=False)\n", + "scen.solve(var_list=[\"INVEST\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's check the content of `INVEST`:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
nodeyearlvlmrg
0World7000.0000007.721735
1World7100.0000004.740475
2World7200.0000002.910241
3Westeros7002380.5806830.000000
4Westeros7103432.0700150.000000
5Westeros7202195.6006460.000000
\n", + "
" + ], + "text/plain": [ + " node year lvl mrg\n", + "0 World 700 0.000000 7.721735\n", + "1 World 710 0.000000 4.740475\n", + "2 World 720 0.000000 2.910241\n", + "3 Westeros 700 2380.580683 0.000000\n", + "4 Westeros 710 3432.070015 0.000000\n", + "5 Westeros 720 2195.600646 0.000000" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "scen.var(\"INVEST\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Congratulations!\n", + "You were able to modify the GAMS code, add a new variable and equation, update the python code accordingly, and keep your modeling workflow working seamlessly." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Part II: Adding a new MESSAGEix parameter \n", + "In the second part of this tutorial, we further modify the GAMS code by adding a new parameter. The goal is to understand what modifications should be done when a new parameter is added." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.1. An upper bound on investment\n", + "The new parameter will be an upper bound on investment called `bound_investment_up`. This parameter can be defined per technology types, but here for simplicity we define this bound per node and year, similar to variable `INVEST`. We can define this bound as an absolute value, i.e., maximum amount that can be invested, or we can define it relative to the total system costs, e.g., specifying that investment can be up to 80% of total costs. We choose the former.\n", + "\n", + "Adding a parameter in the GAMS side is different from adding variables shown in Part I of this tutorial. Because GAMS expects this parameter as an input data to be passed from the python side. Therefore, the following additional modifications are needed:\n", + "\n", + "#### a. GAMS side\n", + "\n", + "1. Define the new parameter in the GAMS file `parameter_def.gms`\n", + "2. Add the new parameter to the items to be loaded from the input GDX file in `data_load.gms`\n", + "\n", + "and as shown in Part I:\n", + "\n", + "3. Introduce a new equation in `model_core.gms` called `INVESTMENT_CONSTRAINT`\n", + "4. Add the following equation to `model_core.gms` for the new bound.\n", + "\n", + "$$\\text{INVEST}_{n,y} \\leq \\text{bound_investment_up}_{n,y}$$\n", + "\n", + "You can check these changes [here](https://github.com/iiasa/message_ix/pull/602/files).\n", + "\n", + "#### Notice:\n", + "Please note that we filter this equation by `bound_investment_up` using the $ sign in GAMS. This means that if this parameter is not defined in a scenario, this equation will not be in effect. This way, we retain backward compatibility, i.e., we can still solve our scenarios that do not have bound on investment.\n", + "\n", + "#### b. Python side\n", + "As mentioned in Part I, we need to update the `message_ix` python code about the new changes. This can be simply done by adding a line of code to the model configuration file (`models.py`) as shown in Section 1.4 above.\n", + "\n", + "5. Add new parameter to the `MESSAGE_ITEMS` in the model configuration file `models.py`:\n", + "\n", + "`\"bound_investment_up\": item(\"par\", \"n y\"),`\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# Now, let's clone a new scenario from \"baseline\" and solve\n", + "scen2 = base.clone(scenario=\"investment_bound\", keep_solution=False)\n", + "scen2.solve(var_list=[\"INVEST\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
nodeyearlvlmrg
0Westeros7002380.5806830.0
1Westeros7103432.0700150.0
2Westeros7202195.6006460.0
\n", + "
" + ], + "text/plain": [ + " node year lvl mrg\n", + "0 Westeros 700 2380.580683 0.0\n", + "1 Westeros 710 3432.070015 0.0\n", + "2 Westeros 720 2195.600646 0.0" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = scen2.var(\"INVEST\", {\"node\": \"Westeros\"})\n", + "df" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Everything seems fine. Now, we can add a value to `bound_investment_up` too. The total investment averaged over the entire modeling horizon is around 2667 million \\\\$ per year (how ?). Let's assume an investment cap of 3000 million $ per year, e.g., to distribute the investment over modeling horizon, and see if the model solves." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
nodeyearvalueunit
0Westeros7003000M$
1Westeros7103000M$
2Westeros7203000M$
\n", + "
" + ], + "text/plain": [ + " node year value unit\n", + "0 Westeros 700 3000 M$\n", + "1 Westeros 710 3000 M$\n", + "2 Westeros 720 3000 M$" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Adding input data for \"bound_investment_up\"\n", + "bound = pd.DataFrame({\n", + " \"node\": \"Westeros\",\n", + " \"year\": [700, 710, 720],\n", + " \"value\": 3000,\n", + " \"unit\": \"M$\",\n", + " })\n", + "# Check the input data before adding to the scenario\n", + "bound" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# Adding the unit to the platform (if not exists yet)\n", + "mp.add_unit(\"M$\")\n", + "\n", + "# Adding data to the scenario\n", + "scen2.remove_solution()\n", + "scen2.check_out()\n", + "scen2.add_par(\"bound_investment_up\", bound)\n", + "scen2.add_par(\"bound_investment_up\", bound)\n", + "scen2.commit(\"bound added\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# Solving the scenario\n", + "scen2.solve(var_list=[\"INVEST\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.2. Checking the results\n", + "In this Section, we check the results after adding the bound compared to the case before. We do this comparison on a few outputs, including:\n", + "- investment needs\n", + "- total system costs\n", + "- price of \"light\"" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
nodeyearlvlmrg
3Westeros7002380.5806830.0
4Westeros7103432.0700150.0
5Westeros7202195.6006460.0
\n", + "
" + ], + "text/plain": [ + " node year lvl mrg\n", + "3 Westeros 700 2380.580683 0.0\n", + "4 Westeros 710 3432.070015 0.0\n", + "5 Westeros 720 2195.600646 0.0" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Investment needs before the bound\n", + "scen.var(\"INVEST\", {\"node\": \"Westeros\"})" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
nodeyearlvlmrg
0Westeros7002812.6506980.0
1Westeros7103000.0000000.0
2Westeros7202458.3196490.0
\n", + "
" + ], + "text/plain": [ + " node year lvl mrg\n", + "0 Westeros 700 2812.650698 0.0\n", + "1 Westeros 710 3000.000000 0.0\n", + "2 Westeros 720 2458.319649 0.0" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Investment needs after the bound\n", + "scen2.var(\"INVEST\", {\"node\": \"Westeros\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Observation (1): Investment dynamics\n", + "The results show that after adding an upper bound on investment, the model has to distribute investment differently, to remain under the maximum threshold. This has resulted in higher investments in 700 and 720 compared to the previous scenario without annual investment bounds. How much has the total investment changed and why?\n", + "\n", + "Let's compare the objective value, i.e., the total system costs" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "2052.6875" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Difference in the total system costs after investment limits (M$)\n", + "scen2.var(\"OBJ\")[\"lvl\"] - scen.var(\"OBJ\")[\"lvl\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Observation (2): Total system costs\n", + "After adding an upper bound on investment, the total costs of the system has increased compared to the previous scenario without investment bounds. More importantly, the increase in total system cost is not only due to an increase in total investment but also a higher O&M costs.\n", + "\n", + "**Question**: how can we calculate the change in O&M cost?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Exercise\n", + "1- Modify the equation `INVESTMENT_EQUIVALENCE` so that the upper bound on investment will be as % of total system costs.\n", + "\n", + "2- How can we relate the upper bound on investment as % of GDP in MESSAGEix?" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# Close the connection to the database\n", + "mp.close_db()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}