diff --git a/docs/make.jl b/docs/make.jl index c52062d7bd3..54ea801b60b 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -377,6 +377,7 @@ const _PAGES = [ "tutorials/algorithms/benders_decomposition.md", "tutorials/algorithms/cutting_stock_column_generation.md", "tutorials/algorithms/tsp_lazy_constraints.md", + "tutorials/algorithms/rolling_horizon.md", "tutorials/algorithms/parallelism.md", ], "Applications" => [ diff --git a/docs/src/tutorials/algorithms/rolling_horizon.csv b/docs/src/tutorials/algorithms/rolling_horizon.csv index dfae1396ab3..d74486700ee 100644 --- a/docs/src/tutorials/algorithms/rolling_horizon.csv +++ b/docs/src/tutorials/algorithms/rolling_horizon.csv @@ -1,169 +1,169 @@ -t,demand_MW,solar_pu -2000-01-01T00:00:00,51.6,0 -2000-01-01T01:00:00,49.2,0 -2000-01-01T02:00:00,46.5,0 -2000-01-01T03:00:00,44.3,0 -2000-01-01T04:00:00,43.3,0 -2000-01-01T05:00:00,42.1,0 -2000-01-01T06:00:00,39.8,0 -2000-01-01T07:00:00,40.2,0 -2000-01-01T08:00:00,41.3,0.212560386 -2000-01-01T09:00:00,45,0.608695652 -2000-01-01T10:00:00,49.3,0.845410628 -2000-01-01T11:00:00,54.3,0.995169082 -2000-01-01T12:00:00,56,1 -2000-01-01T13:00:00,54.9,0.763285024 -2000-01-01T14:00:00,53.3,0.309178744 -2000-01-01T15:00:00,53.5,0.009661836 -2000-01-01T16:00:00,57.5,0 -2000-01-01T17:00:00,65,0 -2000-01-01T18:00:00,66.2,0 -2000-01-01T19:00:00,64.5,0 -2000-01-01T20:00:00,61,0 -2000-01-01T21:00:00,59,0 -2000-01-01T22:00:00,58.7,0 -2000-01-01T23:00:00,54.1,0 -2000-01-02T00:00:00,49.7,0 -2000-01-02T01:00:00,46.5,0 -2000-01-02T02:00:00,44.8,0 -2000-01-02T03:00:00,44.5,0 -2000-01-02T04:00:00,46,0 -2000-01-02T05:00:00,48.6,0 -2000-01-02T06:00:00,52.6,0 -2000-01-02T07:00:00,59,0 -2000-01-02T08:00:00,65.1,0.096618357 -2000-01-02T09:00:00,70.1,0.256038647 -2000-01-02T10:00:00,73.5,0.391304348 -2000-01-02T11:00:00,76.2,0.47826087 -2000-01-02T12:00:00,76.8,0.531400966 -2000-01-02T13:00:00,75.1,0.434782609 -2000-01-02T14:00:00,73.2,0.202898551 -2000-01-02T15:00:00,72.5,0.014492754 -2000-01-02T16:00:00,75.2,0 -2000-01-02T17:00:00,80.7,0 -2000-01-02T18:00:00,80.7,0 -2000-01-02T19:00:00,77.5,0 -2000-01-02T20:00:00,71.3,0 -2000-01-02T21:00:00,67.6,0 -2000-01-02T22:00:00,65.8,0 -2000-01-02T23:00:00,60.4,0 -2000-01-03T00:00:00,54.7,0 -2000-01-03T01:00:00,50.9,0 -2000-01-03T02:00:00,48.5,0 -2000-01-03T03:00:00,47.7,0 -2000-01-03T04:00:00,48.2,0 -2000-01-03T05:00:00,48.5,0 -2000-01-03T06:00:00,49.1,0 -2000-01-03T07:00:00,53.3,0 -2000-01-03T08:00:00,58.9,0.09178744 -2000-01-03T09:00:00,64.6,0.265700483 -2000-01-03T10:00:00,68.8,0.367149758 -2000-01-03T11:00:00,72,0.400966184 -2000-01-03T12:00:00,72.4,0.347826087 -2000-01-03T13:00:00,70.9,0.251207729 -2000-01-03T14:00:00,69.5,0.111111111 -2000-01-03T15:00:00,69.5,0.009661836 -2000-01-03T16:00:00,72.5,0 -2000-01-03T17:00:00,77.3,0 -2000-01-03T18:00:00,77.4,0 -2000-01-03T19:00:00,73.9,0 -2000-01-03T20:00:00,68,0 -2000-01-03T21:00:00,64.1,0 -2000-01-03T22:00:00,62.8,0 -2000-01-03T23:00:00,58.1,0 -2000-01-04T00:00:00,52.8,0 -2000-01-04T01:00:00,49.1,0 -2000-01-04T02:00:00,47,0 -2000-01-04T03:00:00,45.9,0 -2000-01-04T04:00:00,46.1,0 -2000-01-04T05:00:00,45.5,0 -2000-01-04T06:00:00,44.1,0 -2000-01-04T07:00:00,46.5,0.004830918 -2000-01-04T08:00:00,50.3,0.256038647 -2000-01-04T09:00:00,55.6,0.700483092 -2000-01-04T10:00:00,60.3,0.888888889 -2000-01-04T11:00:00,65.6,0.93236715 -2000-01-04T12:00:00,65.9,0.787439614 -2000-01-04T13:00:00,63.2,0.550724638 -2000-01-04T14:00:00,60.7,0.275362319 -2000-01-04T15:00:00,60.1,0.019323671 -2000-01-04T16:00:00,63.4,0 -2000-01-04T17:00:00,71.3,0 -2000-01-04T18:00:00,73.1,0 -2000-01-04T19:00:00,70.9,0 -2000-01-04T20:00:00,66.8,0 -2000-01-04T21:00:00,64.2,0 -2000-01-04T22:00:00,63.9,0 -2000-01-04T23:00:00,58.9,0 -2000-01-05T00:00:00,54,0 -2000-01-05T01:00:00,50.7,0 -2000-01-05T02:00:00,49.4,0 -2000-01-05T03:00:00,49.6,0 -2000-01-05T04:00:00,51.7,0 -2000-01-05T05:00:00,56.9,0 -2000-01-05T06:00:00,66.2,0 -2000-01-05T07:00:00,76.3,0.009661836 -2000-01-05T08:00:00,82,0.29468599 -2000-01-05T09:00:00,83.8,0.628019324 -2000-01-05T10:00:00,85.9,0.777777778 -2000-01-05T11:00:00,87.7,0.893719807 -2000-01-05T12:00:00,87.7,0.874396135 -2000-01-05T13:00:00,86.2,0.743961353 -2000-01-05T14:00:00,84.7,0.444444444 -2000-01-05T15:00:00,83.9,0.057971014 -2000-01-05T16:00:00,85.9,0 -2000-01-05T17:00:00,92,0 -2000-01-05T18:00:00,92,0 -2000-01-05T19:00:00,89,0 -2000-01-05T20:00:00,82,0 -2000-01-05T21:00:00,77.2,0 -2000-01-05T22:00:00,74.1,0 -2000-01-05T23:00:00,67,0 -2000-01-06T00:00:00,61.8,0 -2000-01-06T01:00:00,58,0 -2000-01-06T02:00:00,56.3,0 -2000-01-06T03:00:00,56.4,0 -2000-01-06T04:00:00,57.7,0 -2000-01-06T05:00:00,60.6,0 -2000-01-06T06:00:00,67.4,0 -2000-01-06T07:00:00,75.7,0.009661836 -2000-01-06T08:00:00,79.7,0.256038647 -2000-01-06T09:00:00,81.7,0.584541063 -2000-01-06T10:00:00,84.2,0.821256039 -2000-01-06T11:00:00,86.3,0.942028986 -2000-01-06T12:00:00,86,0.884057971 -2000-01-06T13:00:00,83.8,0.661835749 -2000-01-06T14:00:00,81.5,0.328502415 -2000-01-06T15:00:00,80.9,0.028985507 -2000-01-06T16:00:00,83.8,0 -2000-01-06T17:00:00,90.7,0 -2000-01-06T18:00:00,90.7,0 -2000-01-06T19:00:00,88.2,0 -2000-01-06T20:00:00,82.1,0 -2000-01-06T21:00:00,77.2,0 -2000-01-06T22:00:00,73.9,0 -2000-01-06T23:00:00,67.5,0 -2000-01-07T00:00:00,61.8,0 -2000-01-07T01:00:00,57.9,0 -2000-01-07T02:00:00,56.9,0 -2000-01-07T03:00:00,57.5,0 -2000-01-07T04:00:00,59.2,0 -2000-01-07T05:00:00,64.8,0 -2000-01-07T06:00:00,77.9,0 -2000-01-07T07:00:00,89.3,0.004830918 -2000-01-07T08:00:00,94.1,0.154589372 -2000-01-07T09:00:00,94.4,0.434782609 -2000-01-07T10:00:00,95.9,0.589371981 -2000-01-07T11:00:00,97.3,0.70531401 -2000-01-07T12:00:00,96.7,0.647342995 -2000-01-07T13:00:00,95.6,0.531400966 -2000-01-07T14:00:00,93.7,0.265700483 -2000-01-07T15:00:00,92.7,0.028985507 -2000-01-07T16:00:00,94,0 -2000-01-07T17:00:00,100,0 -2000-01-07T18:00:00,99.2,0 -2000-01-07T19:00:00,95.8,0 -2000-01-07T20:00:00,88.9,0 -2000-01-07T21:00:00,83.3,0 -2000-01-07T22:00:00,79.2,0 -2000-01-07T23:00:00,71.3,0 +day,hour,demand_MW,solar_pu +01,00,51.6,0 +01,01,49.2,0 +01,02,46.5,0 +01,03,44.3,0 +01,04,43.3,0 +01,05,42.1,0 +01,06,39.8,0 +01,07,40.2,0 +01,08,41.3,0.212560386 +01,09,45,0.608695652 +01,10,49.3,0.845410628 +01,11,54.3,0.995169082 +01,12,56,1 +01,13,54.9,0.763285024 +01,14,53.3,0.309178744 +01,15,53.5,0.009661836 +01,16,57.5,0 +01,17,65,0 +01,18,66.2,0 +01,19,64.5,0 +01,20,61,0 +01,21,59,0 +01,22,58.7,0 +01,23,54.1,0 +02,00,49.7,0 +02,01,46.5,0 +02,02,44.8,0 +02,03,44.5,0 +02,04,46,0 +02,05,48.6,0 +02,06,52.6,0 +02,07,59,0 +02,08,65.1,0.096618357 +02,09,70.1,0.256038647 +02,10,73.5,0.391304348 +02,11,76.2,0.47826087 +02,12,76.8,0.531400966 +02,13,75.1,0.434782609 +02,14,73.2,0.202898551 +02,15,72.5,0.014492754 +02,16,75.2,0 +02,17,80.7,0 +02,18,80.7,0 +02,19,77.5,0 +02,20,71.3,0 +02,21,67.6,0 +02,22,65.8,0 +02,23,60.4,0 +03,00,54.7,0 +03,01,50.9,0 +03,02,48.5,0 +03,03,47.7,0 +03,04,48.2,0 +03,05,48.5,0 +03,06,49.1,0 +03,07,53.3,0 +03,08,58.9,0.09178744 +03,09,64.6,0.265700483 +03,10,68.8,0.367149758 +03,11,72,0.400966184 +03,12,72.4,0.347826087 +03,13,70.9,0.251207729 +03,14,69.5,0.111111111 +03,15,69.5,0.009661836 +03,16,72.5,0 +03,17,77.3,0 +03,18,77.4,0 +03,19,73.9,0 +03,20,68,0 +03,21,64.1,0 +03,22,62.8,0 +03,23,58.1,0 +04,00,52.8,0 +04,01,49.1,0 +04,02,47,0 +04,03,45.9,0 +04,04,46.1,0 +04,05,45.5,0 +04,06,44.1,0 +04,07,46.5,0.004830918 +04,08,50.3,0.256038647 +04,09,55.6,0.700483092 +04,10,60.3,0.888888889 +04,11,65.6,0.93236715 +04,12,65.9,0.787439614 +04,13,63.2,0.550724638 +04,14,60.7,0.275362319 +04,15,60.1,0.019323671 +04,16,63.4,0 +04,17,71.3,0 +04,18,73.1,0 +04,19,70.9,0 +04,20,66.8,0 +04,21,64.2,0 +04,22,63.9,0 +04,23,58.9,0 +05,00,54,0 +05,01,50.7,0 +05,02,49.4,0 +05,03,49.6,0 +05,04,51.7,0 +05,05,56.9,0 +05,06,66.2,0 +05,07,76.3,0.009661836 +05,08,82,0.29468599 +05,09,83.8,0.628019324 +05,10,85.9,0.777777778 +05,11,87.7,0.893719807 +05,12,87.7,0.874396135 +05,13,86.2,0.743961353 +05,14,84.7,0.444444444 +05,15,83.9,0.057971014 +05,16,85.9,0 +05,17,92,0 +05,18,92,0 +05,19,89,0 +05,20,82,0 +05,21,77.2,0 +05,22,74.1,0 +05,23,67,0 +06,00,61.8,0 +06,01,58,0 +06,02,56.3,0 +06,03,56.4,0 +06,04,57.7,0 +06,05,60.6,0 +06,06,67.4,0 +06,07,75.7,0.009661836 +06,08,79.7,0.256038647 +06,09,81.7,0.584541063 +06,10,84.2,0.821256039 +06,11,86.3,0.942028986 +06,12,86,0.884057971 +06,13,83.8,0.661835749 +06,14,81.5,0.328502415 +06,15,80.9,0.028985507 +06,16,83.8,0 +06,17,90.7,0 +06,18,90.7,0 +06,19,88.2,0 +06,20,82.1,0 +06,21,77.2,0 +06,22,73.9,0 +06,23,67.5,0 +07,00,61.8,0 +07,01,57.9,0 +07,02,56.9,0 +07,03,57.5,0 +07,04,59.2,0 +07,05,64.8,0 +07,06,77.9,0 +07,07,89.3,0.004830918 +07,08,94.1,0.154589372 +07,09,94.4,0.434782609 +07,10,95.9,0.589371981 +07,11,97.3,0.70531401 +07,12,96.7,0.647342995 +07,13,95.6,0.531400966 +07,14,93.7,0.265700483 +07,15,92.7,0.028985507 +07,16,94,0 +07,17,100,0 +07,18,99.2,0 +07,19,95.8,0 +07,20,88.9,0 +07,21,83.3,0 +07,22,79.2,0 +07,23,71.3,0 diff --git a/docs/src/tutorials/algorithms/rolling_horizon.jl b/docs/src/tutorials/algorithms/rolling_horizon.jl index 535f4f07bc2..ae81a346369 100644 --- a/docs/src/tutorials/algorithms/rolling_horizon.jl +++ b/docs/src/tutorials/algorithms/rolling_horizon.jl @@ -110,12 +110,12 @@ import StatsPlots # optimization window and the move forward. # # **Optimization Window** (optimization_window): It defines how many periods -# (e.g., hours) we will optimize each time. For this example, we set the default +# (for example, hours) we will optimize each time. For this example, we set the default # value in 48h, meaning we will optimize two days each time. optimization_window = 48 -# **Move Forward** (move_forward): It defines how many periods (e.g., hours) we +# **Move Forward** (move_forward): It defines how many periods (for example, hours) we # will move forward to optimize the next optimization window. For this example, # we set the default value in 24h, meaning we will move 1 day ahead each time. @@ -124,7 +124,7 @@ move_forward = 24 # Note that the move forward parameter must be lower or equal to the # optimization window parameter to work correctly. -@assert optimization_window >= move_forward "optimization_window must be greater or equal to move_forward" +@assert optimization_window >= move_forward # Let's explore the input data in file [rolling_horizon.csv](rolling_horizon.csv). # We have a total time horizon of a week (i.e., 168h), an electricity demand, @@ -133,7 +133,7 @@ move_forward = 24 filename = joinpath(@__DIR__, "rolling_horizon.csv") time_series = CSV.read(filename, DataFrames.DataFrame); -# We define the solar investment (e.g., 150 MW) to determine the solar +# We define the solar investment (for example, 150 MW) to determine the solar # production during the operation optimization step. # # In addition, we can determine some basic information about the rolling @@ -150,168 +150,106 @@ time_series.solar_MW = solar_investment * time_series.solar_pu ## input data calculation for the Rolling Horizon total_time_length = size(time_series, 1) -number_of_windows = ceil(Int, total_time_length / move_forward) -println("number of windows:", number_of_windows) +println("number of windows:", ceil(Int, total_time_length / move_forward)) #- -p1 = Plots.plot( - time_series.t, - [time_series.demand_MW, time_series.solar_MW]; - ylabel = "MW", - label = ["demand" "solar"], - color = [:ivory4 :darkorange1], +x_series = 1:total_time_length +y_series = [time_series.demand_MW, time_series.solar_MW] +plot_1 = Plots.plot(x_series, y_series; label = ["demand" "solar"]) +plot_2 = Plots.plot(x_series, y_series; label = false) +window = [0, optimization_window] +Plots.vspan!(plot_1, window; alpha = 0.25, label = false) +Plots.vspan!(plot_2, move_forward .+ window; alpha = 0.25, label = false) +text_1 = Plots.text("optimization\n window 1", :top, :left, 8) +Plots.annotate!(plot_1, 18, time_series.solar_MW[12], text_1) +text_2 = Plots.text("optimization\n window 2", :top, :left, 8) +Plots.annotate!(plot_2, 42, time_series.solar_MW[12], text_2) +Plots.plot( + plot_1, + plot_2; + layout = (2, 1), linewidth = 3, - ylims = (0, solar_investment), xticks = 0:12:total_time_length, -) -Plots.vspan!(p1, [1, optimization_window]; alpha = 0.25, label = "") -Plots.annotate!( - p1, - 18, - time_series[12, :solar_MW], - Plots.text("optimization\n window 1", :top, :left, 8), -) -p2 = Plots.plot( - time_series.t, - [time_series.demand_MW, time_series.solar_MW]; xlabel = "Hours", ylabel = "MW", - label = ["" ""], - color = [:ivory4 :darkorange1], - linewidth = 3, - ylims = (0, solar_investment), - xticks = 0:12:total_time_length, -) -Plots.vspan!( - p2, - [move_forward, move_forward + optimization_window]; - alpha = 0.25, - label = "", -) -Plots.annotate!( - p2, - 42, - time_series[12, :solar_MW], - Plots.text("optimization\n window 2", :top, :left, 8), ) -Plots.plot!( - p2, - [1; move_forward], - [100; 100]; - arrow = 2, - label = "", - c = :black, -) -Plots.annotate!(p2, 5, 130, Plots.text(" move\nforward", :top, :left, 8)) -Plots.plot(p1, p2; layout = (2, 1)) # ## Rolling horizon first window # -# We first sample the initial input data and get the parameter values for the -# first optimization window. -# -# We also create a helper index `t_minus_1` to get easy access to the previous -# hour using the function `mod1`. - -## Create data of the first window -time_series_filter = 1:optimization_window -availability = time_series.solar_pu[time_series_filter] -demand = time_series.demand_MW[time_series_filter] -## Create index t and t-1 -t = 1:optimization_window -t_minus_1 = - mod1.(optimization_window:(2*optimization_window-1), optimization_window) - # We have all the information we need to create and optimize the first window in # the model. model = Model(() -> POI.Optimizer(HiGHS.Optimizer())) set_silent(model) -@variable(model, 0 <= i) -@variable(model, 0 <= r[t]) -@variable(model, 0 <= p[t] <= 150) -@variable(model, 0 <= s[t] <= 40) -@variable(model, 0 <= c[t] <= 10) -@variable(model, 0 <= d[t] <= 10) -@variable(model, D[t] in Parameter.(demand[t])) -@variable(model, A[t] in Parameter.(availability[t])) -@variable(model, So in Parameter(0.0)) -@constraint(model, balance, p[t] .+ r[t] .+ d[t] .== D[t] .+ c[t]) -@constraint( +@variables(model, begin + i == solar_investment + 0 <= r[1:optimization_window] + 0 <= p[1:optimization_window] <= 150 + 0 <= s[1:optimization_window] <= 40 + 0 <= c[1:optimization_window] <= 10 + 0 <= d[1:optimization_window] <= 10 + ## Initialize empty parameters. These values will get updated layer + D[t in 1:optimization_window] in Parameter(0) + A[t in 1:optimization_window] in Parameter(0) + So in Parameter(0) +end) +@constraints( model, - storage[t in 2:optimization_window], - s[t] == s[t-1] + 0.9 * c[t] - d[t] / 0.9 + begin + p .+ r .+ d .== D .+ c + s[1] == So + 0.9 * c[1] - d[1] / 0.9 + [t in 2:optimization_window], s[t] == s[t-1] + 0.9 * c[t] - d[t] / 0.9 + r .<= A .* i + end ) -@constraint(model, init_storage, s[1] == So + 0.9 * c[1] - d[1] / 0.9) -@constraint(model, max_ava, r[t] .<= A[t] * i) -@objective(model, Min, 100 * i + sum(50 * p[t])) -fix(i, solar_investment; force = true) -optimize!(model) +@objective(model, Min, 100 * i + 50 * sum(p)) +model # After the optimization, we can store the results in vectors. It's important to # note that despite optimizing for 48 hours (the default value), we only store -# the values for the "move forward" parameter (e.g., 24 hours or one day using -# the default value). This approach ensures that there is a buffer of additional -# periods or hours beyond the "move forward" parameter to prevent the storage -# from depleting entirely at the end of the specified hours. +# the values for the "move forward" parameter (for example, 24 hours or one day +# using the default value). This approach ensures that there is a buffer of +# additional periods or hours beyond the "move forward" parameter to prevent the +# storage from depleting entirely at the end of the specified hours. -objective_function_per_window = zeros(number_of_windows) +objective_function_per_window = Float64[] renewable_production = zeros(total_time_length) storage_level = zeros(total_time_length) -# Store results from the first window -renewable_production[1:move_forward] = value.(model[:r])[1:move_forward] -storage_level[1:move_forward] = value.(model[:s])[1:move_forward] -objective_function_per_window[1] = objective_value(model) -println("Objective function first window: ", objective_function_per_window[1]) - # ### Rolling horizon for the following windows # # For the following windows on the horizon, we: # -# 1. Update the parameter values from the input data for that window -# 2. Update the parameters in the models using the ParametricOptInterface.jl -# 3. Solve the model for that window -# 4. Store the results -# -# Although this is a small problem, the benefits of using -# ParametricOptInterface.jl can be seen in the simplex iterations in the first -# window compared to the ones in the subsequent ones. +# 1. Update the parameters in the models using the ParametricOptInterface.jl +# 2. Solve the model for that window +# 3. Store the results -for window in 2:number_of_windows - # Update window data - window_start = Int(1 + (window - 1) * move_forward) - window_end = Int(window_start + optimization_window - 1) - time_series_filter = mod1.(window_start:window_end, total_time_length) - availability = time_series.solar_pu[time_series_filter] - demand = time_series.demand_MW[time_series_filter] - initial_storage = storage_level[window_start-1] - # Update parameters in the model - MOI.set.(model, POI.ParameterValue(), model[:D], demand) - MOI.set.(model, POI.ParameterValue(), model[:A], availability) - MOI.set(model, POI.ParameterValue(), model[:So], initial_storage) - # Optimize again +for offset in 0:move_forward:total_time_length-1 + ## Step 1: update the parameter values over the optimization_window + for t in 1:optimization_window + row = mod1(offset + t, size(time_series, 1)) + set_parameter_value(model[:D][t], time_series[row, :demand_MW]) + set_parameter_value(model[:A][t], time_series[row, :solar_pu]) + end + set_parameter_value(model[:So], get(storage_level, offset, 0)) + ## Step 2: solve the model optimize!(model) - # Store results for each window - window_end_output = - minimum([Int(window_start + move_forward - 1) total_time_length]) - output_filter = window_start:window_end_output - last_output_value = - minimum([move_forward (window_end_output - window_start + 1)]) - renewable_production[output_filter] = value.(model[:r])[1:last_output_value] - storage_level[output_filter] = value.(model[:s])[1:last_output_value] - objective_function_per_window[window] = objective_value(model) + ## Step 3: store the results of the move_forward values + push!(objective_function_per_window, objective_value(model)) + for t in 1:move_forward + renewable_production[offset+t] = value(model[:r][t]) + storage_level[offset+t] = value(model[:s][t]) + end end # We can explore the outputs in the following graphs: Plots.plot( - objective_function_per_window; + objective_function_per_window ./ 10^3; label = false, linewidth = 3, xlabel = "Window", - ylabel = "\$", - title = "Objective Function per Window", + ylabel = "[000'] \$", ) #- @@ -322,6 +260,7 @@ Plots.plot( linewidth = 3, xlabel = "Hours", ylabel = "MW", + xticks = 0:12:total_time_length, ) # **Final remark**: [ParametricOptInterface.jl](@ref) offers an easy way to