Skip to content

Commit eee7f49

Browse files
authored
feat(ics data): add additional checks for ics importer (#1680)
* add additional checks with TUs --------- Signed-off-by: CHEN Roxane <roxane.chen@rte-france.com>
1 parent 136383e commit eee7f49

5 files changed

Lines changed: 248 additions & 117 deletions

File tree

data/ics-importer/src/main/java/com/powsybl/openrao/data/icsimporter/IcsData.java

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ public static Set<GeneratorConstraints> createGeneratorConstraints(String raId,
113113
Double shiftKey = entry.getValue();
114114
CSVRecord staticRecord = staticConstraintPerId.get(raId);
115115
GeneratorConstraints.GeneratorConstraintsBuilder builder = GeneratorConstraints.create().withGeneratorId(networkElementIdPerNodeId.get(nodeId));
116+
117+
// Shutdown allowed and startup allowed are mandatory fields
118+
builder.withShutDownAllowed(Boolean.parseBoolean(staticRecord.get(SHUTDOWN_ALLOWED)));
119+
builder.withStartUpAllowed(Boolean.parseBoolean(staticRecord.get(STARTUP_ALLOWED)));
120+
116121
if (!staticRecord.get(MAXIMUM_POSITIVE_POWER_GRADIENT).isEmpty()) {
117122
builder.withUpwardPowerGradient(shiftKey * parseDoubleWithPossibleCommas(staticRecord.get(MAXIMUM_POSITIVE_POWER_GRADIENT)));
118123
} else {
@@ -129,20 +134,7 @@ public static Set<GeneratorConstraints> createGeneratorConstraints(String raId,
129134
if (!staticRecord.get(LAG_TIME).isEmpty()) {
130135
builder.withLagTime(parseDoubleWithPossibleCommas(staticRecord.get(LAG_TIME)));
131136
}
132-
// TODO: instead of throwing an error, just ignore the RA + move the check in the import
133-
if (staticRecord.get(SHUTDOWN_ALLOWED).isEmpty() ||
134-
!staticRecord.get(SHUTDOWN_ALLOWED).equalsIgnoreCase(TRUE) && !staticRecord.get(SHUTDOWN_ALLOWED).equalsIgnoreCase(FALSE)) {
135-
throw new OpenRaoException("Could not parse shutDownAllowed value for raId " + raId + ": " + staticRecord.get(SHUTDOWN_ALLOWED));
136-
} else {
137-
builder.withShutDownAllowed(Boolean.parseBoolean(staticRecord.get(SHUTDOWN_ALLOWED)));
138-
}
139-
// TODO: instead of throwing an error, just ignore the RA + move the check in the import
140-
if (staticRecord.get(STARTUP_ALLOWED).isEmpty() ||
141-
!staticRecord.get(STARTUP_ALLOWED).equalsIgnoreCase(TRUE) && !staticRecord.get(STARTUP_ALLOWED).equalsIgnoreCase(FALSE)) {
142-
throw new OpenRaoException("Could not parse startUpAllowed value for raId " + raId + ": " + staticRecord.get(STARTUP_ALLOWED));
143-
} else {
144-
builder.withStartUpAllowed(Boolean.parseBoolean(staticRecord.get(STARTUP_ALLOWED)));
145-
}
137+
146138
GeneratorConstraints generatorConstraints = builder.build();
147139
generatorConstraintsSet.add(generatorConstraints);
148140
}

data/ics-importer/src/main/java/com/powsybl/openrao/data/icsimporter/IcsDataImporter.java

Lines changed: 76 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -128,46 +128,87 @@ static Map<String, Map<String, Double>> parseGskCsv(InputStream gskInputStream)
128128
return weightPerNodePerGsk;
129129
}
130130

131+
private static boolean isValidBooleanValue(String value) {
132+
return value.equalsIgnoreCase(TRUE) || value.equalsIgnoreCase(FALSE);
133+
}
134+
131135
// Consistency check functions
132136
private static boolean shouldBeImported(CSVRecord staticRecord, List<OffsetDateTime> sortedTimestampToRun, Map<String, Map<String, Double>> weightPerNodePerGsk, Map<String, Map<String, CSVRecord>> timeseriesPerIdAndType) {
133137
String raId = staticRecord.get(RA_RD_ID);
134138

135-
// TODO: checks that all the mandatory fields are defined for all timestamps
139+
// Check static record mandatory fields : Preventive, curative, Generator Name, RD Description mode, UCT Node or GSK ID, Startup allowed and Shutdown allowed
140+
if (staticRecord.get(PREVENTIVE).isEmpty() || staticRecord.get(CURATIVE).isEmpty() ||
141+
staticRecord.get(GENERATOR_NAME).isEmpty() || staticRecord.get(RD_DESCRIPTION_MODE).isEmpty() ||
142+
staticRecord.get(UCT_NODE_OR_GSK_ID).isEmpty() || staticRecord.get(STARTUP_ALLOWED).isEmpty() ||
143+
staticRecord.get(SHUTDOWN_ALLOWED).isEmpty()) {
144+
BUSINESS_WARNS.warn("Redispatching action {} is not imported: missing mandatory static data", raId);
145+
return false;
146+
}
147+
148+
// Check that boolean fields are either "TRUE" or "FALSE"
149+
List<String> booleanFields = List.of(PREVENTIVE, CURATIVE, STARTUP_ALLOWED, SHUTDOWN_ALLOWED);
150+
for (String field : booleanFields) {
151+
String value = staticRecord.get(field);
152+
if (!isValidBooleanValue(value)) {
153+
BUSINESS_WARNS.warn("Redispatching action {} is not imported: invalid '{}' value '{}' (expected TRUE or FALSE)", raId, field, value);
154+
return false;
155+
}
156+
}
136157

137158
// Check that remedial action is defined in series csv and gsk (if defined on a gsk)
138159
if (!timeseriesPerIdAndType.containsKey(raId)) {
139-
BUSINESS_WARNS.warn("Redispatching action {} is not defined in the time series csv", raId);
160+
BUSINESS_WARNS.warn("Redispatching action {} is not imported: not defined in the time series csv", raId);
161+
return false;
162+
}
163+
164+
// Check that mandatory timeseries type (P0, RDP_DOWN, RDP_UP) are defined in the time series csv
165+
Map<String, CSVRecord> seriesPerType = timeseriesPerIdAndType.get(raId);
166+
boolean isDefinedInSeriesCsv = seriesPerType.containsKey(P0) &&
167+
seriesPerType.containsKey(RDP_DOWN) &&
168+
seriesPerType.containsKey(RDP_UP);
169+
170+
if (!isDefinedInSeriesCsv) {
171+
BUSINESS_WARNS.warn("Redispatching action {} is not imported: missing one or several mandatory timeseries type (P0, RDP_DOWN, RDP_UP).", raId);
140172
return false;
141173
}
174+
175+
// Check that data exists for all timestamps to run
176+
List<String> mandatorySeriesTypes = List.of(P0, RDP_DOWN, RDP_UP);
177+
for (OffsetDateTime timestamp : sortedTimestampToRun) {
178+
int columnName = timestamp.getHour() + OFFSET;
179+
for (String seriesType : mandatorySeriesTypes) {
180+
String value = seriesPerType.get(seriesType).get(columnName);
181+
if (value == null || value.isEmpty()) {
182+
BUSINESS_WARNS.warn("Redispatching action {} is not imported: missing {} data for timestamp {}", raId, seriesType, timestamp);
183+
return false;
184+
}
185+
}
186+
}
187+
142188
// If remedial action is defined on a gsk
143189
if (staticRecord.get(RD_DESCRIPTION_MODE).equalsIgnoreCase(GSK)) {
144190
// Check that the gsk is defined in the gsk csv
145191
if (!weightPerNodePerGsk.containsKey(staticRecord.get(UCT_NODE_OR_GSK_ID))) {
146-
BUSINESS_WARNS.warn("Redispatching action {} is defined on a gsk {} but the gsk is not defined in the gsk csv", raId, staticRecord.get(UCT_NODE_OR_GSK_ID));
192+
BUSINESS_WARNS.warn("Redispatching action {} is not imported: defined on a gsk {} but the gsk is not defined in the gsk csv", raId, staticRecord.get(UCT_NODE_OR_GSK_ID));
193+
return false;
194+
}
195+
196+
// Check that the sum of weight if RA is defined on GSK equals to 1
197+
if (!sumOfGskEqualsOne(staticRecord.get(UCT_NODE_OR_GSK_ID), weightPerNodePerGsk)) {
198+
BUSINESS_WARNS.warn("Redispatching action {} is not imported: defined on a GSK but sum of weights is not equal to 1", raId);
147199
return false;
148200
}
149201
}
150202

151203
// Check that remedial action should at least be defined on preventive instant
152204
if (!staticRecord.get(PREVENTIVE).equalsIgnoreCase(TRUE)) {
153-
BUSINESS_WARNS.warn("Redispatching action {} is not defined on preventive instant", raId);
205+
BUSINESS_WARNS.warn("Redispatching action {} is not imported: not defined on preventive instant", raId);
154206
return false;
155207
}
156208

157209
// Check that the remedial action is defined on a node or a gsk
158210
if (!staticRecord.get(RD_DESCRIPTION_MODE).equalsIgnoreCase(NODE) && !staticRecord.get(RD_DESCRIPTION_MODE).equalsIgnoreCase(GSK)) {
159-
BUSINESS_WARNS.warn("Redispatching action {} is not defined on a node or a gsk but on a {}", raId, staticRecord.get(RD_DESCRIPTION_MODE));
160-
return false;
161-
}
162-
163-
// Check that the RA is correctly defined in series csv
164-
Map<String, CSVRecord> seriesPerType = timeseriesPerIdAndType.get(raId);
165-
boolean isDefinedInSeriesCsv = seriesPerType.containsKey(P0) &&
166-
seriesPerType.containsKey(RDP_DOWN) &&
167-
seriesPerType.containsKey(RDP_UP);
168-
169-
if (!isDefinedInSeriesCsv) {
170-
BUSINESS_WARNS.warn("Redispatching action {} is not defined in the time series csv. Missing one or several timeseries type (P0, RDP_DOWN, RDP_UP or P_MIN_RD).", raId);
211+
BUSINESS_WARNS.warn("Redispatching action {} is not imported: not defined on a node or a gsk but on a {}", raId, staticRecord.get(RD_DESCRIPTION_MODE));
171212
return false;
172213
}
173214

@@ -184,6 +225,22 @@ private static boolean shouldBeImported(CSVRecord staticRecord, List<OffsetDateT
184225
return true;
185226
}
186227

228+
/**
229+
* Checks if the sum of the generation shift key (GSK) weights associated with a specific GSK ID equals 1
230+
*
231+
* @param gskId
232+
* @param weightPerNodePerGsk
233+
* @return {@code true} if the sum of the GSK weights equals 1 within a small tolerance;
234+
* {@code false} otherwise.
235+
*/
236+
private static boolean sumOfGskEqualsOne(String gskId, Map<String, Map<String, Double>> weightPerNodePerGsk) {
237+
double sumOfGsk = 0.;
238+
for (Map.Entry<String, Double> entry : weightPerNodePerGsk.get(gskId).entrySet()) {
239+
sumOfGsk += entry.getValue();
240+
}
241+
return Math.abs(sumOfGsk - 1.) < 1e-6;
242+
}
243+
187244
/**
188245
* Determines whether the P0 record values respect the specified power gradients for each time interval.
189246
* It checks the difference in values between consecutive timestamps and ensures that the differences
@@ -209,7 +266,7 @@ private static boolean p0RespectsGradients(CSVRecord staticRecord, CSVRecord p0r
209266
double diff = parseDoubleWithPossibleCommas(p0record.get(nextDateTime.getHour() + OFFSET)) - parseDoubleWithPossibleCommas(p0record.get(currentDateTime.getHour() + OFFSET));
210267
if (diff > maxGradient || diff < minGradient) {
211268
BUSINESS_WARNS.warn(
212-
"Redispatching action {} will not be imported because it does not respect power gradients : min/max/diff = {} / {} / {}",
269+
"Redispatching action {} is not imported: does not respect power gradients : min/max/diff = {} / {} / {}",
213270
staticRecord.get(0), minGradient, maxGradient, diff
214271
);
215272
return false;
@@ -236,12 +293,12 @@ private static boolean rangeIsOkay(Map<String, CSVRecord> seriesPerType, List<Of
236293
double rdpMinus = parseDoubleWithPossibleCommas(seriesPerType.get(RDP_DOWN).get(dateTime.getHour() + OFFSET));
237294
maxRange = Math.max(maxRange, rdpPlus + rdpMinus);
238295
if (rdpPlus < -1e-6 || rdpMinus < -1e-6) {
239-
BUSINESS_WARNS.warn("Redispatching action {} will not be imported because of RDP+ {} or RDP- {} is negative for datetime {}", seriesPerType.get(P0).get(RA_RD_ID), rdpPlus, rdpMinus, dateTime);
296+
BUSINESS_WARNS.warn("Redispatching action {} is not imported: RDP+ {} or RDP- {} is negative for datetime {}", seriesPerType.get(P0).get(RA_RD_ID), rdpPlus, rdpMinus, dateTime);
240297
return false;
241298
}
242299
}
243300
if (maxRange < 1) {
244-
BUSINESS_WARNS.warn("Redispatching action {} will not be imported because max range in the day {} MW is too small", seriesPerType.get(P0).get(RA_RD_ID), maxRange);
301+
BUSINESS_WARNS.warn("Redispatching action {} is not imported: max range in the day {} MW is too small", seriesPerType.get(P0).get(RA_RD_ID), maxRange);
245302
return false;
246303
}
247304
return true;

data/ics-importer/src/main/java/com/powsybl/openrao/data/icsimporter/IcsUtil.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import java.util.Map;
1717
import java.util.Optional;
1818

19+
import static com.powsybl.openrao.commons.logs.OpenRaoLoggerProvider.BUSINESS_WARNS;
20+
1921
/**
2022
* @author Roxane Chen {@literal <roxane.chen at rte-france.com>}
2123
*/
@@ -61,8 +63,11 @@ static Optional<Double> parseValue(Map<String, CSVRecord> seriesPerType, String
6163
if (seriesPerType.containsKey(key)) {
6264
CSVRecord series = seriesPerType.get(key);
6365
String value = series.get(timestamp.getHour() + OFFSET);
64-
if (value != null) {
66+
if (value != null && !value.isEmpty()) {
6567
return Optional.of(parseDoubleWithPossibleCommas(value) * shiftKey);
68+
} else {
69+
BUSINESS_WARNS.warn("Redispatching action {} is missing {} value for datetime {}", series.get(RA_RD_ID), key, timestamp);
70+
return Optional.empty();
6671
}
6772
}
6873
return Optional.empty();

0 commit comments

Comments
 (0)