The Basics

Welcome to the first tutorial, here you will learn the basics of how to run TulipaEnergyModel. Good luck! 🌷

Load data and run Tulipa

If you have not done so already, please follow the steps in the pre-tutorial first. You should have a VS Code project set up before starting this tutorial.

Ensure you are using the necessary packages by running the lines below:

import TulipaIO as TIO
import TulipaEnergyModel as TEM
using DuckDB
using DataFrames
using Plots
Tip

Follow along in this section, copy-pasting into your my_workflow.jl file.
In VS Code, you can press (CTRL+ENTER) to run the current line,
(SHIFT+ENTER) to run the current line and go to the next line.
To run multiple lines together, you need to highlight the part you want to run first,
and then hit (CTRL+ENTER) or (SHIFT+ENTER).
Or paste everything and press the run arrow (top right in VS Code) to run the entire file.

We need to create a connection to DuckDB and point to the input and output folders:

input_dir = joinpath(@__DIR__, "my-awesome-energy-system/tutorial-1")
output_dir = tempdir()
connection = DBInterface.connect(DuckDB.DB)
DuckDB.DB(":memory:")

Here we create a temporary directory for the output. But, you can point to any directory you want. But! remember that if the output directory does not exist yet, you need to create the 'results' folder, otherwise it will error later.

Tip

You can make an output directory in julia using the mkpath function:

mkpath(output_dir)

Let's use TulipaIO to read the files and list them:

TIO.read_csv_folder(connection, input_dir)

TIO.show_tables(connection)  # View all the table names in the DuckDB connection
# If your output window isn't large enough, it'll be cut-off
# Just expand the window and rerun the line
13×1 DataFrame
Rowname
String
1asset
2asset_both
3asset_commission
4asset_milestone
5assets_profiles
6flow
7flow_both
8flow_commission
9flow_milestone
10profiles_rep_periods
11rep_periods_data
12rep_periods_mapping
13timeframe_data

Now try viewing a specific table:

TIO.get_table(connection, "asset") # Or any other table name
9×7 DataFrame
Rowassettypecapacityvintage_methodinvestment_integertechnical_lifetimediscount_rate
StringStringFloat64StringBoolInt64Float64
1ccgtproducer800.0aggregatedtrue150.05
2e_demandconsumer0.0aggregatedfalse150.05
3ensproducer1000.0aggregatedfalse150.05
4ocgtproducer100.0aggregatedtrue150.05
5solarproducer500.0aggregatedtrue150.05
6windproducer400.0aggregatedtrue150.05
7electrolizerconversion100.0aggregatedtrue150.05
8h2_demandconsumer100.0aggregatedfalse150.05
9smr_ccsproducer100.0aggregatedtrue150.05

Add any missing columns and fill them with defaults:

TEM.populate_with_defaults!(connection)

# Explore the tables in DuckDB (again)
TIO.get_table(connection, "asset")
# Notice there are now a lot of new columns filled with default values
9×21 DataFrame
Rowassettypecapacityvintage_methodinvestment_integertechnical_lifetimediscount_ratecapacity_storage_energyconsumer_balance_senseeconomic_lifetimeenergy_to_power_ratioinvestment_integer_storage_energyis_seasonalmax_ramp_downmax_ramp_upmin_operating_pointrampingstorage_method_energyunit_commitmentunit_commitment_integeruse_binary_storage_method
StringStringFloat64StringBoolInt32Float64Float64StringInt32Float64BoolBoolFloat64Float64Float64BoolStringStringBoolString?
1ccgtproducer800.0aggregatedtrue150.050.0==10.0falsefalse0.00.00.0falsenonenonefalsemissing
2e_demandconsumer0.0aggregatedfalse150.050.0==10.0falsefalse0.00.00.0falsenonenonefalsemissing
3ensproducer1000.0aggregatedfalse150.050.0==10.0falsefalse0.00.00.0falsenonenonefalsemissing
4ocgtproducer100.0aggregatedtrue150.050.0==10.0falsefalse0.00.00.0falsenonenonefalsemissing
5solarproducer500.0aggregatedtrue150.050.0==10.0falsefalse0.00.00.0falsenonenonefalsemissing
6windproducer400.0aggregatedtrue150.050.0==10.0falsefalse0.00.00.0falsenonenonefalsemissing
7electrolizerconversion100.0aggregatedtrue150.050.0==10.0falsefalse0.00.00.0falsenonenonefalsemissing
8h2_demandconsumer100.0aggregatedfalse150.050.0==10.0falsefalse0.00.00.0falsenonenonefalsemissing
9smr_ccsproducer100.0aggregatedtrue150.050.0==10.0falsefalse0.00.00.0falsenonenonefalsemissing

Run, baby run!

energy_problem =
    TEM.run_scenario(connection; output_folder=output_dir)
EnergyProblem:
  - Model created!
    - Number of variables: 70080
    - Number of constraints for variable bounds: 70080
    - Number of structural constraints: 87600
  - Model solved!
    - Termination status: OPTIMAL
    - Objective value: 2.175768638386125e8
    - Objective breakdown:
      - assets_fixed_cost_aggregated_vintage_method: 0.0
      - assets_fixed_cost_compact_vintage_method: 0.0
      - assets_investment_cost: 0.0
      - flows_fixed_cost: 0.0
      - flows_investment_cost: 0.0
      - flows_operational_cost: 2.175768638386125e8
      - storage_assets_energy_fixed_cost: 0.0
      - storage_assets_energy_investment_cost: 0.0
      - units_on_operational_cost: 0.0
      - vintage_flows_operational_cost: 0.0

Explore the results

Which files were created in the output folder?
Take a minute to explore them.

Because we specified an output folder to run_scenario, it automatically exported the CSVs.
But instead of exporting, you can also explore results in Julia!

Basic Plots in Julia

Take a look at the electricity production from the "wind" asset that flows to the "e_demand" asset:

using DataFrames
using Plots

flows = TIO.get_table(connection, "var_flow") # Put the "var_flow" table from DuckDB into a Julia dataframe called "flows"

from_asset = "wind"
to_asset = "e_demand"
year = 2030
rep_period = 1


filtered_flow = filter(
    row ->
        row.from_asset == from_asset &&
            row.to_asset == to_asset &&
            row.milestone_year == year &&
            row.rep_period == rep_period,
    flows,
)

plot(
    filtered_flow.time_block_start,
    filtered_flow.solution;
    label=string(from_asset, " -> ", to_asset),
    xlabel="Hour",
    ylabel="[MWh]",
    #dpi=600, # uncomment this line to save the plot in high resolution
)
Example block output

For the prices, work with the dual of the constraint.

balance = TIO.get_table(connection, "cons_balance_consumer")

asset = "e_demand"
year = 2030
rep_period = 1

filtered_asset = filter(
    row ->
        row.asset == asset &&
            row.milestone_year == year &&
            row.rep_period == rep_period,
    balance,
)

plot(
    filtered_asset.time_block_start,
    filtered_asset.dual_balance_consumer;
    label=string(from_asset, " -> ", to_asset),
    xlabel="Hour",
    ylabel="[MWh]",
    ylims=(0,200),
    #dpi=600, # uncomment this line to save the plot in high resolution
)
Example block output
Test Your Knowledge

Inspect the prices in the plot. Notice how the prices mostly match the operational costs of the dispatchable assets. However, there is an outlier. Can you explain the prices of 153.8462€/MWh in the e_demand? Hint: consider the interlinkage between hydrogen and electricity demand

Another important aspect to consider is that we are currently not allowing the model to invest in any of the technologies. It has to solve the energy problem with the currently allocated capacities. There is a column in the asset-milestone.csv file that requires true or false values for whether an asset is investable or not. Try changing the value in this column for the wind asset to true and run the model again. What differences do you see?