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 TulipaBuilderThe 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)")
pltNow, let's attach the profiles to the solar and demand assets for each scenario.
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)
endNow 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)| Row | scenario | profile_name | mean_value |
|---|---|---|---|
| Int64 | String | Float64 | |
| 1 | 2009 | solar-availability-2030 | 0.184904 |
| 2 | 1995 | solar-availability-2030 | 0.184707 |
| 3 | 2008 | solar-availability-2030 | 0.180548 |
| 4 | 1995 | demand-demand-2030 | 0.57874 |
| 5 | 2009 | demand-demand-2030 | 0.584813 |
| 6 | 2008 | demand-demand-2030 | 0.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| Row | asset | commission_year | profile_name | profile_type |
|---|---|---|---|---|
| String | Int64 | String | String | |
| 1 | solar | 2030 | solar-availability-2030 | availability |
| 2 | demand | 2030 | demand-demand-2030 | demand |