Dynamic Line Ratings (DLR)
To follow along, you can download this tutorial as a Julia script (.jl) or Jupyter notebook (.ipynb).
Introduction
Static branch ratings use a fixed thermal limit $R^\text{max}$ for each transmission line. Dynamic Line Ratings (DLR) replace this fixed limit with a time-varying parameter $R^\text{max}_t$, allowing the optimizer to exploit periods when ambient conditions (wind, temperature) permit higher line flows. This reduces curtailment and can lower total generation cost compared to conservative static limits.
This tutorial demonstrates how to:
- Attach a DLR time series to transmission branches in a
PowerSystems.System. - Build a
PTDFPowerModeltemplate that activates the DLR constraints. - Run a multi-step simulation and read the resulting line flows and DLR parameters.
Dynamic Line Ratings are supported for the StaticBranch (or SecurityConstrainedStaticBranch) formulation combined with a PTDFPowerModel (or any AbstractPTDFModel), a DC power flow (DCPPowerModel / any PM.AbstractActivePowerModel), or full AC (ACPPowerModel / any PM.AbstractPowerModel) network model. With StaticBranchUnbounded the formulation does not enforce flow limits, so a time-varying rating would have no effect: template validation emits a warning and the branch rating time series is ignored (the model still builds).
Load packages
using PowerSystems
using PowerSimulations
using HydroPowerSimulations
using PowerNetworkMatrices
using PowerSystemCaseBuilder
using HiGHS
using Dates
using TimeSeriesOptimizer
solver = optimizer_with_attributes(HiGHS.Optimizer, "mip_rel_gap" => 0.01)MathOptInterface.OptimizerWithAttributes(HiGHS.Optimizer, Pair{MathOptInterface.AbstractOptimizerAttribute, Any}[MathOptInterface.RawOptimizerAttribute("mip_rel_gap") => 0.01])Data
PowerSystemCaseBuilder.jl is a helper library that makes it easier to reproduce examples in the documentation and tutorials. Normally you would pass your local files to create the system data instead of calling build_system. For more details visit PowerSystemCaseBuilder Documentation
sys = build_system(PSISystems, "modified_RTS_GMLC_DA_sys")| System | |
| Property | Value |
|---|---|
| Name | |
| Description | |
| System Units Base | SYSTEM_BASE |
| Base Power | 100.0 |
| Base Frequency | 60.0 |
| Num Components | 504 |
| Static Components | |
| Type | Count |
|---|---|
| ACBus | 73 |
| Arc | 109 |
| Area | 3 |
| FixedAdmittance | 3 |
| HydroDispatch | 1 |
| Line | 105 |
| LoadZone | 21 |
| PowerLoad | 51 |
| RenewableDispatch | 29 |
| RenewableNonDispatch | 31 |
| SynchronousCondenser | 3 |
| TapTransformer | 15 |
| ThermalStandard | 54 |
| TwoTerminalGenericHVDCLine | 1 |
| VariableReserve{ReserveDown} | 1 |
| VariableReserve{ReserveUp} | 4 |
| StaticTimeSeries Summary | |||||||
| owner_type | owner_category | name | time_series_type | initial_timestamp | resolution | count | time_step_count |
|---|---|---|---|---|---|---|---|
| String | String | String | String | String | Dates.CompoundPeriod | Int64 | Int64 |
| Area | Component | max_active_power | SingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 3 | 8784 |
| FixedAdmittance | Component | max_active_power | SingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 3 | 8784 |
| HydroDispatch | Component | max_active_power | SingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 1 | 8784 |
| PowerLoad | Component | max_active_power | SingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 51 | 8784 |
| RenewableDispatch | Component | max_active_power | SingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 29 | 8784 |
| RenewableNonDispatch | Component | max_active_power | SingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 31 | 8784 |
| VariableReserve | Component | requirement | SingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 5 | 8784 |
| Forecast Summary | |||||||||
| owner_type | owner_category | name | time_series_type | initial_timestamp | resolution | count | horizon | interval | window_count |
|---|---|---|---|---|---|---|---|---|---|
| String | String | String | String | String | Dates.CompoundPeriod | Int64 | Dates.CompoundPeriod | Dates.CompoundPeriod | Int64 |
| Area | Component | max_active_power | DeterministicSingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 3 | 2 days | 1 day | 365 |
| FixedAdmittance | Component | max_active_power | DeterministicSingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 3 | 2 days | 1 day | 365 |
| HydroDispatch | Component | max_active_power | DeterministicSingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 1 | 2 days | 1 day | 365 |
| PowerLoad | Component | max_active_power | DeterministicSingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 51 | 2 days | 1 day | 365 |
| RenewableDispatch | Component | max_active_power | DeterministicSingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 29 | 2 days | 1 day | 365 |
| RenewableNonDispatch | Component | max_active_power | DeterministicSingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 31 | 2 days | 1 day | 365 |
| VariableReserve | Component | requirement | DeterministicSingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 5 | 2 days | 1 day | 365 |
Preparing DLR Time Series
DLR is represented in PowerSystems.jl as a SingleTimeSeries (or Deterministic) attached directly to each branch component. The time series values are scaling factors applied to the branch's static get_rating. A value of 1.15 means the line can carry 15% more than its rated static capacity during that hour; a value of 0.95 represents a de-rating.
The helper function below iterates over a list of branch names, constructs a SingleTimeSeries from a vector of hourly scaling factors, and attaches it to each branch with scaling_factor_multiplier = get_rating so PowerSimulations knows how to convert the factor to a per-unit limit.
function add_dlr_to_system_branches!(
sys::System,
branches_dlr::Vector{String},
n_steps::Int,
dlr_factors::Vector{Float64};
initial_date::String = "2020-01-01",
)
for branch_name in branches_dlr
branch = get_component(ACTransmission, sys, branch_name)
data_ts = collect(
DateTime("$initial_date 0:00:00", "y-m-d H:M:S"):Hour(1):(
DateTime("$initial_date 23:00:00", "y-m-d H:M:S") + Day(n_steps - 1)
),
)
dlr_data = TimeArray(data_ts, dlr_factors)
PowerSystems.add_time_series!(
sys,
branch,
PowerSystems.SingleTimeSeries(
"dynamic_line_ratings",
dlr_data;
scaling_factor_multiplier = get_rating,
),
)
end
endadd_dlr_to_system_branches! (generic function with 1 method)Define DLR scaling factors. Here we use a daily cycle of four blocks repeated across the simulation horizon: the early morning hours are de-rated (0.95), mid-day has higher capacity (1.15 and 1.05), and evening hours have intermediate capacity (0.95).
n_steps = 2 # simulation length in days
initial_date = "2020-01-01"
data_days = 366 # length of DLR time series in days; must span the system's TS window
dlr_factors_daily = vcat([fill(x, 6) for x in [1.15, 1.05, 0.95, 0.95]]...) # 24 values
dlr_factor_ts = repeat(dlr_factors_daily, data_days)8784-element Vector{Float64}:
1.15
1.15
1.15
1.15
1.15
1.15
1.05
1.05
1.05
1.05
⋮
0.95
0.95
0.95
0.95
0.95
0.95
0.95
0.95
0.95Select the branch names that will receive DLR time series. These names must match branches present in the system.
branches_dlr = [
"A2", "AB1", "A24", "B10", "B18", "CA-1", "C22", "C34",
"A7", "A17", "B14", "B15", "C7", "C17",
]
add_dlr_to_system_branches!(sys, branches_dlr, data_days, dlr_factor_ts; initial_date)Because the simulation uses a rolling horizon of 48 hours (2 days), we transform the SingleTimeSeries into Deterministic forecasts with a 48-hour horizon and a 24-hour interval between forecast windows.
transform_single_time_series!(sys, Hour(48), Day(1))Define the Problem Template
The template must use PTDFPowerModel to enable DLR constraints. The key step is constructing DeviceModel with time_series_names that maps BranchRatingTimeSeriesParameter to the time series name "dynamic_line_ratings" attached to the branches above.
Any branch type that has the "dynamic_line_ratings" time series attached and is configured with BranchRatingTimeSeriesParameter in time_series_names will have time-varying flow limits. Branches without the time series attached will fall back to static limits automatically.
template_uc = ProblemTemplate(
NetworkModel(
PTDFPowerModel;
reduce_radial_branches = false,
use_slacks = false,
PTDF_matrix = PTDF(sys),
),
)| Network Model | |
| Network Model | PTDFPowerModel |
| Slacks | false |
| PTDF | true |
| Duals | None |
| HVDC Network Model | None |
| Device Models | ||
| Device Type | Formulation | Slacks |
|---|---|---|
Branch models with DLR enabled
line_device_model = DeviceModel(
Line,
StaticBranch;
time_series_names = Dict(
BranchRatingTimeSeriesParameter => "dynamic_line_ratings",
),
)
tap_transformer_device_model = DeviceModel(
TapTransformer,
StaticBranch;
time_series_names = Dict(
BranchRatingTimeSeriesParameter => "dynamic_line_ratings",
),
)
set_device_model!(template_uc, line_device_model)
set_device_model!(template_uc, tap_transformer_device_model)Injection device models
set_device_model!(template_uc, ThermalStandard, ThermalStandardUnitCommitment)
set_device_model!(template_uc, RenewableDispatch, RenewableFullDispatch)
set_device_model!(template_uc, RenewableNonDispatch, FixedOutput)
set_device_model!(template_uc, PowerLoad, StaticPowerLoad)
set_device_model!(template_uc, HydroDispatch, HydroDispatchRunOfRiver)
set_device_model!(
template_uc,
DeviceModel(TwoTerminalGenericHVDCLine, HVDCTwoTerminalLossless),
)Reserve models
set_service_model!(template_uc, ServiceModel(VariableReserve{ReserveUp}, RangeReserve))
set_service_model!(template_uc, ServiceModel(VariableReserve{ReserveDown}, RangeReserve))Build and Run a Simulation
We wrap the DecisionModel in a Simulation to run multiple steps. Each step solves a 48-hour unit commitment problem and advances the clock by 24 hours.
model = DecisionModel(
template_uc,
sys;
name = "UC",
optimizer = solver,
initialize_model = true,
store_variable_names = true,
)
models = SimulationModels(; decision_models = [model])
sequence = SimulationSequence(;
models = models,
ini_cond_chronology = InterProblemChronology(),
)
sim = Simulation(;
name = "DLR_example",
steps = n_steps,
models = models,
initial_time = DateTime(initial_date * "T00:00:00"),
sequence = sequence,
simulation_folder = mktempdir(; cleanup = true),
)
build!(sim)
execute!(sim)InfrastructureSystems.Simulation.RunStatusModule.RunStatus.SUCCESSFULLY_FINALIZED = 0Inspecting Results
Line flows
Retrieve the realized active power flows for Line and TapTransformer branches. Each column corresponds to one branch; each row to one time step.
results = SimulationResults(sim)
uc_results = get_decision_problem_results(results, "UC")
line_flows = read_realized_expression(
uc_results,
"PTDFBranchFlow__Line";
table_format = TableFormat.WIDE,
)
transformer_flows = read_realized_expression(
uc_results,
"PTDFBranchFlow__TapTransformer";
table_format = TableFormat.WIDE,
)| DateTime | A14 | A15 | A16 | A17 | A7 | B14 | B15 | B16 | B17 | B7 | C14 | C15 | C16 | C17 | C7 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Dates.DateTime | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 |
| 2020-01-01T00:00:00 | -48.3606077627944 | -29.371056047786066 | -82.94584382801301 | -63.675658938145055 | -156.65665791422822 | -52.00129084819124 | -76.15536179361266 | -89.86149197857307 | -114.3725188815087 | -83.46500736245714 | 44.38323581534967 | 49.17745695411233 | -76.94192117275449 | -72.07684962533871 | 80.2417361187068 |
| 2020-01-01T01:00:00 | -47.827042338999995 | -30.380593015920553 | -81.75463325231831 | -64.05035517493009 | -153.06300154889854 | -51.42928126177808 | -77.11118469658213 | -87.83177401289637 | -113.89321216265893 | -80.43822504193936 | 46.46882590363809 | 53.251187782267465 | -72.34041240852224 | -65.45781879321947 | 84.30746227910176 |
| 2020-01-01T02:00:00 | -44.06428696673389 | -28.21755720614419 | -74.61616861381356 | -58.535251224647965 | -143.2251681634199 | -51.21235815309099 | -76.98169751720542 | -87.52854949139561 | -113.6787157243228 | -79.85702743474555 | 51.06037238309635 | 58.28236957608799 | -69.10739609088667 | -61.778670101369904 | 91.4353415838937 |
| 2020-01-01T03:00:00 | -46.62360201808367 | -26.30001545422798 | -79.8138382221378 | -59.18990372445266 | -156.45581537072493 | -45.70851420368776 | -65.57664645130197 | -80.57086349279903 | -100.73261283653301 | -76.27294101213135 | 10.243778365429325 | 10.988840355329678 | -85.50868590223602 | -84.75261316733078 | -24.89840867695456 |
| 2020-01-01T04:00:00 | -51.01346982964057 | -30.91430619994979 | -86.39156982562011 | -65.9953748501622 | -166.53752023335537 | -47.79532124550941 | -71.99817395541518 | -80.80961169339035 | -105.37014127205924 | -76.69668117226003 | 33.79189548017579 | 36.81088441777512 | -75.22163989188772 | -72.15803544871262 | 49.918925823170056 |
| 2020-01-01T05:00:00 | -42.92738260293923 | -28.32878874055461 | -72.53347326926314 | -57.7191370973405 | -148.86827032438055 | -48.71461773268974 | -70.03845404086549 | -85.84731611956076 | -107.48628234618629 | -76.91874149407421 | 27.310040892613813 | 30.925828456129068 | -79.16772978228181 | -75.49850704774231 | 60.671050938123415 |
| 2020-01-01T06:00:00 | -40.989722919492344 | -28.183496969478327 | -71.2280271204543 | -58.23254700055478 | -148.97476113099964 | -50.16306613769724 | -71.60978315796707 | -92.54107142508349 | -114.30473433107193 | -77.34560555195937 | -2.5969467402417563 | -11.309919671807197 | -95.74267598357153 | -104.58441178921365 | 14.281249321676407 |
| 2020-01-01T07:00:00 | -22.129017824266253 | -14.45295539948387 | -41.96247456826639 | -34.17297303754733 | -80.91192285255104 | -51.62654371399239 | -61.76253474656568 | -93.97280321862185 | -104.25858690488741 | -103.79008729280324 | -42.94531783739248 | -73.42945654089047 | -85.5578244592368 | -116.49246672151354 | -37.22966719886134 |
| 2020-01-01T08:00:00 | -8.93201533808726 | -5.763600409220588 | -24.929376678540805 | -21.714137982894137 | -38.84965196987258 | -48.567694535784504 | -51.72652286636499 | -93.3832016978835 | -96.58871212167772 | -105.50098857332888 | -96.8407443111915 | -106.76167270906105 | -106.11745364698832 | -116.18499643974363 | -123.5719850572261 |
| 2020-01-01T09:00:00 | -2.0449728739988497 | -2.8940154241395977 | -16.462341835860425 | -17.323931786277495 | -14.174893822907702 | -48.565678212357945 | -51.9486457905027 | -93.67956086886473 | -97.1125229358815 | -104.89183694207426 | -80.92138035952128 | -109.6694399519698 | -92.04194396902217 | -121.21485083304191 | -178.25078096644484 |
| ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ |
DLR parameter values
The DLR parameters that were applied at each time step can be read back from the results. The values are in per-unit (MW if multiplied by base power) and already account for the scaling_factor_multiplier = get_rating applied when the time series was attached.
dlr_params = read_parameter(
uc_results,
"BranchRatingTimeSeriesParameter__Line";
table_format = TableFormat.WIDE,
)
first(keys(dlr_params))2020-01-01T00:00:00