diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index d57180349..66f31b6d0 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,8 @@ # Next Release +- [#232](https://github.com/iiasa/message_ix/pull/232): Adding a Westeros tutorial for modelling seasonality and updating the existing ones + # v1.2.0 MESSAGEix 1.2.0 adds an option to set the commodity balance to strict equality, @@ -32,6 +34,7 @@ other improvements. See the ixmp release notes for further details. - [#138](https://github.com/iiasa/message_ix/pull/138): Update documentation and tutorials. - [#131](https://github.com/iiasa/message_ix/pull/131): Update clone function argument `scen` to `scenario` with planned deprecation of the former. + # v1.1.0 ## Warnings diff --git a/tests/test_tutorials.py b/tests/test_tutorials.py index 0b9efef3f..2a30dae5e 100644 --- a/tests/test_tutorials.py +++ b/tests/test_tutorials.py @@ -26,7 +26,8 @@ # b. Expected objective value. tutorials = [ (('westeros', 'westeros_baseline'), - [('solve-objective-value', 207544.09375)]), + [('solve-objective-value', 238193.291167)]), + # on Python 2: # 'solve-objective-value', 187445.953125), (('westeros', 'westeros_emissions_bounds'), []), diff --git a/tutorial/README.rst b/tutorial/README.rst index afe26604f..3c9ce7cba 100644 --- a/tutorial/README.rst +++ b/tutorial/README.rst @@ -62,11 +62,12 @@ Westeros Electrified This tutorial demonstrates how to model a very simple energy system, and then uses it to illustrate a range of framework features. -1. `Build the baseline model `_. -2. `Introduce emissions `_ and a bound on the emissions. -3. `Limit emissions using a tax `_ instead of a bound. -4. `Represent both coal and wind electricity `_, using a “firm capacity” formulation: each generation technology can supply some firm capacity, but the variable, renewable technology (wind) supplies less than coal. -5. Represent coal and wind electricity using a different, `“flexibility requirement” formulation `_, wherein wind *requires* and coal *supplies* flexibility. +1. `Build the baseline model `_. +2. `Introduce emissions `_ and a bound on the emissions. +3. `Limit emissions using a tax `_ instead of a bound. +4. `Represent both coal and wind electricity `_, using a “firm capacity” formulation: each generation technology can supply some firm capacity, but the variable, renewable technology (wind) supplies less than coal. +5. Represent coal and wind electricity using a different, `“flexibility requirement” formulation `_, wherein wind *requires* and coal *supplies* flexibility. +6. `Variablity in energy supply and demand `_, by adding two sub-annual time steps (winter and summer). Austrian energy system ---------------------- @@ -74,7 +75,7 @@ Austrian energy system This tutorial demonstrates a stylized representation of a national electricity sector model, with several fossil and renewable power plant types. -1. Prepare the base model version, in `Python `__ or in `R `__. -2. Plot results, in `Python `__ or in `R `__. -3. `Run a single policy scenario `_. -4. Run multiple policy scenarios. This tutorial has two notebooks: `an introduction with some exercises `_ and `completed code for the exercises `_. +1. Prepare the base model version, in `Python `__ or in `R `__. +2. Plot results, in `Python `__ or in `R `__. +3. `Run a single policy scenario `_. +4. Run multiple policy scenarios. This tutorial has two notebooks: `an introduction with some exercises `_ and `completed code for the exercises `_. diff --git a/tutorial/utils/plotting.py b/tutorial/utils/plotting.py index c7ecbee42..b96bb2d35 100644 --- a/tutorial/utils/plotting.py +++ b/tutorial/utils/plotting.py @@ -79,7 +79,7 @@ def plot_capacity(self, baseyear=False, subset=None): df = self.model_data('CAP', baseyear=baseyear, subset=subset) df.plot.bar(stacked=True) plt.title('{} Energy System Capacity'.format(self.country.title())) - plt.ylabel('GWa') + plt.ylabel('GW') plt.xlabel('Year') plt.legend(loc='center left', bbox_to_anchor=(1.0, 0.5)) @@ -91,7 +91,7 @@ def plot_new_capacity(self, baseyear=False, subset=None): df = pd.concat([h, m]) if not h.empty else m df.plot.bar(stacked=True) plt.title('{} Energy System New Capcity'.format(self.country.title())) - plt.ylabel('GWa') + plt.ylabel('GW') plt.xlabel('Year') plt.legend(loc='center left', bbox_to_anchor=(1.0, 0.5)) diff --git a/tutorial/westeros/_static/cf_wind.png b/tutorial/westeros/_static/cf_wind.png new file mode 100644 index 000000000..e267b7a7e Binary files /dev/null and b/tutorial/westeros/_static/cf_wind.png differ diff --git a/tutorial/westeros/_static/load_seasons.png b/tutorial/westeros/_static/load_seasons.png new file mode 100644 index 000000000..36ce4de73 Binary files /dev/null and b/tutorial/westeros/_static/load_seasons.png differ diff --git a/tutorial/westeros/westeros_baseline.ipynb b/tutorial/westeros/westeros_baseline.ipynb index f5165d62a..1cf7be74f 100644 --- a/tutorial/westeros/westeros_baseline.ipynb +++ b/tutorial/westeros/westeros_baseline.ipynb @@ -12,12 +12,14 @@ "\n", "### *Integrated Assessment Modeling for the 21st Century*\n", "\n", - "Please refer to the [user guidelines](https://github.com/iiasa/message_ix/blob/master/NOTICE.rst)\n", - "for additional information on using *MESSAGEix*, including the recommended citation and how to name new models.\n", - "\n", "This tutorial is based on a presentation by Matthew Gidden ([@gidden](https://github.com/gidden))\n", "for a summer school at the the **Centre National de la Recherche Scientifique (CNRS)**\n", - "on *Integrated Assessment Modeling* in June 2018." + "on *Integrated Assessment Modeling* in June 2018.\n", + "\n", + "For information on how to install `MESSAGEix`, please refer to [Installation page](https://message.iiasa.ac.at/en/stable/getting_started.html) and for getting `MESSAGEix` tutorials, please follow the steps mentioned in [Tutorials](https://message.iiasa.ac.at/en/stable/tutorials.html).\n", + "\n", + "Please refer to the [user guidelines](https://github.com/iiasa/message_ix/blob/master/NOTICE.rst)\n", + "for additional information on using *MESSAGEix*, including the recommended citation and how to name new models." ] }, { @@ -45,30 +47,11 @@ "source": [ "## Online documentation\n", "\n", - "The full framework documentation is available at [https://messageix.iiasa.ac.at](https://messageix.iiasa.ac.at)\n", + "The full framework documentation is available at [https://message.iiasa.ac.at](https://message.iiasa.ac.at)\n", " \n", "" ] }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "fragment" - } - }, - "source": [ - "## Simple installation\n", - "\n", - "When working with Python, you can easily install `MESSAGEix` yourself and get all the tutorials:\n", - "\n", - "```shell\n", - "$ conda install -c conda-forge message-ix # install the MESSAGEix package and all dependencies\n", - "\n", - "$ messageix-dl # download all tutorials to your current directory\n", - "```" - ] - }, { "cell_type": "markdown", "metadata": { @@ -101,13 +84,13 @@ "\n", "More explicitly, the model...\n", "- optimizes an **objective function**, nominally minimizing total **system costs**\n", - "- under a system of **constraints** equations (inequalities or equality conditions)\n", + "- under a system of **constraints** (inequalities or equality conditions)\n", "\n", "The mathematical implementation includes a number of features that make it particularly geared towards the modelling of *energy-water-land systems* in the context of *climate change mitigation and sustainable development*.\n", "\n", "Throughout this document, the mathematical formulation follows the convention that\n", "- decision **VARIABLES** ($x$) are capitalized\n", - "- **parameters** ($A$, $b$) are lower case" + "- input **parameters** ($A$, $b$) are lower case" ] }, { @@ -118,7 +101,7 @@ } }, "source": [ - "## MESSAGEix: connected to the *ix modeling platform*\n", + "## MESSAGEix: connected to the *ix modeling platform (ixmp)*\n", "\n", "The *modeling platform for integrated and cross-cutting analysis* (ixmp) provides a powerful framework for working with scenarios, including a database infrastucture for data version control and interfaces to scientific programming languages.\n", "\n", @@ -135,7 +118,7 @@ "source": [ "## Ready, steady, go!\n", "\n", - "First, we import all the packages we need." + "First, we import all the packages we need. We import a utility function called *make_df*, which can be used to wrap the input data into dataframes that can be saved in model parameters." ] }, { @@ -165,7 +148,7 @@ } }, "source": [ - "The `Platform` is your connection to a database for storing model input data and scenario results." + "The *MESSAGEix* model is built using the *ixmp* `Platform`. The `Platform` is your connection to a database for storing model input data and scenario results." ] }, { @@ -189,7 +172,7 @@ } }, "source": [ - "Once connected, we create a new `Scenario` to build our model." + "Once connected, we create a new `Scenario` to build our model. A `Scenario` instance will contain all the model input data and results." ] }, { @@ -227,7 +210,7 @@ } }, "source": [ - "The model horizon will span 3 decades. Let's assume that we're far in the future after the events of A Song of Ice and Fire (which occur ~300 years after Aegon the conqueror).\n", + "The model horizon will span 3 decades (690-720). Let's assume that we're far in the future after the events of A Song of Ice and Fire (which occur ~300 years after Aegon the conqueror).\n", "\n", "| Math Notation | Model Meaning |\n", "|---------------|------------------------------|\n", @@ -289,7 +272,7 @@ } }, "source": [ - "And we fill in the energy system's `commodities`, `levels`, `technologies`, and `mode` (defining how certain technologies operate). \n", + "And we fill in the energy system's `commodities`, `levels`, `technologies`, and `modes` (i.e., modes of operation of technologies). This information defines how certain technologies operate. \n", "\n", "\n", "| Math Notation | Model Meaning|\n", @@ -363,13 +346,15 @@ } }, "source": [ - "The `COMMODITY_BALANCE` equation ensures that `demand` for each `commodity` is met at each `level` in the energy system.\n", + "The `COMMODITY_BALANCE_GT` and `COMMODITY_BALANCE_LT` equations ensure that `demand` for each `commodity` is met at each `level` in the energy system.\n", + "The equation is copied below in this tutorial notebook, but every model equation is available for reference in\n", + "the [Mathematical formulation](https://message.iiasa.ac.at/en/stable/model/MESSAGE/model_core.html#) section of the MESSAGEix documentation.\n", "\n", "$\\sum_{\\substack{n^L,t,m \\\\ y^V \\leq y}} \\text{output}_{n^L,t,y^V,y,m,n,c,l} \\cdot \\text{ACT}_{n^L,t,y^V,y,m}$\n", "$- \\sum_{\\substack{n^L,t,m, \\\\ y^V \\leq y}} \\text{input}_{n^L,t,y^V,y,m,n,c,l} \\cdot \\text{ACT}_{n^L,t,m,y}$ \n", "$\\geq \\text{demand}_{n,c,l,y} \\quad \\forall \\ l \\in L$\n", "\n", - "While `demand` must be met, it can also be *exceeded* allowing the model to plan for future periods of demand for storable commodities.\n" + "While `demand` must be met, supply can *exceed* demand allowing the model to plan for meeting demand in future periods by storing storable commodities.\n" ] }, { @@ -461,6 +446,7 @@ }, "outputs": [], "source": [ + "# We use add_par for adding data to a MESSAGEix parameter\n", "scenario.add_par(\"demand\", light_demand)" ] }, @@ -472,7 +458,7 @@ } }, "source": [ - "In order to add values for the reference energy system, we define some common keys.\n", + "In order to define the input and output commodites of each technology, we define some common keys.\n", "\n", "- **Input** quantities require `_origin` keys that specify where the inputs are *received from*.\n", "- **Output** quantities require `_dest` keys that specify where the outputs are *transferred to*." @@ -517,7 +503,9 @@ "- receives *input* in the form of the \"electricity\" *commodity* at the \"final [energy]\" *level*, and\n", "- *outputs* the commodity \"light\" at the \"useful [energy]\" level.\n", "\n", - "Because there are no other commodities or forms of useful energy in Westeros, we specify that 100% of the input and output are from/to these commodities/levels." + "The `value` in the input and output parameter is used to represent the effiecieny of a technology (efficiency = output/input).\n", + "For example, input of 1.0 and output of 1.0 for a technology shows that the efficiency of that technology is 100% in converting\n", + "the input commodity to the output commodity." ] }, { @@ -547,7 +535,7 @@ } }, "source": [ - "Next, the electrical `grid`, which…\n", + "Next, we parameterize the electrical `grid`, which…\n", "\n", "- receives electricity at the \"secondary\" energy level.\n", "- also outputs electricity, but at the \"final\" energy level (to be used by the light bulb).\n", @@ -626,7 +614,7 @@ "source": [ "The model has a number of \"reality\" constraints, which relate built *capacity* (`CAP`) to available power, or the *activity* (`ACT`) of that technology.\n", "\n", - "The **capacity constraint** limits the total time a each technology can operate.\n", + "The **capacity constraint** limits the activity of a technology to the installed capacity multiplied by a capacity factor. Capacity factor or is the fraction of installed capacity that can be active in a certain period (here the sub-annual time step *h*).\n", "\n", "$$\\sum_{m} \\text{ACT}_{n,t,y^V,y,m,h}\n", " \\leq \\text{duration_time}_{h} \\cdot \\text{capacity_factor}_{n,t,y^V,y,h} \\cdot \\text{CAP}_{n,t,y^V,y}\n", @@ -675,7 +663,7 @@ "source": [ "capacity_factor = {\n", " 'coal_ppl': 1,\n", - " 'wind_ppl': 1,\n", + " 'wind_ppl': 0.36,\n", " 'bulb': 1, \n", "}\n", "\n", @@ -753,7 +741,7 @@ "source": [ "## Technological Diffusion and Contraction\n", "\n", - "We know from historical precedent that energy systems can not be transformed instantaneously. Therefore, we use a family of constraints on activity and capacity." + "We know from historical precedent that energy systems can not be transformed instantaneously. Therefore, we use a family of dynamic constraints on activity and capacity. These constraints define the upper and lower limit of the domain of activity and capacity over time based on their value in the previous time step, an initial value, and growth/decline rates." ] }, { @@ -827,9 +815,9 @@ } }, "source": [ - "## Defining an Energy Mix\n", + "## Defining an Energy Mix (Model Calibration)\n", "\n", - "To model the transition of an energy system, one must start with the existing system which are defined by the parameters `historical_activity` and `historical_capacity`. These parameters define the energy mix before the model horizon. \n", + "To model the transition of an energy system, one must start with the existing system which are defined by the parameters `historical_activity` and `historical_new_capacity`. These parameters define the energy mix before the model horizon. \n", "\n", "We begin by defining a few key values:\n", "\n", @@ -1010,8 +998,11 @@ "base_inv_cost = {\n", " 'node_loc': country,\n", " 'year_vtg': model_horizon,\n", - " 'unit': 'USD/GWa',\n", - "}" + " 'unit': 'USD/kW',\n", + "}\n", + "\n", + "# Adding a new unit to the library\n", + "mp.add_unit('USD/kW') " ] }, { @@ -1065,7 +1056,7 @@ " 'node_loc': country,\n", " 'year_vtg': vintage_years,\n", " 'year_act': act_years,\n", - " 'unit': 'USD/GWa',\n", + " 'unit': 'USD/kW',\n", "}" ] }, @@ -1121,7 +1112,7 @@ " 'year_act': act_years,\n", " 'mode': 'standard',\n", " 'time': 'year',\n", - " 'unit': 'USD/GWa',\n", + " 'unit': 'USD/kWa',\n", "}" ] }, @@ -1135,7 +1126,7 @@ }, "outputs": [], "source": [ - "# in $ / MWh\n", + "# in $ / kWa\n", "costs = {\n", " 'coal_ppl': 30,\n", " 'grid': 50,\n", @@ -1173,7 +1164,7 @@ "## Time to Solve the Model\n", "\n", "First, we *commit* the model structure and input data (sets and parameters).\n", - "In the `ixmp` backend, this creates a new model version in the database, which is assigned a number automatically:" + "In the `ixmp` backend, this creates a new model version in the database, which is assigned a version number automatically:" ] }, { @@ -1259,7 +1250,7 @@ "source": [ "## Plotting Results\n", "\n", - "We make use of some custom code; see `tools.py` in the tutorial directory." + "We make use of some custom code for plotting the results; see `tools.py` in the tutorial directory." ] }, { @@ -1312,7 +1303,7 @@ "source": [ "### Capacity\n", "\n", - "Given how many new plants are built, how many are actually used?" + "How much capacity of each plant is installed in each period?" ] }, { @@ -1338,9 +1329,11 @@ "source": [ "### Electricity Price\n", "\n", - "And how much does the electricity cost? These prices are taken from the **dual variables** of the solution and are given the name **shadow prices**. They reflect the marginal price of electricity, taken from the most expensive producer. \n", + "And how much does the electricity cost? These prices are in fact **shadow prices** taken from the **dual variables** of the model solution.\n", + "They reflect the marginal cost of electricity generation (i.e., the additional cost of the system for supplying one more unit of\n", + "electricity), which is in fact the marginal cost of the most expensive operating generator. \n", "\n", - "Note that the price drop when the most expensive technology is no longer in the system." + "Note the price drop when the most expensive technology is no longer in the system." ] }, { diff --git a/tutorial/westeros/westeros_emissions_bounds.ipynb b/tutorial/westeros/westeros_emissions_bounds.ipynb index d95e1e7c2..640b58d3d 100644 --- a/tutorial/westeros/westeros_emissions_bounds.ipynb +++ b/tutorial/westeros/westeros_emissions_bounds.ipynb @@ -72,7 +72,7 @@ "metadata": {}, "outputs": [], "source": [ - "# first we introduce the emission specis CO2 and the emission category GHG\n", + "# first we introduce the emission of CO2 and the emission category GHG\n", "scen.add_set('emission', 'CO2')\n", "scen.add_cat('emission', 'GHG', 'CO2')\n", "\n", @@ -82,10 +82,14 @@ " 'year_vtg': vintage_years,\n", " 'year_act': act_years,\n", " 'mode': 'standard',\n", - " 'unit': 'USD/GWa',\n", + " 'unit': 'tCO2/kWa',\n", "}\n", "\n", - "emission_factor = make_df(base_emission_factor, technology= 'coal_ppl', emission= 'CO2', value = 100.)\n", + "# adding new units to the model library (needed only once)\n", + "mp.add_unit('tCO2/kWa')\n", + "mp.add_unit('MtCO2')\n", + "\n", + "emission_factor = make_df(base_emission_factor, technology= 'coal_ppl', emission= 'CO2', value = 7.4)\n", "scen.add_par('emission_factor', emission_factor)" ] }, @@ -95,7 +99,7 @@ "source": [ "## Define a Bound on Emissions\n", "\n", - "The `type_year: cumulative` assigns an upper bound on the *average emissions per year* over the entire time horizon." + "The `type_year: cumulative` assigns an upper bound on the *weighted average of emissions* over the entire time horizon." ] }, { @@ -105,7 +109,7 @@ "outputs": [], "source": [ "scen.add_par('bound_emission', key=[country, 'GHG', 'all', 'cumulative'],\n", - " val=4000., unit='tCO2')" + " val=500., unit='MtCO2')" ] }, { @@ -184,7 +188,7 @@ "source": [ "### Capacity\n", "\n", - "Given how many new plants are built, how many are actually used?" + "How much capacity of each plant is installed in each period?" ] }, { @@ -202,9 +206,9 @@ "source": [ "### Electricity Price\n", "\n", - "And how much does the electricity cost? These prices are taken from the **dual variables** of the solution and are given the name **shadow prices**. They reflect the marginal price of electricity, taken from the most expensive producer. \n", + "And how much does the electricity cost? These prices are in fact **shadow prices** taken from the **dual variables** of the model solution. They reflect the marginal cost of electricity generation (i.e., the additional cost of the system for supplying one more unit of electricity), which is in fact the marginal cost of the most expensive generator. \n", "\n", - "Note that the price drop when the most expensive technology is no longer in the system." + "Note the price drop when the most expensive technology is no longer in the system." ] }, { diff --git a/tutorial/westeros/westeros_emissions_taxes.ipynb b/tutorial/westeros/westeros_emissions_taxes.ipynb index 489a27f22..38cab3358 100644 --- a/tutorial/westeros/westeros_emissions_taxes.ipynb +++ b/tutorial/westeros/westeros_emissions_taxes.ipynb @@ -85,7 +85,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "When setting a cumlulative bound, the optimization model choses an emission trajectory that pushes the cost towards the end of the model horizon. As a consequence, the shadow price or dual variable of the constraint increase exponentially at the discount rate." + "When setting a cumlulative bound, the undiscounted price of emission is the same in different model years (see the marginals of\n", + "equation `EMISSION_CONSTRAINT`). However, considering the year-to-year discount factor, we observe an ascending trend in\n", + "emission prices shown in `PRICE_EMISSION` above. This means the emission price in later years is higher as the value of money in\n", + "the future is lower compared to today. " ] }, { @@ -94,9 +97,11 @@ "source": [ "## Make a new scenario with emission bounds by year\n", "\n", - "In the previous example, we imposed a bound on emissions over the entire model horizon by using the `type_year 'cumulative'`. Now, we will create a similar scenario, but the constraint will be defined per year.\n", + "In the previous example, we imposed a bound on emissions over the entire model horizon by using the `type_year` as 'cumulative'\n", + "in the parameter `bound_emission`. Now, we will create a similar scenario, but the emission constraint will be defined per year.\n", "\n", - "For the sake of comparison, the per-year emission values will be chosen exactly in line with the optimal emission trajectory from the previous scenario." + "For the sake of comparison, the per-year emission values will be chosen exactly in line with the optimal emission trajectory\n", + "obtained from the solution of the previous scenario." ] }, { @@ -133,7 +138,7 @@ " 'node': 'Westeros',\n", " 'type_year': horizon,\n", " 'type_tec': 'all',\n", - " 'unit': 'tCO2',\n", + " 'unit': 'MtCO2',\n", " 'type_emission': 'GHG',\n", " 'value': emissions.set_index('year').loc[[700, 710, 720]].lvl\n", "}\n", @@ -174,7 +179,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Comparing the emission prices between the two scenarios, we see that the values are identical in the year 710 and close in the year 720. However, the bound in the year 700 is not binding, so the shadow price is 0 (and is not shown here)." + "Comparing the emission prices between the two scenarios at this stage, we see that the values are not identical.\n", + "The reason is that when we introduce emission bounds per year, the price of emission in each year reflects the cost occuring\n", + "when reducing one more unit of emission in that year.\n", + "However, in the scenario with a cumulative bound over the entire model horizon, the price of emission reflects the cost of the\n", + "system in reducing one more unit of emission over the entire model horizon." ] }, { @@ -220,11 +229,14 @@ " 'node': 'Westeros',\n", " 'type_year': [700, 710, 720],\n", " 'type_tec': 'all',\n", - " 'unit': 'tCO2',\n", + " 'unit': 'USD/tCO2',\n", " 'type_emission': 'GHG',\n", " 'value': emission_prices.set_index('year').loc[[700, 710, 720]].lvl\n", "}\n", "\n", + "# in case you need to add the new unit to the platform\n", + "mp.add_unit('USD/tCO2')\n", + "\n", "tax_emission = make_df(base_tax_emission)\n", "scen_tax.add_par('tax_emission', tax_emission)" ] @@ -260,11 +272,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Comparing the emissions trajectory in the tax scenario to the outcome in the cumulative budget constraint scenario, we notice that the values in the years 700 and 720 are identical, but the value in 710 is different.\n", - "\n", - "This is the flip side of having an identical shadow price on the constraint in the two previous examples - at that price, the costs between wind and coal (with the tax) are exactly equal, hence the optimal solution is not unique.\n", - "\n", - "This is usually only an issue in small, stylized problems..." + "# Exercises\n", + "- How does these prices compare to the scenario with a cumulative emission bound (`scen_bd`)?\n", + "- Try setting the emission tax again by using emission prices obtained from the scenario with yearly bounds on emissions (`scen_bd_by_year`). What is the difference in emissions (i.e., variable `EMISS`)?" ] }, { diff --git a/tutorial/westeros/westeros_firm_capacity.ipynb b/tutorial/westeros/westeros_firm_capacity.ipynb index e94c1de63..d9b482d81 100644 --- a/tutorial/westeros/westeros_firm_capacity.ipynb +++ b/tutorial/westeros/westeros_firm_capacity.ipynb @@ -4,11 +4,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Westeros Tutorial - Firm capacity\n", + "# Westeros Tutorial Part IV - Firm Capacity\n", "\n", - "In the previous part, we showed how to introduce emissions into a stylized energy systems model, and what happens if you put a constraint on total CO2 emissions.\n", + "In the previous part, we showed how to introduce emissions into a stylized energy system model, and what happens if you put a constraint on total CO2 emissions.\n", "\n", - "In this tutorial notebook, we will illustrate how to add other considerations, in particular the requirement to have sufficient dispatchable (firm) capacity." + "In this tutorial notebook, we will illustrate how to add other constraints, in particular the requirement to have sufficient dispatchable (firm) capacity." ] }, { @@ -65,16 +65,15 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Add Renewable Formulation" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Load Factors - Describing the Electricity Sector\n", - "#### Peak load \n", - "If we further specify our demand, we will find that the demand for electricity (!not the demand for useful light but for electricity!) has a peak load that is different from the annual average load; e.g: if annual average load is 5 GWh peak load during midday might be 10GW --> the peak load factor is equal to 2." + "## Improving the Representation of Electricity Sector\n", + "### Peak load factor\n", + "The input demand for electricity (notice: not the demand for useful light but for electricity) in the model shows the average\n", + "electricity demand in the given time (which in our example is a year).\n", + "However, power systems need enough installed capacity not only to cover the average electricity demand but also to meet the peak\n", + "demand, i.e., maximum load throughout that time.\n", + "This feature can be specified in the model using parameter `peak_load_factor`.\n", + "For example, if annual average load is 5 GW, the peak load may be twice as high, at 10 GW; hence, the peak load factor is equal\n", + "to 2." ] }, { @@ -91,7 +90,7 @@ " 'year': model_horizon,\n", " 'time' : 'year',\n", " 'value' : 2,\n", - " 'unit' : '???'})\n", + " 'unit' : '-'})\n", "\n", "scen.add_par('peak_load_factor', peak_load_factor)" ] @@ -100,9 +99,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Describing the Renewable Technologies Reliability\n", - "### rating and reliability --> Ensuring enough Firm Capacity\n", - "we assume, that up to a share of 20% of total electricity supply, the capacity of wind_ppl adds by 80% to the firm capacity of the power system (r1). The remaining 90% installed capacity, however only 5% contribute to the firm capcity (r2). The coal power plant supplies full reliability independet of the supply share.\n" + "## Reliability of Power System\n", + "In order to meet demand reliably, the power system needs to maintain dispatchable or so-called firm capacity at any time.\n", + "Some technologies like coal power plants can fully contribute to the required firm capacity, i.e.,\n", + "1 MW installed capacity of these technologies provide 1 MW firm capacity.\n", + "However, some other technologies such as variable renewables like wind are not fully reliable when the power system needs them.\n", + "As such, the capacity value of wind power plant is considered to be lower than 1.\n", + "In this example, we assume that 10% of electricity supply by wind is 80% reliable, while the remaining 90% installed capacity can only contribute by 5% to the required firm capacity. As such, we divide wind power into two parts with two different ratings (r1 and r2) and we define this through parameter `rating_bin`.\n" ] }, { @@ -115,7 +118,7 @@ " 'node': country,\n", " 'commodity': 'electricity',\n", " 'level' : 'secondary', \n", - " 'unit': '???',\n", + " 'unit': '-',\n", " 'time': 'year',\n", " 'year_act': model_horizon})" ] @@ -126,23 +129,24 @@ "metadata": {}, "outputs": [], "source": [ - "# add the ratings as a set \n", + "# adding wind ratings to the respective set \n", "scen.add_set('rating', ['r1', 'r2'])\n", "\n", - "# for Wind PPL\n", - "rating_bin = make_df(base_reliability, technology= 'wind_ppl', value = 0.5, rating= 'r1')\n", + "# adding rating bins for wind power plant\n", + "rating_bin = make_df(base_reliability, technology= 'wind_ppl', value = 0.1, rating= 'r1')\n", "scen.add_par('rating_bin', rating_bin)\n", "\n", - "reliability_factor = make_df(base_reliability, technology= 'wind_ppl', value = 0.5, rating= 'r1')\n", - "scen.add_par('reliability_factor', reliability_factor)\n", - "\n", - "rating_bin = make_df(base_reliability, technology= 'wind_ppl', value = 0.5, rating= 'r2')\n", + "rating_bin = make_df(base_reliability, technology= 'wind_ppl', value = 0.9, rating= 'r2')\n", "scen.add_par('rating_bin', rating_bin)\n", "\n", - "reliability_factor = make_df(base_reliability, technology= 'wind_ppl', value = 0.25, rating= 'r2')\n", + "# adding reliability factor for each rating of wind power plant\n", + "reliability_factor = make_df(base_reliability, technology= 'wind_ppl', value = 0.8, rating= 'r1')\n", + "scen.add_par('reliability_factor', reliability_factor)\n", + "\n", + "reliability_factor = make_df(base_reliability, technology= 'wind_ppl', value = 0.05, rating= 'r2')\n", "scen.add_par('reliability_factor', reliability_factor)\n", "\n", - "# for Coal PPL\n", + "# considering coal power plant as firm capacity (adding a reliability factor of 1)\n", "reliability_factor = make_df(base_reliability, technology= 'coal_ppl', value = 1, rating= 'firm')\n", "scen.add_par('reliability_factor', reliability_factor)" ] diff --git a/tutorial/westeros/westeros_flexible_generation.ipynb b/tutorial/westeros/westeros_flexible_generation.ipynb index e3a34cc2e..128c46812 100644 --- a/tutorial/westeros/westeros_flexible_generation.ipynb +++ b/tutorial/westeros/westeros_flexible_generation.ipynb @@ -50,12 +50,14 @@ "metadata": {}, "outputs": [], "source": [ + "# We clone a scenario from the baseline scenario\n", "model = 'Westeros Electrified'\n", "scen = base.clone(model, 'flexibile_generation',\n", " 'illustration of flexible-generation formulation',\n", " keep_solution=False)\n", "scen.check_out()\n", "\n", + "# we get the years of installing capacities (vintage) and years of those capacities being active\n", "year_df = scen.vintage_and_active_years()\n", "vintage_years, act_years = year_df['year_vtg'], year_df['year_act']\n", "model_horizon = scen.set('year')\n", @@ -68,9 +70,7 @@ "source": [ "## Add a carbon tax\n", "\n", - "Note that the example below is not feasible with a strict bound on emissions, hence this tutorial uses a tax.\n", - "\n", - "Below, we repeat the set-up of the emissions formulation from the \"emissions bounds\" tutorial." + "Then, we add a carbon tax to motivate the use of low-carbon technologies in the system. We do this similar to the process explained in the tutorial for adding emissions taxes (`westeros_emission_taxes.ipynb`)." ] }, { @@ -89,26 +89,31 @@ " 'year_vtg': vintage_years,\n", " 'year_act': act_years,\n", " 'mode': 'standard',\n", - " 'unit': 'USD/GWa',\n", + " 'unit': 'tCO2/kWa',\n", "}\n", "\n", - "emission_factor = make_df(base_emission_factor, technology= 'coal_ppl', emission= 'CO2', value = 100)\n", + "# adding the new unit to the model library\n", + "mp.add_unit('tCO2/kWa')\n", + "\n", + "emission_factor = make_df(base_emission_factor, technology= 'coal_ppl', emission= 'CO2', value = 7.4)\n", "scen.add_par('emission_factor', emission_factor)" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "base_tax_emission = {\n", " 'node': country,\n", " 'type_year': [700,710,720],\n", " 'type_tec': 'all',\n", - " 'unit': 'tCO2',\n", + " 'unit': 'USD/tCO2',\n", " 'type_emission': 'GHG',\n", - " 'value': [1., 2., 3.]\n", + " 'value': [10., 20., 30.]\n", "}\n", "\n", "tax_emission = make_df(base_tax_emission)\n", @@ -119,11 +124,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Describing the Renewable Technologies Flexibility \n", - "\n", - "### Flexibility of demand and supply\n", + "## Describing flexibility requirements\n", "\n", - "The wind power plant has a flexibility demand of 5% of its activity. The coal powerplant can provide 20% of its activity as flexibility." + "Demand and supply of electricity varies over time. To meet the demand at any time, the suppy technologies must be flexible enough to ramp up and down their generation. Some renewable energy technologies like wind power are \"variable\", meaning that their generation changes based on wind availability and not based on the electricity demand. As such, to balance the variability of wind power, the system requires flexibility from other units." ] }, { @@ -137,7 +140,7 @@ " 'commodity': 'electricity',\n", " 'level' : 'secondary',\n", " 'mode': 'standard',\n", - " 'unit': '???',\n", + " 'unit': '-',\n", " 'time': 'year',\n", " 'year_vtg': vintage_years,\n", " 'year_act': act_years,\n", @@ -147,7 +150,7 @@ " 'node': country,\n", " 'commodity': 'electricity',\n", " 'level' : 'secondary', \n", - " 'unit': '???',\n", + " 'unit': '-',\n", " 'time': 'year',\n", " 'year_act': model_horizon})" ] @@ -158,29 +161,37 @@ "metadata": {}, "outputs": [], "source": [ - "# add the ratings as a set \n", + "# adding two different ratings for flexibility \n", "scen.add_set('rating', ['r1', 'r2'])\n", "\n", - "# For the Load \n", - "flexibility_factor = make_df(base_flexibility_factor, technology= 'grid', rating= 'unrated', value = -0.1)\n", - "scen.add_par('flexibility_factor',flexibility_factor)\n", + "# assuming a flexibility requirment of 10% for load \n", + "flexibility_factor = make_df(base_flexibility_factor, technology='grid', rating='unrated', value=-0.1)\n", + "scen.add_par('flexibility_factor', flexibility_factor)\n", "\n", - "# For the Wind PPL\n", - "rating_bin = make_df(base_rating, technology= 'wind_ppl', value = 0.2, rating= 'r1')\n", + "# dividing wind generation into two parts (bins) for each rating (r1=20% and r2=80%) \n", + "rating_bin = make_df(base_rating, technology='wind_ppl', value=0.2, rating='r1')\n", "scen.add_par('rating_bin', rating_bin)\n", - "\n", - "flexibility_factor = make_df(base_flexibility_factor, technology= 'wind_ppl', rating= 'r1', value = -0.2)\n", - "scen.add_par('flexibility_factor',flexibility_factor)\n", - "\n", - "rating_bin = make_df(base_rating, technology= 'wind_ppl', value = 0.8, rating= 'r2')\n", + "rating_bin = make_df(base_rating, technology='wind_ppl', value=0.8, rating='r2')\n", "scen.add_par('rating_bin', rating_bin)\n", "\n", - "flexibility_factor = make_df(base_flexibility_factor, technology= 'wind_ppl', rating= 'r2', value = -0.7)\n", - "scen.add_par('flexibility_factor',flexibility_factor)\n", + "# assuming a flexibility requirment of 25% for wind in rating r1\n", + "flexibility_factor = make_df(base_flexibility_factor, technology='wind_ppl', rating='r1', value=-0.25)\n", + "scen.add_par('flexibility_factor', flexibility_factor)\n", + "\n", + "# assuming a flexibility requirment of 45% for wind in rating r2\n", + "flexibility_factor = make_df(base_flexibility_factor, technology='wind_ppl', rating='r2', value=-0.45)\n", + "scen.add_par('flexibility_factor', flexibility_factor)\n", "\n", - "# For the Coal PPL\n", - "flexibility_factor = make_df(base_flexibility_factor, technology= 'coal_ppl', rating= 'unrated', value = 1)\n", - "scen.add_par('flexibility_factor',flexibility_factor)" + "# assuming a flexibility provision of 80% for coal power plant\n", + "flexibility_factor = make_df(base_flexibility_factor, technology='coal_ppl', rating='unrated', value=0.8)\n", + "scen.add_par('flexibility_factor', flexibility_factor)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we could see, the need for flexibility is added by negative values to the parameter `flexibility_factor`, while the provision of flexibility is specified by positive values. For more information please refer to the [mathematical specification of flexibility](https://message.iiasa.ac.at/en/stable/model/MESSAGE/model_core.html#system-reliability-and-flexibility-requirements) in the model." ] }, { @@ -272,18 +283,21 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "mp.close_db()" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], - "source": [] + "source": [ + "## Questions and discussion\n", + "- Run the same scenario with and without adding `flexibility_factor`. what is the difference in the results when we consider the flexibility requirements?" + ] } ], "metadata": { diff --git a/tutorial/westeros/westeros_seasonality.ipynb b/tutorial/westeros/westeros_seasonality.ipynb new file mode 100644 index 000000000..363ef5b55 --- /dev/null +++ b/tutorial/westeros/westeros_seasonality.ipynb @@ -0,0 +1,610 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# \"*Winter is coming!*\": Modeling of variability in energy supply and demand\n", + "Time-dependent variations in demand and supply are common characteristics of an electricity system, and Westeros is not an\n", + "exception. This tutorial helps to learn how to add sub-annual time steps to a MESSAGEix model and investigate the impact of the\n", + "variability in supply and demand. It is structured as follows:\n", + "1. A short note on seasonality\n", + "2. Adding sub-annual time steps\n", + "3. Analyzing the results\n", + "\n", + "### Requirements for running this tutorial\n", + "- You have MESSAGEix framework installed and working\n", + "- You have run Westeros baseline scenario and solved it successfully\n", + "\n", + "This tutorial was developed by Behnam Zakeri ([@behnam-zakeri](https://github.com/behnam-zakeri)) for the course Energy\n", + "Economics and Modeling held at the International Summer School in Energy Technology, St. Petersburg Polytechnique University in August 2018." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Seasonal variations in demand and supply\n", + "The electricity demand can vary in different time scales, e.g., seasonally, monthly, daily, and hourly. In Westeros, the winter time is typically cold and long, which increases the demand for electricity and lighting. We investigate this seasonality in this tutorial, but the procedure discussed here can be adopted for studying different lengths of temporal resolution.\n", + "" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Seasonality can be relevant to some supply technologies as well. For example, Westeros has more windy days in winter months compared to summer. This means the capacity factor of a wind power plant is higher in winter compared to summer. Figure below, rendered from renewables.ninja*, shows the monthly capacity factor of wind power plant somewhere near Westeros. \n", + "\n", + "\n", + " *You can also find the capacity factor of wind and solar PV for your location at www.renewables.ninja, as we found for a place near Westeros." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Implementation of seasonality\n", + "In this tutorial we add two sub-annual time steps, winter and summer. First, we load the baseline scenario, then we add the required sets related to seasonality, next we modify the parameters, and finally we analyze the results.\n", + "\n", + "### 2.1. Loading Westeros baseline scenario " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# Importing required packages\n", + "import pandas as pd\n", + "import ixmp as ix\n", + "import message_ix\n", + "\n", + "from message_ix.utils import make_df\n", + "\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Loading modelling platform\n", + "mp = ix.Platform(dbtype='HSQLDB')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Specifying model/scenario to be loaded from the database\n", + "model = 'Westeros Electrified'\n", + "scenario='baseline'\n", + "base = message_ix.Scenario(mp, model, scenario)\n", + "\n", + "# Cloning a scenario for adding time steps\n", + "scen = base.clone(model, 'westeros_seasonal', 'introducing seasonality', keep_solution=False)\n", + "scen.check_out()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.2 Modifying sets\n", + "First, we specify subannual time steps and add them to relevant MESSAGE sets. In the MESSAGEix framework set \"time\" is devoted for sub-annual time steps, denoted as index *h* in the mathematical formulation:\n", + "\n", + "| Set name | Math notation | Explanation |\n", + "|----------|---------------|----------------------------------------------|\n", + "| time | $h \\in H$ | subannual time periods |" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# Adding sub-annual time steps\n", + "time_steps = ['winter', 'summer']\n", + "scen.add_set('time', time_steps)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# We can see the elements of the set\n", + "scen.set('time')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Defining a new temporal level\n", + "time_level = 'season'\n", + "scen.add_set('lvl_temporal', time_level)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, the temporal hierarchy will be defined to map different levels of time with respect to \"year\", which is the parent temporal level for any \"time\". " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Adding temporal hierarchy\n", + "for t in time_steps:\n", + " scen.add_set('map_temporal_hierarchy', [time_level, t, 'year'])\n", + " \n", + "# We can see the content of the set\n", + "scen.set('map_temporal_hierarchy')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.3. Modifying parameters\n", + "In this Section, we modify some parameters based on the new time steps. In principle, we need to examine all parameters that have an index of \"time\" to see if we need to modify them or not." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# All parameters with at least one sub-annual time index\n", + "parameters = [p for p in scen.par_list() if 'time' in scen.idx_sets(p)]\n", + "\n", + "# Those parameters with \"time\" index that are not empty in our model\n", + "[p for p in parameters if not scen.par(p).empty]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Parameter \"duration_time\"\n", + "We start by modifying the parameter \"duration_time\", which shows the length of each subannual time step relative to the whole year:\n", + "\n", + "| Parameter | Index set| Explanation|\n", + "|-----------|----------| -----------|\n", + "| duration_time\t| time\t|duration of sub-annual time slices (relative to 1)| \n", + "\n", + "In our example, winter and summer are defined as each half of the year. However, the duration of time steps can be\n", + "different in a MESSAGEix model, e.g., winter 0.4 and summer 0.6. But the sum of the duration times must be equal to 1." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# Adding duration time\n", + "for t in time_steps:\n", + " scen.add_par('duration_time', [t], 0.5, '-')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### A function for modifying parameters\n", + "In this stage, we introduce a function that helps us to modify the parameters after adding new time steps. This function called \"yearly_to_season\" does the following:\n", + "- removing old values, where the \"time\" index was \"year\"\n", + "- populating data for new \"time\" indexes\n", + "- using the ratios defined by the user to convert yearly values to seasonal ones\n", + "\n", + "This is a common code pattern when modelling using MESSAGEix:\n", + "writing re-usable code that helps modify existing parameter data to reflect some desired change in the reference energy system." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# A function for adding sub-annual data to a parameter\n", + "def yearly_to_season(scen, parameter, data, filters=None):\n", + " if filters:\n", + " old = scen.par(parameter, filters)\n", + " else:\n", + " old = scen.par(parameter)\n", + " scen.remove_par(parameter, old)\n", + " \n", + " # Finding \"time\" related indexes\n", + " time_idx = [x for x in scen.idx_names(parameter) if 'time' in x]\n", + " for h in data.keys():\n", + " new = old.copy()\n", + " for time in time_idx:\n", + " new[time] = h\n", + " new['value'] = data[h] * old['value']\n", + " scen.add_par(parameter, new)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Modifying electricity demand in sub-annual time steps\n", + "The seasonality in demand for electricity and lighting can be taken into account by estimating different values for each time step. For example, the share of electricity demand in winter and summer in Westeros is approximately 0.6 and 0.4 of the yearly demand. We can get the yearly demand from baseline scenario and divide it to the two seasons according to their shares." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Before modifying, let's look at \"demand\" in baseline\n", + "scen.par('demand')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Modifying demand for each season\n", + "demand_data = {'winter': 0.60, 'summer': 0.40}\n", + "yearly_to_season(scen, 'demand', demand_data)\n", + "\n", + "# let's look at \"demand\" now\n", + "scen.par('demand')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Modifying \"input\" and \"output\"\n", + "However, not all parameters that have subannual time steps need to divide their annual values for each time step. For example, \"output\" parameter shows the output efficiency, commodities and the level of a technology. Hence, as far as the efficiency of a technology remains unchanged in different seasons, the value of \"output\" will be the same. As such, we only need to add the sub-annual time steps but with the same value as for the yearly one." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# Modifying input and output parameters for each season\n", + "# output\n", + "fixed_data = {'winter': 1, 'summer': 1}\n", + "yearly_to_season(scen, 'output', fixed_data)\n", + "\n", + "# input\n", + "yearly_to_season(scen, 'input', fixed_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Modifying dynamic constraints\n", + "Next, we modify dynamic constraints with a \"time\" index, i.e. growth and decline rates of activities. In the Westeros baseline scenario, there is only growth_activity_up, so this parameter will be modified for seasonality but with the same values. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# Modifying growth rates for each season\n", + "yearly_to_season(scen, 'growth_activity_up', fixed_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Modifying capacity factor\n", + "We discussed about the variation in the capacity factor of wind power in each month. By averaging the values for the respective months, we reach a capacity factor of 0.46 for winter and 0.25 for summer in Westeros.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Modifying capacity factor\n", + "# Let's get the yearly capacity factor of wind in the baseline scenario\n", + "cf_wind = scen.par('capacity_factor', {'technology': 'wind_ppl'})['value'].mean()\n", + "\n", + "# Converting yearly capacity factor to seasonal\n", + "cf_data = {'winter': 0.46 / cf_wind, 'summer': 0.25 / cf_wind} \n", + "cf_filters = {'technology': 'wind_ppl'}\n", + "yearly_to_season(scen, 'capacity_factor', cf_data, cf_filters)\n", + "\n", + "# Capacity factor of other technologies remains unchanged in each season\n", + "cf_filters = {'technology': ['coal_ppl', 'bulb', 'grid']}\n", + "yearly_to_season(scen, 'capacity_factor', fixed_data, cf_filters)\n", + "\n", + "# Let's look at capacity factor in year 710\n", + "scen.par('capacity_factor', {'year_act':710, 'year_vtg':710})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Modifying historical activity\n", + "\"historical_activity\" is one of the parameters that the data should be divided between the seasons. In the absence of recorded data in Westeros before 690, we assume historical values can be divided by half for each season:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# Modifying historical activity \n", + "hist_data = {'winter': 0.5, 'summer': 0.5}\n", + "yearly_to_season(scen, 'historical_activity', hist_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Economic parameters\n", + "Investment cost is defined per installed capacity per year and it is not related to subannual time steps, so it remains unchanged. Variable cost is time-dependent so the \"time\" index should be updated. However, as variable cost is defined per unit of activity, the \"value\" can remain unchanged for different time steps." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# Modifying variable cost\n", + "yearly_to_season(scen, 'var_cost', fixed_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.4. Solving the model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "scen.commit(comment='introducing seasonality')\n", + "scen.set_as_default()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "scen.solve()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "scen.var('OBJ')['lvl']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Postprocessing and analyzing results\n", + "### 3.1. Plotting results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "from tools import Plots\n", + "p = Plots(scen, country='Westeros', firstyear=700)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Activity\n", + "\n", + "How much energy is generated in each time period from the different potential sources?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "p.plot_activity(baseyear=True, subset=['coal_ppl', 'wind_ppl'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Installed capacity\n", + "\n", + "Given how many new plants are built, how many are actually used?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "p.plot_capacity(baseyear=True, subset=['coal_ppl', 'wind_ppl'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Electricity Price\n", + "\n", + "Electricity prices are **dual variables** of the optimization solution called **shadow prices** as well. They reflect the marginal cost of producing one more unit of electricity, hence, representing the marginal cost of the most expensive generator in the system. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "p.plot_prices(subset=['light'], baseyear=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.2. Questions and discussion\n", + "1. Compare the objective value of this scenario with the baseline; which power system is more costly? Given the fact that electricity demand and the cost of technologies are equal between these two scenarios, how do you justify the change in the total costs?\n", + "\n", + "2. Compare the figure for \"Activity\" between this scenario and the baseline; do you observe any changes in the energy mix? The results show more wind integration in this scenario. What is the reason?\n", + " \n", + "3. If you check out the figure for \"Capacity\" in both scenarios, you'll notice that the increase in the installed capacity of wind is much higher than the growth in wind generation. Can you explain the reason?\n", + "\n", + "4. Rerun this tutorial for a scenario with 4 sub-annual time steps: winter, spring, summer and autumn (each one-forth of the whole year)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### *Discussion*: \n", + "The greater avilability of wind in wintertime coincides with the higher demand for electricity in winter compared to summer. Hence, when coal power plants are not able to cover the increased electricity demand in winter, the system needs more wind generation compared to the baseline (for example, see the results for \"Activity\" in 700):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "scen.var('ACT', {'year_act': 700})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Adding seasonality to the model shows the impact of any coincidence or mismatch between wind generation and electricity demand. This seasonality can be averaged out if the yearly values are taken into account. The capacity factor of wind is different in winter and summer, while it was an average yearly value in the baseline scenario. Without any bound or tax on emissions, coal is the cheapest option. Wind will be installed only when coal cannot ramp up quickly to meet the whole demand, mainly in winter. Therefore, we observe relatively much higher installed capacity for wind compared in this scenario to the baseline, even wind is more expensive. Thus, we see a more expensive system when considering seasonality (compare the objective values). " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Final note\n", + "\n", + "Thank you for trying this tutorial!\n", + "\n", + "Check us out on Github: https://github.com/iiasa/message_ix \n", + "\n", + "Get in touch with us online: https://groups.google.com/forum/message-ix \n", + "\n", + "And feel free to contact us with any further questions or feedback for improving this tutorial: zakeri@iiasa.ac.at" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "mp.close_db()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [] + } + ], + "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.6.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}