diff --git a/OSeMOSYS.ipynb b/OSeMOSYS.ipynb index 5eeb0f5..9d0b4a7 100644 --- a/OSeMOSYS.ipynb +++ b/OSeMOSYS.ipynb @@ -6,7 +6,12 @@ "metadata": {}, "outputs": [], "source": [ + "import plotly.graph_objects as go\n", + "from plotly.subplots import make_subplots\n", + "\n", + "\n", "import pandas as pd\n", + "pd.options.plotting.backend = \"plotly\"\n", "from plotly import express as ex\n", "from tqdm import trange\n", "import os" @@ -18,79 +23,84 @@ "source": [ "# LIFE Academy Exercise\n", "\n", - "This lab aims to provide an inside view on what a supply curve is and how it can be generated while using OSeMOSYS. Reminder, a supply curve is a graphical representation of the law of supply. It slopes upward because the quantity supplied increases as price increases, with other things constant.\n", + "This material draws upon, adapts and extends Lab 2 from MJ2380, authored by Roberto Heredia Fonseca, \n", + "Shravan Kumar and Francesco Gardumi and the OpTIMUS.community \n", + "and is licensed under the [Creative Commons Attribution 4.0 International License](http://creativecommons.org/licenses/by/4.0/).\n", + "\n", + "This notebook provides a simple hands-on introduction to energy system modelling using OSeMOSYS.\n", + "We will run OSeMOSYS in the background using this Jupyter Notebook to control the input data, \n", + "run the model and extract and visualise the results.\n", "\n", - "In this lab, we'll be using OSeMOSYS, but we'll be running it in the background using this Jupyter Notebook to control the input data, run the model and extract and visualise the results.\n", + "If you click on the **Jupyter** logo in the top left-hand corner of the screen, \n", + "you will see the folder structure containing the OSeMOSYS models used in this lab.\n", "\n", - "If you click on the **Jupyter** logo in the top left-hand corner of the screen, you will see the folder structure containing the OSeMOSYS models used in this lab.\n", + "In the `model` folder, you will find subfolders containing OSeMOSYS models of increasing complexity.\n", + "Each subfolder contains a data folder in which you see a number of CSV (comma-separated value) \n", + "files which you can edit directly in the browser. \n", + "Each file relates to one parameter in OSeMOSYS. \n", + "For example, you could edit the `CapitalCost` of technologies of the `year` model by editing \n", + "the respective [CSV file](model/year/data/CapitalCost.csv).\n", "\n", - "In the `model` folder, you will find subfolders containing OSeMOSYS models of increasing complexity. Each subfolder contains a data folder in which you see a number of CSV (comma-separated value) files which you can edit directly in the browser. Each file relates to one parameter in OSeMOSYS. For example, you could edit the `CapitalCost` of technologies by editing the respective [CSV file](model/gas/data/CapitalCost.csv).\n", + "We use the multi-year model called `year`, which you can find in the [`model/year/data`](model/year/data) folder.\n", "\n", "## Units\n", "\n", - "Parameter | Unit \n", - ":-- | --:\n", - "Demand | PJ\n", - "Capacity | GW\n", - "Activity | PJ\n", - "Capital Cost | M\\$/GW\n", - "Fixed Cost | M\\$/GW\n", - "Variable Cost, Fossil Fuels Extraction/Import | M\\$/PJ\n", - "Variable Cost, Renewables | M\\$/GWh\n", - "Operational Life | Yr\n", - "Emission Activity Ratio | MtCO2/PJ\n", - "Annual Emission Limit | MtCO2\n", - "Emission Penalty | \\$/tCO2\n", + "Here are the units used throughout the models\n", "\n", - "## Parameters\n", + "Parameter | Unit | Description\n", + ":-- | --: | --:\n", + "Demand | PJ | Petajoules\n", + "Capacity | GW | Gigawatts\n", + "Activity | PJ | Petajoules\n", + "Capital Cost | M\\$/GW | Million US dollars per Gigawatt\n", + "Fixed Cost | M\\$/GW | Million US dollars per Gigawatt\n", + "Variable Cost, Fossil Fuels Extraction/Import | M\\$/PJ | Million US dollars per Petajoule\n", + "Variable Cost, Renewables | M\\$/GWh | Million US dollars per Gigawatt-hour\n", + "Operational Life | Yr | Year\n", + "Emission Activity Ratio | MtCO2/PJ | Million tonnes of CO2 equivalent per petajoule\n", + "Annual Emission Limit | MtCO2 | Million tonnes of CO2 equivalent\n", + "Emission Penalty | \\$/tCO2 | US dollars per tonne of CO2 equivalent\n", "\n", - "**If you are not yet familiar with OSeMOSYS parameters, open the [manual](https://osemosys.readthedocs.io/en/latest/manual/Structure%20of%20OSeMOSYS.html#parameters) in a browser window for reference throughout the lab.**\n", + "## Model Structure\n", "\n", - "## Running a model\n", + "### Sets\n", "\n", - "Running a model is a two step process. Firstly, you need to create an OSeMOSYS datafile by running the following:\n", + "Sets are used to define the model structure. For example \n", + "[YEAR](model/year/data/YEAR.csv), \n", + "[TIMESLICE](model/year/data/TIMESLICE.csv), \n", + "[TECHNOLOGY](model/year/data/TECHNOLOGY.csv), \n", + "[MODE_OF_OPERATION](model/year/data/MODE_OF_OPERATION.csv), \n", + "[REGION](model/year/data/REGION.csv), \n", + "[FUEL](model/year/data/FUEL.csv), \n", + "[EMISSION](model/year/data/EMISSION.csv) etc.\n", "\n", - " !otoole convert datapackage datafile model/gas temp.txt\n", - " \n", - "Note you need the `!` prepended to the command (this tells the notebook to run this command using the shell rather than Python). Here we create datafile called `temp.txt` from the model data stored in `model/gas`.\n", + "_Sets define the basic model structure_\n", "\n", - "After generating the OSeMOSYS datafile (in this case, called `temp.txt`) we then solve the model using `glpsol`, which is the open-source solver provided by the GNU Math Programming kit.\n", + "### Parameters (\"inputs\")\n", "\n", - " !glpsol -d temp.txt -m osemosys.txt > osemosys.log\n", - " \n", - "The results are then available in the `results` folder - see them [here](results/). You should also check the `osemosys.log` file to ensure that the model ran correctly. View it [here](osemosys.log).\n", - "\n", - "In the stages below, we load data from the model inputs and results using a Python library called `pandas` and plot them in interactive charts.\n", - "\n", - "## Contents\n", - "\n", - "- [Stage 1](#Stage-1:-A-super-simple-supply-curve) - In this section, we explore a very simple model with a two-stage supply curve for natural gas to develop our economic interpretation of OSeMOSYS\n", - "- [Stage 2](#Stage-2:-Adding-Resources) - We add complexity, by increasing the number of steps in our supply curve by adding resources. We explore what difference this makes to the electricity price under different conditions.\n", - "- [Stage 3](#Stage-3:-A-tax-on-pollution) - We add an emissions penalty, imposing a tax upon CO2\n", - "- [Stage 4](#Stage-4:-Renewable-electricity-%22there's-no-such-thing-as-a-free-lunch%22) - We add renewable technologies to the electricity sector, whose marginal cost of generation is 0. However, this creates a demand for backup capacity. How does this affect the marginal price of electricity?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Stage 1: A super simple supply curve\n", + "Parameters define the numerical inputs to the model such as costs, demands, constraints for capacity etc.\n", + "Parameters are indexed over the sets, for example [`CapitalCost`](model/year/data/CapitalCost.csv). \n", + "This represents an array of costs for every region, technology and year.\n", "\n", - "We start with the simplest supply cost curve you can imagine. In this simple OSeMOSYS model, there are two natural gas commodities (fuels) - imports of natural gas `GasImport`, and extraction `GasExtraction`. Both of these generate CO2, and feed a natural gas combined-cycle electricity generation plant `NGCC`. This produces secondary electricity `SEC_EL` which enters a transmission and distribution technology `TD` which produces final electricity `FEL`.\n", + "_Parameters are the inputs that the modeller needs to provide to the model._\n", "\n", - "![](img/simple.svg)\n", + "**Open the [manual](https://osemosys.readthedocs.io/en/latest/manual/Structure%20of%20OSeMOSYS.html#parameters) in a browser window for reference throughout the exercise**\n", "\n", - "(Note, this image is produced using the command `!otoole viz res model/gas/datapackage.json img/simple.svg`)\n", + "### Variables\n", "\n", - "---\n", + "The value of variables is decided by the solver, a piece of software used to \"solve\" the system of equations we use to define the energy system.\n", + "Broadly, variables are defined the investment decisions, and operational decisions. \n", + "By changing the value of the decision variables, the solver can minimise the objective function.\n", "\n", - "First, let's have a think about the supply curve in this system.\n", + "Variables are also indexed over sets, for example: `Generation(Technology, Year)`. \n", + "This represents an array of generation by every technology in every year\n", "\n", - "Shown on the left of the image, there are two natural gas resources. `GasExtraction` has a [maximum production capacity (`TotalAnnualMaxCapacity`)](../edit/model/gas/data/TotalAnnualMaxCapacity.csv) of 6 PJ/year at a [variable cost (`VariableCost`)](../edit/model/gas/data/VariableCost.csv) of €8/PJ. However, `GasImport` has no upper bound, but a higher cost of €12/PJ.\n", + "_Variables are the results that the modeller gets from the model_\n", "\n", - "Plotted, this looks rather uninspiring, but at least gives an impression of a supply curve for natural gas.\n", + "## Running a model\n", "\n", - "__Run the next cell to view the graph__\n" + "Running an OSeMOSYS model using this Jupyter notebook is a two step process. \n", + "Firstly, you need to create an OSeMOSYS datafile by running the following." ] }, { @@ -99,62 +109,17 @@ "metadata": {}, "outputs": [], "source": [ - "gas_extraction = [[\"GasExtraction\", x, 8] for x in range(7)]\n", - "gas_import = [[\"GasImport\", x, 12] for x in range(6, 21)]\n", - "gas_supply_curve = pd.DataFrame(gas_extraction + gas_import, \n", - " columns=['name', 'supply', 'marginal_cost'])\n", - "fig = ex.line(gas_supply_curve, x='supply', y='marginal_cost', range_x=[0, 20], range_y=[0, 20], line_shape='vh')\n", - "fig.update_traces(mode=\"markers+lines\")\n", - "fig.update_xaxes(showspikes=True)\n", - "fig.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In OSeMOSYS, supply of energy must equal demand for energy. As the quantity of energy demanded increases, energy supply increases accordingly. Given that our demand curve is a straight line (imagine a vertical line moving along the x-axis) the equilibrium point (where the lines cross) denotes the marginal cost of production and in this case is equal to the price.\n", - "\n", - "Within most energy systems, there are multiple markets for different, related, energy commodities. Later, we will use our simple OSeMOSYS model to begin to understand the interactions between these different energy markets." + "!otoole convert csv datafile model/year/data temp.txt config.yaml" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Exercise 1: Manually computing the marginal cost of electricity\n", "\n", - "Think about how our simple supply curve for natural gas interacts with the demand for electricity. \n", + "Note you need the `!` prepended to the command (this tells the notebook to run this command using the shell rather than Python). Here we create datafile called `temp.txt` from the model data stored in `model/year`.\n", "\n", - "In our simple example, we demand a specific quantity of electricity. The electricity is produced by the `NGCC` technology which requires almost 2 units of gas to produce 1 unit of electricity.\n", - "\n", - "**Now answer the quiz question 1 on Canvas.**" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Marginal Prices\n", - "\n", - "In OSeMOSYS, there is a key equation called a \"balancing constraint\". This ensures that energy is conserved in each timeslice. \n", - "\n", - "```ampl\n", - "s.t. EBa11_EnergyBalanceEachTS5{r in REGION, l in TIMESLICE, f in FUEL, y in YEAR}: \n", - "\tProduction[r,l,f,y] >= Demand[r,l,f,y] + Use[r,l,f,y] \n", - "\t+ sum{rr in REGION} Trade[r,rr,l,f,y] * TradeRoute[r,rr,f,y];\n", - "```\n", - "\n", - "We can extract some useful information from this equation when we solve the optimisation problem to minimise costs. These data are stored in the file `results/ProductionDual.csv`." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Exercise 2: Understanding production\n", - "\n", - "We'll now run OSeMOSYS from this Jupyter Notebook to compute the equilibrium price:" + "After generating the OSeMOSYS datafile (in this case, called `temp.txt`) we then solve the model using `glpsol`, which is the open-source solver provided by the GNU Math Programming kit.\n" ] }, { @@ -163,219 +128,173 @@ "metadata": {}, "outputs": [], "source": [ - "def write_demand(model, value):\n", - " \"\"\"Write a ``value`` into the SpecifiedAnnualDemand file\n", - " \"\"\"\n", - " demand = pd.DataFrame([['SIMPLICITY', 'FEL', 2014, value]], columns=[\"REGION\",\"FUEL\",\"YEAR\",\"VALUE\"])\n", - " demand.to_csv(os.path.join(model, \"data/SpecifiedAnnualDemand.csv\"), index=False)" + "!glpsol -d temp.txt -m osemosys.txt > osemosys.log" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "In the plot below, we can see the total production by fuel over the year. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "demand = 7\n", - "write_demand(\"model/gas\", demand)\n", - "!otoole convert datapackage datafile model/gas temp.txt\n", - "# Solve the model, writing the results into ./results\n", - "!glpsol -d temp.txt -m osemosys.txt > osemosys.log\n", - "production = pd.read_csv('results/ProductionByTechnologyAnnual.csv').groupby(by=['TECHNOLOGY', 'FUEL']).sum()\n", - "ex.bar(production.reset_index(), x='FUEL', y='VALUE', color='TECHNOLOGY')" + " \n", + "The results are then available in the `results` folder - see them [here](results/). You should also check the `osemosys.log` file to ensure that the model ran correctly. View it [here](osemosys.log).\n", + "\n", + "In the stages below, we load data from the model inputs and results using a Python library called `pandas` and plot them in interactive charts.\n", + "\n", + "## Contents\n", + "\n", + "- [Stage 1](#Stage-1:-Exploring-the-model) - In this section, we introduce and explore a simple energy system model.\n", + "- [Stage 2](#Stage-2:-A-new-technology) - We add a new technology to the model.\n", + "- [Stage 3](#Stage-3:-A-tax-on-pollution) - We add an emissions penalty, imposing a tax upon CO2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "However, production differs across the year - in OSeMOSYS we use `TIMESLICES` to represent the fractions of the year in which different levels of demand exist. This approximates the average demand profile for electricity and other fuels across days and seasons. In our example model, we have defined 6 time-slices:\n", + "## Stage 1: Exploring the model\n", + "\n", + "### Setting up the timeslices, model horizon and region\n", + "\n", + "Production differs across the year - in OSeMOSYS we use `TIMESLICES` to represent the fractions of the year in which different levels of demand exist. This approximates the average demand profile for electricity and other fuels across days and seasons. In our example model, we have defined 6 time-slices:\n", "\n", "TIMESLICE | Description\n", ":--|:--\n", - "ID | Intermediate day\n", - "IN | Intermediate night\n", - "SD | Summer day\n", - "SN | Summer night\n", - "WD | Winter day\n", - "WN | Winter night" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "production = pd.read_csv('results/ProductionByTechnology.csv').groupby(by=['TIMESLICE', 'FUEL']).sum()\n", - "fig = ex.bar(production.reset_index(), x='FUEL', y='VALUE', color='TIMESLICE')\n", - "fig.update_layout(barmode='group')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Now answer quiz question 2 on Canvas**" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Exercise 3: Using OSeMOSYS to compute the marginal price\n", + "`ID` | Intermediate day\n", + "`IN` | Intermediate night\n", + "`SD` | Summer day\n", + "`SN` | Summer night\n", + "`WD` | Winter day\n", + "`WN` | Winter night\n", "\n", - "Below, we use a Python library called `pandas` to read the `ProductionDual` comma-separated values file. We extract the data point for `GAS` during the winter day (`WD`) timeslice." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "cost = pd.read_csv('results/ProductionDual.csv').set_index(['TIMESLICE', 'FUEL'])\n", - "cost['DUAL'] = cost['DUAL'] / (1.05**-0.5)\n", - "cost.loc[('WD', 'GAS'),'DUAL']" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "By running the cell, we see that the marginal cost of producing gas, is equal to the cost of importing natural gas: about €12/PJ. Interesting. That must mean that the model is importing gas, rather than extracting it (which is cheaper).\n", + "This example model only runs between 2022 and 2030. \n", + "Note that this is not long enough to demonstrate what happens when \n", + "a technology reaches the end of its lifetime, \n", + "which is particularly important when you impose an emissions constraint. \n", + "However, it does show how the model reacts to increasing demand.\n", "\n", - "NB: The division of `(1.05**-0.5)` compounds the marginal value back to the base year. In OSeMOSYS, fixed and variable operating costs are discounted to the middle of the year.\n", + "This model uses a single region `SIMPLICITY` \n", + "(which shows that this is an adaptation of an example model we use for teaching called Simplicity).\n", "\n", - "**Now answer quiz question 3 on Canvas. Hint: Consider tweaking and re-running selected parts of above code.**" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Plotted onto the supply curve we plotted earlier, we see that the results from the OSeMOSYS model make sense. The marginal cost of gas production from the OSeMOSYS model sits on the supply cost curve of the two gas resources.\n", + "### The reference energy system\n", "\n", - "However, we only have one point for electricity. In the next stages, we will use the model to compute the marginal cost of electricity production." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "fig = ex.line(gas_supply_curve, x='supply', y='marginal_cost', \n", - " range_x=[0, 20], range_y=[0, 30], line_shape='vh')\n", + "![Reference energy system for a ](img/gassolarwind.svg)\n", "\n", - "gas_demand = production.groupby(by='FUEL').sum().loc['GAS', 'VALUE']\n", - "elec_demand = production.groupby(by='FUEL').sum().loc['FEL', 'VALUE']\n", + "### Technologies and Commodities/Fuels\n", "\n", - "fig.add_shape( # add a vertical \"demand\" line\n", - " type=\"line\", line_color=\"salmon\", line_width=3, opacity=1, line_dash=\"dot\",\n", - " x0=gas_demand, x1=gas_demand, xref=\"x\", y0=0, y1=cost.loc[('WD', 'GAS'),'DUAL'])\n", + "In this simple OSeMOSYS model, there are four natural gas technologies - imports of natural gas (`GasImport`), \n", + "and extraction (`GasExtraction`, `GasExtraction2`, `GasExtraction3`). \n", + "These technologies feed a natural gas combined-cycle electricity generation plant `NGCC`. \n", + "This plant produces secondary electricity `SEC_EL` which enters a transmission and distribution technology `TD`. \n", + "The transmission and distribution technology produces final electricity `FEL`.\n", "\n", - "fig.add_annotation( # add a text callout with arrow\n", - " text=\"Marginal cost of gas\", x=gas_demand, y=cost.loc[('WD', 'GAS'),'DUAL'], \n", - " arrowhead=1, showarrow=True\n", - ")\n", + "There are also four coal extraction and import technologies, \n", + "and these are linked via a coal fired generation plant (`SCC`) and the transmission and distribution technology `TD` \n", + "in a similar manner to produce final electricity `FEL`.\n", "\n", - "fig.add_annotation( # add a text callout with arrow\n", - " text=\"Marginal cost of electricity\", x=elec_demand, \n", - " y=cost.loc[('WD', 'FEL'),'DUAL'], arrowhead=1, showarrow=True\n", - ")\n", + "This model also has a solar PV technology (`SOLPV`) and a wind power technology `WIND`.\n", "\n", - "fig.add_shape( # add a vertical \"demand\" line\n", - " type=\"line\", line_color=\"salmon\", line_width=3, opacity=1, line_dash=\"dot\",\n", - " x0=elec_demand, x1=elec_demand, xref=\"x\", y0=0, y1=cost.loc[('WD', 'FEL'),'DUAL'])\n", + "The solar PV technology (`SOLPV`) and wind turbine technology (`WIND`) have the following characteristics:\n", + "\n", + "Plant | Parameter | Value\n", + ":--|:--|--:\n", + "`SOLPV` | `CapitalCost` | 1700.0\n", + "`WIND` | `CapitalCost` | 1845.0\n", + "`SOLPV` | `VariableCost` | 2.5\n", + "`WIND`| `VariableCost` | 4.17\n", + "`SOLPV` | `OutputActivityRatio(FEL)` | 1.0\n", + "`WIND` | `OutputActivityRatio(SEC_EL)` | 1.0\n", + "`SOLPV` | `ResidualCapacity(2022)` | 2\n", + "`WIND`| `ResidualCapacity(2022)` | 3\n", "\n", - "fig.show()" + "Solar PV and Wind have the following capacity factors defined in the file `CapacityFactor.csv`:\n", + "\n", + "Timeslice | SOLPV | WIND\n", + ":--|:--|--:\n", + "`ID` | 0.3 | 0.25\n", + "`IN` | 0.0 | 0.25\n", + "`SD` | 0.4 | 0.25\n", + "`SN` | 0.0 | 0.25\n", + "`WD` | 0.25 | 0.25\n", + "`WN` | 0.0 | 0.25\n", + "\n", + "As you can see from the figure above, this model contains multiple gas resources, \n", + "plus imports, multiple coal resources, plus an import, and a new super-critical coal fired power station.\n", + "\n", + "The prices and quantities of the resources are as follows:\n", + "\n", + "Resource | Quantity available | Cost of extraction/import\n", + ":--|--:|--:\n", + "`GasExtraction` | 10 | 8.0\n", + "`GasExtraction2` | 15 | 10.0\n", + "`GasExtraction3` | 30 | 11.0\n", + "`GasImport` | - | 12.0\n", + "`CoalExtraction` | 10 | 4.0\n", + "`CoalExtraction2` | 15 | 5.0\n", + "`CoalExtraction3` | 30 | 5.5\n", + "`CoalImport` | - | 12.0\n", + "\n", + "These prices and quantities are defined in [`VariableCost.csv`](../edit/model/year/data/VariableCost.csv) and [`TotalTechnologyModelPeriodActivityUpperLimit.csv`](../edit/model/year/data/TotalTechnologyModelPeriodActivityUpperLimit.csv) respectively.\n", + "\n", + "The gas-fired (`NGCC`) and coal-fired (`SCC`) power plants have the following characteristics:\n", + "\n", + "Plant | Parameter | Value\n", + ":--|:--|--:\n", + "`NGCC` | `CapitalCost` | 1100.0\n", + "`SCC` | `CapitalCost` | 1600.0\n", + "`NGCC` | `FixedCost` | 44.0\n", + "`SCC` | `FixedCost` | 56.0\n", + "`NGCC` | `InputActivityRatio(GAS)` | 1.992\n", + "`SCC` | `InputActivityRatio(COA)` | 2.120\n", + "`NGCC` | `OutputActivityRatio(SEC_EL)` | 1.0\n", + "`SCC` | `OutputActivityRatio(SEC_EL)` | 1.0\n", + "`NGCC` | `ResidualCapacity` | 30\n", + "`SCC`| `ResidualCapacity` | 30\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Notes\n", + "### Model representation of energy market\n", + "\n", + "In OSeMOSYS, supply of energy must equal demand for energy. As the quantity of energy demanded increases, energy supply increases accordingly.\n", + "Demand is completely inelastic - the demand curve is a straight line (imagine a vertical line moving along the x-axis) \n", + "so the equilibrium point (where the supply and demand curves cross) denotes the marginal cost of production and in this case is equal to the price.\n", "\n", - "- The x-axis shows the supply quantity of energy\n", - "- The marginal cost of electricity is plotted for reference against the quantity of electricity supplied, and marginal cost of gas against the quantity of gas supplied" + "Within most energy systems, there are multiple markets for different, related, energy commodities. \n", + "We can use an OSeMOSYS model to understand the interactions between these different energy markets.\n", + "\n", + "In OSeMOSYS, there is a key equation called a \"balancing constraint\". This ensures that energy is conserved in each timeslice. \n", + "\n", + "```ampl\n", + "s.t. EBa11_EnergyBalanceEachTS5{r in REGION, l in TIMESLICE, f in FUEL, y in YEAR}: \n", + "\tProduction[r,l,f,y] >= Demand[r,l,f,y] + Use[r,l,f,y] \n", + "\t+ sum{rr in REGION} Trade[r,rr,l,f,y] * TradeRoute[r,rr,f,y];\n", + "```\n", + "\n", + "We can extract some useful information from this equation when we solve the optimisation problem to minimise costs.\n", + "These data are stored in the result files [`ProductionDual.csv`](results/ProductionDual.csv) and [`ProductionDualAnnual.csv`](results/ProductionDualAnnual.csv)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Running the model to extract the marginal cost of production of electricity\n", + "## 1. Edit the Data\n", "\n", - "We can automate the running of the OSeMOSYS model over multiple levels of demand for electricity. The following code loops over demands ranging from 1 to *n* in increments of *x*. We write the demand into the CSV file `SpecifiedAnnualDemand.csv`, create the modelfile and then solve the model. We then extract the results from the `results/ProductionDual.csv` file and store them in a list. Finally, the function returns the list of results for further processing." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def extract_result(param: str, production, cost):\n", - " \"\"\"Read the marginal cost values from the results\n", - " \"\"\"\n", - " marginal = pd.read_csv('results/ProductionDual.csv'\n", - " ).set_index(['TIMESLICE', 'FUEL'])\n", - " marg_ann = pd.read_csv('results/ProductionDualAnnual.csv'\n", - " ).set_index(['FUEL'])\n", - " production = pd.read_csv('results/ProductionByTechnology.csv').groupby(by=['TIMESLICE', 'FUEL']).sum()\n", - " try:\n", - " demand = production.groupby(by='FUEL').sum().loc[param, 'VALUE']\n", - " except KeyError:\n", - " demand = 0\n", - " try:\n", - " value = marginal.loc[('WD', param), 'DUAL'] / (1.05**-0.5)\n", - " except KeyError:\n", - " try:\n", - " value = marg_ann.loc[param, 'DUAL'] / (1.05**-0.5)\n", - " except KeyError:\n", - " value = 0\n", - " observation = {'value': value, 'param': param, 'quantity': demand} \n", - " return observation\n", - "\n", - "def run_model(path_to_model: str, start: int, stop: int, step=int):\n", - " \"\"\"Run the model for a range of demand values\n", - " \n", - " Returns\n", - " -------\n", - " List\n", - " A list of dual values for GAS, FEL and COA\n", - " \"\"\"\n", - " results = []\n", - " for dem in trange(start, stop, step):\n", - " observation = {}\n", - " write_demand(path_to_model, dem)\n", - " !otoole convert datapackage datafile $path_to_model temp.txt\n", - " !glpsol -d temp.txt -m osemosys.txt > osemosys.log\n", - " results.append(extract_result('FEL', production, cost))\n", - " results.append(extract_result('GAS', production, cost))\n", - " results.append(extract_result('COA', production, cost))\n", + "Try editing some of the model parameters and see what happens to the results of the model.\n", "\n", - " return results\n" + "Start with:\n", + "1. Increasing or decreasing demand [`SpecifiedAnnualDemand`](model/year/data/SpecifiedAnnualDemand.csv)\n", + "2. Play with changing the [`CapitalCost`](model/year/data/CapitalCost.csv) of the generation technologies\n", + "3. Try making the fuels more expensive by changing [`VariableCost.csv`](../edit/model/year/data/VariableCost.csv)" ] }, { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], + "cell_type": "markdown", + "metadata": {}, "source": [ - "gas_results = run_model(\"model/gas\", 1, 20, 1)" + "## 2. Run The Model" ] }, { @@ -384,86 +303,45 @@ "metadata": {}, "outputs": [], "source": [ - "gas_data = pd.DataFrame(gas_results)\n", - "ex.line(gas_data, x='quantity', y='value', facet_col='param', range_x=[0, 20], range_y=[0, 40], \n", - " line_shape='vh')" + "!otoole convert csv datafile model/year/data temp.txt config.yaml\n", + "!glpsol -d temp.txt -m osemosys.txt > osemosys.log" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "In the plot above, we now see the supply cost curves derived from the OSeMOSYS model. Given the simple supply cost curve for gas, with two stages and a breakpoint between 4.19 and 6.29 (we know that the cost of gas production changes at 6), you can see that the marginal cost of producing 2 units of electricity is 16.77 and the marginal cost of producing 3 or more units is 25.16." + "## 3. View the plots\n", + "\n", + "The results are then available in the `results` folder - see them [here](results/). You should also check the `osemosys.log` file to ensure that the model ran correctly. View it [here](osemosys.log)." ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "## Summary of Stage 1\n", - "\n", - "In this first part of the lab we learned the following:\n", - "\n", - "- A simple supply curve can be constructed in OSeMOSYS using one technology per resource step. Parameters `TotalAnnualMaxCapacity` and `VariableCost` may be used to define each step in the supply cost curve for a resource.\n", - "- The OSeMOSYS code can be extended to write out marginal costs for all commodities in the model. This is particularly useful to show the marginal cost of derived commodities, such as electricity, which are produced from a variety of different energy chains.\n", - "- By running our model at different levels of demand, we can use the model to compute the derived supply cost curve for each commodity.\n", - "- We can use OSeMOSYS to explore the relationship between the structure of the energy system, supply cost curves for primary resources, and supply cost curves for secondary and tertiary energy commodities." + "outputs = pd.read_csv('model/year/data/OutputActivityRatio.csv')\n", + "techs_sec_el = outputs[outputs['FUEL'].isin(['SEC_EL'])]['TECHNOLOGY'].unique().tolist()\n", + "primary_extraction = outputs[outputs['FUEL'].isin(['COA', 'GAS'])]['TECHNOLOGY'].unique().tolist()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Stage 2: Adding Resources\n", - "\n", - "In this part of the lab, we add a number of resources to our model and explore how this affects the supply curve for electricity. We now move towards a slightly more realistic energy system than the simplified model we used to introduce the concepts in Stage 1.\n", - "\n", - "![](img/gasmore.svg)\n", - "\n", - "(Note, this image is produced using the command `!otoole viz res model/gas_more/datapackage.json img/simple.svg`)\n", - "\n", - "As you can see from the figure above, this model contains multiple gas resources, plus imports, multiple coal resources, plus an import, and a new super-critical coal fired power station.\n", - "\n", - "The prices and quantities of the resources are as follows:\n", - "\n", - "Resource | Quantity available | Cost of extraction/import\n", - ":--|--:|--:\n", - "GasExtraction | 10 | 8.0\n", - "GasExtraction2 | 15 | 10.0\n", - "GasExtraction3 | 30 | 11.0\n", - "GasImport | - | 12.0\n", - "CoalExtraction | 10 | 4.0\n", - "CoalExtraction2 | 15 | 5.0\n", - "CoalExtraction3 | 30 | 5.5\n", - "CoalImport | - | 12.0\n", - "\n", - "These prices and quantities are defined in [`VariableCost.csv`](../edit/model/gas_more/data/VariableCost.csv) and [`TotalTechnologyModelPeriodActivityUpperLimit.csv`](../edit/model/gas_more/data/TotalTechnologyModelPeriodActivityUpperLimit.csv) respectively.\n", - "\n", - "The gas-fired (`NGCC`) and coal-fired (`SCC`) power plants have the following characteristics:\n", - "\n", - "Plant | Parameter | Value\n", - ":--|:--|--:\n", - "NGCC | CapitalCost | 1100.0\n", - "SCC | CapitalCost | 1600.0\n", - "NGCC | FixedCost | 44.0\n", - "SCC | FixedCost | 56.0\n", - "NGCC | InputActivityRatio(GAS) | 1.992\n", - "SCC | InputActivityRatio(COA) | 2.120\n", - "NGCC | OutputActivityRatio(SEC_EL) | 1.0\n", - "SCC | OutputActivityRatio(SEC_EL) | 1.0\n", - "NGCC | ResidualCapacity | 30\n", - "SCC| ResidualCapacity |30" + "### Costs" ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "tags": [] - }, + "metadata": {}, "outputs": [], "source": [ - "more_results = run_model(\"model/gas_more\", 1, 60, 2)" + "total_cost = pd.read_csv('results/TotalDiscountedCost.csv')\n", + "total_cost.plot(x='YEAR', y='VALUE', title='Total Discounted Costs (M$)', kind='bar')" ] }, { @@ -472,16 +350,7 @@ "metadata": {}, "outputs": [], "source": [ - "more_data = pd.DataFrame(more_results)\n", - "ex.line(more_data, x='quantity', y='value', facet_col='param', \n", - " range_x=[0, 130], range_y=[0, 45], line_shape='hv')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We now have a much more complicated supply cost curve for all commodities. Four distinct steps are evident in the supply cost curve for gas (`GAS`), three/four for coal (`COA`), and seven in the the plot for final electricity (`FEL`). Each of these steps in the gas and coal plots correspond to one of the resources that we defined in this more complicated model. However, the supply cost curve for final electricity (`FEL`) is a function of the combination of the gas and coal cost curves, and depends on the blend of coal and gas used to supply electricity at each level of demand." + "print(f\"Objective Function: $M {total_cost['VALUE'].sum():.2f}\")" ] }, { @@ -490,80 +359,56 @@ "metadata": {}, "outputs": [], "source": [ - "production = pd.read_csv('results/ProductionByTechnologyAnnual.csv').groupby(by=['TECHNOLOGY', 'FUEL']).sum()\n", - "ex.bar(production.reset_index(), x='FUEL', y='VALUE', color='TECHNOLOGY')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Exercise 4\n", - "\n", - "We can confirm the outputs from the model run with maximum demand by viewing the production of the different technologies. In the plot above you can view the quantity of coal and gas extracted or imported, and the quantity of secondary electricity produced by the power plants and final electricity transmitted to meet the demands.\n", - "\n", - "**Now, answer quiz question 4 on Canvas**" + "capital_investment = pd.read_csv('results/CapitalInvestment.csv')\n", + "capital_investment.plot(x='YEAR', y='VALUE', color='TECHNOLOGY', \n", + " kind='bar', title='Capital Investment by Technology (M$)')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Exercise 5\n", - "\n", - "To answer this question, you will need to edit the OSeMOSYS parameter file which defines the price of resources. These is [`VariableCost.csv`](../edit/model/gas_more/data/VariableCost.csv). Don't forget saving the changes after editing.\n", - "\n", - "**Now, answer quiz question 5 on Canvas**" + "### Demand" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "### Exercise 6\n", - "\n", - "To answer this question, you will need to edit the OSeMOSYS parameter files which defines the efficiency of a technology. These are [`InputActivityRatio.csv`](../edit/model/gas_more/data/InputActivityRatio.csv) and [`OutputActivityRatio.csv`](../edit/model/gas_more/data/OutputActivityRatio.csv)\n", - "\n", - "**Now, answer quiz question 6 on Canvas**" + "demand = pd.read_csv('results/Demand.csv').groupby(['REGION', 'FUEL', 'YEAR']).sum(numeric_only=True).reset_index()\n", + "demand.plot(x='YEAR', y='VALUE', color='FUEL', title='Demand (PJ)')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Summary of Stage 2\n", - "\n", - "In stage 2 we learnt the following:\n", - "\n", - "- Our computation of marginal costs using OSeMOSYS holds for a more complicated energy system structure\n", - "- The short-run marginal cost of electricity is a function of the cost of resources and efficiency of the conversion plants" + "### Total Capacity" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "# Stage 3: A tax on pollution\n", - "\n", - "In this stage, we'll now implement a financial penalty for emission of CO2. \n", + "total_capacity = pd.read_csv('results/TotalCapacityAnnual.csv')\n", + "total_generation_capacity = total_capacity[total_capacity['TECHNOLOGY'].isin(techs_sec_el)]\n", + "total_extraction_capacity = total_capacity[total_capacity['TECHNOLOGY'].isin(primary_extraction)]\n", "\n", - "### Exercise 7\n", - "\n", - "Before proceeding, please **answer quiz question 7 on Canvas**." + "total_generation_capacity.plot(x='YEAR', y='VALUE', color='TECHNOLOGY', kind='bar', \n", + " title='Total Annual Generation Capacity (GW)')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Adding an emissions penalty to OSeMOSYS\n", - "To impose a financial penalty in OSeMOSYS we can edit the [EmissionsPenalty](../edit/model/gas_more/data/EmissionsPenalty.csv) parameter file. Units are \\$/tCO2.\n", + "### New Capacity\n", "\n", - "You'll need to add a new line to the parameter file like so:\n", - "\n", - " SIMPLICITY,CO2,2014,50\n", - " \n", - "First try a value of $50/kCO2. You may use the plot below to compare your results with a zero emission price case (from the previous exercise)." + "This plot shows new investments in generation technologies" ] }, { @@ -572,7 +417,9 @@ "metadata": {}, "outputs": [], "source": [ - "emission_results = run_model(\"model/gas_more\", 1, 60, 2)" + "capacity = pd.read_csv('results/NewCapacity.csv')\n", + "new_generation_capacity = capacity[capacity['TECHNOLOGY'].isin(techs_sec_el)]\n", + "new_extraction_capacity = capacity[capacity['TECHNOLOGY'].isin(primary_extraction)]" ] }, { @@ -581,82 +428,50 @@ "metadata": {}, "outputs": [], "source": [ - "emissions_data = pd.DataFrame(emission_results)\n", - "emit_more_data = more_data.rename(columns={'value': 'noCO2price'})\n", - "all_data = emissions_data.set_index(['param', 'quantity']).merge(\n", - " emit_more_data.set_index(['param', 'quantity']), left_index=True, right_index=True)\n", - "ex.line(all_data.reset_index(), x='quantity', y=['value','noCO2price'], facet_col='param', \n", - " range_x=[0, 65], range_y=[0, 80], line_shape='vh')" + "new_generation_capacity.plot(x='YEAR', y='VALUE', color='TECHNOLOGY', kind='bar', title='New Capacity (GW)')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Exercise 8\n", - "Now please go to **quiz question 8 in Canvas**." + "### Accumulated New Capacity in Extraction and Import Technologies" ] }, { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Exercise 9\n", - "Please answer **quiz question 9 on Canvas**." - ] - }, - { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "### Summary of Stage 3\n", - "In this stage, we have explored the effects of a carbon tax on the supply curves of gas, coal and final electricity. In the exercises on Canvas we have thought about the differences between carbon taxes and emission trading systems. Furthermore, we have analysed how a carbon price affects different fuels and how we can see these effects in the marginal cost curve of electricity." + "new_extraction_capacity.plot(x='YEAR', y='VALUE', color='TECHNOLOGY', kind='bar', title='Accumulated New Capacity in Extraction and Import Technologies (PJ)')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Stage 4: Renewable electricity\n", - "In this stage we are adding a solar PV technology and a wind power technology to our model. We are now working with the model in `model/gas_solar_wind`. In the following we want to explore and think about the effects that the addition of solar PV and Wind have on the modelled system.\n", - "![](img/gassolarwind.svg)" + "### Production of electricity" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "The solar PV technology (`SOLPV`) and wind turbine technology (`WIND`) have the following characteristics:\n", - "\n", - "Plant | Parameter | Value\n", - ":--|:--|--:\n", - "SOLPV | CapitalCost | 1700.0\n", - "WIND | CapitalCost | 1845.0\n", - "SOLPV | VariableCost | 2.5\n", - "WIND| VariableCost |4.17\n", - "SOLPV | OutputActivityRatio(FEL) | 1.0\n", - "WIND | OutputActivityRatio(SEC_EL) | 1.0\n", - "SOLPV | ResidualCapacity | 2\n", - "WIND| ResidualCapacity |3\n", + "production = pd.read_csv('results/ProductionByTechnologyAnnual.csv')\n", + "generation_production = production[production['TECHNOLOGY'].isin(techs_sec_el)].groupby(by=['TECHNOLOGY', 'FUEL', 'YEAR']).sum(numeric_only=True).reset_index()\n", + "extraction_production = production[production['TECHNOLOGY'].isin(primary_extraction)].groupby(by=['TECHNOLOGY', 'FUEL', 'YEAR']).sum(numeric_only=True)\n", "\n", - "Solar PV and Wind have the following availability defined in the file `CapacityFactor.csv`:\n", - "\n", - "Timeslice | SOLPV | WIND\n", - ":--|:--|--:\n", - "ID | 0.3 | 0.25\n", - "IN | 0.0 | 0.25\n", - "SD | 0.4 | 0.25\n", - "SN | 0.0 | 0.25\n", - "WD | 0.25 | 0.25\n", - "WN | 0.0 | 0.25" + "generation_production.plot(x='YEAR', y='VALUE', color='TECHNOLOGY', kind='bar', title='Electricity Production (PJ)')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Exercise 10\n", - "**Before** running our new model, please think about the effect that solar PV and wind power will have on the marginal cost curves that we looked at in the previous stages. And please **answer quiz question 10 on Canvas**." + "### Extraction Production" ] }, { @@ -665,17 +480,14 @@ "metadata": {}, "outputs": [], "source": [ - "results = run_model(\"model/gas_solar_wind\", 1, 60, 2)" + "extraction_production.reset_index().plot(x='YEAR', y='VALUE', color='TECHNOLOGY', kind='bar', title='Extraction (PJ)')" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "renewable_data = pd.DataFrame(results)\n", - "ex.line(renewable_data, x='quantity', y='value', facet_col='param', range_x=[0,65], range_y=[0,45], line_shape='vh')" + "### Emissions" ] }, { @@ -684,37 +496,28 @@ "metadata": {}, "outputs": [], "source": [ - "production = pd.read_csv('results/ProductionByTechnologyAnnual.csv').groupby(by=['TECHNOLOGY', 'FUEL']).sum()\n", - "ex.bar(production.reset_index(), x='FUEL', y='VALUE', color='TECHNOLOGY')" + "emissions = pd.read_csv('results/AnnualEmissions.csv')\n", + "emissions.plot(x='YEAR', y='VALUE', title='CO2 emissions (MtCO2e)', kind='bar')" ] }, { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Exercise 11\n", - "Now please go to **quiz question 11 on Canvas**." - ] - }, - { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "### Summary of Stage 4\n", - "In this stage we explored the effects that renewable power generation technologies have on the marginal cost curve of electricity. We discovered that the introduction of renewable power sources:\n", - "- does not necessarily push the technologies with the highest emissions out of the market\n", - "- from a operational point of view a market does not necessarily create a well functionoing technology mix, e.g. the combination of coal power with renewable technologies creates operational challegnes due to the inertia of coal fired power plants" + "tech_emissions = pd.read_csv('results/AnnualTechnologyEmission.csv')\n", + "tech_emissions.plot(x='YEAR', y='VALUE', color='TECHNOLOGY', kind='bar', title='CO2 Emissions by Technology (MtCO2e)')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Extension Tasks\n", + "# Stage 2: A new technology\n", "\n", - "If you finish during the lab, you might want to complete one of the following optional tasks:\n", + "To add a new technology into the OSeMOSYS model stored you will need to edit the following files: \n", "\n", - "- Return to stage 2 and add a wind or solar technology into the OSeMOSYS model stored in `model/gas_more/data`. You'll need to edit the following files: \n", " - `InputActivityRatio.csv`, \n", " - `OutputActivityRatio.csv`,\n", " - `CapacityFactor.csv`\n", @@ -722,38 +525,47 @@ " - `ResidualCapacity.csv`, \n", " - `TECHNOLOGY.csv`, \n", " - `FixedCost.csv` and \n", - " - `VariableCost.csv`.\n", - " \n", - " Once you've implemented the renewable technology, observe what happens to the supply cost curve.\n", - " \n", - " \n", - "- Return to stage 4 and add an `EmissionsPenalty` to the model, observe what happens to the supply cost curve depending on the hight of penalty you implement." + " - `VariableCost.csv`." ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], - "source": [] + "source": [ + "# Stage 3: A tax on pollution\n", + "\n", + "In this stage, we'll now implement a financial penalty for emission of CO2. \n" + ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Summary of lab 3\n", - "In this lab we have explored the following:\n", - "- the behaviour of marignal cost curves of different fuels in an OSeMOSYS model in different system settings\n", - "- the functioning of electricity markets and their sensitivity to price changes\n", - "- the possibility to extract marginal costs from OSeMOSYS models\n", - "- the use of a range of parameters to define a power system in OSeMOSYS" + "### Adding an emissions penalty to OSeMOSYS\n", + "\n", + "To impose a financial penalty in OSeMOSYS we can edit the [EmissionsPenalty](../edit/model/year/data/EmissionsPenalty.csv) parameter file. Units are \\$/tCO2.\n", + "\n", + "You'll need to add a new line to the parameter file like so for each year from 2022 to 2030:\n", + "\n", + " SIMPLICITY,CO2,2022,50\n", + " SIMPLICITY,CO2,2023,50 \n", + " etc. \n", + " \n", + "First try a value of $50/tCO2.\n", + "\n", + "### Use an emissions cap instead\n", + "\n", + "To enter an emissions cap in OSeMOSYS, edit the [ModelPeriodEmissionLimit.csv](../edit/model/year/data/ModelPeriodEmissionLimit.csv) parameter file.\n", + "For example, to specify that the only 20 MtCO2e are emitted between 2022 and 2030, enter:\n", + "\n", + " SIMPLICITY,CO2,20" ] } ], "metadata": { "celltoolbar": "Raw Cell Format", "kernelspec": { - "display_name": "Python 3.9.10 64-bit", + "display_name": "Python 3.10.6 ('life_academy')", "language": "python", "name": "python3" }, @@ -767,11 +579,11 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.10" + "version": "3.10.6" }, "vscode": { "interpreter": { - "hash": "aee8b7b246df8f9039afb4144a1f6fd8d2ca17a180786b69acc140d282b71a49" + "hash": "26f1cd28636088d2f453074d5b1e396f09b35d00a6d9016caa66ed78947024e8" } } }, diff --git a/README.rst b/README.rst index f7f605b..717ce7a 100644 --- a/README.rst +++ b/README.rst @@ -46,6 +46,14 @@ For example:: License ~~~~~~~ +This material draws upon, adapts and extends Lab 2 from MJ2380, authored by Roberto Heredia Fonseca, +Shravan Kumar and Francesco Gardumi and the OpTIMUS.community +and is licensed under the Creative Commons Attribution 4.0 International License. +To view a copy of this license, visit http://creativecommons.org/licenses/by/4.0/. + +It also builds on material from MJ2383 authored by Will Usher, Camillo Ramirez and Agnese Beltramo +licensed under the Creative Commons Attribution 4.0 International License. + .. raw:: html