diff --git a/.gitignore b/.gitignore index 7c0c99014a..7391dda00e 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ docs/src/concept_reference/relationship_classes.md docs/src/concept_reference/Object Classes.md docs/src/concept_reference/Parameter Value Lists.md docs/src/concept_reference/Relationship Classes.md +test/Manifest.toml +Manifest.toml diff --git a/Project.toml b/Project.toml index 6c1ad82be3..47cc489a1c 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "SpineOpt" uuid = "0d8fc150-4032-4b6e-9540-20efcb304861" authors = ["Spine Project consortium "] -version = "0.6.15" +version = "0.6.16" [deps] Arrow = "69666777-d1a9-59fb-9406-91d4454c9d45" @@ -17,6 +17,5 @@ Requires = "ae029012-a4dd-5104-9daa-d747884805df" SpineInterface = "0cda1612-498a-11e9-3c92-77fa82595a4f" [compat] -SpineInterface = "0.11.6" +SpineInterface = "0.11.9" julia = "^1.2" -JuMP = "< 1.14" diff --git a/docs/make.jl b/docs/make.jl index 66ae7a3436..8f1f7baaf2 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -73,7 +73,8 @@ SpineOpt.populate_empty_chapters!(pages, joinpath(path, "src")) # Create and deploy the documentation makedocs( sitename="SpineOpt.jl", - format=Documenter.HTML(prettyurls=get(ENV, "CI", nothing) == "true"), # uncomment to deploy locally + format=Documenter.HTML(prettyurls=get(ENV, "CI", nothing) == "true", size_threshold=409600), # uncomment to deploy locally pages=pages, + warnonly=true ) deploydocs(repo="github.com/spine-tools/SpineOpt.jl.git", versions=["stable" => "v^", "v#.#"]) diff --git a/docs/src/tutorial/case_study_a5.md b/docs/src/tutorial/case_study_a5.md index 3317ca6b39..7f00e852a4 100644 --- a/docs/src/tutorial/case_study_a5.md +++ b/docs/src/tutorial/case_study_a5.md @@ -68,13 +68,13 @@ power station in the river is modelled using the following elements: Below is a schematic of the model. For clarity, only the Rebnis station is presented in full detail: -![image](../figs_a5/case_study_a5_schematic.png) +![image](figs_a5/case_study_a5_schematic.png) In order to run this tutorial you must first execute some preliminary -steps from the [Simple System](../../simple_system/) tutorial. +steps from the [Simple System](simple_system.md) tutorial. Specifically, execute all steps from the -[guide](../../simple_system/index.html#guide), up to and including the step of -[importing the spineopt database template](../../simple_system/index.html#importing-the-spineopt-database-template). +[guide](simple_system.md#guide), up to and including the step of +[importing the spineopt database template](simple_system.md#importing-the-spineopt-database-template). It is advisable to go through the whole tutorial in order to familiarise yourself with Spine. @@ -85,10 +85,10 @@ yourself with Spine. ### Importing the SpineOpt database template -- Download [the SpineOpt database template](../../../templates/spineopt_template.json) (right click on the link, then select *Save link as...*) +- Download [the SpineOpt database template](https://raw.githubusercontent.com/spine-tools/SpineOpt.jl/master/templates/spineopt_template.json) (right click on the link, then select *Save link as...*) - Select the *input* Data Store item in the *Design View*. - Go to *Data Store Properties* and hit **Open editor**. This will open the newly created database in the *Spine DB Editor*, looking similar to image below. - ![image](../figs_a5/case_study_a5_spine_db_editor_empty.png) + ![image](figs_a5/case_study_a5_spine_db_editor_empty.png) !!! note The Spine DB editor is a dedicated interface within Spine Toolbox for visualizing and managing Spine databases. - Press **Alt + F** to display the editor menu, select **File -> Import...**, @@ -116,7 +116,7 @@ yourself with Spine. There are two options in this tutorial to enter data in the Database. The first one is to enter data manually and the second to - [use the importer](../index.html#Using-the-Importer) functionality. These are described in the + [use the importer](../index.md#Using-the-Importer) functionality. These are described in the next two subsections respectively and produce similar models. The model created when using the importer creates a model with two-segments efficiency curves for converting water to electricity (while the model @@ -147,7 +147,7 @@ yourself with Spine. Kvistforsen_pwr_plant - Go to *Object tree* (on the top left of the window, usually), right-click on `unit` and select **Add objects** from the context menu. This will open the *Add objects* dialog. - Select the first cell under the **object name** column and press **Ctrl+V**. This will paste the list of plant names from the clipboard into that column; the **object class name** column will be filled automatically with ‘unit‘. The form should now be looking similar to this: - ![image](../figs_a5/add_power_plant_units.png) + ![image](figs_a5/add_power_plant_units.png) - Click **Ok**. - Back in the *Spine DB Editor*, under *Object tree*, double click on `unit` to confirm that the objects are effectively there. - Commit changes with the message ‘Add power plants’. @@ -240,16 +240,16 @@ yourself with Spine. - To specify the general behaviour of our model, stay in the *Spine DB Editor* and enter model parameter values as follows: - Select the model parameter value data (i.e. select all, **Ctrl+A**) from the file below and copy it to the clipboard (**Ctrl+C**): - [cs-a5-model-parameter-values.txt](../../data/cs-a5-model-parameter-values.txt) + [cs-a5-model-parameter-values.txt](data/cs-a5-model-parameter-values.txt) - Select `instance` in the *Object tree* and inspect the table in *Object parameter value* (on the top-center of the window, usually). Make sure that the columns in the table are ordered as follows (drag and drop columns if you need to change their order): object_class_name | object_name | parameter_name | alternative_name | value | database - Select the first cell under `object_class_name` and press **Ctrl+V**. This will paste the model parameter value data from the clipboard into the table. The form should be looking like this: - ![image](../figs_a5/case_study_a5_model_parameters.png) + ![image](figs_a5/case_study_a5_model_parameters.png) - Specify the resolution of our temporal block `some_week` in the same way using the data below: - [cs-a5-temporal_block-parameter-values.txt](../../data/cs-a5-temporal_block-parameter-values.txt) + [cs-a5-temporal_block-parameter-values.txt](data/cs-a5-temporal_block-parameter-values.txt) - Specify the behaviour of all system nodes with the data below, where: - `demand` represents the local inflow (negative in most cases). @@ -261,7 +261,7 @@ yourself with Spine. that there aren't any loses). - `node_state_cap` is the maximum level of the reservoirs. To do this in one single step, simply select `node` in the *Object tree* and paste the following values in the first empty cell: - [cs-a5-node-parameter-values.txt](../../data/cs-a5-node-parameter-values.txt) + [cs-a5-node-parameter-values.txt](data/cs-a5-node-parameter-values.txt) #### Establishing relationships @@ -275,7 +275,7 @@ yourself with Spine. common electricity node. Both the power plants and the electricity load belong to the class `unit`. - Select the list of unit and node names from below and copy it to the clipboard (**Ctrl+C**). - [cs-a5-unit__from_node.txt](../../data/cs-a5-unit__from_node.txt) + [cs-a5-unit__from_node.txt](data/cs-a5-unit__from_node.txt) - In the *Spine DB Editor*, go to *Relationship tree* (on the bottom left of the window, usually), right-click on `unit__from_node` and select **Add relationships** from the @@ -284,7 +284,7 @@ yourself with Spine. **Ctrl+V**. This will paste the list of plant and node names from the clipboard into the table. The form should be looking like this: - ![image](../figs_a5/add_pwr_plant_water_from_node.png) + ![image](figs_a5/add_pwr_plant_water_from_node.png) - Click **Ok**. - Back in the *Spine DB Editor*, under *Relationship tree*, double click on `unit__from_node` to confirm that the relationships are @@ -297,7 +297,7 @@ yourself with Spine. a power plant releases water to the station's lower water node, and that the power plants supply electricity to the common electricity node. Use the following data and do as before: - [cs-a5-unit__to_node.txt](../../data/cs-a5-unit__to_node.txt) + [cs-a5-unit__to_node.txt](data/cs-a5-unit__to_node.txt) !!! note At this point, you might be wondering what's the purpose of the @@ -315,13 +315,13 @@ yourself with Spine. discharged, it is taken from the *lower* water node of the station, if spilled it is taken from the *upper* water node of the station. Use the following data and do as before: - [cs-a5-connection__from_node.txt](../../data/cs-a5-connection__from_node.txt) + [cs-a5-connection__from_node.txt](data/cs-a5-connection__from_node.txt) - Create relationships of the class `connection__to_node` to represent that both discharge and spill are released into the *upper* node of the next downstream station. Use the following data and do as before: - [cs-a5-connection__to_node.txt](../../data/cs-a5-connection__to_node.txt) + [cs-a5-connection__to_node.txt](data/cs-a5-connection__to_node.txt) !!! note At this point, you might be wondering what's the purpose of the @@ -338,7 +338,7 @@ yourself with Spine. water, for electricity nodes with respect to electricity. This way, you link all nodes to either the commocity `water` or the commodity `electricity`. Use the following data and do as before: - [cs-a5-node__commodity.txt](../../data/cs-a5-node__commodity.txt) + [cs-a5-node__commodity.txt](data/cs-a5-node__commodity.txt) - Define that all nodes in our model have to be balanced at each time step. To do this, you create a relationship of the class @@ -363,20 +363,20 @@ yourself with Spine. - Finally, the values of all parameters have to be entered. Specify the capacity of all hydropower plants, and their respective variable operating cost by entering `unit__from_node` parameter values as follows: - - Select the data from the text-box below and copy it to the clipboard (**Ctrl+C**): [cs-a5-unit__from_node-relationship-parameter-values.txt](../../data/cs-a5-unit__from_node-relationship-parameter-values.txt) + - Select the data from the text-box below and copy it to the clipboard (**Ctrl+C**): [cs-a5-unit__from_node-relationship-parameter-values.txt](data/cs-a5-unit__from_node-relationship-parameter-values.txt) - In the *Spine DB Editor*, go to *Relationship tree* (on the bottom left of the window, usually), and click on `unit__from_node`. Then, go to *Relationship parameter value* (on the bottom-center of the window, usually). Make sure that the columns in the table are ordered as follows (drag and drop columns if you need to change their order): relationship_class_name | object_name_list | parameter_name | alternative_name | value | database - Select the first cell under `relationship_class_name` and press **Ctrl+V**. This will paste the parameter value data from the clipboard into the table. - Specify the conversion ratio between discharged water and generated electricity for each hydropower station as well as a similar conversion rate (set to 1) to represent that water cannot be lost between upper and lower water nodes. Use the following data to enter the parameter values `unit__from_node`: - [cs-a5-unit\_\_node\_\_node-relationship-parameter-values.txt](../../data/cs-a5-unit__node__node-relationship-parameter-values.txt) + [cs-a5-unit\_\_node\_\_node-relationship-parameter-values.txt](data/cs-a5-unit__node__node-relationship-parameter-values.txt) - Specify the average discharge and spillage in the first hours of the simulation. Use the following data to enter the parameter values `connection__from_node`: - [cs-a5-connection__from_node-relationship-parameter-values.txt](../../data/cs-a5-connection__from_node-relationship-parameter-values.txt) + [cs-a5-connection__from_node-relationship-parameter-values.txt](data/cs-a5-connection__from_node-relationship-parameter-values.txt) - Finally, specify the delay (the time it takes for the water to run between water nodes) and the transfer ratio (being equal to 1) of different water connections. Use the following data to enter the parameter values `connection__node_node`: - [cs-a5-connection\_\_node\_\_node-relationship-parameter-values.txt](../../data/cs-a5-connection__node__node-relationship-parameter-values.txt) + [cs-a5-connection\_\_node\_\_node-relationship-parameter-values.txt](data/cs-a5-connection__node__node-relationship-parameter-values.txt) - When you're ready, commit all changes to the database via the main menu (**Alt + F**). @@ -385,7 +385,7 @@ yourself with Spine. #### Additional Steps for Project Setup -- Drag the Data Connection icon ![image](../figs_a5/file-alt.png) from the tool bar and drop it into the +- Drag the Data Connection icon ![image](figs_a5/file-alt.png) from the tool bar and drop it into the Design View. This will open the *Add Data connection dialogue*. Type in ‘Data Connection’ and click on **Ok**. - To import the model into the Spine database, you need to create an @@ -395,7 +395,7 @@ yourself with Spine. will pop-up. - Type ‘Import Model’ as the name of the specification. Save the specification by using **Ctrl+S** and close the window. -- Drag the newly created Import Model Importer item icon ![image](../figs_a5/database-import.png) +- Drag the newly created Import Model Importer item icon ![image](figs_a5/database-import.png) from the tool bar and drop it into the *Design View*. This will open the Add Importer dialogue. Type in ‘Import Model’ and click on **Ok**. @@ -403,13 +403,13 @@ yourself with Spine. one of the Data Connection’s connectors and then on one of the Importer’s connectors. Connect similarly the importer with the input database. Now the project should look similar to this: - ![image](../figs_a5/items_connections.png) + ![image](figs_a5/items_connections.png) - From the main menu, select **File -> Save project**. #### Importing the model -- Download [the data](../../data/a5.xlsx) - and [the accompanying mapping](../../data/A5_importer_specification.json) +- Download [the data](data/a5.xlsx) + and [the accompanying mapping](data/A5_importer_specification.json) (right click on the links, then select *Save link as...*). - Add a reference to the file containing the model. - Select the *Data Connection item* in the *Design View* to show the *Data Connection properties* window (on the right side of the window usually). @@ -419,7 +419,7 @@ yourself with Spine. ## Executing the workflow Once the workflow is defined and input data is in place, the project is -ready to be executed. Hit the **Execute project** button ![image](../figs_a5/play-circle.png) +ready to be executed. Hit the **Execute project** button ![image](figs_a5/play-circle.png) on the tool bar. You should see ‘Executing All Directed Acyclic Graphs’ printed in the @@ -432,7 +432,7 @@ is complete. Select the output data store and open the Spine DB editor. -![image](../figs_a5/case_study_a5_output.png) +![image](figs_a5/case_study_a5_output.png) To checkout the flow on the electricity load (i.e., the total @@ -443,7 +443,7 @@ double-click the first cell under `value`. The *Parameter value editor* will pop up. You should see something like this: -![image](../figs_a5/case_study_a5_output_electricity_load_unit_flow.png) +![image](figs_a5/case_study_a5_output_electricity_load_unit_flow.png) !!! note diff --git a/docs/src/tutorial/figs_reserves/reserves_tutorial_power_plant_a_ramp_up_limit.png b/docs/src/tutorial/figs_reserves/reserves_tutorial_power_plant_a_ramp_up_limit.png new file mode 100644 index 0000000000..2990dd5727 Binary files /dev/null and b/docs/src/tutorial/figs_reserves/reserves_tutorial_power_plant_a_ramp_up_limit.png differ diff --git a/docs/src/tutorial/figs_reserves/reserves_tutorial_power_plant_a_res_procurement_cost.png b/docs/src/tutorial/figs_reserves/reserves_tutorial_power_plant_a_res_procurement_cost.png new file mode 100644 index 0000000000..1f243c1aec Binary files /dev/null and b/docs/src/tutorial/figs_reserves/reserves_tutorial_power_plant_a_res_procurement_cost.png differ diff --git a/docs/src/tutorial/figs_reserves/reserves_tutorial_power_plant_a_reserve_capacity.png b/docs/src/tutorial/figs_reserves/reserves_tutorial_power_plant_a_reserve_capacity.png new file mode 100644 index 0000000000..fb4c731b39 Binary files /dev/null and b/docs/src/tutorial/figs_reserves/reserves_tutorial_power_plant_a_reserve_capacity.png differ diff --git a/docs/src/tutorial/figs_reserves/reserves_tutorial_power_plant_b_ramp_up_limit.png b/docs/src/tutorial/figs_reserves/reserves_tutorial_power_plant_b_ramp_up_limit.png new file mode 100644 index 0000000000..1955d05672 Binary files /dev/null and b/docs/src/tutorial/figs_reserves/reserves_tutorial_power_plant_b_ramp_up_limit.png differ diff --git a/docs/src/tutorial/figs_reserves/reserves_tutorial_power_plant_b_res_procurement_cost.png b/docs/src/tutorial/figs_reserves/reserves_tutorial_power_plant_b_res_procurement_cost.png new file mode 100644 index 0000000000..a821af4449 Binary files /dev/null and b/docs/src/tutorial/figs_reserves/reserves_tutorial_power_plant_b_res_procurement_cost.png differ diff --git a/docs/src/tutorial/figs_reserves/reserves_tutorial_power_plant_b_reserve_capacity.png b/docs/src/tutorial/figs_reserves/reserves_tutorial_power_plant_b_reserve_capacity.png new file mode 100644 index 0000000000..be405fcd10 Binary files /dev/null and b/docs/src/tutorial/figs_reserves/reserves_tutorial_power_plant_b_reserve_capacity.png differ diff --git a/docs/src/tutorial/figs_reserves/reserves_tutorial_report__output_relationships.png b/docs/src/tutorial/figs_reserves/reserves_tutorial_report__output_relationships.png new file mode 100644 index 0000000000..0343307405 Binary files /dev/null and b/docs/src/tutorial/figs_reserves/reserves_tutorial_report__output_relationships.png differ diff --git a/docs/src/tutorial/figs_reserves/reserves_tutorial_reserve_group.png b/docs/src/tutorial/figs_reserves/reserves_tutorial_reserve_group.png new file mode 100644 index 0000000000..83cf7d03fb Binary files /dev/null and b/docs/src/tutorial/figs_reserves/reserves_tutorial_reserve_group.png differ diff --git a/docs/src/tutorial/figs_reserves/reserves_tutorial_reserve_group_parameters.png b/docs/src/tutorial/figs_reserves/reserves_tutorial_reserve_group_parameters.png new file mode 100644 index 0000000000..3b845da2f0 Binary files /dev/null and b/docs/src/tutorial/figs_reserves/reserves_tutorial_reserve_group_parameters.png differ diff --git a/docs/src/tutorial/figs_reserves/reserves_tutorial_reserve_node.png b/docs/src/tutorial/figs_reserves/reserves_tutorial_reserve_node.png new file mode 100644 index 0000000000..924f11ba12 Binary files /dev/null and b/docs/src/tutorial/figs_reserves/reserves_tutorial_reserve_node.png differ diff --git a/docs/src/tutorial/figs_reserves/reserves_tutorial_reserve_node_parameters.png b/docs/src/tutorial/figs_reserves/reserves_tutorial_reserve_node_parameters.png new file mode 100644 index 0000000000..e37a307f39 Binary files /dev/null and b/docs/src/tutorial/figs_reserves/reserves_tutorial_reserve_node_parameters.png differ diff --git a/docs/src/tutorial/figs_reserves/reserves_tutorial_results_pivot_table.png b/docs/src/tutorial/figs_reserves/reserves_tutorial_results_pivot_table.png new file mode 100644 index 0000000000..6adde62a57 Binary files /dev/null and b/docs/src/tutorial/figs_reserves/reserves_tutorial_results_pivot_table.png differ diff --git a/docs/src/tutorial/figs_reserves/reserves_tutorial_results_pivot_table_reserve_cost.png b/docs/src/tutorial/figs_reserves/reserves_tutorial_results_pivot_table_reserve_cost.png new file mode 100644 index 0000000000..ed17d4580e Binary files /dev/null and b/docs/src/tutorial/figs_reserves/reserves_tutorial_results_pivot_table_reserve_cost.png differ diff --git a/docs/src/tutorial/figs_reserves/reserves_tutorial_results_report__model.png b/docs/src/tutorial/figs_reserves/reserves_tutorial_results_report__model.png new file mode 100644 index 0000000000..26c25e6cda Binary files /dev/null and b/docs/src/tutorial/figs_reserves/reserves_tutorial_results_report__model.png differ diff --git a/docs/src/tutorial/figs_reserves/reserves_tutorial_results_report__model_reserve_cost.png b/docs/src/tutorial/figs_reserves/reserves_tutorial_results_report__model_reserve_cost.png new file mode 100644 index 0000000000..2694de9021 Binary files /dev/null and b/docs/src/tutorial/figs_reserves/reserves_tutorial_results_report__model_reserve_cost.png differ diff --git a/docs/src/tutorial/figs_reserves/reserves_tutorial_schematic.png b/docs/src/tutorial/figs_reserves/reserves_tutorial_schematic.png new file mode 100644 index 0000000000..ad9adfd599 Binary files /dev/null and b/docs/src/tutorial/figs_reserves/reserves_tutorial_schematic.png differ diff --git a/docs/src/tutorial/figs_reserves/reserves_tutorial_unit__to_node_relationships.png b/docs/src/tutorial/figs_reserves/reserves_tutorial_unit__to_node_relationships.png new file mode 100644 index 0000000000..dc91ea687c Binary files /dev/null and b/docs/src/tutorial/figs_reserves/reserves_tutorial_unit__to_node_relationships.png differ diff --git a/docs/src/tutorial/reserves.md b/docs/src/tutorial/reserves.md new file mode 100644 index 0000000000..876a4110d8 --- /dev/null +++ b/docs/src/tutorial/reserves.md @@ -0,0 +1,166 @@ +# Reserve definition tutorial + +This tutorial provides a step-by-step guide to include reserve requirements in a simple energy system with Spine Toolbox for SpineOpt. + +## Introduction + +Welcome to our tutorial, where we will walk you through the process of adding a new reserve node in SpineOpt using Spine Toolbox. To get the most out of this tutorial, we suggest first completing the Simple System tutorial, which can be found [here](https://spine-tools.github.io/SpineOpt.jl/latest/tutorial/simple_system/). + +Reserves refer to the capacity or energy that is kept as a backup to ensure the power system's reliability. This reserve capacity can be brought online automatically or manually in the event of unforeseen system disruptions such as generation failure, transmission line failure, or a sudden increase in demand. Operating reserves are essential to ensure that there is always enough generation capacity available to meet demand, even in the face of unforeseen system disruptions. + +### Model assumptions + +- The reserve node has a requirement of 20MW for upwards reserve +- Power plants 'a' and 'b' can both provide reserve to this node + +![image](figs_reserves/reserves_tutorial_schematic.png) + +## Guide + +### Entering input data + +- Launch the Spine Toolbox and select **File** and then **Open Project** or use the keyboard shortcut **Alt + O** to open the desired project. +- Locate the folder that you saved in the Simple System tutorial and click *Ok*. This will prompt the Simple System workflow to appear in the *Design View* section for you to start working on. +- Select the 'input' Data Store item in the *Design View*. +- Go to *Data Store Properties* and hit **Open editor**. This will open the database in the *Spine DB editor*. + +In this tutorial, you will learn how to add a new reserve node to the Simple System. + +#### Creating objects + +- Always in the Spine DB editor, locate the *Object tree* (typically at the top-left). Expand the [root] element if not expanded. +- Right click on the [node] class, and select *Add objects* from the context menu. The *Add objects* dialog will pop up. +- Enter the names for the new reserve node as seen in the image below, then press *Ok*. This will create a new object of class *node*, called *upward\_reserve\_node*. + +![image](figs_reserves/reserves_tutorial_reserve_node.png) + +- Right click on the *node* class, and select *Add object group* from the context menu. The *Add object group* dialog will pop up. In the *Group name* field write *upward\_reserve\_group* to refer to this group. Then, add as a members of the group the nodes *electricity\_node* and *upward\_reserve\_node*, as shown in the image below; then press *Ok*. + +!!! note +In SpineOpt, groups of nodes allow the user to create constraints that involve variables from its members. Later in this tutorial, the group named *upward\_reserve\_group* will help to link the flow variables for electricity production and reserve procurement. + +![image](figs_reserves/reserves_tutorial_reserve_group.png) + +#### Establishing relationships + +- Always in the Spine DB editor, locate the *Relationship tree* (typically at the bottom-left). Expand the *root* element if not expanded. +- Right click on the *unit\_\_to_node* class, and select *Add relationships* from the context menu. The *Add relationships* dialog will pop up. +- Select the names of the two units and their **receiving** nodes, as seen in the image below; then press *Ok*. This will establish that both *power\_plant\_a* and *power\_plant\_b* release energy into both the *upward\_reserve\_node* and the *upward\_reserve\_group*. + +![image](figs_reserves/reserves_tutorial_unit__to_node_relationships.png) + +- Right click on the *report\_\_output* class, and select *Add relationships* from the context menu. The *Add relationships* dialog will pop up. + +- Enter *report1* under *report*, and *variable\_om\_costs* under *output*. Repeat the same procedure in the second line to add the *res\_proc\_costs* under *output* as seen in the image below; then press *Ok*. This will write the total *vom\_cost* and *procurement reserve cost* values in the objective function to the output database as a part of *report1*. + +![image](figs_reserves/reserves_tutorial_report__output_relationships.png) + +#### Specifying object parameter values + +- Back to *Object tree*, expand the *node* class and select *upward\_reserve\_node*. +- Locate the *Object parameter* table (typically at the top-center). +- In the *Object parameter* table (typically at the top-center), select the following parameter as seen in the image below: + - *demand* parameter and the *Base* alternative, and enter the value *20*. This will establish that there's a demand of '20' at the reverse node. + - *is\_reserve\_node* parameter and the *Base* alternative, and enter the value *True*. This will establish that it is a reverse node. + - *upward\_reserve* parameter and the *Base* alternative, then right-click on the value cell and then, in the context menu, select 'Edit...' and select the option *True*. This will establish the direction of the reserve is upwards. + - *nodal\_balance\_sense* parameter and the *Base* alternative, and enter the value $\geq$. This will establish that the total reserve procurement must be greater or equal than the reserve demand. + +![image](figs_reserves/reserves_tutorial_reserve_node_parameters.png) + +- Select *upward\_reserve\_group* in the *Object tree*. + +- In the *Object parameter* table, select the *balance\_type* parameter and the *Base* alternative, and enter the value *balance\_type\_none* as seen in the image below. This will establish that there is no need to create an extra balance between the members of the group. + +![image](figs_reserves/reserves_tutorial_reserve_group_parameters.png) + +#### Specifying relationship parameter values + +- In *Relationship tree*, expand the *unit\_\_to\_node* class and select *power\_plant\_a | upward\_reserve\_node*. + +- In the *Relationship parameter* table (typically at the bottom-center), select the *unit\_capacity* parameter and the *Base* alternative, and enter the value *100* as seen in the image below. This will set the capacity to provide reserve for *power\_plant\_a*. + +!!! note +The value is equal to the unit capacity defined for the electricity node. However, the value can be lower if the unit cannot provide reserves with its total capacity. + +![image](figs_reserves/reserves_tutorial_power_plant_a_reserve_capacity.png) + +- In *Relationship tree*, expand the *unit\_\_to\_node* class and select *power\_plant\_b | upward\_reserve\_node*. + +- In the *Relationship parameter* table (typically at the bottom-center), select the *unit\_capacity* parameter and the *Base* alternative, and enter the value *200* as seen in the image below. This will set the capacity to provide reserve for *power\_plant\_b*. + +![image](figs_reserves/reserves_tutorial_power_plant_b_reserve_capacity.png) + +- In *Relationship tree*, expand the *unit\_\_to\_node* class and select *power\_plant\_a | upward\_reserve\_group*. + +- In the *Relationship parameter* table (typically at the bottom-center), select the following parameter as seen in the image below: + - *unit\_capacity* parameter and the *Base* alternative, and enter the value *100*. This will set the total capacity for *power\_plant\_a* in the group. + - *ramp\_up\_limit* parameter and the *Base* alternative, and enter the value *1*. This will set the ramping up capacity to 100% of the unit capacity for *power\_plant\_a*. + +!!! note +The *ramp\_up\_limit* parameter triggers the [Splitting unit flows into ramps]@(ref) constraint, which links the unit's flow and reserve variables. + +![image](figs_reserves/reserves_tutorial_power_plant_a_ramp_up_limit.png) + +- In *Relationship tree*, expand the *unit\_\_to\_node* class and select *power\_plant\_b | upward\_reserve\_group*. + +- In the *Relationship parameter* table (typically at the bottom-center), select the following parameter as seen in the image below: + - *unit\_capacity* parameter and the *Base* alternative, and enter the value *200*. This will set the total capacity for *power\_plant\_b* in the group. + - *ramp\_up\_limit* parameter and the *Base* alternative, and enter the value *1*. This will set the ramping up capacity to 100% of the unit capacity for *power\_plant\_b*. + +![image](figs_reserves/reserves_tutorial_power_plant_b_ramp_up_limit.png) + +When you're ready, save/commit all changes to the database. + +### Executing the workflow + +- Go back to Spine Toolbox's main window, and hit the **Execute project** button ![image](figs_simple_system/play-circle.png) from the tool bar. You should see 'Executing All Directed Acyclic Graphs' printed in the *Event log* (at the bottom left by default). + +- Select the 'Run SpineOpt' Tool. You should see the output from SpineOpt in the *Julia Console* after clicking the *object activity control*. + +### Examining the results + +- Select the output data store and open the Spine DB editor. You can already inspect the fields in the displayed tables or use a pivot table. + +- For the pivot table, press **Alt + F** for the shortcut to the hamburger menu, and select **Pivot -> Index**. + +- Select *report\_\_unit\_\_node\_\_direction\_\_stochastic\_scenario* under **Relationship tree**, and the first cell under **alternative** in the *Frozen table*. + +- Under alternative in the Frozen table, you can choose results from different runs. Pick the run you want to view. If the workflow has been run several times, the most recent run will usually be found at the bottom. + +- The *Pivot table* will be populated with results from the SpineOpt run. It will look something like the image below. + +![image](figs_reserves/reserves_tutorial_results_pivot_table.png) + +As anticipated, the *power\_plant\_b* is supplying the necessary reserve due to its surplus capacity, while *power\_plant\_a* is operating at full capacity. Additionally, in this model, we have not allocated a cost for reserve procurement. One way to double-check it is by selecting *report\_\_model* under **Relationship tree** and look at the costs the *Pivot table*, see image below. + +![image](figs_reserves/reserves_tutorial_results_report__model.png) + +So, is it possible to assign costs to this reserve procurement in SpineOpt? Yes, it is indeed possible. + +#### Specifying a reserve procurement cost value + +- In *Relationship tree*, expand the *unit\_\_to\_node* class and select *power\_plant\_a | upward\_reserve\_node*. + +- In the *Relationship parameter* table (typically at the bottom-center), select the *reserve\_procurement\_cost* parameter and the *Base* alternative, and enter the value *5* as seen in the image below. This will set the cost of providing reserve for *power\_plant\_a*. + +![image](figs_reserves/reserves_tutorial_power_plant_a_res_procurement_cost.png) + +- In *Relationship tree*, expand the *unit\_\_to\_node* class and select *power\_plant\_b | upward\_reserve\_node*. + +- In the *Relationship parameter* table (typically at the bottom-center), select the *reserve\_procurement\_cost* parameter and the *Base* alternative, and enter the value *35* as seen in the image below. This will set the cost of providing reserve for *power\_plant\_b*. + +![image](figs_reserves/reserves_tutorial_power_plant_b_res_procurement_cost.png) + +**Don't forget to commit the new changes to the database!** + +#### Executing the worflow and examining the results again + +- Go back to Spine Toolbox's main window, and hit again the **Execute project** button as before. + +- Select the output data store and open the Spine DB editor. You can inspect results as before, which should look like the image below. + +![image](figs_reserves/reserves_tutorial_results_pivot_table_reserve_cost.png) + +Since the cost of reserve procurement is way cheaper in *power\_plant\_a* than in *power\_plant\_b*, then the optimal solution is to reduce the production of electricity in *power\_plant\_a* to provide reserve with this unit rather than *power\_plant\_b* as before. By looking at the total costs, we can see that the reserve procurement costs are no longer zero. + +![image](figs_reserves/reserves_tutorial_results_report__model_reserve_cost.png) diff --git a/docs/src/tutorial/simple_system.md b/docs/src/tutorial/simple_system.md index baa5685df0..0849caba30 100644 --- a/docs/src/tutorial/simple_system.md +++ b/docs/src/tutorial/simple_system.md @@ -20,7 +20,7 @@ system on Spine Toolbox. - The demand at the electricity node is 150 MWh. - The fuel node is able to provide infinite energy. -![image](../figs_simple_system/simple_system_schematic.png) +![image](figs_simple_system/simple_system_schematic.png) ### Installation and upgrades @@ -53,7 +53,7 @@ If you are not sure whether you have the latest version, please upgrade to ensur - Go to *Data Store Properties* and hit **Open editor**. This will open the newly created database in the *Spine DB editor*, looking similar to this: -![image](../figs_a5/case_study_a5_spine_db_editor_empty.png) +![image](figs_a5/case_study_a5_spine_db_editor_empty.png) !!! note The *Spine DB editor* is a dedicated interface within Spine Toolbox @@ -91,7 +91,7 @@ If you are not sure whether you have the latest version, please upgrade to ensur then press *Ok*. This will create two objects of class *node*, called *fuel\_node* and *electricity\_node*. -![image](../figs_simple_system/simple_system_add_nodes.png) +![image](figs_simple_system/simple_system_add_nodes.png) - Right click on the *unit* class, and select *Add objects* from the context menu. The *Add objects* dialog will pop @@ -106,7 +106,7 @@ If you are not sure whether you have the latest version, please upgrade to ensur then press *Ok*. This will create two objects of class *unit*, called *power\_plant\_a* and *power\_plant\_b*. -![image](../figs_simple_system/simple_system_add_units.png) +![image](figs_simple_system/simple_system_add_units.png) !!! note To modify an object after you enter it, right click on it and select @@ -126,7 +126,7 @@ If you are not sure whether you have the latest version, please upgrade to ensur seen in the image below; then press *Ok*. This will establish that both *power\_plant\_a* and *power\_plant\_b* take energy from the *fuel\_node*. -![image](../figs_simple_system/simple_system_add_unit__from_node_relationships.png) +![image](figs_simple_system/simple_system_add_unit__from_node_relationships.png) - Right click on the *unit\_\_to_node* class, and select *Add relationships* from the context menu. The *Add relationships* @@ -136,7 +136,7 @@ If you are not sure whether you have the latest version, please upgrade to ensur seen in the image below; then press *Ok*. This will establish that both *power\_plant\_a* and *power\_plant\_b* release energy into the *electricity\_node*. -![image](../figs_simple_system/simple_system_add_unit__to_node_relationships.png) +![image](figs_simple_system/simple_system_add_unit__to_node_relationships.png) - Right click on the *report\_\_output* class, and select *Add relationships* from the context menu. The *Add relationships* @@ -147,7 +147,7 @@ If you are not sure whether you have the latest version, please upgrade to ensur then press *Ok*. This will tell SpineOpt to write the value of the *unit\_flow* optimization variable to the output database, as part of *report1*. -![image](../figs_simple_system/simple_system_add_report__output_relationships.png) +![image](figs_simple_system/simple_system_add_report__output_relationships.png) !!! note In SpineOpt, outputs represent optimization variables that can be @@ -165,7 +165,7 @@ If you are not sure whether you have the latest version, please upgrade to ensur alternative, and enter the value *100* as seen in the image below. This will establish that there's a demand of '100' at the electricity node. -![image](../figs_simple_system/simple_system_electricity_demand.png) +![image](figs_simple_system/simple_system_electricity_demand.png) - Select *fuel\_node* in the *Object tree*. @@ -174,7 +174,7 @@ If you are not sure whether you have the latest version, please upgrade to ensur alternative, and enter the value *balance\_type\_none* as seen in the image below. This will establish that the fuel node is not balanced, and thus provide as much fuel as needed. -![image](../figs_simple_system/simple_system_fuel_balance_type.png) +![image](figs_simple_system/simple_system_fuel_balance_type.png) #### Specifying relationship parameter values @@ -186,7 +186,7 @@ If you are not sure whether you have the latest version, please upgrade to ensur *Base* alternative, and enter the value *25* as seen in the image below. This will set the operating cost for *power\_plant\_a*. -![image](../figs_simple_system/simple_system_power_plant_a_vom_cost.png) +![image](figs_simple_system/simple_system_power_plant_a_vom_cost.png) - Select *power\_plant\_b | fuel\_node* in the *Relationship tree*. @@ -196,7 +196,7 @@ If you are not sure whether you have the latest version, please upgrade to ensur alternative, and enter the value *50* as seen in the image below. This will set the operating cost for *power\_plant\_b*. -![image](../figs_simple_system/simple_system_power_plant_b_vom_cost.png) +![image](figs_simple_system/simple_system_power_plant_b_vom_cost.png) - In *Relationship tree*, expand the *unit\_\_to_node* class and select *power\_plant\_a | electricity\_node*. @@ -206,7 +206,7 @@ If you are not sure whether you have the latest version, please upgrade to ensur alternative, and enter the value *100* as seen in the image below. This will set the capacity for *power\_plant\_a*. -![image](../figs_simple_system/simple_system_power_plant_a_capacity.png) +![image](figs_simple_system/simple_system_power_plant_a_capacity.png) - Select *power\_plant\_b | electricity\_node* in the *Relationship tree*. @@ -216,7 +216,7 @@ If you are not sure whether you have the latest version, please upgrade to ensur alternative, and enter the value *200* as seen in the image below. This will set the capacity for *power\_plant\_b*. -![image](../figs_simple_system/simple_system_power_plant_b_capacity.png) +![image](figs_simple_system/simple_system_power_plant_b_capacity.png) - In *Relationship tree*, select the *unit\_\_node\_\_node* class, and come back to the @@ -232,14 +232,14 @@ If you are not sure whether you have the latest version, please upgrade to ensur electricity for *power\_plant\_a* and *power\_plant\_b* to *0.7* and *0.8*, respectively. It should like the image below. -![image](../figs_simple_system/simple_system_fix_ratio_out_in_unit_flow.png) +![image](figs_simple_system/simple_system_fix_ratio_out_in_unit_flow.png) When you're ready, commit all changes to the database. ### Executing the workflow - Go back to Spine Toolbox's main window, and hit the **Execute - project** button ![image](../figs_a5/play-circle.png) from the tool bar. + project** button ![image](figs_a5/play-circle.png) from the tool bar. You should see 'Executing All Directed Acyclic Graphs' printed in the *Event log* (at the bottom left by default). @@ -261,4 +261,4 @@ When you're ready, commit all changes to the database. the bottom. - The *Pivot table* will be populated with results from the SpineOpt run. It will look something like the image below. -![image](../figs_simple_system/simple_system_results_pivot_table.png) \ No newline at end of file +![image](figs_simple_system/simple_system_results_pivot_table.png) \ No newline at end of file diff --git a/docs/src/tutorial/tutorialTwoHydro.md b/docs/src/tutorial/tutorialTwoHydro.md index c35245679b..4e62a8a636 100644 --- a/docs/src/tutorial/tutorialTwoHydro.md +++ b/docs/src/tutorial/tutorialTwoHydro.md @@ -21,15 +21,15 @@ upstream power plant follows the river route and becomes available to the downstream power plant. -![A system of two hydropower plants.](../figs_two_hydro/two_hydro.png) +![A system of two hydropower plants.](figs_two_hydro/two_hydro.png) *A system of two hydropower plants* In order to run this tutorial you must first execute some preliminary -steps from the [Simple System](../../simple_system/index.html) tutorial. +steps from the [Simple System](simple_system.md) tutorial. Specifically, execute all steps from the -[guide](../../simple_system/index.html#guide), up to and including the step of -[importing-the-spineopt-database-template](../../simple_system/index.html#importing-the-spineopt-database-template). +[guide](simple_system.md#guide), up to and including the step of +[importing-the-spineopt-database-template](simple_system.md#importing-the-spineopt-database-template). It is advisable to go through the whole tutorial in order to familiarise yourself with Spine. @@ -61,11 +61,12 @@ Toolbox). Since we are modelling a hydropower system we will have to define two commodities, water and electricity. In the Spine DB editor, locate the *Object tree*, expand the root element if -required, right click on the commodity class, and select *Add objects* from the context menu. In the *Add objects* dialogue that should pop up, enter +required, right click on the commodity class, and select *Add objects* from the context menu. +In the *Add objects* dialogue that should pop up, enter the object names for the commodities as you see in the image below and then press Ok. -![image](../figs_two_hydro/two_hydro_commodities.png) +![image](figs_two_hydro/two_hydro_commodities.png) *Defining commodities.* @@ -75,7 +76,7 @@ Follow a similar path to add nodes, right click on the node class, and select *Add objects* from the context menu. In the dialogue, enter the node names as shown: -![image](../figs_two_hydro/two_hydro_nodes.png) +![image](figs_two_hydro/two_hydro_nodes.png) *Defining nodes.* @@ -93,7 +94,7 @@ Similarly, add connections, right click on the connection class, select *Add objects* from the context menu and add the following connections: -![image](../figs_two_hydro/two_hydro_connections.png) +![image](figs_two_hydro/two_hydro_connections.png) *Defining connections.* @@ -109,7 +110,7 @@ another, you need a unit. You guessed it! Right click on the unit class, select *Add objects* from the context menu and add the following units: -![image](../figs_two_hydro/two_hydro_units.png) +![image](figs_two_hydro/two_hydro_units.png) *Defining units.* @@ -128,7 +129,7 @@ required, right click on the *node\_\_commodity* class, and select *Add relation *Add relationships* dialogue, enter the following relationships as you see in the image below and then press Ok. -![image](../figs_two_hydro/two_hydro_node_commodities.png) +![image](figs_two_hydro/two_hydro_node_commodities.png) *Introducing node\_\_commodity relationships.* @@ -137,14 +138,14 @@ following relationships as you see in the image below and then press Ok. Next step is to define the topology of flows between the nodes. To do that insert the following relationships in the *connection\_\_from\_node* class: -![image](../figs_two_hydro/two_hydro_connection_from_node.png) +![image](figs_two_hydro/two_hydro_connection_from_node.png) *Introducing connection\_\_from\_node relationships.* as well as the following the following *connection\_\_node_node* relationships as you see in the figure: -![Introducing connection__node_node relationships.](../figs_two_hydro/two_hydro_connection_node_node.png) +![Introducing connection__node_node relationships.](figs_two_hydro/two_hydro_connection_node_node.png) *Introducing connection\_\_node\_node relationships.* @@ -154,20 +155,20 @@ To define the topology of the units and be able to introduce their parameters later on, you need to define the following relationships in the *unit\_\_from\_node* class: -![Introducing unit__from_node relationships.](../figs_two_hydro/two_hydro_unit_from_node.png) +![Introducing unit__from_node relationships.](figs_two_hydro/two_hydro_unit_from_node.png) *Introducing unit\_\_from\_node relationships.* in the *unit\_\_node_node* class: -![Introducing unit__node_node relationships.](../figs_two_hydro/two_hydro_unit_node_node.png) +![Introducing unit__node_node relationships.](figs_two_hydro/two_hydro_unit_node_node.png) *Introducing unit\_\_node\_node relationships.* and in the *unit\_\_to\_node* class as you see in the following figure: -![Introducing unit__to_node relationships.](../figs_two_hydro/two_hydro_unit_to_node.png) +![Introducing unit__to_node relationships.](figs_two_hydro/two_hydro_unit_to_node.png) *Introducing unit\_\_to\_node relationships.* @@ -178,7 +179,7 @@ variables to the output database you need to specify them in the form of *report\_output* relationships. Add the following relationships to the *report\_output* class: -![Introducing report outputs with report_output relationships.](../figs_two_hydro/two_hydro_report.png) +![Introducing report outputs with report_output relationships.](figs_two_hydro/two_hydro_report.png) *Introducing report outputs with report\_output relationships.* @@ -191,7 +192,7 @@ need to introduce respective parameter values. To introduce object parameter values first select the *model* class in the Object tree and enter the following values in the *Object parameter value* pane: -![Defining model execution parameters.](../figs_two_hydro/two_hydro_model_parameters.png) +![Defining model execution parameters.](figs_two_hydro/two_hydro_model_parameters.png) *Defining model execution parameters.* @@ -223,7 +224,7 @@ this time selecting the *node* class from the *Object tree*, we need to add the following entries: -![Defining model execution parameters.](../figs_two_hydro/two_hydro_node_parameters.png) +![Defining model execution parameters.](figs_two_hydro/two_hydro_node_parameters.png) *Defining model execution parameters.* @@ -265,7 +266,7 @@ resolution we select the *temporal\_block* class in the *Object tree* and we set the *resolution* parameter value to *1h* as shown in the figure: -![Setting the temporal resolution of the model.](../figs_two_hydro/two_hydro_temporal_block.png) +![Setting the temporal resolution of the model.](figs_two_hydro/two_hydro_temporal_block.png) *Setting the temporal resolution of the model.* @@ -277,7 +278,7 @@ this we need to select the *connection\_\_node\_node* class in the Relationship tree and add the following entries in the *Relationship parameter value* pane, as shown next: -![Defining discharge and spillage ratio flows.](../figs_two_hydro/two_hydro_connection_node_node_parameters.png) +![Defining discharge and spillage ratio flows.](figs_two_hydro/two_hydro_connection_node_node_parameters.png) *Defining discharge and spillage ratio flows.* @@ -287,7 +288,7 @@ Similarly, for each one of the *unit\_\_from_node*, *unit\_\_node_node*, and *un add the the maximal water that can be discharged by each hydropower plant: -![Setting the maximal water discharge of each plant.](../figs_two_hydro/two_hydro_unit_from_node_parameters.png) +![Setting the maximal water discharge of each plant.](figs_two_hydro/two_hydro_unit_from_node_parameters.png) *Setting the maximal water discharge of each plant.* @@ -300,7 +301,7 @@ select all values, copy, and paste them, after having selected the value cell of the corresponding row. You can plot and edit the timeseries data by double clicking on the same cell afterwards: -![Previewing and editing the electricity prices timeseries.](../figs_two_hydro/two_hydro_vom_cost.png) +![Previewing and editing the electricity prices timeseries.](figs_two_hydro/two_hydro_vom_cost.png) *Previewing and editing the electricity prices timeseries.* @@ -310,19 +311,19 @@ ratios between the nodes. Assuming that water is not "lost" from the discharged water with a given efficiency we define the following parameter values for each hydropower plant, in the *unit\_\_node_node* class: -![Defining conversion efficiencies.](../figs_two_hydro/two_hydro_unit_node_node_parameters.png) +![Defining conversion efficiencies.](figs_two_hydro/two_hydro_unit_node_node_parameters.png) *Defining conversion efficiencies.* Lastly, we can define the maximal electricity production of each plant by inserting the following *unit\_\_to\_node* relationship parameter values: -![Setting the maximal electricity production of each plant.](../figs_two_hydro/two_hydro_unit_to_node_parameters.png) +![Setting the maximal electricity production of each plant.](figs_two_hydro/two_hydro_unit_to_node_parameters.png) *Setting the maximal electricity production of each plant.* Hooray! You can now commit the database, close the Spine DB Editor and -run your model! Go to the main Spine window and click on Execute ![image](../figs_a5/play-circle.png). +run your model! Go to the main Spine window and click on Execute ![image](figs_a5/play-circle.png). ### Examining the results @@ -339,7 +340,7 @@ examine closer and retrieve the data, as shown in the next figure. The *unit\_flow* variable of the *electricity\_load* unit represents the total electricity production in the system: -![Total electricity produced in the system.](../figs_two_hydro/two_hydro_results_electricity.png) +![Total electricity produced in the system.](figs_two_hydro/two_hydro_results_electricity.png) *Total electricity produced in the system.* @@ -347,7 +348,7 @@ Now, take to a minute to reflect on how you could retrieve the data representing the water that is discharged by each hydropower plant as shown in the next figure: -![Water discharge of Språnget hydropower plant.](../figs_two_hydro/two_hydro_results_discharge.png) +![Water discharge of Språnget hydropower plant.](figs_two_hydro/two_hydro_results_discharge.png) *Water discharge of Språnget hydropower plant.* @@ -406,20 +407,20 @@ to the downstream plant and this should be taken into account. To model the value of stored water we need to make some additions and modifications to the initial model. -- First, add a new node (see [adding nodes](../index.html#Nodes)) and give it a +- First, add a new node (see [adding nodes](../index.md#Nodes)) and give it a name (e.g., *stored\_water*). This node will accumulate the water stored in the reservoirs at the end of the planning horizon. Associate the node with the water - commodity (see [node__commodity](../index.html#Assinging-commodities-to-nodes)). + commodity (see [node__commodity](../index.md#Assinging-commodities-to-nodes)). -- Add three more units (see [adding units](../index.html#Units)); two will +- Add three more units (see [adding units](../index.md#Units)); two will transfer the water at the end of the planning horizon in the new node that we just added (e.g., *Språnget\_stored\_water*, *Fallet\_stored\_water*), and one will be used as a *sink* introducing the value of stored water in the objective function (e.g., *value\_stored\_water*). - To establish the topology of the new units and nodes (see - [adding unit relationships](../index.html#Placing-the-units-in-the-model)): + [adding unit relationships](../index.md#Placing-the-units-in-the-model)): - add one *unit\_\_from_node* relationship, between the *value\_stored\_water* unit from the *stored\_water* node, another one between the *Språnget\_stored\_water* unit from @@ -452,7 +453,7 @@ modifications to the initial model. and the *electricity\_load* and set 0 as the price of electricity for the last hour *2021-01-02T00:00:00*. The price is set to zero to ensure no electricity is sold during this hour. - ![Modify the fix_node_state parameter value of Språnget_upper and Fallet_upper nodes.](../figs_two_hydro/two_hydro_fix_node_state.png) + ![Modify the fix_node_state parameter value of Språnget_upper and Fallet_upper nodes.](figs_two_hydro/two_hydro_fix_node_state.png) *Modify the fix\_node\_state parameter value of Språnget\_upper and Fallet\_upper nodes.* - Finally, we need to add some relationship parameter values for the new units: @@ -466,7 +467,7 @@ modifications to the initial model. imposed a zero cost for all the optimisation horizon, while we use an assumed future electricity value for the additional time step at the end of the horizon. - ![Adding vom_cost parameter value on the value_stored_water unit.](../figs_two_hydro/two_hydro_max_stored_water_unit_values.png) + ![Adding vom_cost parameter value on the value_stored_water unit.](figs_two_hydro/two_hydro_max_stored_water_unit_values.png) *Adding vom\_cost parameter value on the value\_stored\_water unit.* - Add two *fix\_ratio\_out\_in\_unit\_flow* parameter values as you see in the figure bellow. The efficiency of @@ -477,11 +478,11 @@ modifications to the initial model. the water from Språnget's reservoir will be used both by Fallet and Språnget plant, therefore we use the sum of the two efficiencies in the parameter value of *Språnget\_stored\_water*. - ![Adding fix_ratio_out_in_unit_flow parameter values on the Språnget_stored_water and Fallet_stored_water units.](../figs_two_hydro/two_hydro_max_stored_water_unit_node_node.png) + ![Adding fix_ratio_out_in_unit_flow parameter values on the Språnget_stored_water and Fallet_stored_water units.](figs_two_hydro/two_hydro_max_stored_water_unit_node_node.png) *Adding fix\_ratio\_out\_in\_unit\_flow parameter values on the Språnget\_stored\_water and Fallet\_stored\_water units.* You can now commit your changes in the database, execute the project and -[examine the results](../index.html#Examining-the-results)! As an exercise, try to retrieve +[examine the results](../index.md#Examining-the-results)! As an exercise, try to retrieve the value of stored water as it is calculated by the model. ## Spillage Constraints - Minimisation of Spilt Water @@ -493,7 +494,7 @@ level. At the same time, to avoid wasting water that could be used for producing electricity, we could explicitly impose the spillage minimisation to be added in the objective function. -- Add one unit (see [adding units](../index.html#Units)) to impose the spillage +- Add one unit (see [adding units](../index.md#Units)) to impose the spillage constraints to each plant and name it (for example *Språnget\_spill*). - Remove the *Språnget\_to\_Fallet\_spill* @@ -502,7 +503,7 @@ minimisation to be added in the objective function. **Remove**). - To establish the topology of the unit (see - [adding unit relationships](../index.html#Placing-the-units-in-the-model)): + [adding unit relationships](../index.md#Placing-the-units-in-the-model)): - Add a *unit\_\_from_node* relationship, between the *Språnget\_spill* unit from the *Språnget\_upper* node, - add a *unit\_\_node\_\_node* @@ -516,7 +517,7 @@ minimisation to be added in the objective function. percentage of the *unit\_capacity*) to impose a minimum, and the *vom\_cost* to penalise the water that is spilt: - ![Setting minimum (the minimal value is defined as percentage of capacity), maximum, and spillage penalty.](../figs_two_hydro/two_hydro_min_spill_unit_node_node.png) + ![Setting minimum (the minimal value is defined as percentage of capacity), maximum, and spillage penalty.](figs_two_hydro/two_hydro_min_spill_unit_node_node.png) *Setting minimum (the minimal value is defined as percentage of capacity), maximum, and spillage penalty.* @@ -524,10 +525,10 @@ minimisation to be added in the objective function. define the *fix\_ratio\_out\_in\_unit\_flow* parameter value of the *min\_spillage|Fallet\_upper|Språnget\_upper* relationship to **1** (see - [adding unit relationships](../index.html#Placing-the-units-in-the-model)). + [adding unit relationships](../index.md#Placing-the-units-in-the-model)). Commit your changes in the database, execute the project and -[examine the results](../index.html#Examining-the-results)! As an exercise, you can perform +[examine the results](../index.md#Examining-the-results)! As an exercise, you can perform this process for and Fallet plant (you would also need to add another water node, downstream of Fallet). @@ -541,10 +542,10 @@ to do is set a demand in the form of a timeseries to the *electricity\_node*. timeseries](https://raw.githubusercontent.com/spine-tools/Spine-Toolbox/master/docs/source/data/contracted_load.txt), to the *demand* parameter value of the *electricity\_node* (see - [adding node parameter values](../index.html#Defining-node-parameter-values)). + [adding node parameter values](../index.md#Defining-node-parameter-values)). Commit your changes in the database, execute the project and -[examine the results](../index.html#Examining-the-results)! +[examine the results](../index.md#Examining-the-results)! This concludes the tutorial, we hope that you enjoyed building hydropower systems in Spine as much as we do! diff --git a/src/SpineOpt.jl b/src/SpineOpt.jl index a9cd0c13de..2b9882f8e7 100644 --- a/src/SpineOpt.jl +++ b/src/SpineOpt.jl @@ -33,6 +33,9 @@ import DataStructures: OrderedDict import Dates: CompoundPeriod import LinearAlgebra: BLAS.gemm, LAPACK.getri!, LAPACK.getrf! +# Resolve JuMP and SpineInterface `Parameter` and `parameter_value` conflicts. +import SpineInterface: Parameter, parameter_value + export run_spineopt export rerun_spineopt export run_spineopt_kernel! @@ -184,7 +187,9 @@ include("constraints/constraint_node_voltage_angle.jl") include("constraints/constraint_connection_unitary_gas_flow.jl") include("constraints/constraint_mp_any_invested_cuts.jl") include("constraints/constraint_mp_min_res_gen_to_demand_ratio.jl") -include("constraints/constraint_entity_investment_group.jl") +include("constraints/constraint_investment_group_equal_investments.jl") +include("constraints/constraint_investment_group_entities_invested_available.jl") +include("constraints/constraint_investment_group_capacity_invested_available.jl") export unit_flow_indices diff --git a/src/constraints/constraint_connection_flow_lodf.jl b/src/constraints/constraint_connection_flow_lodf.jl index 99c4267493..bf7e62b03f 100644 --- a/src/constraints/constraint_connection_flow_lodf.jl +++ b/src/constraints/constraint_connection_flow_lodf.jl @@ -36,7 +36,7 @@ function add_constraint_connection_flow_lodf!(m::Model) - connection_minimum_emergency_capacity(m, conn_mon, s, t) <= + connection_post_contingency_flow(m, connection_flow, conn_cont, conn_mon, s, t, expr_sum) - * connection_availability_factor[(connection=conn_mon, stochastic_scenario=s, analysis_time=t0, t=t)] + * connection_availability_factor[(connection=conn_mon, stochastic_scenario=s, analysis_time=t0, t=t)] <= + connection_minimum_emergency_capacity(m, conn_mon, s, t) ) diff --git a/src/constraints/constraint_investment_group_capacity_invested_available.jl b/src/constraints/constraint_investment_group_capacity_invested_available.jl new file mode 100644 index 0000000000..076cbff535 --- /dev/null +++ b/src/constraints/constraint_investment_group_capacity_invested_available.jl @@ -0,0 +1,124 @@ +############################################################################# +# Copyright (C) 2017 - 2023 Spine Project +# +# This file is part of SpineOpt. +# +# Spine Model is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Spine Model is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +############################################################################# + +""" + add_constraint_investment_group_minimum_capacity_invested_available!(m::Model) + +Force capacity invested available in a group to be greater than the minimum. +""" +function add_constraint_investment_group_minimum_capacity_invested_available!(m::Model) + t0 = _analysis_time(m) + m.ext[:spineopt].constraints[:investment_group_minimum_capacity_invested_available] = Dict( + (investment_group=ig, stochastic_scenario=s, t=t) => @constraint( + m, + _group_capacity_invested_available(m, ig, s, t) + >= + minimum_capacity_invested_available[(investment_group=ig, stochastic_scenario=s, analysis_time=t0, t=t)] + ) + for ig in indices(minimum_capacity_invested_available) + for (s, t) in _capacity_entities_invested_available_s_t(m) + ) +end + +""" + add_constraint_investment_group_maximum_capacity_invested_available!(m::Model) + +Force capacity invested available in a group to be lower than the maximum. +""" +function add_constraint_investment_group_maximum_capacity_invested_available!(m::Model) + t0 = _analysis_time(m) + m.ext[:spineopt].constraints[:investment_group_maximum_capacity_invested_available] = Dict( + (investment_group=ig, stochastic_scenario=s, t=t) => @constraint( + m, + _group_capacity_invested_available(m, ig, s, t) + <= + maximum_capacity_invested_available[(investment_group=ig, stochastic_scenario=s, analysis_time=t0, t=t)] + ) + for ig in indices(maximum_capacity_invested_available) + for (s, t) in _capacity_entities_invested_available_s_t(m) + ) +end + +function _capacity_entities_invested_available_s_t(m) + [ + (stochastic_scenario=s, t=t) + for (t, path) in t_lowest_resolution_path( + m, vcat(units_invested_available_indices(m), connections_invested_available_indices(m)) + ) + for s in path + ] +end + +function _group_capacity_invested_available(m, ig, s, t) + t0 = _analysis_time(m) + @fetch units_invested_available, connections_invested_available = m.ext[:spineopt].variables + ( + + expr_sum( + + units_invested_available[u, s, t] + * unit_capacity[(unit=u, node=n, direction=d, stochastic_scenario=s, analysis_time=t0, t=t)] + for (u, s, t) in units_invested_available_indices( + m; unit=unit__investment_group(investment_group=ig), stochastic_scenario=s, t=t_in_t(m; t_long=t) + ) + for (u, n, d) in Iterators.flatten( + ( + indices( + unit_capacity; + unit=u, + node=unit__from_node__investment_group(unit=u, investment_group=ig), + direction=direction(:from_node), + ), + indices( + unit_capacity; + unit=u, + node=unit__to_node__investment_group(unit=u, investment_group=ig), + direction=direction(:to_node), + ), + ) + ); + init=0 + ) + + expr_sum( + + connections_invested_available[conn, s, t] + * connection_capacity[(connection=conn, node=n, direction=d, stochastic_scenario=s, analysis_time=t0, t=t)] + for (conn, s, t) in connections_invested_available_indices( + m; + connection=connection__investment_group(investment_group=ig), + stochastic_scenario=s, + t=t_in_t(m; t_long=t) + ) + for (conn, n, d) in Iterators.flatten( + ( + indices( + connection_capacity; + connection=conn, + node=connection__from_node__investment_group(connection=conn, investment_group=ig), + direction=direction(:from_node), + ), + indices( + connection_capacity; + connection=conn, + node=connection__to_node__investment_group(connection=conn, investment_group=ig), + direction=direction(:to_node), + ), + ) + ); + init=0 + ) + ) +end \ No newline at end of file diff --git a/src/constraints/constraint_investment_group_entities_invested_available.jl b/src/constraints/constraint_investment_group_entities_invested_available.jl new file mode 100644 index 0000000000..6749519e2b --- /dev/null +++ b/src/constraints/constraint_investment_group_entities_invested_available.jl @@ -0,0 +1,103 @@ +############################################################################# +# Copyright (C) 2017 - 2023 Spine Project +# +# This file is part of SpineOpt. +# +# Spine Model is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Spine Model is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +############################################################################# + +""" + add_constraint_investment_group_minimum_entities_invested_available!(m::Model) + +Force number of entities invested available in a group to be greater than the minimum. +""" +function add_constraint_investment_group_minimum_entities_invested_available!(m::Model) + t0 = _analysis_time(m) + m.ext[:spineopt].constraints[:investment_group_minimum_entities_invested_available] = Dict( + (investment_group=ig, stochastic_scenario=s, t=t) => @constraint( + m, + _group_entities_invested_available(m, ig, s, t) + >= + minimum_entities_invested_available[(investment_group=ig, stochastic_scenario=s, analysis_time=t0, t=t)] + ) + for ig in indices(minimum_entities_invested_available) + for (s, t) in _entities_invested_available_s_t(m) + ) +end + +""" + add_constraint_investment_group_maximum_entities_invested_available!(m::Model) + +Force number of entities invested available in a group to be lower than the maximum. +""" +function add_constraint_investment_group_maximum_entities_invested_available!(m::Model) + t0 = _analysis_time(m) + m.ext[:spineopt].constraints[:investment_group_maximum_entities_invested_available] = Dict( + (investment_group=ig, stochastic_scenario=s, t=t) => @constraint( + m, + _group_entities_invested_available(m, ig, s, t) + <= + maximum_entities_invested_available[(investment_group=ig, stochastic_scenario=s, analysis_time=t0, t=t)] + ) + for ig in indices(maximum_entities_invested_available) + for (s, t) in _entities_invested_available_s_t(m) + ) +end + +function _entities_invested_available_s_t(m) + [ + (stochastic_scenario=s, t=t) + for (t, path) in t_lowest_resolution_path( + m, + vcat( + units_invested_available_indices(m), + connections_invested_available_indices(m), + storages_invested_available_indices(m) + ) + ) + for s in path + ] +end + +function _group_entities_invested_available(m, ig, s, t) + @fetch ( + units_invested_available, connections_invested_available, storages_invested_available + ) = m.ext[:spineopt].variables + ( + + expr_sum( + units_invested_available[u, s, t] + for (u, s, t) in units_invested_available_indices( + m; unit=unit__investment_group(investment_group=ig), stochastic_scenario=s, t=t_in_t(m; t_long=t) + ); + init=0 + ) + + expr_sum( + connections_invested_available[conn, s, t] + for (conn, s, t) in connections_invested_available_indices( + m; + connection=connection__investment_group(investment_group=ig), + stochastic_scenario=s, + t=t_in_t(m; t_long=t) + ); + init=0 + ) + + expr_sum( + storages_invested_available[n, s, t] + for (n, s, t) in storages_invested_available_indices( + m; node=node__investment_group(investment_group=ig), stochastic_scenario=s, t=t_in_t(m; t_long=t) + ); + init=0 + ) + ) +end \ No newline at end of file diff --git a/src/constraints/constraint_entity_investment_group.jl b/src/constraints/constraint_investment_group_equal_investments.jl similarity index 68% rename from src/constraints/constraint_entity_investment_group.jl rename to src/constraints/constraint_investment_group_equal_investments.jl index 67b90f06f1..66d9229395 100644 --- a/src/constraints/constraint_entity_investment_group.jl +++ b/src/constraints/constraint_investment_group_equal_investments.jl @@ -18,70 +18,78 @@ ############################################################################# """ - add_constraint_entity_investment_group!(m::Model) + add_constraint_investment_group_equal_investments!(m::Model) Force investment variables for first entity in the group and all other entities in the group to be equal. """ -function add_constraint_entity_investment_group!(m::Model) +function add_constraint_investment_group_equal_investments!(m::Model) @fetch ( units_invested_available, connections_invested_available, storages_invested_available ) = m.ext[:spineopt].variables - m.ext[:spineopt].constraints[:entity_investment_group] = Dict( + + m.ext[:spineopt].constraints[:investment_group_equal_investments] = Dict( (investment_group=ig, entity1=e, entity2=other_e, stochastic_scenario=s, t=t) => @constraint( m, + expr_sum( units_invested_available[e, s, t] - for (e, s, t) in units_invested_available_indices(m; unit=e, stochastic_scenario=s, t=t); + for (e, s, t) in units_invested_available_indices( + m; unit=e, stochastic_scenario=s, t=t_in_t(m; t_long=t) + ); init=0 ) + expr_sum( connections_invested_available[e, s, t] for (e, s, t) in connections_invested_available_indices( - m; connection=e, stochastic_scenario=s, t=t + m; connection=e, stochastic_scenario=s, t=t_in_t(m; t_long=t) ); init=0 ) + expr_sum( storages_invested_available[e, s, t] - for (e, s, t) in storages_invested_available_indices(m; node=e, stochastic_scenario=s, t=t); + for (e, s, t) in storages_invested_available_indices( + m; node=e, stochastic_scenario=s, t=t_in_t(m; t_long=t) + ); init=0 ) == + expr_sum( units_invested_available[other_e, s, t] - for (other_e, s, t) in units_invested_available_indices(m; unit=other_e, stochastic_scenario=s, t=t); + for (other_e, s, t) in units_invested_available_indices( + m; unit=other_e, stochastic_scenario=s, t=t_in_t(m; t_long=t) + ); init=0 ) + expr_sum( connections_invested_available[other_e, s, t] for (other_e, s, t) in connections_invested_available_indices( - m; connection=other_e, stochastic_scenario=s, t=t + m; connection=other_e, stochastic_scenario=s, t=t_in_t(m; t_long=t) ); init=0 ) + expr_sum( storages_invested_available[other_e, s, t] - for (other_e, s, t) in storages_invested_available_indices(m; node=other_e, stochastic_scenario=s, t=t); + for (other_e, s, t) in storages_invested_available_indices( + m; node=other_e, stochastic_scenario=s, t=t_in_t(m; t_long=t) + ); init=0 ) ) - for ig in investment_group() + for ig in investment_group(equal_investments=true) for e in _first_entity_investment_group(ig) - for (e, s, t) in vcat( - units_invested_available_indices(m; unit=e), - connections_invested_available_indices(m; connection=e), - storages_invested_available_indices(m; node=e) - ) - for other_e in setdiff(_all_entities_investment_group(ig), e) - for (other_e, s, t) in vcat( - units_invested_available_indices(m; unit=other_e, stochastic_scenario=s, t=t), - connections_invested_available_indices(m; connection=other_e, stochastic_scenario=s, t=t), - storages_invested_available_indices(m; node=other_e, stochastic_scenario=s, t=t), + for other_e in setdiff(entity_investment_group(ig), e) + for (t, path) in t_lowest_resolution_path( + m, + vcat( + units_invested_available_indices(m; unit=[e, other_e]), + connections_invested_available_indices(m; connection=[e, other_e]), + storages_invested_available_indices(m; node=[e, other_e]) + ) ) + for s in path ) end -function _all_entities_investment_group(ig) +function entity_investment_group(ig) vcat( unit__investment_group(investment_group=ig), connection__investment_group(investment_group=ig), @@ -90,6 +98,6 @@ function _all_entities_investment_group(ig) end function _first_entity_investment_group(ig) - entities = _all_entities_investment_group(ig) + entities = entity_investment_group(ig) isempty(entities) ? () : first(entities) end \ No newline at end of file diff --git a/src/constraints/constraint_mp_any_invested_cuts.jl b/src/constraints/constraint_mp_any_invested_cuts.jl index f1905b674d..d73c5e2fc7 100644 --- a/src/constraints/constraint_mp_any_invested_cuts.jl +++ b/src/constraints/constraint_mp_any_invested_cuts.jl @@ -34,9 +34,9 @@ function add_constraint_mp_any_invested_cuts!(m::Model) merge!( get!(m.ext[:spineopt].constraints, :mp_any_invested_cut, Dict()), Dict( - (benders_iteration=bi, t=t1) => @constraint( + (benders_iteration=bi, t=t) => @constraint( m, - + sp_objective_upperbound[t1] + + sp_objective_upperbound[t] >= + sp_objective_value_bi(benders_iteration=bi) # operating cost benefit from investments in units @@ -71,7 +71,7 @@ function add_constraint_mp_any_invested_cuts!(m::Model) ) ) for bi in last(benders_iteration()) - for (t1,) in sp_objective_upperbound_indices(m) + for (t,) in sp_objective_upperbound_indices(m) ) ) end \ No newline at end of file diff --git a/src/constraints/constraint_mp_min_res_gen_to_demand_ratio.jl b/src/constraints/constraint_mp_min_res_gen_to_demand_ratio.jl index ed8348450f..5754768afc 100644 --- a/src/constraints/constraint_mp_min_res_gen_to_demand_ratio.jl +++ b/src/constraints/constraint_mp_min_res_gen_to_demand_ratio.jl @@ -38,23 +38,30 @@ function add_constraint_mp_min_res_gen_to_demand_ratio!(m::Model) (commodity=comm,) => @constraint( m, + sum( - window_sum(sp_unit_flow(unit=u, node=n, direction=d, stochastic_scenario=s), current_window(m)) + window_sum_duration(m, sp_unit_flow(unit=u, node=n, direction=d, stochastic_scenario=s), window) + for window in m.ext[:spineopt].temporal_structure[:sp_windows] for (u, s) in unit_stochastic_indices(m; unit=unit(is_renewable=true)) for (u, n, d) in unit__to_node(unit=u, node=node__commodity(commodity=comm), _compact=false); init=0 ) + sum( - ( + sum( + units_invested_available[u, s, t] - internal_fix_units_invested_available(unit=u, stochastic_scenario=s, t=t, _default=0) + for (u, s, t) in units_invested_available_indices( + m; unit=u, stochastic_scenario=s, t=to_time_slice(m; t=window) + ); + init=0 ) - * window_sum( + * window_sum_duration( + m, + unit_availability_factor(unit=u, stochastic_scenario=s) * unit_capacity(unit=u, node=n, direction=d, stochastic_scenario=s) * unit_conv_cap_to_flow(unit=u, node=n, direction=d, stochastic_scenario=s), - t + window ) - for (u, s, t) in units_invested_available_indices(m; unit=unit(is_renewable=true)) + for window in m.ext[:spineopt].temporal_structure[:sp_windows] + for (u, s) in unit_stochastic_indices(m; unit=unit(is_renewable=true)) for (u, n, d) in unit__to_node(unit=u, node=node__commodity(commodity=comm), _compact=false); init=0, ) @@ -63,17 +70,20 @@ function add_constraint_mp_min_res_gen_to_demand_ratio!(m::Model) + mp_min_res_gen_to_demand_ratio(commodity=comm) * ( sum( - window_sum(demand(node=n, stochastic_scenario=s), current_window(m)) + window_sum_duration(m, demand(node=n, stochastic_scenario=s), window) + for window in m.ext[:spineopt].temporal_structure[:sp_windows] for (n, s) in node_stochastic_indices( m; node=intersect(indices(demand), node__commodity(commodity=comm)) ); init=0 ) + sum( - window_sum( + window_sum_duration( + m, fractional_demand(node=n, stochastic_scenario=s) * demand(node=ng, stochastic_scenario=s), - current_window(m) + window ) + for window in m.ext[:spineopt].temporal_structure[:sp_windows] for (n, s) in node_stochastic_indices( m; node=intersect(indices(fractional_demand), node__commodity(commodity=comm)) ) @@ -84,4 +94,4 @@ function add_constraint_mp_min_res_gen_to_demand_ratio!(m::Model) ) for comm in indices(mp_min_res_gen_to_demand_ratio) ) -end +end \ No newline at end of file diff --git a/src/constraints/constraint_unit_flow_op_bounds.jl b/src/constraints/constraint_unit_flow_op_bounds.jl index d93dad47e7..371355a7ab 100644 --- a/src/constraints/constraint_unit_flow_op_bounds.jl +++ b/src/constraints/constraint_unit_flow_op_bounds.jl @@ -32,6 +32,10 @@ function add_constraint_unit_flow_op_bounds!(m::Model) + unit_flow_op[u, n, d, op, s, t] <= ( + ordered_unit_flow_op(unit = u, node=n, direction=d, _default=false) ? + unit_flow_op_active[u, n, d, op, s, t] : units_on[u, s, t] + ) + * ( + operating_points[(unit=u, node=n, direction=d, stochastic_scenario=s, analysis_time=t0, i=op)] - ( (op > 1) ? operating_points[ @@ -39,14 +43,11 @@ function add_constraint_unit_flow_op_bounds!(m::Model) ] : 0 ) ) - * ( - ordered_unit_flow_op(unit = u, node=n, direction=d, _default=false) ? - unit_flow_op_active[u, n, d, op, s, t] : units_on[u, s, t] - ) * unit_availability_factor[(unit=u, stochastic_scenario=s, analysis_time=t0, t=t)] * unit_capacity[(unit=u, node=n, direction=d, stochastic_scenario=s, analysis_time=t0, t=t)] * unit_conv_cap_to_flow[(unit=u, node=n, direction=d, stochastic_scenario=s, analysis_time=t0, t=t)] - ) for (u, n, d) in indices(unit_capacity) + ) + for (u, n, d) in indices(unit_capacity) for (u, n, d, op, s, t) in unit_flow_op_indices(m; unit=u, node=n, direction=d) ) end diff --git a/src/constraints/constraint_units_available.jl b/src/constraints/constraint_units_available.jl index 410dd0aba5..38f1fc534a 100644 --- a/src/constraints/constraint_units_available.jl +++ b/src/constraints/constraint_units_available.jl @@ -32,15 +32,15 @@ function add_constraint_units_available!(m::Model) units_available[u, s, t] for (u, s, t) in units_on_indices(m; unit=u, stochastic_scenario=s, t=t); init=0, ) - <= - + number_of_units[(unit=u, stochastic_scenario=s, analysis_time=t0, t=t)] - + expr_sum( + - expr_sum( units_invested_available[u, s, t1] for (u, s, t1) in units_invested_available_indices( m; unit=u, stochastic_scenario=s, t=t_overlaps_t(m; t=t) ); init=0, ) + <= + number_of_units[(unit=u, stochastic_scenario=s, analysis_time=t0, t=t)] ) for (u, s, t) in constraint_units_available_indices(m) ) diff --git a/src/data_structure/benders_data.jl b/src/data_structure/benders_data.jl index 4e322448c0..fd0bc27bd4 100644 --- a/src/data_structure/benders_data.jl +++ b/src/data_structure/benders_data.jl @@ -76,7 +76,7 @@ end function process_subproblem_solution!(m, win_weight) _save_sp_marginal_values!(m, win_weight) _save_sp_objective_value!(m, win_weight) - _save_sp_unit_flow!(m, win_weight) + _save_sp_unit_flow!(m) _save_sp_solution!(m) end @@ -123,9 +123,9 @@ function _save_sp_objective_value!(m, win_weight) ) end -function _save_sp_unit_flow!(m, win_weight, tail=false) +function _save_sp_unit_flow!(m, tail=false) window_values = Dict( - k => win_weight * v for (k, v) in m.ext[:spineopt].values[:unit_flow] if iscontained(k.t, current_window(m)) + k => v for (k, v) in m.ext[:spineopt].values[:unit_flow] if iscontained(k.t, current_window(m)) ) pval_by_ent = _pval_by_entity(window_values) pvals_to_node = Dict( @@ -162,7 +162,7 @@ function save_mp_objective_bounds_and_gap!(m_mp) sp_obj_val = sp_objective_value_bi(benders_iteration=current_bi, _default=0) invest_costs = value(realize(total_costs(m_mp, anything; operations=false))) obj_ub = m_mp.ext[:spineopt].objective_upper_bound[] = sp_obj_val + invest_costs - gap = 2 * (obj_ub - obj_lb) / (obj_ub + obj_lb) + gap = (obj_ub == obj_lb) ? 0 : 2 * (obj_ub - obj_lb) / (obj_ub + obj_lb) push!(m_mp.ext[:spineopt].benders_gaps, gap) end diff --git a/src/data_structure/postprocess_results.jl b/src/data_structure/postprocess_results.jl index 964df4b994..353b786655 100644 --- a/src/data_structure/postprocess_results.jl +++ b/src/data_structure/postprocess_results.jl @@ -36,12 +36,14 @@ function postprocess_results!(m::Model) end function save_connection_avg_throughflow!(m::Model) - @fetch connection_flow = m.ext[:spineopt].values + connection_flow = get(m.ext[:spineopt].values, :connection_flow, nothing) + connection_flow === nothing && return _save_connection_avg_throughflow!(m, :connection_avg_throughflow, connection_flow) end function save_connection_avg_intact_throughflow!(m::Model) - @fetch connection_intact_flow = m.ext[:spineopt].values + connection_intact_flow = get(m.ext[:spineopt].values, :connection_intact_flow, nothing) + connection_intact_flow === nothing && return _save_connection_avg_throughflow!(m, :connection_avg_intact_throughflow, connection_intact_flow) end @@ -83,7 +85,8 @@ function _contingency_is_binding(m, connection_flow, conn_cont, conn_mon, s, t) end function save_contingency_is_binding!(m::Model) - @fetch connection_flow = m.ext[:spineopt].values + connection_flow = get(m.ext[:spineopt].values, :connection_flow, nothing) + connection_flow === nothing && return m.ext[:spineopt].values[:contingency_is_binding] = Dict( ( connection_contingency=conn_cont, connection_monitored=conn_mon, stochastic_path=s, t=t diff --git a/src/data_structure/preprocess_data_structure.jl b/src/data_structure/preprocess_data_structure.jl index bd4b0fe1ef..ccc4119859 100644 --- a/src/data_structure/preprocess_data_structure.jl +++ b/src/data_structure/preprocess_data_structure.jl @@ -289,7 +289,7 @@ function _build_ptdf(connections, nodes, unavailable_connections=Set()) n_from, n_to = connection__from_node(connection=conn, direction=anything) A[node_numbers[n_from], ix] = 1 A[node_numbers[n_to], ix] = -1 - reactance = max(connection_reactance(connection=conn), 1e-6) + reactance = max(connection_reactance(connection=conn, _default=0), 1e-6) if conn in unavailable_connections reactance *= 1e3 end diff --git a/src/data_structure/temporal_structure.jl b/src/data_structure/temporal_structure.jl index 5f449d5733..4fee1e147b 100644 --- a/src/data_structure/temporal_structure.jl +++ b/src/data_structure/temporal_structure.jl @@ -85,6 +85,21 @@ function _model_duration_unit(instance::Object) get(Dict(:minute => Minute, :hour => Hour), duration_unit(model=instance, _strict=false), Minute) end +function _model_window_duration(instance) + m_start = model_start(model=instance) + m_end = model_end(model=instance) + m_duration = m_end - m_start + w_duration = window_duration(model=instance, _strict=false) + if w_duration === nothing + w_duration = roll_forward(model=instance, i=1, _strict=false) + end + if w_duration === nothing || m_start + w_duration > m_end + m_duration + else + w_duration + end +end + """ _generate_current_window!(m::Model) @@ -93,12 +108,9 @@ Generate the current window TimeSlice for given model. function _generate_current_window!(m::Model) instance = m.ext[:spineopt].instance w_start = model_start(model=instance) - m_end = model_end(model=instance) - w_duration = window_duration(model=instance, _strict=false) - w_duration = w_duration !== nothing ? w_duration : roll_forward(model=instance, i=1, _strict=false) - w_end = w_duration === nothing ? m_end : min(w_start + w_duration, m_end) + w_end = w_start + _model_window_duration(instance) m.ext[:spineopt].temporal_structure[:current_window] = TimeSlice( - w_start, w_end; duration_unit=_model_duration_unit(m.ext[:spineopt].instance) + w_start, w_end; duration_unit=_model_duration_unit(instance) ) end @@ -407,6 +419,25 @@ function _generate_representative_time_slice!(m::Model) end end +function _generate_sp_windows!(m::Model) + instance = m.ext[:spineopt].instance + w_start = model_start(model=instance) + w_duration = _model_window_duration(instance) + w_end = w_start + w_duration + m.ext[:spineopt].temporal_structure[:sp_windows] = windows = [] + push!(windows, TimeSlice(w_start, w_end; duration_unit=_model_duration_unit(instance))) + i = 1 + while true + rf = roll_forward(model=instance, i=i, _strict=false) + rf in (nothing, Minute(0)) && break + w_end >= model_end(model=instance) && break + w_start += rf + w_end += rf + push!(windows, TimeSlice(w_start, w_end; duration_unit=_model_duration_unit(instance))) + i += 1 + end +end + """ Find indices in `source` that overlap `t` and return values for those indices in `target`. Used by `to_time_slice`. @@ -459,6 +490,7 @@ Create the master problem temporal structure for SpineOpt benders. """ function generate_master_temporal_structure!(m_mp::Model) _generate_master_window_and_time_slice!(m_mp) + _generate_sp_windows!(m_mp) _generate_output_time_slices!(m_mp) _generate_time_slice_relationships!(m_mp) end diff --git a/src/run_spineopt_benders.jl b/src/run_spineopt_benders.jl index 07dfcc94ce..a77599cba0 100644 --- a/src/run_spineopt_benders.jl +++ b/src/run_spineopt_benders.jl @@ -45,11 +45,18 @@ function rerun_spineopt_benders!( log_level=log_level ) _init_mp_model!(m_mp; log_level=log_level) + min_benders_iterations = min_iterations(model=m_mp.ext[:spineopt].instance) max_benders_iterations = max_iterations(model=m_mp.ext[:spineopt].instance) j = 1 + undo_force_starting_investments! = nothing while optimize @log log_level 0 "\nStarting Benders iteration $j" - optimize_model!(m_mp; log_level=log_level, save_outputs=false) || break + if j == 1 + undo_force_starting_investments! = _force_starting_investments!(m_mp) + elseif j == 2 + undo_force_starting_investments!() + end + optimize_model!(m_mp; log_level=log_level) || break @timelog log_level 2 "Processing master problem solution" process_master_problem_solution!(m_mp) k = 1 subproblem_solved = nothing @@ -85,7 +92,7 @@ function rerun_spineopt_benders!( @log log_level 1 "Objective upper bound: $(@sprintf("%.5e", m_mp.ext[:spineopt].objective_upper_bound[])); " gap = last(m_mp.ext[:spineopt].benders_gaps) @log log_level 1 "Gap: $(@sprintf("%1.4f", gap * 100))%" - if gap <= max_gap(model=m_mp.ext[:spineopt].instance) + if gap <= max_gap(model=m_mp.ext[:spineopt].instance) && j >= min_benders_iterations @log log_level 1 "Benders tolerance satisfied, terminating..." break end @@ -101,6 +108,7 @@ function rerun_spineopt_benders!( j += 1 global current_bi = add_benders_iteration(j) end + write_report(m_mp, url_out; alternative=alternative, log_level=log_level) write_report(m, url_out; alternative=alternative, log_level=log_level) m end @@ -111,8 +119,8 @@ Initialize the given model for SpineOpt Master Problem: add variables, add const function _init_mp_model!(m; log_level=3) @timelog log_level 2 "Adding MP variables...\n" _add_mp_variables!(m; log_level=log_level) @timelog log_level 2 "Adding MP constraints...\n" _add_mp_constraints!(m; log_level=log_level) - #@timelog log_level 2 "Adding MP renewing constraints...\n" _add_mp_renewing_constraints!(m; log_level=log_level) @timelog log_level 2 "Setting MP objective..." _set_mp_objective!(m) + _init_outputs!(m) end """ @@ -152,7 +160,11 @@ function _add_mp_constraints!(m; log_level=3) add_constraint_storage_lifetime!, add_constraint_storages_invested_transition!, add_constraint_storages_invested_available!, - add_constraint_entity_investment_group!, + add_constraint_investment_group_equal_investments!, + add_constraint_investment_group_minimum_entities_invested_available!, + add_constraint_investment_group_maximum_entities_invested_available!, + add_constraint_investment_group_minimum_capacity_invested_available!, + add_constraint_investment_group_maximum_capacity_invested_available!, ) name = name_from_fn(add_constraint!) @timelog log_level 3 "- [$name]" add_constraint!(m) @@ -185,14 +197,23 @@ Minimize total investment costs plus upperbound on subproblem objective. """ function _set_mp_objective!(m::Model) @fetch sp_objective_upperbound = m.ext[:spineopt].variables + _create_mp_objective_terms!(m) + investment_costs = sum(in_window for (in_window, _bw) in values(m.ext[:spineopt].objective_terms)) @objective( m, Min, + expr_sum(sp_objective_upperbound[t] for (t,) in sp_objective_upperbound_indices(m); init=0) - + total_costs(m, anything; operations=false) + + investment_costs ) end +function _create_mp_objective_terms!(m) + for term in objective_terms(m; operations=false) + func = eval(term) + m.ext[:spineopt].objective_terms[term] = (func(m, anything), 0) + end +end + """ Add benders cuts to master problem. """ @@ -217,3 +238,40 @@ end _unfix(v::VariableRef) = is_fixed(v) && unfix(v) _unfix(::Call) = nothing + +""" +Force starting investments and return a function to be called without arguments to undo the operation. +""" +function _force_starting_investments!(m::Model) + callbacks = vcat( + _do_force_starting_investments!(m, :units_invested_available, benders_starting_units_invested), + _do_force_starting_investments!(m, :connections_invested_available, benders_starting_connections_invested), + _do_force_starting_investments!(m, :storages_invested_available, benders_starting_storages_invested), + ) + () -> for c in callbacks c() end +end + +function _do_force_starting_investments!(m::Model, variable_name::Symbol, benders_starting_invested::Parameter) + callbacks = [] + for (ind, var) in m.ext[:spineopt].variables[variable_name] + start(ind.t) >= start(current_window(m)) || continue + starting_invested = benders_starting_invested(; ind..., _strict=false) + starting_invested === nothing && continue + push!(callbacks, () -> unfix(var)) + if has_lower_bound(var) + x = lower_bound(var) + push!(callbacks, () -> set_lower_bound(var, x)) + end + if has_upper_bound(var) + x = upper_bound(var) + push!(callbacks, () -> set_upper_bound(var, x)) + end + if is_fixed(var) + x = fix_value(var) + push!(callbacks, () -> fix(var, x; force=true)) + end + fix(var, starting_invested; force=true) + end + callbacks +end + diff --git a/src/run_spineopt_standard.jl b/src/run_spineopt_standard.jl index 243a82bd05..9cceeb1b50 100644 --- a/src/run_spineopt_standard.jl +++ b/src/run_spineopt_standard.jl @@ -209,7 +209,11 @@ function _add_constraints!(m; add_constraints=m -> nothing, log_level=3) add_constraint_node_voltage_angle!, add_constraint_max_node_voltage_angle!, add_constraint_min_node_voltage_angle!, - add_constraint_entity_investment_group!, + add_constraint_investment_group_equal_investments!, + add_constraint_investment_group_minimum_entities_invested_available!, + add_constraint_investment_group_maximum_entities_invested_available!, + add_constraint_investment_group_minimum_capacity_invested_available!, + add_constraint_investment_group_maximum_capacity_invested_available!, ) name = name_from_fn(add_constraint!) @timelog log_level 3 "- [$name]" add_constraint!(m) @@ -330,7 +334,7 @@ end Optimize the given model. If an optimal solution is found, save results and return `true`, otherwise return `false`. """ -function optimize_model!(m::Model; log_level=3, calculate_duals=false, save_outputs=true, iterations=nothing) +function optimize_model!(m::Model; log_level=3, calculate_duals=false, iterations=nothing) write_mps_file(model=m.ext[:spineopt].instance) == :write_mps_always && write_to_file(m, "model_diagnostics.mps") # NOTE: The above results in a lot of Warning: Variable connection_flow[...] is mentioned in BOUNDS, # but is not mentioned in the COLUMNS section. @@ -345,10 +349,8 @@ function optimize_model!(m::Model; log_level=3, calculate_duals=false, save_outp m; iterations=iterations ) calculate_duals && _calculate_duals(m; log_level=log_level) - if save_outputs - @timelog log_level 2 "Postprocessing results..." postprocess_results!(m) - @timelog log_level 2 "Saving outputs..." _save_outputs!(m; iterations=iterations) - end + @timelog log_level 2 "Postprocessing results..." postprocess_results!(m) + @timelog log_level 2 "Saving outputs..." _save_outputs!(m; iterations=iterations) else m.ext[:spineopt].has_results[] = false @warn "no solution available for window $(current_window(m)) - moving on..." @@ -580,7 +582,7 @@ function _value_by_entity_non_aggregated(m, value::Dict, crop_to_window) analysis_time = start(current_window(m)) for (ind, val) in value t_keys = collect(_time_slice_keys(ind)) - t = maximum(ind[k] for k in t_keys) + t = !isempty(t_keys) ? maximum(ind[k] for k in t_keys) : current_window(m) t <= analysis_time && continue crop_to_window && start(t) >= end_(current_window(m)) && continue entity = _drop_key(ind, t_keys...) diff --git a/src/util/misc.jl b/src/util/misc.jl index f0fa909064..cfcfe29deb 100644 --- a/src/util/misc.jl +++ b/src/util/misc.jl @@ -201,5 +201,14 @@ function print_solution(m, variable_patterns...) println() end +function window_sum_duration(m, ts::TimeSeries, window; init=0) + dur_unit = _model_duration_unit(m.ext[:spineopt].instance) + time_slice_value_iter = ( + (TimeSlice(t1, t2; duration_unit=dur_unit), v) for (t1, t2, v) in zip(ts.indexes, ts.indexes[2:end], ts.values) + ) + sum(v * duration(t) for (t, v) in time_slice_value_iter if iscontained(start(t), window) && !isnan(v); init=init) +end +window_sum_duration(m, x::Number, window; init=0) = x * duration(window) + init + window_sum(ts::TimeSeries, window; init=0) = sum(v for (t, v) in ts if iscontained(t, window) && !isnan(v); init=init) window_sum(x::Number, window; init=0) = x + init diff --git a/src/variables/variable_connections_invested_available.jl b/src/variables/variable_connections_invested_available.jl index 8f99b78c36..a8e030156a 100644 --- a/src/variables/variable_connections_invested_available.jl +++ b/src/variables/variable_connections_invested_available.jl @@ -34,7 +34,9 @@ function connections_invested_available_indices( unique( (connection=conn, stochastic_scenario=s, t=t) for (conn, tb) in connection__investment_temporal_block( - connection=connection, temporal_block=temporal_block, _compact=false + connection=intersect(indices(candidate_connections), connection), + temporal_block=temporal_block, + _compact=false ) for (conn, s, t) in connection_investment_stochastic_time_indices( m; connection=conn, stochastic_scenario=stochastic_scenario, temporal_block=tb, t=t diff --git a/src/variables/variable_sp_objective_upperbound.jl b/src/variables/variable_sp_objective_upperbound.jl index 39bce6a67f..1c56c49cd5 100644 --- a/src/variables/variable_sp_objective_upperbound.jl +++ b/src/variables/variable_sp_objective_upperbound.jl @@ -21,6 +21,6 @@ function add_variable_sp_objective_upperbound!(m::Model) add_variable!(m, :sp_objective_upperbound, sp_objective_upperbound_indices) end -function sp_objective_upperbound_indices(m::Model; t=anything, temporal_block=anything) - [(t=first(time_slice(m)),)] +function sp_objective_upperbound_indices(m::Model; kwargs...) + [(t=current_window(m),)] end diff --git a/src/variables/variable_storages_invested_available.jl b/src/variables/variable_storages_invested_available.jl index 812507cef6..6fd3b9befb 100644 --- a/src/variables/variable_storages_invested_available.jl +++ b/src/variables/variable_storages_invested_available.jl @@ -33,7 +33,9 @@ function storages_invested_available_indices( node=members(node) unique( (node=n, stochastic_scenario=s, t=t) - for (n, tb) in node__investment_temporal_block(node=node, temporal_block=temporal_block, _compact=false) + for (n, tb) in node__investment_temporal_block( + node=intersect(indices(candidate_storages), node), temporal_block=temporal_block, _compact=false + ) for (n, s, t) in node_investment_stochastic_time_indices( m; node=n, stochastic_scenario=stochastic_scenario, temporal_block=tb, t=t ) diff --git a/src/variables/variable_units_invested_available.jl b/src/variables/variable_units_invested_available.jl index d43384893b..51fa87b04c 100644 --- a/src/variables/variable_units_invested_available.jl +++ b/src/variables/variable_units_invested_available.jl @@ -33,7 +33,8 @@ function units_invested_available_indices( unit = members(unit) unique( (unit=u, stochastic_scenario=s, t=t) - for (u, tb) in unit__investment_temporal_block(unit=unit, temporal_block=temporal_block, _compact=false) + for (u, tb) in unit__investment_temporal_block( + unit=intersect(indices(candidate_units), unit), temporal_block=temporal_block, _compact=false) for (u, s, t) in unit_investment_stochastic_time_indices( m; unit=u, stochastic_scenario=stochastic_scenario, temporal_block=tb, t=t ) diff --git a/templates/spineopt_template.json b/templates/spineopt_template.json index 6a09f9ed4f..a52a88aea7 100644 --- a/templates/spineopt_template.json +++ b/templates/spineopt_template.json @@ -16,10 +16,13 @@ ], "relationship_classes": [ ["connection__from_node", ["connection", "node"], "Defines the `nodes` the `connection` can take input from, and holds most `connection_flow` variable specific parameters.", 280378317271897], + ["connection__from_node__investment_group", ["connection", "node", "investment_group"], "Indicates which connection capacities are included in the capacity invested available of an investment group"], ["connection__from_node__user_constraint", ["connection", "node", "user_constraint"], "when specified this relationship allows the relevant flow connection flow variable to be included in the specified user constraint"], + ["connection__investment_group", ["connection", "investment_group"], "Indicates that a `connection` belongs in an `investment_group`."], ["connection__investment_stochastic_structure", ["connection", "stochastic_structure"], "Defines the stochastic structure of the connections investments variable"], ["connection__investment_temporal_block", ["connection", "temporal_block"], "Defines the temporal resolution of the connections investments variable"], ["connection__node__node", ["connection", "node", "node"], "Holds parameters spanning multiple `connection_flow` variables to and from multiple `nodes`."], + ["connection__to_node__investment_group", ["connection", "node", "investment_group"], "Indicates which connection capacities are included in the capacity invested available of an investment group"], ["connection__to_node", ["connection", "node"], "Defines the `nodes` the `connection` can output to, and holds most `connection_flow` variable specific parameters.", 280378317271898], ["connection__to_node__user_constraint", ["connection", "node", "user_constraint"], "when specified this relationship allows the relevant flow connection flow variable to be included in the specified user constraint"], ["connection__user_constraint", ["connection", "user_constraint"], "Relationship required to involve a connections investment variables in a user_constraint"], @@ -31,6 +34,7 @@ ["model__stochastic_structure", ["model", "stochastic_structure"], "Defines which `stochastic_structure`s are included in which `model`s."], ["model__temporal_block", ["model", "temporal_block"], "Defines which `temporal_block`s are included in which `model`s."], ["node__commodity", ["node", "commodity"], "Define a `commodity` for a `node`. Only a single `commodity` is permitted per `node`"], + ["node__investment_group", ["node", "investment_group"], "Indicates that a `node` belongs in a `investment_group`."], ["node__investment_stochastic_structure", ["node", "stochastic_structure"], "defines the stochastic structure for node related investments, currently only storages"], ["node__investment_temporal_block", ["node", "temporal_block"], "defines the temporal resolution for node related investments, currently only storages"], ["node__node", ["node", "node"], "Holds parameters for direct interactions between two `nodes`, e.g. `node_state` diffusion coefficients."], @@ -42,17 +46,17 @@ ["stochastic_structure__stochastic_scenario", ["stochastic_structure", "stochastic_scenario"], "Defines which `stochastic_scenarios` are included in which `stochastic_structure`, and holds the parameters required for realizing the structure in combination with the `temporal_blocks`."], ["unit__commodity", ["unit", "commodity"], "Holds parameters for `commodities` used by the `unit`."], ["unit__from_node", ["unit", "node"], "Defines the `nodes` the `unit` can take input from, and holds most `unit_flow` variable specific parameters.", 281470681805657], + ["unit__from_node__investment_group", ["unit", "node", "investment_group"], "Indicates which unit capacities are included in the capacity invested available of an investment group"], ["unit__from_node__user_constraint", ["unit", "node", "user_constraint"], "Defines which input `unit_flows` are included in the `user_constraint`, and holds their parameters."], + ["unit__investment_group", ["unit", "investment_group"], "Indicates that a `unit` belongs in an `investment_group`."], ["unit__investment_stochastic_structure", ["unit", "stochastic_structure"], "Sets the stochastic structure for investment decisions - overrides `model__default_investment_stochastic_structure`."], ["unit__investment_temporal_block", ["unit", "temporal_block"], "Sets the temporal resolution of investment decisions - overrides `model__default_investment_temporal_block`"], ["unit__node__node", ["unit", "node", "node"], "Holds parameters spanning multiple `unit_flow` variables to and from multiple `nodes`."], ["unit__to_node", ["unit", "node"], "Defines the `nodes` the `unit` can output to, and holds most `unit_flow` variable specific parameters.", 281470681805658], + ["unit__to_node__investment_group", ["unit", "node", "investment_group"], "Indicates which unit capacities are included in the capacity invested available of an investment group"], ["unit__to_node__user_constraint", ["unit", "node", "user_constraint"], "Defines which output `unit_flows` are included in the `user_constraint`, and holds their parameters."], ["unit__user_constraint", ["unit", "user_constraint"], "Defines which `units_on` variables are included in the `user_constraint`, and holds their parameters."], ["units_on__stochastic_structure", ["unit", "stochastic_structure"], "Defines which specific `stochastic_structure` is used for the `units_on` variable of the `unit`. Only one `stochastic_structure` is permitted per `unit`."], - ["unit__investment_group", ["unit", "investment_group"], "Indicates that a `unit` belongs in an `investment_group`."], - ["connection__investment_group", ["connection", "investment_group"], "Indicates that a `connection` belongs in an `investment_group`."], - ["node__investment_group", ["node", "investment_group"], "Indicates that a `node` belongs in a `investment_group`."], ["units_on__temporal_block", ["unit", "temporal_block"], "Defines which specific `temporal_blocks` are used by the `units_on` variable of the `unit`."] ], "parameter_value_lists": [ @@ -135,6 +139,7 @@ ["commodity", "mp_min_res_gen_to_demand_ratio_slack_penalty", null, null, "Penalty for violating the minimum renewable generation to demand ratio."], ["commodity", "is_active", true, "boolean_value_list", "If false, the object is excluded from the model if the tool filter object activity control is specified"], ["connection", "candidate_connections", null, null, "The number of connections that may be invested in"], + ["connection", "benders_starting_connections_invested", null, null, "Fixes the number of connections invested during the first Benders iteration"], ["connection", "forced_availability_factor", null, null, "Availability factor due to outages/deratings."], ["connection", "connection_availability_factor", 1.0, null, "Availability of the `connection`, acting as a multiplier on its `connection_capacity`. Typically between 0-1."], ["connection", "connection_contingency", null, "boolean_value_list", "A boolean flag for defining a contingency `connection`."], @@ -156,9 +161,15 @@ ["connection", "connections_invested_big_m_mga", null, null, "big_m_mga should be chosen as small as possible but sufficiently large. For units_invested_mga an appropriate big_m_mga would be twice the candidate connections."], ["connection", "connections_invested_mga", false, "boolean_value_list", "Defines whether a certain variable (here: connections_invested) will be considered in the maximal-differences of the mga objective"], ["connection", "connections_invested_mga_weight", 1, null, "Used to scale mga variables. For weightd sum mga method, the length of this weight given as an Array will determine the number of iterations."], + ["investment_group", "equal_investments", false, "boolean_value_list", "Whether all entities in the group must have the same investment decision."], + ["investment_group", "minimum_entities_invested_available", null, null, "Lower bound on the number of entities invested available in the group at any point in time."], + ["investment_group", "maximum_entities_invested_available", null, null, "Upper bound on the number of entities invested available in the group at any point in time."], + ["investment_group", "minimum_capacity_invested_available", null, null, "Lower bound on the capacity invested available in the group at any point in time."], + ["investment_group", "maximum_capacity_invested_available", null, null, "Upper bound on the capacity invested available in the group at any point in time."], ["model", "duration_unit", "hour", "duration_unit_list", "Defines the base temporal unit of the `model`. Currently supported values are either an `hour` or a `minute`."], ["model", "is_active", true, "boolean_value_list", "If false, the object is excluded from the model if the tool filter object activity control is specified"], ["model", "max_gap", 0.05, null, "Specifies the maximum optimality gap for the model. Currently only used for the master problem within a decomposed structure"], + ["model", "min_iterations", 1.0, null, "Specifies the minimum number of iterations for the model. Currently only used for the master problem within a decomposed structure"], ["model", "max_iterations", 10.0, null, "Specifies the maximum number of iterations for the model. Currently only used for the master problem within a decomposed structure"], ["model", "max_mga_iterations", null, null, "Define the number of mga iterations, i.e. how many alternative solutions will be generated."], ["model", "max_mga_slack", 0.05, null, "Defines the maximum slack by which the alternative solution may differ from the original solution (e.g. 5% more than initial objective function value)"], @@ -178,6 +189,7 @@ ["model", "window_weight", 1, null, "The weight of the window in the rolling subproblem"], ["node", "balance_type", "balance_type_node", "balance_type_list", "A selector for how the `:nodal_balance` constraint should be handled."], ["node", "candidate_storages", null, null, "Determines the maximum number of new storages which may be invested in"], + ["node", "benders_starting_storages_invested", null, null, "Fixes the number of storages invested during the first Benders iteration"], ["node", "demand", 0.0, null, "Demand for the `commodity` of a `node`. Energy gains can be represented using negative `demand`."], ["node", "downward_reserve", false, null, "Identifier for `node`s providing downward reserves"], ["node", "fix_node_state", null, null, "Fixes the corresponding `node_state` variable to the provided value. Can be used for e.g. fixing boundary conditions."], @@ -234,6 +246,7 @@ ["temporal_block", "weight", 1.0, null, "Weighting factor of the temporal block associated with the objective function"], ["temporal_block", "representative_periods_mapping", null, null, "Map from date time to representative temporal block name"], ["unit", "candidate_units", null, null, "Number of units which may be additionally constructed"], + ["unit", "benders_starting_units_invested", null, null, "Fixes the number of units invested during the first Benders iteration"], ["unit", "curtailment_cost", null, null, "Costs for curtailing generation. Essentially, accrues costs whenever `unit_flow` not operating at its maximum available capacity. E.g. EUR/MWh"], ["unit", "fix_units_invested", null, null, "Fix the value of the `units_invested` variable."], ["unit", "fix_units_invested_available", null, null, "Fix the value of the `units_invested_available` variable"], diff --git a/test/constraints/constraint_investment_group.jl b/test/constraints/constraint_investment_group.jl new file mode 100644 index 0000000000..c4ee1da031 --- /dev/null +++ b/test/constraints/constraint_investment_group.jl @@ -0,0 +1,210 @@ + +############################################################################# +# Copyright (C) 2017 - 2018 Spine Project +# +# This file is part of SpineOpt. +# +# SpineOpt is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SpineOpt is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +############################################################################# + +function _test_constraint_investment_group_setup() + url_in = "sqlite://" + test_data = Dict( + :objects => [ + ["model", "instance"], + ["temporal_block", "investments_two_hourly"], + ["temporal_block", "investments_four_hourly"], + ["stochastic_structure", "investments_deterministic"], + ["stochastic_structure", "investments_stochastic"], + ["stochastic_scenario", "parent"], + ["stochastic_scenario", "child"], + ["node", "node_a"], + ["node", "node_b"], + ["node", "node_c"], + ["unit", "unit_ab"], + ["connection", "connection_bc"], + ["investment_group", "ig"], + ], + :relationships => [ + ["model__temporal_block", ["instance", "investments_two_hourly"]], + ["model__temporal_block", ["instance", "investments_four_hourly"]], + ["model__default_temporal_block", ["instance", "investments_four_hourly"]], + ["model__stochastic_structure", ["instance", "investments_deterministic"]], + ["model__stochastic_structure", ["instance", "investments_stochastic"]], + ["model__default_stochastic_structure", ["instance", "investments_stochastic"]], + ["stochastic_structure__stochastic_scenario", ["investments_deterministic", "parent"]], + ["stochastic_structure__stochastic_scenario", ["investments_stochastic", "parent"]], + ["stochastic_structure__stochastic_scenario", ["investments_stochastic", "child"]], + ["parent_stochastic_scenario__child_stochastic_scenario", ["parent", "child"]], + ["node__investment_temporal_block", ["node_a", "investments_two_hourly"]], + ["node__investment_temporal_block", ["node_b", "investments_four_hourly"]], + ["node__investment_temporal_block", ["node_c", "investments_two_hourly"]], + ["node__investment_stochastic_structure", ["node_a", "investments_stochastic"]], + ["node__investment_stochastic_structure", ["node_b", "investments_deterministic"]], + ["node__investment_stochastic_structure", ["node_c", "investments_deterministic"]], + ["unit__investment_temporal_block", ["unit_ab", "investments_four_hourly"]], + ["unit__investment_stochastic_structure", ["unit_ab", "investments_deterministic"]], + ["connection__investment_temporal_block", ["connection_bc", "investments_four_hourly"]], + ["connection__investment_stochastic_structure", ["connection_bc", "investments_deterministic"]], + ["unit__from_node", ["unit_ab", "node_a"]], + ["unit__to_node", ["unit_ab", "node_b"]], + ["connection__from_node", ["connection_bc", "node_b"]], + ["connection__to_node", ["connection_bc", "node_c"]], + ["unit__investment_group", ["unit_ab", "ig"]], + ["connection__investment_group", ["connection_bc", "ig"]], + ["node__investment_group", ["node_c", "ig"]], + ], + :object_parameter_values => [ + ["model", "instance", "model_start", Dict("type" => "date_time", "data" => "2000-01-01T00:00:00")], + ["model", "instance", "model_end", Dict("type" => "date_time", "data" => "2000-01-01T04:00:00")], + ["model", "instance", "duration_unit", "hour"], + ["model", "instance", "model_type", "spineopt_standard"], + ["node", "node_c", "has_state", true], + ["node", "node_c", "node_state_cap", 100], + ["node", "node_c", "candidate_storages", 2], + ["node", "node_c", "storage_investment_cost", 1000], + ["unit", "unit_ab", "candidate_units", 3], + ["unit", "unit_ab", "unit_investment_cost", 1000], + ["connection", "connection_bc", "candidate_connections", 1], + ["connection", "connection_bc", "connection_investment_cost", 1000], + ["temporal_block", "investments_two_hourly", "resolution", Dict("type" => "duration", "data" => "2h")], + ["temporal_block", "investments_four_hourly", "resolution", Dict("type" => "duration", "data" => "4h")], + ["model", "instance", "db_mip_solver", "HiGHS.jl"], + ["model", "instance", "db_lp_solver", "HiGHS.jl"], + ], + :relationship_parameter_values => [ + [ + "stochastic_structure__stochastic_scenario", + ["stochastic", "parent"], + "stochastic_scenario_end", + Dict("type" => "duration", "data" => "1h"), + ] + ], + ) + _load_test_data(url_in, test_data) + url_in +end + +function _test_equal_investments() + @testset "equal_investments" begin + url_in = _test_constraint_investment_group_setup() + object_parameter_values = [["investment_group", "ig", "equal_investments", true]] + SpineInterface.import_data(url_in; object_parameter_values=object_parameter_values) + m = run_spineopt(url_in; log_level=0, optimize=false) + constraint = m.ext[:spineopt].constraints[:investment_group_equal_investments] + unit_ab = unit(:unit_ab) + connection_bc = connection(:connection_bc) + node_c = node(:node_c) + parent = stochastic_scenario(:parent) + t4h = first(time_slice(m; temporal_block=temporal_block(:investments_four_hourly))) + key_head = (investment_group=investment_group(:ig), entity1=unit_ab) + key_tail = (stochastic_scenario=parent, t=t4h) + u_ab_inv_avail = m.ext[:spineopt].variables[:units_invested_available][unit_ab, parent, t4h] + conn_bc_inv_avail = [m.ext[:spineopt].variables[:connections_invested_available][connection_bc, parent, t4h]] + node_c_inv_avail = [ + m.ext[:spineopt].variables[:storages_invested_available][node_c, parent, t] + for t in time_slice(m; temporal_block=temporal_block(:investments_two_hourly)) + ] + @testset for entity2 in (connection_bc, node_c) + con_key = (; key_head..., entity2=entity2, key_tail...) + observed_con = constraint_object(constraint[con_key]) + other_inv_avail = Dict(connection_bc => conn_bc_inv_avail, node_c => node_c_inv_avail)[entity2] + expected_con = @build_constraint(u_ab_inv_avail == sum(other_inv_avail)) + @test _is_constraint_equal(observed_con, expected_con) + end + end +end + +function _test_min_max_entities_invested_available() + @testset "min_max_entities_invested_available" begin + url_in = _test_constraint_investment_group_setup() + object_parameter_values = [ + ["investment_group", "ig", "minimum_entities_invested_available", 3], + ["investment_group", "ig", "maximum_entities_invested_available", 8], + ] + SpineInterface.import_data(url_in; object_parameter_values=object_parameter_values) + m = run_spineopt(url_in; log_level=0, optimize=false) + constraint = m.ext[:spineopt].constraints[:investment_group_minimum_entities_invested_available] + unit_ab = unit(:unit_ab) + connection_bc = connection(:connection_bc) + node_c = node(:node_c) + parent = stochastic_scenario(:parent) + t4h = first(time_slice(m; temporal_block=temporal_block(:investments_four_hourly))) + con_key = (investment_group=investment_group(:ig), stochastic_scenario=parent, t=t4h) + u_ab_inv_avail = m.ext[:spineopt].variables[:units_invested_available][unit_ab, parent, t4h] + conn_bc_inv_avail = m.ext[:spineopt].variables[:connections_invested_available][connection_bc, parent, t4h] + node_c_inv_avail = [ + m.ext[:spineopt].variables[:storages_invested_available][node_c, parent, t] + for t in time_slice(m; temporal_block=temporal_block(:investments_two_hourly)) + ] + observed_con = constraint_object( + m.ext[:spineopt].constraints[:investment_group_minimum_entities_invested_available][con_key] + ) + expected_con = @build_constraint(u_ab_inv_avail + conn_bc_inv_avail + sum(node_c_inv_avail) >= 3) + @test _is_constraint_equal(observed_con, expected_con) + observed_con = constraint_object( + m.ext[:spineopt].constraints[:investment_group_maximum_entities_invested_available][con_key] + ) + expected_con = @build_constraint(u_ab_inv_avail + conn_bc_inv_avail + sum(node_c_inv_avail) <= 8) + @test _is_constraint_equal(observed_con, expected_con) + end +end + +function _test_min_max_capacity_invested_available() + @testset "min_max_capacity_invested_available" begin + url_in = _test_constraint_investment_group_setup() + object_parameter_values = [ + ["investment_group", "ig", "minimum_capacity_invested_available", 300], + ["investment_group", "ig", "maximum_capacity_invested_available", 800], + ] + relationships = [ + ("unit__from_node__investment_group", ("unit_ab", "node_a", "ig")), + ("connection__to_node__investment_group", ("connection_bc", "node_c", "ig")), + ] + relationship_parameter_values = [ + ("unit__from_node", ("unit_ab", "node_a"), "unit_capacity", 150), + ("connection__to_node", ("connection_bc", "node_c"), "connection_capacity", 250), + ] + SpineInterface.import_data( + url_in; + object_parameter_values=object_parameter_values, + relationship_parameter_values=relationship_parameter_values, + relationships=relationships, + ) + m = run_spineopt(url_in; log_level=0, optimize=false) + unit_ab = unit(:unit_ab) + connection_bc = connection(:connection_bc) + parent = stochastic_scenario(:parent) + t4h = first(time_slice(m; temporal_block=temporal_block(:investments_four_hourly))) + con_key = (investment_group=investment_group(:ig), stochastic_scenario=parent, t=t4h) + u_ab_inv_avail = m.ext[:spineopt].variables[:units_invested_available][unit_ab, parent, t4h] + conn_bc_inv_avail = m.ext[:spineopt].variables[:connections_invested_available][connection_bc, parent, t4h] + observed_con = constraint_object( + m.ext[:spineopt].constraints[:investment_group_minimum_capacity_invested_available][con_key] + ) + expected_con = @build_constraint(150 * u_ab_inv_avail + 250 * conn_bc_inv_avail >= 300) + @test _is_constraint_equal(observed_con, expected_con) + observed_con = constraint_object( + m.ext[:spineopt].constraints[:investment_group_maximum_capacity_invested_available][con_key] + ) + expected_con = @build_constraint(150 * u_ab_inv_avail + 250 * conn_bc_inv_avail <= 800) + @test _is_constraint_equal(observed_con, expected_con) + end +end + +@testset "investment_group" begin + _test_equal_investments() + _test_min_max_entities_invested_available() + _test_min_max_capacity_invested_available() +end diff --git a/test/run_spineopt.jl b/test/run_spineopt.jl index e03c77b2aa..f12864bc58 100644 --- a/test/run_spineopt.jl +++ b/test/run_spineopt.jl @@ -834,6 +834,35 @@ function _test_time_limit() end end +function _test_only_linear_model_has_duals() + objects = [["output", "bound_units_on"]] + relationships = [["report__output", ["report_x", "bound_units_on"]]] + @testset "linear_model_has_duals" begin + url_in, url_out, file_path_out = _test_run_spineopt_setup() + SpineInterface.import_data(url_in; objects=objects, relationships=relationships) + rm(file_path_out; force=true) + m = run_spineopt(url_in, url_out; log_level=0) + @test has_duals(m) + end + object_parameter_values = [ + ["unit", "unit_ab", "online_variable_type", "unit_online_variable_type_binary"] + ] + @testset "integer_model_doesnt_have_duals" begin + url_in, url_out, file_path_out = _test_run_spineopt_setup() + objects = [["output", "bound_units_on"]] + relationships = [["report__output", ["report_x", "bound_units_on"]]] + object_parameter_values = [ + ["unit", "unit_ab", "online_variable_type", "unit_online_variable_type_binary"] + ] + SpineInterface.import_data( + url_in; objects=objects, relationships=relationships, object_parameter_values=object_parameter_values + ) + rm(file_path_out; force=true) + m = run_spineopt(url_in, url_out; log_level=0) + @test !has_duals(m) + end +end + @testset "run_spineopt" begin _test_rolling() _test_rolling_with_updating_data() @@ -854,4 +883,5 @@ end _test_fix_unit_flow_with_rolling() _test_fix_node_state_using_map_with_rolling() _test_time_limit() + _test_only_linear_model_has_duals() end \ No newline at end of file diff --git a/test/run_spineopt_benders.jl b/test/run_spineopt_benders.jl index 73fc0a45fd..72f25b1080 100644 --- a/test/run_spineopt_benders.jl +++ b/test/run_spineopt_benders.jl @@ -93,6 +93,7 @@ function _test_benders_unit() ["output", "units_invested"], ["output", "units_mothballed"], ["output", "units_invested_available"], + ["output", "unit_investment_costs"], ["temporal_block", "investments_hourly"], ] relationships = [ @@ -106,6 +107,7 @@ function _test_benders_unit() ["report__output", ["report_x", "units_invested"]], ["report__output", ["report_x", "units_mothballed"]], ["report__output", ["report_x", "units_invested_available"]], + ["report__output", ["report_x", "unit_investment_costs"]], ] object_parameter_values = [ ["model", "instance", "roll_forward", unparse_db_value(Hour(rf))], @@ -144,6 +146,11 @@ function _test_benders_unit() @test Y.total_costs(model=Y.model(:instance), t=t) == (should_invest ? 60 : 120) end end + @testset "unit_investment_costs" begin + @test Y.objective_unit_investment_costs(model=Y.model(:instance), t=DateTime(2000, 1, 1)) == ( + should_invest ? u_inv_cost : 0 + ) + end @testset "invested" begin @testset for t in DateTime(2000, 1, 1):Hour(6):DateTime(2000, 1, 2) @test Y.units_invested(unit=Y.unit(:unit_ab_alt), t=t) == ( @@ -354,7 +361,7 @@ function _test_benders_unit_storage() ["node", "node_b", "demand", dem], ["node", "node_a", "node_slack_penalty", penalty], ["temporal_block", "hourly", "block_end", unparse_db_value(Hour(rf + look_ahead))], - ["temporal_block", "investments_hourly", "block_end", unparse_db_value(Hour(rf + look_ahead))], + ["temporal_block", "investments_hourly", "block_end", unparse_db_value(Hour(24 + look_ahead))], ["temporal_block", "hourly", "resolution", unparse_db_value(Hour(res))], ["temporal_block", "investments_hourly", "resolution", unparse_db_value(Hour(res))], ] @@ -643,6 +650,7 @@ function _test_benders_mp_min_res_gen_to_demand_ratio() ["output", "units_invested"], ["output", "units_mothballed"], ["output", "units_invested_available"], + ["output", "mp_min_res_gen_to_demand_ratio_slack"], ["temporal_block", "investments_hourly"], ] relationships = [ @@ -657,6 +665,7 @@ function _test_benders_mp_min_res_gen_to_demand_ratio() ["report__output", ["report_x", "units_invested"]], ["report__output", ["report_x", "units_mothballed"]], ["report__output", ["report_x", "units_invested_available"]], + ["report__output", ["report_x", "mp_min_res_gen_to_demand_ratio_slack"]], ] object_parameter_values = [ ["commodity", "electricity", "mp_min_res_gen_to_demand_ratio", mrg2d_ratio], @@ -691,7 +700,7 @@ function _test_benders_mp_min_res_gen_to_demand_ratio() relationship_parameter_values=relationship_parameter_values ) rm(file_path_out; force=true) - m = run_spineopt(url_in, url_out; log_level=3) + m = run_spineopt(url_in, url_out; log_level=0) m_mp = master_problem_model(m) cons = m_mp.ext[:spineopt].constraints[:mp_min_res_gen_to_demand_ratio] invest_vars = m_mp.ext[:spineopt].variables[:units_invested_available] @@ -699,9 +708,12 @@ function _test_benders_mp_min_res_gen_to_demand_ratio() @test length(cons) == 1 observed_con = constraint_object(only(values(cons))) expected_con = @build_constraint( - ucap * sum(v for (k, v) in invest_vars if start(k.t) >= DateTime(2000)) + only(values(slack_vars)) + + ucap * sum( + duration(k.t) * v for (k, v) in invest_vars if DateTime(2000) <= start(k.t) < DateTime(2000, 1, 2) + ) + + only(values(slack_vars)) >= - dem * mrg2d_ratio + + 24 * dem * mrg2d_ratio ) @test _is_constraint_equal(observed_con, expected_con) using_spinedb(url_out, Y) @@ -727,6 +739,110 @@ function _test_benders_mp_min_res_gen_to_demand_ratio() @test Y.units_invested_available(unit=Y.unit(:unit_ab_alt), t=t) == (should_invest ? 1 : 0) end end + t0 = DateTime(2000, 1, 1) + @test Y.mp_min_res_gen_to_demand_ratio_slack(commodity=Y.commodity(:electricity), t=t0) == 0 + end + end +end + +function _test_benders_starting_units_invested() + @testset "benders_starting_units_invested" begin + benders_gap = 1e-6 # needed so that we get the exact master problem solution + mip_solver_options_benders = unparse_db_value(Map(["HiGHS.jl"], [Map(["mip_rel_gap"], [benders_gap])])) + res = 6 + dem = ucap = 10 + rf = 6 + look_ahead = 3 + vom_cost_ = 2 + vom_cost_alt = vom_cost_ / 2 + op_cost_no_inv = ucap * vom_cost_ * (24 + look_ahead) + op_cost_inv = ucap * vom_cost_alt * (24 + look_ahead) + do_not_inv_cost = op_cost_no_inv - op_cost_inv # minimum cost at which investment is not profitable, 270.0 + u_inv_cost = do_not_inv_cost + 1 # needed, not sure why + @testset for (max_iters, should_invest) in ((10, false), (1, true)) + url_in, url_out, file_path_out = _test_run_spineopt_benders_setup() + objects = [ + ["unit", "unit_ab_alt"], + ["output", "total_costs"], + ["output", "units_invested"], + ["output", "units_mothballed"], + ["output", "units_invested_available"], + ["output", "unit_investment_costs"], + ["temporal_block", "investments_hourly"], + ] + relationships = [ + ["unit__to_node", ["unit_ab_alt", "node_b"]], + ["units_on__temporal_block", ["unit_ab_alt", "hourly"]], + ["units_on__stochastic_structure", ["unit_ab_alt", "deterministic"]], + ["model__temporal_block", ["instance", "investments_hourly"]], + ["model__default_investment_temporal_block", ["instance", "investments_hourly"]], + ["model__default_investment_stochastic_structure", ["instance", "deterministic"]], + ["report__output", ["report_x", "total_costs"]], + ["report__output", ["report_x", "units_invested"]], + ["report__output", ["report_x", "units_mothballed"]], + ["report__output", ["report_x", "units_invested_available"]], + ["report__output", ["report_x", "unit_investment_costs"]], + ] + object_parameter_values = [ + ["model", "instance", "roll_forward", unparse_db_value(Hour(rf))], + ["model", "instance", "model_type", "spineopt_benders"], + ["model", "instance", "max_iterations", max_iters], + ["model", "instance", "db_mip_solver_options", mip_solver_options_benders], + ["node", "node_b", "demand", dem], + ["unit", "unit_ab_alt", "number_of_units", 0], + ["unit", "unit_ab_alt", "candidate_units", 1], + ["unit", "unit_ab_alt", "benders_starting_units_invested", 1], + ["unit", "unit_ab_alt", "unit_investment_variable_type", "unit_investment_variable_type_integer"], + ["unit", "unit_ab_alt", "online_variable_type", "unit_online_variable_type_integer"], + ["unit", "unit_ab_alt", "unit_investment_cost", u_inv_cost], + ["temporal_block", "hourly", "block_end", unparse_db_value(Hour(rf + look_ahead))], + ["temporal_block", "investments_hourly", "block_end", unparse_db_value(Hour(24 + look_ahead))], + ["temporal_block", "hourly", "resolution", unparse_db_value(Hour(res))], + ["temporal_block", "investments_hourly", "resolution", unparse_db_value(Hour(res))], + ] + relationship_parameter_values = [ + ["unit__to_node", ["unit_ab", "node_b"], "unit_capacity", ucap], + ["unit__to_node", ["unit_ab", "node_b"], "vom_cost", vom_cost_], + ["unit__to_node", ["unit_ab_alt", "node_b"], "unit_capacity", ucap], + ["unit__to_node", ["unit_ab_alt", "node_b"], "vom_cost", vom_cost_alt], + ] + SpineInterface.import_data( + url_in; + objects=objects, + relationships=relationships, + object_parameter_values=object_parameter_values, + relationship_parameter_values=relationship_parameter_values + ) + rm(file_path_out; force=true) + run_spineopt(url_in, url_out; log_level=0) + using_spinedb(url_out, Y) + @testset "total_cost" begin + for t in DateTime(2000, 1, 1):Hour(6):DateTime(2000, 1, 1, 23) + @test Y.total_costs(model=Y.model(:instance), t=t) == (should_invest ? 60 : 120) + end + end + @testset "unit_investment_costs" begin + @test Y.objective_unit_investment_costs(model=Y.model(:instance), t=DateTime(2000, 1, 1)) == ( + should_invest ? u_inv_cost : 0 + ) + end + @testset "invested" begin + @testset for t in DateTime(2000, 1, 1):Hour(6):DateTime(2000, 1, 2) + @test Y.units_invested(unit=Y.unit(:unit_ab_alt), t=t) == ( + should_invest && t == DateTime(2000, 1, 1) ? 1 : 0 + ) + end + end + @testset "mothballed" begin + @testset for t in DateTime(2000, 1, 1):Hour(6):DateTime(2000, 1, 2) + @test Y.units_mothballed(unit=Y.unit(:unit_ab_alt), t=t) == 0 + end + end + @testset "available" begin + @testset for t in DateTime(2000, 1, 1):Hour(6):DateTime(2000, 1, 2) + @test Y.units_invested_available(unit=Y.unit(:unit_ab_alt), t=t) == (should_invest ? 1 : 0) + end + end end end end @@ -737,5 +853,6 @@ end _test_benders_rolling_representative_periods() _test_benders_rolling_representative_periods_yearly_investments_multiple_units() _test_benders_mp_min_res_gen_to_demand_ratio() + _test_benders_starting_units_invested() # FIXME: _test_benders_unit_storage() end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index b2152740ae..4b1f29d38f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -24,6 +24,8 @@ using Dates using JuMP using PyCall +# Resolve JuMP and SpineInterface `Parameter` and `parameter_value` conflicts. +import SpineInterface: Parameter, parameter_value import SpineOpt: time_slice, @@ -103,6 +105,7 @@ end include("constraints/constraint_node.jl") include("constraints/constraint_connection.jl") include("constraints/constraint_user_constraint.jl") + include("constraints/constraint_investment_group.jl") include("objective/objective.jl") include("util/misc.jl") include("util/docs_utils.jl")