Time-coupled tutorial#
In this tutorial we will see how to run a time-coupled RAO with several timestamps. We will work with a very basic 2-node network with only one line.
Input data#
Network#
The network is very basic with two nodes - one in Belgium and one in France - with a single line connecting them. Both nodes have a generator with a maximal operating power of 1000 MW. The Belgian node also has a load. Initially, 1000 MW are injected into the network at the French node and withdrawn at the Belgian node.
XIIDM network file content
Copy and paste the following snippet to an XIIDM file. In the following, we will assume that the network description is written in a file named
2-node.xiidm.
<?xml version="1.0" encoding="UTF-8"?>
<iidm:network xmlns:iidm="http://www.powsybl.org/schema/iidm/1_15" id="2Nodes" caseDate="2026-02-16T13:37:35.978+01:00" forecastDistance="0" sourceFormat="UCTE" minimumValidationLevel="STEADY_STATE_HYPOTHESIS">
<iidm:substation id="BBE1AA" country="BE">
<iidm:voltageLevel id="BBE1AA1" nominalV="380.0" topologyKind="BUS_BREAKER">
<iidm:busBreakerTopology>
<iidm:bus id="BBE1AA1 ">
<iidm:property name="geographicalName" value="BE1"/>
</iidm:bus>
</iidm:busBreakerTopology>
<iidm:generator id="BBE1AA1 _generator" energySource="OTHER" minP="0.0" maxP="1000.0" voltageRegulatorOn="true" targetP="0.0" targetV="400.0" targetQ="0.0" bus="BBE1AA1 " connectableBus="BBE1AA1 ">
<iidm:minMaxReactiveLimits minQ="-9000.0" maxQ="9000.0"/>
</iidm:generator>
<iidm:load id="BBE1AA1 _load" loadType="UNDEFINED" p0="1000.0" q0="0.0" bus="BBE1AA1 " connectableBus="BBE1AA1 "/>
</iidm:voltageLevel>
</iidm:substation>
<iidm:substation id="FFR1AA" country="FR">
<iidm:voltageLevel id="FFR1AA1" nominalV="380.0" topologyKind="BUS_BREAKER">
<iidm:busBreakerTopology>
<iidm:bus id="FFR1AA1 ">
<iidm:property name="geographicalName" value="FR1"/>
</iidm:bus>
</iidm:busBreakerTopology>
<iidm:generator id="FFR1AA1 _generator" energySource="OTHER" minP="0.0" maxP="1000.0" voltageRegulatorOn="true" targetP="1000.0" targetV="400.0" targetQ="0.0" bus="FFR1AA1 " connectableBus="FFR1AA1 ">
<iidm:minMaxReactiveLimits minQ="-9000.0" maxQ="9000.0"/>
</iidm:generator>
</iidm:voltageLevel>
</iidm:substation>
<iidm:line id="BBE1AA1 FFR1AA1 1" r="0.0" x="10.0" g1="0.0" b1="0.0" g2="0.0" b2="0.0" voltageLevelId1="BBE1AA1" bus1="BBE1AA1 " connectableBus1="BBE1AA1 " voltageLevelId2="FFR1AA1" bus2="FFR1AA1 " connectableBus2="FFR1AA1 " selectedOperationalLimitsGroupId1="DEFAULT" selectedOperationalLimitsGroupId2="DEFAULT">
<iidm:operationalLimitsGroup1 id="DEFAULT">
<iidm:currentLimits permanentLimit="1000.0"/>
</iidm:operationalLimitsGroup1>
<iidm:operationalLimitsGroup2 id="DEFAULT">
<iidm:currentLimits permanentLimit="1000.0"/>
</iidm:operationalLimitsGroup2>
</iidm:line>
<iidm:area id="BE" areaType="ControlArea">
<iidm:voltageLevelRef id="BBE1AA1"/>
</iidm:area>
<iidm:area id="FR" areaType="ControlArea">
<iidm:voltageLevelRef id="FFR1AA1"/>
</iidm:area>
</iidm:network>
CRACs#
For this tutorial, we will simply rely on two timestamps: one at 0:30 AM and one at 1:30 AM. Both CRACs have the exact same content:
one preventive flow CNEC on the only line of the network;
one redispatching action that involves the French generator and the Belgian load in symmetric ways.
The only difference between both CRACs is the CNEC’s threshold: 1000 MW at 0:30 AM and 0 MW at 1:30 AM.
JSON 0:30 AM CRAC file content
Copy and paste the following snippet to a JSON file. In the following, we will assume that the 0:30 CRAC content is written in a file named
crac-0030.json.
{
"type": "CRAC",
"version": "2.9",
"info": "Generated by PowSyBl OpenRAO https://powsybl.readthedocs.io/projects/openrao/",
"id": "crac-202602160030",
"name": "crac-202602160030",
"timestamp": "2026-02-16T00:30:00Z",
"instants": [
{
"id": "preventive",
"kind": "PREVENTIVE"
},
{
"id": "outage",
"kind": "OUTAGE"
}
],
"networkElementsNamePerId": {},
"flowCnecs": [
{
"id": "BE1-FR1-preventive",
"name": "BE1-FR1-preventive",
"networkElementId": "BBE1AA1 FFR1AA1 1",
"operator": "FR",
"instant": "preventive",
"contingencyId": null,
"optimized": true,
"monitored": false,
"thresholds": [
{
"unit": "megawatt",
"min": -1000.0,
"max": 1000.0,
"side": 1
}
]
}
],
"injectionRangeActions": [
{
"id": "redispatchingAction",
"name": "redispatchingAction",
"operator": "FR",
"activationCost": 10.0,
"variationCosts": {
"up": 50.0,
"down": 50.0
},
"onInstantUsageRules": [
{
"instant": "preventive"
}
],
"networkElementIdsAndKeys": {
"BBE1AA1 _load": -1.0,
"FFR1AA1 _generator": 1.0
},
"ranges": [
{
"min": 0.0,
"max": 1000.0
}
]
}
]
}
JSON 1:30 AM CRAC file content
Copy and paste the following snippet to a JSON file. In the following, we will assume that the 1:30 CRAC content is written in a file named
crac-0130.json.
{
"type": "CRAC",
"version": "2.9",
"info": "Generated by PowSyBl OpenRAO https://powsybl.readthedocs.io/projects/openrao/",
"id": "crac-202602160130",
"name": "crac-202602160130",
"timestamp": "2026-02-16T01:30:00Z",
"instants": [
{
"id": "preventive",
"kind": "PREVENTIVE"
},
{
"id": "outage",
"kind": "OUTAGE"
}
],
"networkElementsNamePerId": {},
"flowCnecs": [
{
"id": "BE1-FR1-preventive",
"name": "BE1-FR1-preventive",
"networkElementId": "BBE1AA1 FFR1AA1 1",
"operator": "FR",
"instant": "preventive",
"contingencyId": null,
"optimized": true,
"monitored": false,
"thresholds": [
{
"unit": "megawatt",
"min": -0.0,
"max": 0.0,
"side": 1
}
]
}
],
"injectionRangeActions": [
{
"id": "redispatchingAction",
"name": "redispatchingAction",
"operator": "FR",
"activationCost": 10.0,
"variationCosts": {
"up": 50.0,
"down": 50.0
},
"onInstantUsageRules": [
{
"instant": "preventive"
}
],
"networkElementIdsAndKeys": {
"BBE1AA1 _load": -1.0,
"FFR1AA1 _generator": 1.0
},
"ranges": [
{
"min": 0.0,
"max": 1000.0
}
]
}
]
}
RAO Parameters#
The RAO is configured to run in DC with a costly optimization of the remedial actions, i.e. the aim is to minimize the expenses for the remedial actions among all timestamps.
JSON RAO parameters file content
Copy and paste the following snippet to a JSON file. In the following, we will assume that the RAO parameters are written in a file named
rao-parameters.json.
{
"version": "3.4",
"objective-function": {
"type": "MIN_COST",
"enforce-curative-security": false
},
"range-actions-optimization": {
"pst-ra-min-impact-threshold": 0.01,
"hvdc-ra-min-impact-threshold": 0.001,
"injection-ra-min-impact-threshold": 0.001
},
"topological-actions-optimization": {
"relative-minimum-impact-threshold": 0.0,
"absolute-minimum-impact-threshold": 0.0
},
"not-optimized-cnecs": {
"do-not-optimize-curative-cnecs-for-tsos-without-cras": false
},
"extensions": {
"open-rao-search-tree-parameters": {
"objective-function": {
"curative-min-obj-improvement": 10000.0
},
"range-actions-optimization": {
"max-mip-iterations": 5,
"pst-sensitivity-threshold": 1.0E-6,
"pst-model": "APPROXIMATED_INTEGERS",
"hvdc-sensitivity-threshold": 1.0E-6,
"injection-ra-sensitivity-threshold": 1.0E-6,
"linear-optimization-solver": {
"solver": "CBC",
"relative-mip-gap": 0.001,
"solver-specific-parameters": null
}
},
"topological-actions-optimization": {
"max-preventive-search-tree-depth": 5,
"max-curative-search-tree-depth": 5,
"predefined-combinations": [],
"skip-actions-far-from-most-limiting-element": false,
"max-number-of-boundaries-for-skipping-actions": 0
},
"second-preventive-rao": {
"execution-condition": "DISABLED",
"hint-from-first-preventive-rao": false
},
"load-flow-and-sensitivity-computation": {
"load-flow-provider": "OpenLoadFlow",
"sensitivity-provider": "OpenLoadFlow",
"sensitivity-failure-overcost": 100000.0,
"sensitivity-parameters": {
"version": "1.0",
"load-flow-parameters": {
"version": "1.10",
"voltageInitMode": "UNIFORM_VALUES",
"transformerVoltageControlOn": false,
"phaseShifterRegulationOn": false,
"useReactiveLimits": true,
"twtSplitShuntAdmittance": true,
"shuntCompensatorVoltageControlOn": false,
"readSlackBus": false,
"writeSlackBus": true,
"dc": true,
"distributedSlack": true,
"balanceType": "PROPORTIONAL_TO_GENERATION_P",
"dcUseTransformerRatio": false,
"countriesToBalance": [
"PL",
"NL",
"IT",
"ES",
"BA",
"MK",
"AT",
"ME",
"FR",
"UA",
"AL",
"TR",
"SK",
"CH",
"GR",
"PT",
"BE",
"CZ",
"HR",
"SI",
"RO",
"RS",
"DE",
"BG",
"HU"
],
"componentMode": "MAIN_CONNECTED",
"hvdcAcEmulation": true,
"dcPowerFactor": 1.0,
"extensions": {
"open-load-flow-parameters": {
"slackBusSelectionMode": "MOST_MESHED",
"slackBusesIds": [],
"slackDistributionFailureBehavior": "LEAVE_ON_SLACK_BUS",
"voltageRemoteControl": true,
"lowImpedanceBranchMode": "REPLACE_BY_ZERO_IMPEDANCE_LINE",
"loadPowerFactorConstant": false,
"plausibleActivePowerLimit": 10000.0,
"slackBusPMaxMismatch": 1.0,
"voltagePerReactivePowerControl": false,
"maxNewtonRaphsonIterations": 30,
"newtonRaphsonConvEpsPerEq": 1.0E-4,
"voltageInitModeOverride": "NONE",
"transformerVoltageControlMode": "WITH_GENERATOR_VOLTAGE_CONTROL",
"shuntVoltageControlMode": "WITH_GENERATOR_VOLTAGE_CONTROL",
"minPlausibleTargetVoltage": 0.8,
"maxPlausibleTargetVoltage": 1.2,
"minRealisticVoltage": 0.5,
"maxRealisticVoltage": 1.5,
"lowImpedanceThreshold": 1.0E-8,
"reactiveRangeCheckMode": "MAX",
"networkCacheEnabled": false,
"svcVoltageMonitoring": true,
"stateVectorScalingMode": "NONE",
"maxSlackBusCount": 1,
"debugDir": null,
"incrementalTransformerRatioTapControlOuterLoopMaxTapShift": 3,
"secondaryVoltageControl": false,
"reactiveLimitsMaxPqPvSwitch": 3
}
}
}
}
},
"multi-threading": {
"available-cpus": 1
},
"costly-min-margin-parameters": {
"shifted-violation-penalty": 1000.0
}
}
}
}
Time-coupled constraints#
We will also apply simple time-coupled constraints on the French generator by using 500 MW/h power gradients. This means that the power of the generator cannot vary more than 500 MW upward or downward between the two timestamps.
JSON time-coupled constraints file content
Copy and paste the following snippet to a JSON file. In the following, we will assume that the time-coupled constraints are written in a file named
time-coupled-constraints.json. For more information on the JSON time-coupled constraints format, see the dedicated page.
{
"type": "OpenRAO Time-Coupled Constraints",
"version": "1.0",
"generatorConstraints": [
{
"generatorId": "FFR1AA1 _generator",
"upwardPowerGradient": 500.0,
"downwardPowerGradient": -500.0
}
]
}
Import data#
Network#
Network network = Network.read("2-nodes.xiidm");
CRACs#
Crac crac0030 = Crac.read("crac-0030.json", new FileInputStream("crac-0030.json"), network);
Crac crac0130 = Crac.read("crac-0130.json", new FileInputStream("crac-0130.json"), network);
RAO Parameters#
RaoParameters raoParameters = JsonRaoParameters.read(new FileInputStream("rao-parameters.json"));
Time-coupled constraints#
TimeCoupledConstraints timeCoupledConstraints = JsonTimeCoupledConstraints.read(new FileInputStream("time-coupled-constraints.json"));
Prepare inputs#
The first step is to gather the individual RAO inputs in a TemporalData object that acts like an OffsetDateTime to
RaoInput map.
TemporalData<RaoInputWithNetworkPaths> inputPerTimestamp = new TemporalDataImpl<>();
RaoInputWithNetworkPaths input0030 = RaoInputWithNetworkPaths.build("2-nodes.xiidm", "2-nodes.xiidm", crac0030).build();
inputPerTimestamp.put(OffsetDateTime.of(2026, 2, 16, 0, 30, 0, 0, ZoneOffset.UTC), input0030);
RaoInputWithNetworkPaths input0130 = RaoInputWithNetworkPaths.build("2-nodes.xiidm", "2-nodes.xiidm", crac0130).build();
inputPerTimestamp.put(OffsetDateTime.of(2026, 2, 16, 1, 30, 0, 0, ZoneOffset.UTC), input0130);
Note: currently, the time-coupled RAO provider API requires to pass networks as paths to avoid memory issues on large cases with too many timestamps, leading to this cumbersome writing. This will be fixed soon.
We can then use this TemporalData to create proper time-coupled RAO inputs. We will create two versions: one with
time-coupled constraints and one without:
TimeCoupledRaoInputWithNetworkPaths inputNoConstraints = new TimeCoupledRaoInputWithNetworkPaths(inputPerTimestamp, new TimeCoupledConstraints());
TimeCoupledRaoInputWithNetworkPaths inputWithConstraints = new TimeCoupledRaoInputWithNetworkPaths(inputPerTimestamp, timeCoupledConstraints);
Run the RAO#
Without time-coupled constraints#
Let’s run the RAO. As a first step, we will not include the time-coupled constraints on the generator so the injection remedial action is free to be activated at any set-point between 0 and 1000 MW.
What is expected is that the set-point is kept at 1000 MW at 0:30 because the CNEC’s threshold is 1000 MW too so there is no need to reduce the generator power. However, at 1:30, the threshold drops to 0 MW so to ensure no violation, the set-point must also go down to 0 MW such that both the generator and the load are deactivated ensuring that no power is injected in the network.
With an activation cost of 10 and a variation cost of 50 per MW (for a 1000 MW variation), a global expense of 50010 is expected.
Running the RAO can be performed with the following code:
TimeCoupledRao.find("TimeCoupledRao").run(inputNoConstraints, raoParameters);
Now let’s have a look at the logs:
DEBUG c.p.o.commons.logs.TechnicalLogs - redispatchingAction variation of -1000.00 MW at state preventive - 202602160130 (1000.00 -> 0.00)
INFO c.p.o.commons.logs.RaoBusinessLogs - [MARMOT] Before topological optimizations: cost = 1000000.0 (functional: 0.0, virtual: 1000000.0 {min-margin-violation-evaluator=1000000.0})
INFO c.p.o.commons.logs.RaoBusinessLogs - Limiting element #01: margin = -1000.0 MW, element BBE1AA1 FFR1AA1 1 at state preventive - 202602160130, CNEC ID = "BE1-FR1-preventive"
INFO c.p.o.commons.logs.RaoBusinessLogs - Limiting element #02: margin = 0.0 MW, element BBE1AA1 FFR1AA1 1 at state preventive - 202602160030, CNEC ID = "BE1-FR1-preventive"
INFO c.p.o.commons.logs.RaoBusinessLogs - [MARMOT] Before global linear optimization: cost = 50010.0 (functional: 50010.0, virtual: 0.0)
INFO c.p.o.commons.logs.RaoBusinessLogs - Limiting element #01: margin = 0.0 MW, element BBE1AA1 FFR1AA1 1 at state preventive - 202602160130, CNEC ID = "BE1-FR1-preventive"
INFO c.p.o.commons.logs.RaoBusinessLogs - Limiting element #02: margin = 0.0 MW, element BBE1AA1 FFR1AA1 1 at state preventive - 202602160030, CNEC ID = "BE1-FR1-preventive"
INFO c.p.o.commons.logs.RaoBusinessLogs - [MARMOT] After global linear optimization: cost = 50010.0 (functional: 50010.0, virtual: 0.0)
INFO c.p.o.commons.logs.RaoBusinessLogs - Limiting element #01: margin = 0.0 MW, element BBE1AA1 FFR1AA1 1 at state preventive - 202602160130, CNEC ID = "BE1-FR1-preventive"
INFO c.p.o.commons.logs.RaoBusinessLogs - Limiting element #02: margin = 0.0 MW, element BBE1AA1 FFR1AA1 1 at state preventive - 202602160030, CNEC ID = "BE1-FR1-preventive"
We can see that the injection set-point was indeed shifted from 1000 MW to 0 MW at 1:30 but kept constant at 0:30 for a global cost of 50010.
With time-coupled constraints#
We will now add the time-coupled constraints to the RAO to apply the power gradient constraints on the generator. When taking them in account, the generator is no longer free and its power cannot vary more than 500 MW between the two timestamps.
We still expect the generator to be completely shut down at 1:30 to avoid any flow violation. However, the injection set-point cannot drop from 1000 MW to 0 MW in a single timestamp because of 500 MW/h the power gradient. The solution is to activate the redispatching action at 0:30 too at a 500 MW set-point so that this very set-point can drop to 0 MW at the following timestamp, even if this means activating the remedial action earlier and thus having greater expenses. In this situation we expect a cost of 25010 at 0:30 (10 for activation, 25000 for variation) and 50010 at 1:30 (10 for activation, 50000 for variation).
Let’s run the RAO with the time-coupled constraints:
TimeCoupledRao.find("TimeCoupledRao").run(inputWithConstraints, raoParameters);
As previously, let us have a look at the logs:
DEBUG c.p.o.commons.logs.TechnicalLogs - redispatchingAction variation of -500.00 MW at state preventive - 202602160030 (1000.00 -> 500.00)
DEBUG c.p.o.commons.logs.TechnicalLogs - redispatchingAction variation of -1000.00 MW at state preventive - 202602160130 (1000.00 -> 0.00)
INFO c.p.o.commons.logs.RaoBusinessLogs - [MARMOT] Before topological optimizations: cost = 1000000.0 (functional: 0.0, virtual: 1000000.0 {min-margin-violation-evaluator=1000000.0})
INFO c.p.o.commons.logs.RaoBusinessLogs - Limiting element #01: margin = -1000.0 MW, element BBE1AA1 FFR1AA1 1 at state preventive - 202602160130, CNEC ID = "BE1-FR1-preventive"
INFO c.p.o.commons.logs.RaoBusinessLogs - Limiting element #02: margin = 0.0 MW, element BBE1AA1 FFR1AA1 1 at state preventive - 202602160030, CNEC ID = "BE1-FR1-preventive"
INFO c.p.o.commons.logs.RaoBusinessLogs - [MARMOT] Before global linear optimization: cost = 50010.0 (functional: 50010.0, virtual: 0.0)
INFO c.p.o.commons.logs.RaoBusinessLogs - Limiting element #01: margin = 0.0 MW, element BBE1AA1 FFR1AA1 1 at state preventive - 202602160130, CNEC ID = "BE1-FR1-preventive"
INFO c.p.o.commons.logs.RaoBusinessLogs - Limiting element #02: margin = 0.0 MW, element BBE1AA1 FFR1AA1 1 at state preventive - 202602160030, CNEC ID = "BE1-FR1-preventive"
INFO c.p.o.commons.logs.RaoBusinessLogs - [MARMOT] After global linear optimization: cost = 75020.0 (functional: 75020.0, virtual: 0.0)
INFO c.p.o.commons.logs.RaoBusinessLogs - Limiting element #01: margin = 0.0 MW, element BBE1AA1 FFR1AA1 1 at state preventive - 202602160130, CNEC ID = "BE1-FR1-preventive"
INFO c.p.o.commons.logs.RaoBusinessLogs - Limiting element #02: margin = 500.0 MW, element BBE1AA1 FFR1AA1 1 at state preventive - 202602160030, CNEC ID = "BE1-FR1-preventive"
Once again, the expected behavior happened: the redispatching action was first activated at 0:30 at a 500 MW set-point and activated once again at 1:30 at a 0 MW set-point.
We can see that before the global linear optimization, the global expense was 50010 because the time-coupled constraints had not been taken in account yet. The goal of the linear optimization is to smooth out the injection set-points by forcing the generator to respect their constraints, hence the different cost after the global linear optimization that matches the expectations.
Full code example#
You can find the full Java code below, ready to be copied, pasted and run!
Full Java code example
package com.powsybl.openrao.searchtreerao.castor.algorithm;
import com.powsybl.iidm.network.Network;
import com.powsybl.openrao.commons.TemporalData;
import com.powsybl.openrao.commons.TemporalDataImpl;
import com.powsybl.openrao.data.crac.api.Crac;
import com.powsybl.openrao.data.timecoupledconstraints.TimeCoupledConstraints;
import com.powsybl.openrao.data.timecoupledconstraints.io.JsonTimeCoupledConstraints;
import com.powsybl.openrao.raoapi.TimeCoupledRao;
import com.powsybl.openrao.raoapi.TimeCoupledRaoInputWithNetworkPaths;
import com.powsybl.openrao.raoapi.RaoInputWithNetworkPaths;
import com.powsybl.openrao.raoapi.json.JsonRaoParameters;
import com.powsybl.openrao.raoapi.parameters.RaoParameters;
import java.io.FileInputStream;
import java.io.IOException;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
public class Main {
public static void main(String[] args) throws IOException {
// import data
Network network = Network.read("2-nodes.xiidm");
Crac crac0030 = Crac.read("crac-202602160030.json", new FileInputStream("crac-202602160030.json"), network);
Crac crac0130 = Crac.read("crac-202602160130.json", new FileInputStream("crac-202602160130.json"), network);
RaoParameters raoParameters = JsonRaoParameters.read(new FileInputStream("rao-parameters.json"));
TimeCoupledConstraints timeCoupledConstraints = JsonTimeCoupledConstraints.read(new FileInputStream("time-coupled-constraints.json"));
// create time-coupled inputs
TemporalData<RaoInputWithNetworkPaths> inputPerTimestamp = new TemporalDataImpl<>();
RaoInputWithNetworkPaths input0030 = RaoInputWithNetworkPaths.build("2-nodes.xiidm", "2-nodes.xiidm", crac0030).build();
inputPerTimestamp.put(OffsetDateTime.of(2026, 2, 16, 0, 30, 0, 0, ZoneOffset.UTC), input0030);
RaoInputWithNetworkPaths input0130 = RaoInputWithNetworkPaths.build("2-nodes.xiidm", "2-nodes.xiidm", crac0130).build();
inputPerTimestamp.put(OffsetDateTime.of(2026, 2, 16, 1, 30, 0, 0, ZoneOffset.UTC), input0130);
TimeCoupledRaoInputWithNetworkPaths inputNoConstraints = new TimeCoupledRaoInputWithNetworkPaths(inputPerTimestamp, new TimeCoupledConstraints());
TimeCoupledRaoInputWithNetworkPaths inputWithConstraints = new TimeCoupledRaoInputWithNetworkPaths(inputPerTimestamp, timeCoupledConstraints);
// run time-coupled RAO without time-coupled constraints
TimeCoupledRao.find("TimeCoupledRao").run(inputNoConstraints, raoParameters);
// add time-coupled constraints and re-run time-coupled RAO
TimeCoupledRao.find("TimeCoupledRao").run(inputWithConstraints, raoParameters);
}
}