Basic example with renewable producer and battery

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 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}}())

At this point, we can visualise the network using the internal graph. Note that this is optional, and you can just skip ahead.

# OPTIONAL: Plotting the graph
using Karnak
using Colors
using MetaGraphsNext

graph = tulipa.graph
assets = collect(labels(graph))
color_per_type = Dict(:producer => colorant"red", :consumer => colorant"green", :storage => colorant"blue")

function vertexfillcolors(vtx)
    asset = assets[vtx]
    asset_type = graph[asset].type
    return color_per_type[asset_type]
end

@drawsvg begin
    background("lightblue")
    sethue("black")
    drawgraph(
        graph.graph,
        edgegaps = 20,
        vertexlabels = assets,
        vertexfillcolors = vertexfillcolors,
        vertexshapes = (vtx) -> box(O, 6.5 * length(assets[vtx]), 20, :fill),
        vertexlabeltextcolors = colorant"white",
    )
end 600 400
Example block output

Now, let's attach the profiles to the solar and demand assets. Notice that we need to pass the year in which these profiles are defined. In a single-year problem, the year doesn't matter, so any integer value could be used.

using CSV
using DataFrames

profiles_data = joinpath(@__DIR__, "..", "..", "..", "test", "tiny-profiles.csv")
df = DataFrame(CSV.File(profiles_data))
attach_profile!(tulipa, "solar", :availability, 2030, df[!, "availability-solar"])
attach_profile!(tulipa, "demand", :demand, 2030, df[!, "demand-demand"])

# Plotting the first week of the profiles
using Plots
plt = plot()
plot!(plt, df[1:168, "availability-solar"], c=:orange, lw=2, label="solar")
plot!(plt, df[1:168, "demand-demand"], c=:green, lw=2, label="demand")
Example block output

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:")

Notice that the profile names are automatically created from the attached data:

using DuckDB

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