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
PropertyValue
Name
Description
System Units BaseSYSTEM_BASE
Base Power100.0
Base Frequency60.0
Num Components504
Static Components
TypeCount
ACBus73
Arc109
Area3
FixedAdmittance3
HydroDispatch1
Line105
LoadZone21
PowerLoad51
RenewableDispatch29
RenewableNonDispatch31
SynchronousCondenser3
TapTransformer15
ThermalStandard54
TwoTerminalGenericHVDCLine1
VariableReserve{ReserveDown}1
VariableReserve{ReserveUp}4
StaticTimeSeries Summary
owner_typeowner_categorynametime_series_typeinitial_timestampresolutioncounttime_step_count
StringStringStringStringStringDates.CompoundPeriodInt64Int64
AreaComponentmax_active_powerSingleTimeSeries2020-01-01T00:00:001 hour38784
FixedAdmittanceComponentmax_active_powerSingleTimeSeries2020-01-01T00:00:001 hour38784
HydroDispatchComponentmax_active_powerSingleTimeSeries2020-01-01T00:00:001 hour18784
PowerLoadComponentmax_active_powerSingleTimeSeries2020-01-01T00:00:001 hour518784
RenewableDispatchComponentmax_active_powerSingleTimeSeries2020-01-01T00:00:001 hour298784
RenewableNonDispatchComponentmax_active_powerSingleTimeSeries2020-01-01T00:00:001 hour318784
VariableReserveComponentrequirementSingleTimeSeries2020-01-01T00:00:001 hour58784
Forecast Summary
owner_typeowner_categorynametime_series_typeinitial_timestampresolutioncounthorizonintervalwindow_count
StringStringStringStringStringDates.CompoundPeriodInt64Dates.CompoundPeriodDates.CompoundPeriodInt64
AreaComponentmax_active_powerDeterministicSingleTimeSeries2020-01-01T00:00:001 hour32 days1 day365
FixedAdmittanceComponentmax_active_powerDeterministicSingleTimeSeries2020-01-01T00:00:001 hour32 days1 day365
HydroDispatchComponentmax_active_powerDeterministicSingleTimeSeries2020-01-01T00:00:001 hour12 days1 day365
PowerLoadComponentmax_active_powerDeterministicSingleTimeSeries2020-01-01T00:00:001 hour512 days1 day365
RenewableDispatchComponentmax_active_powerDeterministicSingleTimeSeries2020-01-01T00:00:001 hour292 days1 day365
RenewableNonDispatchComponentmax_active_powerDeterministicSingleTimeSeries2020-01-01T00:00:001 hour312 days1 day365
VariableReserveComponentrequirementDeterministicSingleTimeSeries2020-01-01T00:00:001 hour52 days1 day365

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 ModelPTDFPowerModel
Slacksfalse
PTDFtrue
DualsNone
HVDC Network ModelNone
Device Models
Device TypeFormulationSlacks

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
DateTimeA14A15A16A17A7B14B15B16B17B7C14C15C16C17C7
Dates.DateTimeFloat64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64
2020-01-01T00:00:00-48.5716342145811-29.179652382124527-83.35453627308806-63.67597403525195-157.729868109477-52.91964994838552-76.9805088054322-91.79800492505363-116.21444222398445-85.2987690068848744.469326485217149.091366284244856-76.85455822967211-72.1642125684211280.70675628343014
2020-01-01T01:00:00-49.23285683993406-26.43984511538936-85.40211350365618-62.27225995467729-165.2448210851105-49.653478809436905-68.90161435587643-88.74073277283613-108.27332292197462-83.6182571480307851.65354191090851548.06647177499709-67.07907514469449-70.7191560570473108.79926506606041
2020-01-01T02:00:00-48.520529274400936-31.79645927870774-82.57812218745556-65.60689897140716-153.26011632174027-51.70868571432652-77.63483029616981-87.72013622718664-114.0294249914237-81.0615772327016751.1615231028134158.1812188563709-69.00475053611295-61.8813156561436191.98170885478542
2020-01-01T03:00:00-51.63796632502952-30.50514452006368-88.45252897975357-67.00740012479119-167.12919541673472-51.76173972271481-73.34912701494677-90.31152712804207-112.2179391726042-85.920213779793241.08264236288806774.282217371049975-78.8012125930288-75.5543533252795438.311812791870416
2020-01-01T04:00:00-55.36999568758048-38.20725642166231-93.14846867272747-75.7320934030865-169.05458450337554-50.419613332161404-79.43781073710639-81.71400469600067-111.16104154848325-77.2773012751782538.3369821633966143.853206039369944-73.75533788845215-68.1575936352323360.713885270391735
2020-01-01T05:00:00-51.177559212829806-37.6493318239489-87.07354678945123-73.34539528127871-163.24593132496523-48.85020278489537-75.21375396544012-84.54301247184979-111.29617195802513-74.7455179821730327.24238993780433331.8812821385141-79.47741679429863-74.7699696816218959.3643978564443
2020-01-01T06:00:00-47.31179858162059-33.87182475643239-83.78987548536405-70.15128177797965-164.61534524231126-49.09808715134531-77.96091993645189-90.0251748214739-119.31455103011604-68.18029036166931-2.5941395064533253-11.365990566468101-95.72536630866057-104.6268503610341414.359641879458412
2020-01-01T07:00:00-30.659781572668454-21.188491021254645-57.19829622571828-47.58703615962855-100.5426703564803-52.193226029573566-69.51686041451485-92.28872660628944-109.86837475029745-99.99643642255992-61.34400429684321-68.92598585156054-100.45589466639004-108.14992497230283-16.77892297079793
2020-01-01T08:00:00-17.357226392836928-12.703351890058979-39.80853996578525-35.08588913817804-57.10691757360837-49.322345741368245-60.480364672701995-91.56747252426096-102.89038793787282-101.6334063369833-113.1742599058998-100.52973546816627-119.94979666320074-107.11840772804216-108.70465239890166
2020-01-01T09:00:00-10.453876802370528-9.394886432174417-31.430370404354836-30.35572996330948-33.00191515101901-49.26398226370462-61.2229417990259-91.68064788610393-103.81634043928574-100.47770935151678-125.11785257322911-112.89897165888799-124.01551937019167-111.61606424347923-112.04998823421192

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.