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:

  1. Attach a DLR time series to transmission branches in a PowerSystems.System.
  2. Build a PTDFPowerModel template that activates the DLR constraints.
  3. Run a multi-step simulation and read the resulting line flows and DLR parameters.
Note

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 TimeSeries

Optimizer

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

Note

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
end
add_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.95

Select 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.

Tip

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 = 0

Inspecting 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,
)
38 rows omitted
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
Tip

To verify that DLR constraints are binding, compare the line flows in line_flows against the corresponding DLR parameter values in dlr_params. When demand is high and the DLR limit is tight, the flow should be at or near the DLR limit rather than the static rating.