Basic example with renewable producer and battery and scenarios

This tutorial goes over a creation of a simple Tulipa problem, with the following:

  • 1 thermal generator, with 1 existing unit, and capacity for 500 KW;
  • 1 solar generator, with 1 existing unit, capacity for 200 KW and an availability profile named "solar";
  • 1 consumer, with peak demand of 500 KW, and a demand profile named "demand";
  • 1 battery node;
  • The profiles per scenario are stored in a CSV file.
using TulipaBuilder

The first step is to create a TulipaData object.

tulipa = TulipaData()
TulipaData{String}(Meta graph based on a Graphs.SimpleGraphs.SimpleDiGraph{Int64} with vertex labels of type String, vertex metadata of type TulipaBuilder.TulipaAsset, edge metadata of type TulipaBuilder.TulipaFlow, graph metadata given by nothing, and default weight 1.0, false, Dict{Int64, Dict{Symbol, Any}}(), Dict{Tuple{String, Int64}, Dict{Symbol, Any}}())

Then, we add each asset with their respective characteristics.

add_asset!(tulipa, "thermal", :producer, capacity = 500.0, initial_units = 1.0)
add_asset!(tulipa, "solar", :producer, capacity = 200.0, initial_units = 1.0)
add_asset!(tulipa, "demand", :consumer, peak_demand = 500.0)
add_asset!(tulipa, "battery", :storage)
TulipaData{String}(Meta graph based on a Graphs.SimpleGraphs.SimpleDiGraph{Int64} with vertex labels of type String, vertex metadata of type TulipaBuilder.TulipaAsset, edge metadata of type TulipaBuilder.TulipaFlow, graph metadata given by nothing, and default weight 1.0, false, Dict{Int64, Dict{Symbol, Any}}(), Dict{Tuple{String, Int64}, Dict{Symbol, Any}}())

Next, we need to define the flows between these assets, and the operational cost, if defined.

add_flow!(tulipa, "thermal", "demand", operational_cost = 0.05)
add_flow!(tulipa, "solar", "demand")
add_flow!(tulipa, "demand", "battery")
add_flow!(tulipa, "battery", "demand")
TulipaData{String}(Meta graph based on a Graphs.SimpleGraphs.SimpleDiGraph{Int64} with vertex labels of type String, vertex metadata of type TulipaBuilder.TulipaAsset, edge metadata of type TulipaBuilder.TulipaFlow, graph metadata given by nothing, and default weight 1.0, false, Dict{Int64, Dict{Symbol, Any}}(), Dict{Tuple{String, Int64}, Dict{Symbol, Any}}())

To visualise the network using the internal graph, please check the tutorial Basic example with renewable producer and battery.

Let's load the profiles from a CSV file and explore the data. The scenarios here represent different weather years, e.g.,

using CSV
using DataFrames

profiles_data = joinpath(@__DIR__, "..", "..", "..", "test", "tiny-profiles-scenarios.csv")
df = DataFrame(CSV.File(profiles_data))

# Group by scenario and plot first week for each scenario
using Plots
plt = plot()
grouped = groupby(df, :scenario)
linestyles = [:solid, :dash, :dot, :dashdot]
for (i, scenario_df) in enumerate(grouped)
    scenario_id = scenario_df.scenario[1]
    ls = linestyles[mod1(i, length(linestyles))]
    plot!(
        plt,
        scenario_df[1:168, "solar"],
        c = :orange,
        lw = 2,
        alpha = 0.7,
        label = "solar ($scenario_id)",
        linestyle = ls,
    )
    plot!(
        plt,
        scenario_df[1:168, "demand"],
        c = :green,
        lw = 2,
        alpha = 0.7,
        label = "demand ($scenario_id)",
        linestyle = ls,
    )
end
plot!(
    plt,
    legend = :outerbottom,
    legendcolumns = 2,
    title = "Profiles for different weather years (scenarios)",
)
xlabel!(plt, "Hour")
ylabel!(plt, "Profile value (pu)")
plt
Example block output

Now, let's attach the profiles to the solar and demand assets for each scenario.

Use keyword `scenario` when attaching profiles to assets for different scenarios

The function attach_profile! has a keyword scenario to specify the scenario for which the profile is attached. Notice that in the loop below, we are adding profiles for each scenario in the keyword, so the profiles are attached to each scenario. If we don't pass the keyword scenario, the profiles would have been attached to a default scenario (with value 1). For and example of attaching profiles without scenarios, please check the tutorial Basic example with renewable producer and battery. Please check the reference section for more information about this function.

year = 2030
for scenario_df in grouped
    scenario_id = scenario_df.scenario[1]
    solar_profile = Vector(scenario_df[!, "solar"])
    demand_profile = Vector(scenario_df[!, "demand"])
    attach_profile!(tulipa, "solar", :availability, year, solar_profile; scenario = scenario_id)
    attach_profile!(tulipa, "demand", :demand, year, demand_profile; scenario = scenario_id)
end

Now we can create the connection with the data of the Tulipa problem using the create_connection function.

using TulipaEnergyModel: TulipaEnergyModel as TEM

connection = create_connection(tulipa, TEM.schema)
DuckDB.DB(":memory:")

Let's check the inserted profiles in the profiles table, which now contains the scenario information. Here we summarize the mean value per scenario and profile name to verify that the data was correctly inserted.

using DuckDB
tulipa_profiles = DuckDB.query(connection, "FROM profiles") |> DataFrame

# summarize per scenario and profile_name
using Statistics
combine(groupby(tulipa_profiles, [:scenario, :profile_name]), :value => mean => :mean_value)
6×3 DataFrame
Rowscenarioprofile_namemean_value
Int64StringFloat64
12009solar-availability-20300.184904
21995solar-availability-20300.184707
32008solar-availability-20300.180548
41995demand-demand-20300.57874
52009demand-demand-20300.584813
62008demand-demand-20300.577493

Notice that the profile names are automatically created from the attached data in the assets_profiles table, but the scenario column is not included there since it is implied by the profiles attached to each asset:

using DuckDB
DuckDB.query(connection, "FROM assets_profiles") |> DataFrame
2×4 DataFrame
Rowassetcommission_yearprofile_nameprofile_type
StringInt64StringString
1solar2030solar-availability-2030availability
2demand2030demand-demand-2030demand