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 -49.03906539444133 -20.186223370267932 -86.07235163032816 -56.79311382901968 -169.88508370129145 -51.72673231675249 -77.05345172482006 -89.41358867476865 -115.11459378388551 -81.58129092973425 45.115761674476964 48.444931094985066 -76.19856983124622 -72.82020096684694 84.2975725820552
2020-01-01T01:00:00 -48.804667863758766 -20.12136983185363 -85.75219555343257 -56.6450073153265 -169.4008186889375 -50.73523423695239 -76.49689803410115 -87.48267958635753 -113.62505682053516 -78.715374575667 48.24909063636253 51.470923049543075 -70.5338384004904 -67.26439280125133 93.14750557809177
2020-01-01T02:00:00 -49.18763117852465 -22.40768899242354 -85.770010074849 -58.59430603449071 -166.90714978740814 -51.40688717368364 -78.83391624035946 -87.17026626094686 -115.00262002193149 -78.63630473090248 51.79656079066637 57.546181168518004 -68.36032807471861 -62.5257381175379 95.4118753370026
2020-01-01T03:00:00 -49.53413646088961 -22.587309664886433 -86.81754325046782 -59.4724883304074 -169.6329637058251 -50.24455497282856 -74.98437770878252 -87.23014295275972 -112.33557805829719 -78.76927040740725 45.85263300242595 49.075048628430885 -69.38162434228946 -66.11158691133554 80.58814899539996
2020-01-01T04:00:00 -49.21147867741407 -25.142888499564275 -85.02040673337643 -60.59612385791191 -166.10708206148792 -48.58945924402024 -77.46292010600318 -80.88986448377447 -110.19002583413588 -72.59918784826152 38.353766334602405 43.02274162999953 -73.51739346076704 -68.77941867659048 61.91144612222943
2020-01-01T05:00:00 -40.82460872402647 -22.964607558328808 -70.1714949473353 -52.047553436686535 -145.69300059470075 -44.448259425594266 -74.59601321975028 -75.59694388359608 -106.19023004150199 -62.360910145522986 27.108591713370707 32.01508036294779 -79.6131923282063 -74.6341941477142 59.052936326119784
2020-01-01T06:00:00 -35.68718210645221 -18.076059226873838 -65.65477195496203 -47.78338672687163 -146.00292685050835 -43.369683021553115 -75.69063219574876 -80.5418360330567 -113.34043369086875 -52.17223733786891 -2.6960486854170123 -9.330485414671731 -96.3537481575721 -103.0862305421787 11.513807936681792
2020-01-01T07:00:00 -20.871747187014712 -6.094056984279845 -41.75412124115536 -26.75804199054166 -86.55540465149147 -47.551483137180476 -69.80764410608856 -82.91775746421888 -105.50282651935376 -85.22681627766075 -61.47737858531268 -67.39075085506728 -100.97184178816477 -106.97260361082161 -18.84215791852825
2020-01-01T08:00:00 -7.553431446797088 2.184687196591241 -24.282305861661285 -14.400274439653066 -42.76018168743902 -44.69950396123026 -60.82153523492517 -82.19687230099605 -98.55715968991804 -86.86932474762618 -113.27414133784659 -99.19825456079211 -120.38553044866276 -106.10162608096199 -110.51729973594719
2020-01-01T09:00:00 -0.6614217516979326 5.5405822060635534 -15.94262318061116 -9.648964186054695 -18.803030565654808 -44.62121150167194 -61.47261186605819 -82.32007699789649 -99.42051230823931 -85.74853034104427 -127.9326956698378 -110.40490133695062 -126.78487201329484 -108.99804678564269 -107.97788027427404

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.