- Navigate to a new folder and start a Julia REPL, by either clicking on the Julia icon in Windows or by typing
juliaand pressing enter in a Mac or Windows Terminal.
$ julia
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.10.4 (2024-06-04)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |
julia>- Setup an environment and output folder:
using Pkg
Pkg.activate(".")
Pkg.resolve()
Pkg.instantiate()Create an output folder to hold results and other outputs:
results_dir = "outputs"
if !ispath(results_dir)
mkdir(results_dir)
end- Install Sienna\Data and Sienna\Ops packages, and an optimization problem solver -- we'll use HiGHS, which is open-source:
Pkg.add([
"PowerSystems",
"PowerSystemCaseBuilder",
"PowerSimulations",
"PowerAnalytics",
"HiGHS",
"DataFrames",
"CSV",
]);Some other (optional) packages include
- Now load the packages we'll need for now:
using CSV
using DataFrames
using Dates
using HiGHS
using PowerSystems
using PowerSystemCaseBuilder
using PowerSimulations
using PowerAnalyticsThese steps may take awhile on the first run while Julia sets up your programming environment. It will be quicker on future runs.
- We can start by listing all the available test system categories:
show_categories()- What PowerSimulations.jl test systems are available?
show_systems(PSISystems)- Load in a version of the Reliability Test System with time-series for day-ahead modeling:
system = build_system(PSISystems, "modified_RTS_GMLC_DA_sys");- Print the system to take a look around:
systemWhat are all these Types? Take a look at the Type Tree.
- Take a look at what renewable generators are in this power system:
show_components(system, RenewableDispatch)- Let's look at more information about a particular renewable generator, retrieving by name:
g = get_component(RenewableDispatch, system, "101_PV_4")Each field has a getter and setter function of the form: get_ or set_
- Get the rating:
get_rating(g)- We have a rating of
0.516.... what does this number mean?
get_units_base(system)Getters and setters handle per-unitization for you, but different per-unitization options trip up new users! When in doubt, switch your system units base and spotcheck your data with different bases to make sure you're confident. See Changing System Per-Unit Settings in Create and Explore a Power System.
- Change the settings to device base (per-unitized by
base_powervalue):
set_units_base_system!(system, "DEVICE_BASE")- Now get the rating again:
get_rating(g)- Change the settings to natural units (MW, MVAR, etc.)
set_units_base_system!(system, "NATURAL_UNITS")- Now get the rating again:
get_rating(g)- Now let's set a new rating -- still in natural units! (MVAR)
set_rating!(g, 25.8)- And we can always just print the component again to review its data:
g- Some fields link to other objects in the
System:
get_bus(g)- Get many components by
Type:
res = get_components(RenewableDispatch, system)- What exactly did we just retrieve?
typeof(res)- Iterators get us the data access we need, without really large memory allocations. Use an iterator to easy update data:
for g in get_components(RenewableDispatch, system)
set_available!(g, false)
end- Be mindful of those units when filtering!
get_name.(get_components(x -> get_prime_mover_type(x) == PrimeMovers.PVe && get_rating(x) >= 100.0, RenewableDispatch, system))- Just update combustion turbine ramp limits:
for comp in get_components(x -> get_prime_mover_type(x) == PrimeMovers.CT, ThermalStandard, system)
pmax = get_active_power_limits(comp).max
set_ramp_limits!(comp, (up = (pmax*0.2)/60, down = (pmax*0.2)/60)) # Ramp rate is expected to be in MW/min
end- We can review all the data we just updated in a batch:
show_components(system, ThermalStandard, [:base_power, :active_power_limits, :ramp_limits])- See a summary:
show_time_series(system)Note: The function is sensitive to which Units Base settings you have defined!
- Get the entire year-long time series:
get_time_series_array(SingleTimeSeries, g, "max_active_power")- Get one forecast window:
get_time_series_array(Deterministic, g, "max_active_power")- We have turned off the renewable generators and updated CT ramp rates. Export the system to json and .h5 files for the time series:
to_json(system, "my_new_RTS_DA_system.json", force=true)- Use show_components and get_components to filter for the data or access that you need, without large memory allocations.
- Use getters (
get_) and setters (set_) to access and change data, with per-unitization handling built it.
- Before you run the simulation, you need to define the solver parameters:
solver = optimizer_with_attributes(
HiGHS.Optimizer,
"time_limit" => 150.0,
"threads" => 12,
"log_to_console" => true,
"mip_abs_gap" => 1e-5
)- Set up a default unit commitment model
template = template_unit_commitment()How do you learn more about the different formulations? See the Formulation Library.
- Apply that template to our data to make a complete model, with both data and equations:
models = SimulationModels(
decision_models=[
DecisionModel(template, system, optimizer=solver, name="UC"),
],
)We just have one model: the unit commitment model.
- In addition to defining the formulation template, sequential simulations require definitions for how information flows between problems to set the initial conditions of the next problem:
DA_sequence = SimulationSequence(
models=models,
ini_cond_chronology=InterProblemChronology(),
)- Finally, put together the models and the flow of information between them to simulate 3 days of a unit commitment:
sim = Simulation(
name = "default_UC",
steps = 3,
models=models,
sequence=DA_sequence,
simulation_folder=results_dir,
)- Build the simulation, which does all the equation preparation before executing:
build!(sim, recorders = [:simulation])- Simulate!
execute!(sim)- Now, load in the simulation results from the output files:
results = SimulationResults(sim)
uc_results = get_decision_problem_results(results, "UC")-
If we load the PowerGraphics.jl package, we can visualize the results using the
plot_fuelfunction:plot_fuel(uc_results); -
We can also look at how much it cost to operate the thermal generators at each time step:
costs = read_realized_expressions(uc_results, list_expression_names(uc_results))["ProductionCostExpression__ThermalStandard"]Wind, solar, and hydro have zero operating cost and do not contribute to total cost in this model
- We can sum over the set of generators and time-steps to get total production cost for this window
total_costs_prod = sum(costs[:, :value])Now, let's change some things in the system:
- Let's change how the thermal generators behave by adding in ramping and minimum up and down time constraints:
set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment)You can see the formulations options in the documentation for ThermalGen.
- We've changed our template, so we need to re-build the DecisionModels within our simulation, which includes a copy of the template:
models = SimulationModels(
decision_models=[
DecisionModel(template, system, optimizer=solver, name="UC"),
],
)
DA_sequence = SimulationSequence(
models=models,
ini_cond_chronology=InterProblemChronology(),
)
sim = Simulation(
name = "UC_with_ramping",
steps = 3,
models=models,
sequence=DA_sequence,
simulation_folder=results_dir,
)- Now, let's re-build, and execute our simulation:
build!(sim, recorders = [:simulation]);
execute!(sim)- Let's re-extract the results and take a look at the impact on the dispatch stack:
results = SimulationResults(sim)
uc_results = get_decision_problem_results(results, "UC")With the PowerGraphics.jl package, we can visualize this using
plot_fuel(uc_results);
- Finally, let's extract the operating costs again and see the impact on total cost:
costs = read_realized_expressions(uc_results, list_expression_names(uc_results))["ProductionCostExpression__ThermalStandard"]
total_costs_uc = sum(costs[:, :value])- Now, let's add in those large-scale wind and solar plants by making them available again:
for g in get_components(RenewableDispatch, system)
set_available!(g, true)
end- We've changed the data, not the template formulations, so this time we could just re-build and simulate, but let's redefine our simulation name:
sim = Simulation(
name = "UC_with_renewables",
steps = 3,
models=models,
sequence=DA_sequence,
simulation_folder=results_dir,
)
build!(sim, recorders = [:simulation]);
execute!(sim)- Let's re-extract the results and take a look at the impact on the dispatch stack:
results = SimulationResults(sim)
uc_results = get_decision_problem_results(results, "UC")With the PowerGraphics.jl package, we can visualize this using
plot_fuel(uc_results);
- Finally, let's extract the operating costs again and see the impact on total cost:
costs = read_realized_expressions(uc_results, list_expression_names(uc_results))["ProductionCostExpression__ThermalStandard"]
total_costs_renew = sum(costs[:, :value])- Map to all our results scenarios:
results_all = create_problem_results_dict(results_dir, "UC"; populate_system=true)- Define which time-independent metrics we're interested in:
PA = PowerAnalytics
timeless_computations = [PA.calc_sum_objective_value, PA.calc_sum_solve_time, PA.calc_sum_bytes_alloc]
timeless_names = ["Objective Value", "Solve Time", "Memory Allocated"];For more, see PowerAnalytics' Built-In Metrics.
- We'll also make selectors, which help us calculate our metrics across whichever dimensions and/or groupings are of interest:
thermal_standard_selector_sys = make_selector(ThermalStandard; groupby=:all)
renewable_dispatch_selector_sys = make_selector(RenewableDispatch; groupby=:all)- Let's also calculate some time dependent metrics of performance:
time_computations = [
(PA.calc_total_cost, thermal_standard_selector_sys, "Total Cost (\$)"),
(PA.calc_active_power, thermal_standard_selector_sys, "Thermal Generation (MWh)"),
(PA.calc_curtailment, renewable_dispatch_selector_sys, "Renewables Curtailment (MWh)"),
]- A few utility functions help us calculate across metrics and scenarios:
function analyze_one(results)
time_series_analytics = compute_all(results, time_computations...)
aggregated_time = aggregate_time(time_series_analytics)
computed_all = compute_all(results, timeless_computations, nothing, timeless_names)
all_time_analytics = hcat(aggregated_time, computed_all)
return time_series_analytics, all_time_analytics
end
function save_one(output_dir, time_series_analytics, all_time_analytics)
CSV.write(joinpath(output_dir, "summary_dataframe_new.csv"), time_series_analytics)
CSV.write(joinpath(output_dir, "summary_stats_new.csv"), all_time_analytics)
end
function post_processing(all_results)
summaries = []
for (scenario_name, results) in pairs(all_results)
println("Computing for scenario: ", scenario_name)
(time_series_analytics, all_time_analytics) = analyze_one(results)
save_one(results.results_output_folder, time_series_analytics, all_time_analytics)
push!(summaries, hcat(DataFrame("Scenario" => scenario_name), all_time_analytics))
end
summaries_df = vcat(summaries...)
CSV.write("all_scenarios_summary_new.csv", summaries_df)
return summaries_df
end- Post-process all scenarios together:
post_processing(results_all)