diff --git a/pom.xml b/pom.xml index 77598a9..384c70d 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,8 @@ 47.0.0 powsybl/java-dynawo:3.1.0 + + 1.21.4 org.gridsuite.dynamicmargincalculation.server gridsuite org.gridsuite:dynamic-margin-calculation-server @@ -86,6 +88,11 @@ + + org.testcontainers + testcontainers + ${testcontainers.version} + @@ -102,10 +109,6 @@ - - org.gridsuite - gridsuite-computation - org.springframework.boot spring-boot-starter-web @@ -147,6 +150,24 @@ com.powsybl powsybl-dynawo-margin-calculation + + com.powsybl + powsybl-dynawo-contingencies + 3.1.0 + + + com.powsybl + powsybl-dynamic-security-analysis + 7.1.1 + + + org.gridsuite + gridsuite-computation + + + org.gridsuite + gridsuite-filter + diff --git a/src/main/java/com/powsybl/dynawo/suppliers/dynamicmodels/DynamicModelConfigJsonUtils.java b/src/main/java/com/powsybl/dynawo/suppliers/dynamicmodels/DynamicModelConfigJsonUtils.java new file mode 100644 index 0000000..f157391 --- /dev/null +++ b/src/main/java/com/powsybl/dynawo/suppliers/dynamicmodels/DynamicModelConfigJsonUtils.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package com.powsybl.dynawo.suppliers.dynamicmodels; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.powsybl.commons.json.JsonUtil; + +import java.util.List; + +/** + * @author Thang PHAM + */ +public final class DynamicModelConfigJsonUtils { + + private DynamicModelConfigJsonUtils() { + throw new AssertionError("Utility class should not be instantiated"); + } + + public static ObjectMapper createObjectMapper() { + ObjectMapper mapper = JsonUtil.createObjectMapper(); + + SimpleModule module = new SimpleModule("dynamic-model-configs"); + module.addSerializer(new DynamicModelConfigsJsonSerializer()); + module.addDeserializer(List.class, new DynamicModelConfigsJsonDeserializer()); + + mapper.registerModule(module); + return mapper; + } +} diff --git a/src/main/java/com/powsybl/dynawo/suppliers/dynamicmodels/DynamicModelConfigsJsonSerializer.java b/src/main/java/com/powsybl/dynawo/suppliers/dynamicmodels/DynamicModelConfigsJsonSerializer.java new file mode 100644 index 0000000..8bdc326 --- /dev/null +++ b/src/main/java/com/powsybl/dynawo/suppliers/dynamicmodels/DynamicModelConfigsJsonSerializer.java @@ -0,0 +1,170 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package com.powsybl.dynawo.suppliers.dynamicmodels; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.powsybl.dynawo.suppliers.Property; +import com.powsybl.dynawo.suppliers.PropertyType; +import com.powsybl.iidm.network.TwoSides; + +import java.io.IOException; +import java.util.List; + +/** + * Serialize a List with "models" as root: + * + * { + * "models": [ + * { + * "model": "...", + * "group": "...", + * "groupType": "FIXED", + * "properties": [ ... ] + * } + * ] + * } + * + * This matches {@link DynamicModelConfigsJsonDeserializer}. + * + * TODO : to remove when available at powsybl-dynawo + * + * @author Thang PHAM + */ +public class DynamicModelConfigsJsonSerializer extends StdSerializer> { + + public DynamicModelConfigsJsonSerializer() { + super((Class>) (Class) List.class); + } + + @Override + public void serialize(List configs, + JsonGenerator gen, + SerializerProvider provider) throws IOException { + + gen.writeStartObject(); + + gen.writeFieldName("models"); + writeDynamicModelConfigs(configs, gen); + + gen.writeEndObject(); + } + + private static void writeDynamicModelConfigs(List configs, JsonGenerator gen) throws IOException { + gen.writeStartArray(); + if (configs != null) { + for (DynamicModelConfig cfg : configs) { + writeDynamicModelConfig(cfg, gen); + } + } + gen.writeEndArray(); + } + + private static void writeDynamicModelConfig(DynamicModelConfig cfg, + JsonGenerator gen) throws IOException { + + gen.writeStartObject(); + + gen.writeStringField("model", cfg.model()); + gen.writeStringField("group", cfg.group()); + + if (cfg.groupType() != null) { + gen.writeStringField("groupType", cfg.groupType().name()); + } + + gen.writeFieldName("properties"); + writeProperties(cfg.properties(), gen); + + gen.writeEndObject(); + } + + private static void writeProperties(List properties, JsonGenerator gen) throws IOException { + gen.writeStartArray(); + if (properties != null) { + for (Property p : properties) { + writeProperty(p, gen); + } + } + gen.writeEndArray(); + } + + /** + * See {@code PropertyParserUtils.parseProperty}: + * - always writes "name" + * - writes exactly one of: "value" (string value), "values" (string array), "arrays" (array of string arrays) + * - writes "type" when it can be inferred from propertyClass + */ + private static void writeProperty(Property property, JsonGenerator gen) throws IOException { + gen.writeStartObject(); + + gen.writeStringField("name", property.name()); + + Object value = property.value(); + if (value instanceof List list) { + if (!list.isEmpty()) { + // lists: List + gen.writeFieldName("values"); + gen.writeStartArray(); + for (Object v : list) { + gen.writeString(String.valueOf(v)); + } + gen.writeEndArray(); + writeOptionalType(gen, property); + } + } else if (value != null && value.getClass().isArray()) { + // arrays: List> + gen.writeFieldName("arrays"); + gen.writeStartArray(); + for (List row : (List[]) value) { + gen.writeStartArray(); + for (Object v : row) { + gen.writeString(String.valueOf(v)); + } + gen.writeEndArray(); + } + gen.writeEndArray(); + writeOptionalType(gen, property); + } else { + if (value instanceof TwoSides ts) { + gen.writeStringField("value", ts.name()); + writeOptionalType(gen, property); + } else if (value != null) { + gen.writeStringField("value", String.valueOf(value)); + writeOptionalType(gen, property); + } + } + + gen.writeEndObject(); + } + + /** + * Writes the "type" field if we can (or if Property already carries an explicit type via propertyClass()). + * + * This keeps compatibility with PropertyParserUtils: + * case "type" -> builder.type(PropertyType.valueOf(parser.nextTextValue())); + */ + private static void writeOptionalType(JsonGenerator gen, Property property) throws IOException { + PropertyType inferredType = PropertyType.STRING; + + Class propertyClass = property.propertyClass(); + if (propertyClass != null) { + if (propertyClass == double.class) { + inferredType = PropertyType.DOUBLE; + } else if (propertyClass == int.class) { + inferredType = PropertyType.INTEGER; + } else if (propertyClass == boolean.class) { + inferredType = PropertyType.BOOLEAN; + } else if (propertyClass == TwoSides.class) { + inferredType = PropertyType.TWO_SIDES; + } + } + + gen.writeStringField("type", inferredType.name()); + } +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/config/RestTemplateConfig.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/config/RestTemplateConfig.java new file mode 100644 index 0000000..5198ca4 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/config/RestTemplateConfig.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.dynamicmargincalculation.server.config; + +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.powsybl.commons.report.ReportNodeJsonModule; +import com.powsybl.dynamicsimulation.json.DynamicSimulationParametersJsonModule; +import com.powsybl.dynawo.margincalculation.json.MarginCalculationParametersJsonModule; +import com.powsybl.security.dynamic.json.DynamicSecurityAnalysisJsonModule; +import org.gridsuite.computation.ComputationConfig; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.client.RestTemplate; + +/** + * @author Thang PHAM + */ +@Configuration +@Import(ComputationConfig.class) +public class RestTemplateConfig { + @Bean + public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder, ObjectMapper objectMapper) { + MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter(objectMapper); + + return restTemplateBuilder + .additionalMessageConverters(messageConverter) + .build(); + } + + @Bean + public Jackson2ObjectMapperBuilderCustomizer appJsonCustomizer() { + return builder -> builder + .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .featuresToEnable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + .modulesToInstall(new MarginCalculationParametersJsonModule(), new DynamicSecurityAnalysisJsonModule(), + new DynamicSimulationParametersJsonModule(), new ReportNodeJsonModule()); + } +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/controller/DynamicMarginCalculationController.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/controller/DynamicMarginCalculationController.java new file mode 100644 index 0000000..36e77e9 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/controller/DynamicMarginCalculationController.java @@ -0,0 +1,155 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.dynamicmargincalculation.server.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.apache.commons.collections4.CollectionUtils; +import org.gridsuite.computation.dto.ReportInfos; +import org.gridsuite.dynamicmargincalculation.server.dto.DynamicMarginCalculationStatus; +import org.gridsuite.dynamicmargincalculation.server.service.DynamicMarginCalculationResultService; +import org.gridsuite.dynamicmargincalculation.server.service.DynamicMarginCalculationService; +import org.gridsuite.dynamicmargincalculation.server.service.ParametersService; +import org.gridsuite.dynamicmargincalculation.server.service.contexts.DynamicMarginCalculationRunContext; +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +import static org.gridsuite.computation.service.AbstractResultContext.*; +import static org.gridsuite.computation.service.NotificationService.*; +import static org.gridsuite.dynamicmargincalculation.server.DynamicMarginCalculationApi.API_VERSION; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE; + +/** + * @author Thang PHAM + */ +@RestController +@RequestMapping(value = "/" + API_VERSION) +@Tag(name = "Dynamic margin calculation server") +public class DynamicMarginCalculationController { + + private final DynamicMarginCalculationService dynamicMarginCalculationService; + private final DynamicMarginCalculationResultService dynamicMarginCalculationResultService; + private final ParametersService parametersService; + + public DynamicMarginCalculationController(DynamicMarginCalculationService dynamicMarginCalculationService, + DynamicMarginCalculationResultService dynamicMarginCalculationResultService, + ParametersService parametersService) { + this.dynamicMarginCalculationService = dynamicMarginCalculationService; + this.dynamicMarginCalculationResultService = dynamicMarginCalculationResultService; + this.parametersService = parametersService; + } + + @PostMapping(value = "/networks/{networkUuid}/run", produces = APPLICATION_JSON_VALUE) + @Operation(summary = "run the dynamic margin calculation") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Run dynamic margin calculation")}) + public ResponseEntity run(@PathVariable("networkUuid") UUID networkUuid, + @RequestParam(name = VARIANT_ID_HEADER, required = false) String variantId, + @RequestParam(name = HEADER_RECEIVER, required = false) String receiver, + @RequestParam(name = "reportUuid", required = false) UUID reportId, + @RequestParam(name = REPORTER_ID_HEADER, required = false) String reportName, + @RequestParam(name = REPORT_TYPE_HEADER, required = false, defaultValue = "DynamicMarginCalculation") String reportType, + @RequestParam(name = HEADER_PROVIDER, required = false) String provider, + @RequestParam(name = HEADER_DEBUG, required = false, defaultValue = "false") boolean debug, + @RequestParam(name = "dynamicSecurityAnalysisParametersUuid") UUID dynamicSecurityAnalysisParametersUuid, + @RequestParam(name = "parametersUuid") UUID parametersUuid, + @RequestBody String dynamicSimulationParametersJson, + @RequestHeader(HEADER_USER_ID) String userId) { + + DynamicMarginCalculationRunContext dynamicMarginCalculationRunContext = parametersService.createRunContext( + networkUuid, + variantId, + receiver, + provider, + ReportInfos.builder().reportUuid(reportId).reporterId(reportName).computationType(reportType).build(), + userId, + dynamicSimulationParametersJson, + dynamicSecurityAnalysisParametersUuid, + parametersUuid, + debug); + + UUID resultUuid = dynamicMarginCalculationService.runAndSaveResult(dynamicMarginCalculationRunContext); + return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(resultUuid); + } + + @GetMapping(value = "/results/{resultUuid}/status", produces = APPLICATION_JSON_VALUE) + @Operation(summary = "Get the dynamic margin calculation status from the database") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The dynamic margin calculation status"), + @ApiResponse(responseCode = "204", description = "Dynamic margin calculation status is empty"), + @ApiResponse(responseCode = "404", description = "Dynamic security analysis result uuid has not been found")}) + public ResponseEntity getStatus(@Parameter(description = "Result UUID") @PathVariable("resultUuid") UUID resultUuid) { + DynamicMarginCalculationStatus result = dynamicMarginCalculationService.getStatus(resultUuid); + return ResponseEntity.ok().body(result); + } + + @PutMapping(value = "/results/invalidate-status", produces = APPLICATION_JSON_VALUE) + @Operation(summary = "Invalidate the dynamic margin calculation status from the database") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The dynamic margin calculation result uuids have been invalidated"), + @ApiResponse(responseCode = "404", description = "Dynamic margin calculation result has not been found")}) + public ResponseEntity> invalidateStatus(@Parameter(description = "Result UUIDs") @RequestParam("resultUuid") List resultUuids) { + List result = dynamicMarginCalculationResultService.updateStatus(resultUuids, DynamicMarginCalculationStatus.NOT_DONE); + return CollectionUtils.isEmpty(result) ? ResponseEntity.notFound().build() : + ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(result); + } + + @DeleteMapping(value = "/results/{resultUuid}") + @Operation(summary = "Delete a dynamic margin calculation result from the database") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The dynamic margin calculation result has been deleted")}) + public ResponseEntity deleteResult(@Parameter(description = "Result UUID") @PathVariable("resultUuid") UUID resultUuid) { + dynamicMarginCalculationResultService.delete(resultUuid); + return ResponseEntity.ok().build(); + } + + @DeleteMapping(value = "/results", produces = APPLICATION_JSON_VALUE) + @Operation(summary = "Delete dynamic margin calculation results from the database") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Dynamic margin calculation results have been deleted")}) + public ResponseEntity deleteResults(@Parameter(description = "Results UUID") @RequestParam(value = "resultsUuids", required = false) List resultsUuids) { + dynamicMarginCalculationService.deleteResults(resultsUuids); + return ResponseEntity.ok().build(); + } + + @PutMapping(value = "/results/{resultUuid}/stop", produces = APPLICATION_JSON_VALUE) + @Operation(summary = "Stop a dynamic margin calculation computation") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The dynamic margin calculation has been stopped")}) + public ResponseEntity stop(@Parameter(description = "Result UUID") @PathVariable("resultUuid") UUID resultUuid, + @Parameter(description = "Result receiver") @RequestParam(name = "receiver", required = false, defaultValue = "") String receiver) { + dynamicMarginCalculationService.stop(resultUuid, receiver); + return ResponseEntity.ok().build(); + } + + @GetMapping(value = "/providers", produces = APPLICATION_JSON_VALUE) + @Operation(summary = "Get all margin calculation providers") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Dynamic margin calculation providers have been found")}) + public ResponseEntity> getProviders() { + return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON) + .body(dynamicMarginCalculationService.getProviders()); + } + + @GetMapping(value = "/default-provider", produces = TEXT_PLAIN_VALUE) + @Operation(summary = "Get dynamic margin calculation default provider") + @ApiResponses(@ApiResponse(responseCode = "200", description = "The dynamic margin calculation default provider has been found")) + public ResponseEntity getDefaultProvider() { + return ResponseEntity.ok().body(dynamicMarginCalculationService.getDefaultProvider()); + } + + @GetMapping(value = "/results/{resultUuid}/download-debug-file", produces = APPLICATION_JSON_VALUE) + @Operation(summary = "Download a dynamic margin calculation debug file") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Dynamic margin calculation debug file"), + @ApiResponse(responseCode = "404", description = "Dynamic margin calculation debug file has not been found")}) + public ResponseEntity downloadDebugFile(@Parameter(description = "Result UUID") @PathVariable("resultUuid") UUID resultUuid) { + return dynamicMarginCalculationService.downloadDebugFile(resultUuid); + } + +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/controller/DynamicMarginCalculationParametersController.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/controller/DynamicMarginCalculationParametersController.java new file mode 100644 index 0000000..8f4cd20 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/controller/DynamicMarginCalculationParametersController.java @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.dynamicmargincalculation.server.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.gridsuite.dynamicmargincalculation.server.DynamicMarginCalculationApi; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.DynamicMarginCalculationParametersInfos; +import org.gridsuite.dynamicmargincalculation.server.service.ParametersService; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +import static org.gridsuite.computation.service.NotificationService.HEADER_USER_ID; + +/** + * @author Thang PHAM + */ +@RestController +@RequestMapping(value = "/" + DynamicMarginCalculationApi.API_VERSION + "/parameters") +@Tag(name = "Dynamic security analysis server - Parameters") +public class DynamicMarginCalculationParametersController { + + private final ParametersService parametersService; + + public DynamicMarginCalculationParametersController(ParametersService parametersService) { + this.parametersService = parametersService; + } + + @PostMapping(value = "", consumes = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Create parameters") + @ApiResponse(responseCode = "200", description = "parameters were created") + public ResponseEntity createParameters( + @RequestBody DynamicMarginCalculationParametersInfos parametersInfos) { + return ResponseEntity.ok(parametersService.createParameters(parametersInfos)); + } + + @PostMapping(value = "/default") + @Operation(summary = "Create default parameters") + @ApiResponse(responseCode = "200", description = "Default parameters were created") + public ResponseEntity createDefaultParameters() { + return ResponseEntity.ok(parametersService.createDefaultParameters()); + } + + @PostMapping(value = "", params = "duplicateFrom", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Duplicate parameters") + @ApiResponse(responseCode = "200", description = "parameters were duplicated") + public ResponseEntity duplicateParameters( + @Parameter(description = "source parameters UUID") @RequestParam("duplicateFrom") UUID sourceParametersUuid) { + return ResponseEntity.ok(parametersService.duplicateParameters(sourceParametersUuid)); + } + + @GetMapping(value = "/{uuid}", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Get parameters") + @ApiResponse(responseCode = "200", description = "parameters were returned") + @ApiResponse(responseCode = "404", description = "parameters were not found") + public ResponseEntity getParameters( + @Parameter(description = "parameters UUID") @PathVariable("uuid") UUID parametersUuid, + @RequestHeader(HEADER_USER_ID) String userId) { + return ResponseEntity.ok(parametersService.getParameters(parametersUuid, userId)); + } + + @GetMapping(value = "", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Get all parameters") + @ApiResponse(responseCode = "200", description = "The list of all parameters was returned") + public ResponseEntity> getAllParameters() { + return ResponseEntity.ok().body(parametersService.getAllParameters()); + } + + @PutMapping(value = "/{uuid}", consumes = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Update parameters") + @ApiResponse(responseCode = "200", description = "parameters were updated") + public ResponseEntity updateParameters( + @Parameter(description = "parameters UUID") @PathVariable("uuid") UUID parametersUuid, + @RequestBody(required = false) DynamicMarginCalculationParametersInfos parametersInfos) { + parametersService.updateParameters(parametersUuid, parametersInfos); + return ResponseEntity.ok().build(); + } + + @DeleteMapping(value = "/{uuid}") + @Operation(summary = "Delete parameters") + @ApiResponse(responseCode = "200", description = "parameters were deleted") + public ResponseEntity deleteParameters( + @Parameter(description = "parameters UUID") @PathVariable("uuid") UUID parametersUuid) { + parametersService.deleteParameters(parametersUuid); + return ResponseEntity.ok().build(); + } + + @GetMapping(value = "/{uuid}/provider", produces = MediaType.TEXT_PLAIN_VALUE) + @Operation(summary = "Get provider") + @ApiResponse(responseCode = "200", description = "provider were returned") + @ApiResponse(responseCode = "404", description = "provider were not found") + public ResponseEntity getProvider( + @Parameter(description = "parameters UUID") @PathVariable("uuid") UUID parametersUuid) { + return ResponseEntity.ok(parametersService.getProvider(parametersUuid)); + } + + @PutMapping(value = "/{uuid}/provider") + @Operation(summary = "Update provider") + @ApiResponse(responseCode = "200", description = "provider was updated") + public ResponseEntity updateProvider( + @Parameter(description = "parameters UUID") @PathVariable("uuid") UUID parametersUuid, + @RequestBody(required = false) String provider) { + parametersService.updateProvider(parametersUuid, provider); + return ResponseEntity.ok().build(); + } + +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/dto/ElementAttributes.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/dto/ElementAttributes.java new file mode 100644 index 0000000..71f7a96 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/dto/ElementAttributes.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.UUID; + +/** + * partial type from ElementAttributes (Directory-server) + * @author Thang PHAM + */ +@AllArgsConstructor +@NoArgsConstructor +public class ElementAttributes { + @Getter + private UUID elementUuid; + @Setter + @Getter + private String elementName; +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/dto/parameters/DynamicMarginCalculationParametersInfos.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/dto/parameters/DynamicMarginCalculationParametersInfos.java new file mode 100644 index 0000000..f247e7c --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/dto/parameters/DynamicMarginCalculationParametersInfos.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.dto.parameters; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.powsybl.dynawo.margincalculation.MarginCalculationParameters.CalculationType; +import com.powsybl.dynawo.margincalculation.MarginCalculationParameters.LoadModelsRule; +import lombok.*; + +import java.util.List; +import java.util.UUID; + +/** + * @author Thang PHAM + */ +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class DynamicMarginCalculationParametersInfos { + private UUID id; + + private String provider; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Double startTime; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Double stopTime; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Double marginCalculationStartTime; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Double loadIncreaseStartTime; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Double loadIncreaseStopTime; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private CalculationType calculationType; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Integer accuracy; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private LoadModelsRule loadModelsRule; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private List loadsVariations; + +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/dto/parameters/DynamicSecurityAnalysisParametersValues.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/dto/parameters/DynamicSecurityAnalysisParametersValues.java new file mode 100644 index 0000000..8fa9437 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/dto/parameters/DynamicSecurityAnalysisParametersValues.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.dynamicmargincalculation.server.dto.parameters; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.powsybl.contingency.Contingency; +import lombok.*; + +import java.util.List; + +/** + * @author Thang PHAM + */ +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class DynamicSecurityAnalysisParametersValues { + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Double contingenciesStartTime; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + List contingencies; +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/dto/parameters/DynamicSimulationParametersValues.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/dto/parameters/DynamicSimulationParametersValues.java new file mode 100644 index 0000000..433376d --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/dto/parameters/DynamicSimulationParametersValues.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.dynamicmargincalculation.server.dto.parameters; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.powsybl.dynawo.DynawoSimulationParameters; +import com.powsybl.dynawo.suppliers.dynamicmodels.DynamicModelConfig; +import com.powsybl.dynawo.suppliers.dynamicmodels.DynamicModelConfigsJsonDeserializer; +import com.powsybl.dynawo.suppliers.dynamicmodels.DynamicModelConfigsJsonSerializer; +import lombok.*; + +import java.util.List; + +/** + * @author Thang PHAM + */ +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class DynamicSimulationParametersValues { + @JsonSerialize(using = DynamicModelConfigsJsonSerializer.class) + @JsonDeserialize(using = DynamicModelConfigsJsonDeserializer.class) + @JsonInclude(JsonInclude.Include.NON_EMPTY) + List dynamicModel; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + DynawoSimulationParameters dynawoParameters; +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/dto/parameters/IdNameInfos.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/dto/parameters/IdNameInfos.java new file mode 100644 index 0000000..c3afa28 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/dto/parameters/IdNameInfos.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.dto.parameters; + +import lombok.*; + +import java.util.UUID; + +/** + * @author Thang PHAM + */ +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IdNameInfos { + @Getter + private UUID id; + @Setter + @Getter + private String name; +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/dto/parameters/LoadsVariationInfos.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/dto/parameters/LoadsVariationInfos.java new file mode 100644 index 0000000..46b9359 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/dto/parameters/LoadsVariationInfos.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.dto.parameters; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.*; + +import java.util.List; +import java.util.UUID; + +/** + * @author Thang PHAM + */ +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class LoadsVariationInfos { + + private UUID id; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private List loadFilters; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Double variation; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Boolean active; +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/DynamicMarginCalculationStatusEntity.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/DynamicMarginCalculationStatusEntity.java index a495883..7e4fc26 100644 --- a/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/DynamicMarginCalculationStatusEntity.java +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/DynamicMarginCalculationStatusEntity.java @@ -8,6 +8,7 @@ package org.gridsuite.dynamicmargincalculation.server.entities; import jakarta.persistence.*; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -22,6 +23,7 @@ @Setter @Table(name = "dynamic_margin_calculation_status") @NoArgsConstructor +@AllArgsConstructor @Entity public class DynamicMarginCalculationStatusEntity { @@ -38,4 +40,7 @@ public DynamicMarginCalculationStatusEntity(UUID resultUuid, DynamicMarginCalcul @Enumerated(EnumType.STRING) private DynamicMarginCalculationStatus status; + @Column(name = "debugFileLocation") + private String debugFileLocation; + } diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/parameters/DynamicMarginCalculationParametersEntity.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/parameters/DynamicMarginCalculationParametersEntity.java new file mode 100644 index 0000000..5f819f0 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/parameters/DynamicMarginCalculationParametersEntity.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.entities.parameters; + +import com.powsybl.dynawo.margincalculation.MarginCalculationParameters.CalculationType; +import com.powsybl.dynawo.margincalculation.MarginCalculationParameters.LoadModelsRule; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.commons.collections4.CollectionUtils; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.DynamicMarginCalculationParametersInfos; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.LoadsVariationInfos; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +import static jakarta.persistence.CascadeType.ALL; + +/** + * @author Thang PHAM + */ +@NoArgsConstructor +@Getter +@Setter +@Entity +@Table(name = "dynamic_margin_calculation_parameters") +public class DynamicMarginCalculationParametersEntity { + @Id + @Column(name = "id") + private UUID id; + + @Column(name = "provider") + private String provider; + + @Column(name = "start_time") + private Double startTime; + + @Column(name = "stop_time") + private Double stopTime; + + @Column(name = "margin_calculation_start_time") + private Double marginCalculationStartTime; + + @Column(name = "load_increase_start_time") + private Double loadIncreaseStartTime; + + @Column(name = "load_increase_stop_time") + private Double loadIncreaseStopTime; + + @Column(name = "calculation_type") + @Enumerated(EnumType.STRING) + private CalculationType calculationType; + + @Column(name = "accuracy") + private Integer accuracy; + + @Column(name = "load_models_rule") + @Enumerated(EnumType.STRING) + private LoadModelsRule loadModelsRule; + + @OneToMany(fetch = FetchType.EAGER, cascade = ALL, orphanRemoval = true) + @JoinColumn(name = "dynamic_margin_calculation_parameters_id", foreignKey = @ForeignKey(name = "dynamic_margin_calculation_parameters_id_fk")) + @OrderColumn(name = "pos") + private List loadsVariations = new ArrayList<>(); + + public DynamicMarginCalculationParametersEntity(DynamicMarginCalculationParametersInfos parametersInfos) { + assignAttributes(parametersInfos); + } + + private void assignAttributes(DynamicMarginCalculationParametersInfos parametersInfos) { + if (id == null) { + id = UUID.randomUUID(); + } + provider = parametersInfos.getProvider(); + startTime = parametersInfos.getStartTime(); + stopTime = parametersInfos.getStopTime(); + marginCalculationStartTime = parametersInfos.getMarginCalculationStartTime(); + loadIncreaseStartTime = parametersInfos.getLoadIncreaseStartTime(); + loadIncreaseStopTime = parametersInfos.getLoadIncreaseStopTime(); + calculationType = parametersInfos.getCalculationType(); + accuracy = parametersInfos.getAccuracy(); + loadModelsRule = parametersInfos.getLoadModelsRule(); + // assign loads variations + assignLoadsVariations(parametersInfos.getLoadsVariations()); + } + + private void assignLoadsVariations(List loadsVariationInfosList) { + if (CollectionUtils.isEmpty(loadsVariationInfosList)) { + loadsVariations.clear(); + return; + } + // build existing loads variation Map + Map loadsVariationsByIdMap = loadsVariations.stream().collect(Collectors.toMap(LoadsVariationEntity::getId, loadVariationEntity -> loadVariationEntity)); + + // merge existing and add new loads variations + List mergedLoadsVariations = new ArrayList<>(); + for (LoadsVariationInfos loadsVariationInfos : loadsVariationInfosList) { + if (loadsVariationInfos.getId() != null) { + LoadsVariationEntity existingEntity = loadsVariationsByIdMap.get(loadsVariationInfos.getId()); + existingEntity.update(loadsVariationInfos); + mergedLoadsVariations.add(existingEntity); + } else { + mergedLoadsVariations.add(new LoadsVariationEntity(loadsVariationInfos)); + } + } + + // by clear/addAll, existing elements that are not present in the new list will be removed systematically + loadsVariations.clear(); + loadsVariations.addAll(mergedLoadsVariations); + } + + public void update(DynamicMarginCalculationParametersInfos parametersInfos) { + assignAttributes(parametersInfos); + } + + public DynamicMarginCalculationParametersInfos toDto(boolean toDuplicate) { + return DynamicMarginCalculationParametersInfos.builder() + .id(toDuplicate ? null : id) + .provider(provider) + .startTime(startTime) + .stopTime(stopTime) + .marginCalculationStartTime(marginCalculationStartTime) + .loadIncreaseStartTime(loadIncreaseStartTime) + .loadIncreaseStopTime(loadIncreaseStopTime) + .calculationType(calculationType) + .accuracy(accuracy) + .loadModelsRule(loadModelsRule) + .loadsVariations(loadsVariations.stream().map(loadsVariationEntity -> loadsVariationEntity.toDto(toDuplicate)).toList()) + .build(); + } +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/parameters/LoadsVariationEntity.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/parameters/LoadsVariationEntity.java new file mode 100644 index 0000000..058f34c --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/parameters/LoadsVariationEntity.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.entities.parameters; + +import jakarta.persistence.*; +import lombok.*; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.IdNameInfos; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.LoadsVariationInfos; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * @author Thang PHAM + */ +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +@Entity +@Table(name = "loads_variation", indexes = @Index(name = "idx_loads_variation_dynamic_margin_calculation_parameters_id", + columnList = "dynamic_margin_calculation_parameters_id")) +public class LoadsVariationEntity { + @Id + @Column(name = "id") + private UUID id; + + @Builder.Default + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name = "loads_variation_load_filter", + joinColumns = @JoinColumn(name = "loads_variation_id"), + foreignKey = @ForeignKey(name = "loads_variation_id_fk"), + indexes = { @Index(name = "idx_loads_variation_load_filter_loads_variation_id", columnList = "loads_variation_id") } + ) + @Column(name = "load_filter_id", nullable = false) + private List loadFilterIds = new ArrayList<>(); + + private Double variation; + + private Boolean active; // means this variation is used in calculation + + LoadsVariationEntity(LoadsVariationInfos loadsVariationInfos) { + assignAttributes(loadsVariationInfos); + } + + public void assignAttributes(LoadsVariationInfos loadsVariationInfos) { + if (id == null) { + id = UUID.randomUUID(); + } + variation = loadsVariationInfos.getVariation(); + active = loadsVariationInfos.getActive(); + loadFilterIds = loadsVariationInfos.getLoadFilters().stream().map(IdNameInfos::getId).toList(); + } + + void update(LoadsVariationInfos loadsVariationInfos) { + assignAttributes(loadsVariationInfos); + } + + LoadsVariationInfos toDto(boolean toDuplicate) { + return LoadsVariationInfos.builder() + .id(toDuplicate ? null : id) + .variation(variation) + .active(active) + .loadFilters(loadFilterIds.stream().map(loadFilterId -> new IdNameInfos(loadFilterId, null)).toList()) + .build(); + } +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/result/FailedCriterionEmbeddable.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/result/FailedCriterionEmbeddable.java new file mode 100644 index 0000000..7c1fa39 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/result/FailedCriterionEmbeddable.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.entities.result; + +import com.powsybl.dynawo.contingency.results.FailedCriterion; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * @author Thang PHAM + */ +@NoArgsConstructor +@Getter +@Setter +@Embeddable +public class FailedCriterionEmbeddable { + + @Column(name = "description") + private String description; + + @Column(name = "time") + private double time; + + public static FailedCriterionEmbeddable fromDomain(FailedCriterion failedCriterion) { + FailedCriterionEmbeddable embeddable = new FailedCriterionEmbeddable(); + embeddable.setDescription(failedCriterion.description()); + embeddable.setTime(failedCriterion.time()); + return embeddable; + } + + public FailedCriterion toDto() { + return new FailedCriterion(description, time); + } +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/result/LoadIncreaseResultEntity.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/result/LoadIncreaseResultEntity.java new file mode 100644 index 0000000..6bfc6de --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/result/LoadIncreaseResultEntity.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.entities.result; + +import com.powsybl.dynawo.contingency.results.FailedCriterion; +import com.powsybl.dynawo.contingency.results.ScenarioResult; +import com.powsybl.dynawo.contingency.results.Status; +import com.powsybl.dynawo.margincalculation.results.LoadIncreaseResult; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * @author Thang PHAM + */ +@NoArgsConstructor +@Getter +@Setter +@Entity +@Table(name = "load_increase_result", indexes = {@Index(name = "idx_load_increase_result_dynamic_margin_calculation_result_uuid", + columnList = "dynamic_margin_calculation_result_uuid")}) +public class LoadIncreaseResultEntity { + @Id + @Column(name = "id") + private UUID id; + + @Column(name = "load_level") + double loadLevel; + + @Column(name = "status") + @Enumerated(EnumType.STRING) + Status status; + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn( + name = "load_increase_result_id", + referencedColumnName = "id", + foreignKey = @ForeignKey(name = "scenario_result_load_increase_result_id_fk") + ) + @OrderColumn(name = "pos") + private List scenarioResults = new ArrayList<>(); + + @ElementCollection + @CollectionTable( + name = "load_increase_result_failed_criteria", + joinColumns = @JoinColumn( + name = "load_increase_result_id", + referencedColumnName = "id", + foreignKey = @ForeignKey(name = "load_increase_result_failed_criteria_load_increase_result_id_fk") + ), + indexes = { + @Index(name = "idx_load_increase_result_failed_criteria_load_increase_result_id", columnList = "load_increase_result_id") + } + ) + @OrderColumn(name = "pos") + private List failedCriteria = new ArrayList<>(); + + public static LoadIncreaseResultEntity fromDomain(LoadIncreaseResult loadIncreaseResult) { + LoadIncreaseResultEntity entity = new LoadIncreaseResultEntity(); + entity.setId(UUID.randomUUID()); + entity.setLoadLevel(loadIncreaseResult.loadLevel()); + entity.setStatus(loadIncreaseResult.status()); + + List scenarioResults = loadIncreaseResult.scenarioResults(); + entity.setScenarioResults(scenarioResults.stream().map(ScenarioResultEntity::fromDomain).toList()); + + List failedCriteria = loadIncreaseResult.failedCriteria(); + entity.setFailedCriteria(failedCriteria.stream().map(FailedCriterionEmbeddable::fromDomain).toList()); + + return entity; + } + + public LoadIncreaseResult toDto() { + List scenarioResultList = scenarioResults.stream().map(ScenarioResultEntity::toDto).toList(); + List failedCriterionList = failedCriteria.stream().map(FailedCriterionEmbeddable::toDto).toList(); + + return new LoadIncreaseResult(loadLevel, status, scenarioResultList, failedCriterionList); + } +} + diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/result/MarginCalculationResultEntity.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/result/MarginCalculationResultEntity.java new file mode 100644 index 0000000..5a9b6c3 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/result/MarginCalculationResultEntity.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.entities.result; + +import com.powsybl.dynawo.margincalculation.results.MarginCalculationResult; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * @author Thang PHAM + */ +@Getter +@Setter +@Table(name = "dynamic_margin_calculation_result") +@NoArgsConstructor +@Entity +public class MarginCalculationResultEntity { + @Id + @Column(name = "result_uuid") + private UUID resultUuid; + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn( + name = "dynamic_margin_calculation_result_uuid", + referencedColumnName = "result_uuid", + foreignKey = @ForeignKey(name = "load_increase_result_dynamic_margin_calculation_result_uuid_fk")) + @OrderColumn(name = "pos") + private List loadIncreaseResults = new ArrayList<>(); + + public static MarginCalculationResultEntity fromDomain(UUID resultUuid, MarginCalculationResult marginCalculationResult) { + MarginCalculationResultEntity entity = new MarginCalculationResultEntity(); + entity.setResultUuid(resultUuid); + + entity.setLoadIncreaseResults(marginCalculationResult.getLoadIncreaseResults().stream().map(LoadIncreaseResultEntity::fromDomain).toList()); + + return entity; + } + + public MarginCalculationResult toDto() { + return new MarginCalculationResult(loadIncreaseResults.stream().map(LoadIncreaseResultEntity::toDto).toList()); + } + +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/result/ScenarioResultEntity.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/result/ScenarioResultEntity.java new file mode 100644 index 0000000..d715d6c --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/entities/result/ScenarioResultEntity.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.entities.result; + +import com.powsybl.dynawo.contingency.results.FailedCriterion; +import com.powsybl.dynawo.contingency.results.ScenarioResult; +import com.powsybl.dynawo.contingency.results.Status; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * @author Thang PHAM + */ +@NoArgsConstructor +@Getter +@Setter +@Entity +@Table(name = "scenario_result", indexes = @Index(name = "idx_scenario_result_load_increase_result_id", columnList = "load_increase_result_id") +) +public class ScenarioResultEntity { + + @Id + @Column(name = "id") + private UUID id; + + @Column(name = "equipment_id") + private String equipmentId; + + @Column(name = "status") + @Enumerated(EnumType.STRING) + private Status status; + + @ElementCollection + @CollectionTable( + name = "scenario_result_failed_criteria", + joinColumns = @JoinColumn( + name = "scenario_result_id", + referencedColumnName = "id", + foreignKey = @ForeignKey(name = "scenario_result_failed_criteria_scenario_result_id_fk") + ), + indexes = { + @Index(name = "idx_scenario_result_failed_criteria_scenario_result_id", columnList = "scenario_result_id") + } + ) + @OrderColumn(name = "pos") + private List failedCriteria = new ArrayList<>(); + + public static ScenarioResultEntity fromDomain(ScenarioResult scenarioResult) { + ScenarioResultEntity embeddable = new ScenarioResultEntity(); + embeddable.setId(UUID.randomUUID()); + embeddable.setStatus(scenarioResult.status()); + + List failedCriteriaList = scenarioResult.failedCriteria(); + embeddable.setFailedCriteria(failedCriteriaList.stream().map(FailedCriterionEmbeddable::fromDomain).toList()); + + return embeddable; + } + + public ScenarioResult toDto() { + List failedCriterionList = failedCriteria.stream().map(FailedCriterionEmbeddable::toDto).toList(); + + return new ScenarioResult(equipmentId, status, failedCriterionList); + } +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/error/DynamicMarginCalculationBusinessErrorCode.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/error/DynamicMarginCalculationBusinessErrorCode.java new file mode 100644 index 0000000..91b7bcc --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/error/DynamicMarginCalculationBusinessErrorCode.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.dynamicmargincalculation.server.error; + +import com.powsybl.ws.commons.error.BusinessErrorCode; + +/** + * @author Thang PHAM + */ +public enum DynamicMarginCalculationBusinessErrorCode implements BusinessErrorCode { + PROVIDER_NOT_FOUND("dynamicMarginCalculation.providerNotFound"), + LOAD_FILTERS_NOT_FOUND("dynamicMarginCalculation.loadFilterNotFound"); + + private final String code; + + DynamicMarginCalculationBusinessErrorCode(String code) { + this.code = code; + } + + public String value() { + return code; + } +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/error/DynamicMarginCalculationException.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/error/DynamicMarginCalculationException.java new file mode 100644 index 0000000..7b3ec44 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/error/DynamicMarginCalculationException.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.dynamicmargincalculation.server.error; + +import com.powsybl.ws.commons.error.AbstractBusinessException; +import lombok.Getter; +import lombok.NonNull; + +import java.util.Map; +import java.util.Objects; + +/** + * @author Thang PHAM + */ +@Getter +public class DynamicMarginCalculationException extends AbstractBusinessException { + + private final DynamicMarginCalculationBusinessErrorCode errorCode; + + private final transient Map businessErrorValues; + + @NonNull + @Override + public DynamicMarginCalculationBusinessErrorCode getBusinessErrorCode() { + return errorCode; + } + + @NonNull + @Override + public Map getBusinessErrorValues() { + return businessErrorValues; + } + + public DynamicMarginCalculationException(DynamicMarginCalculationBusinessErrorCode errorCode, String message) { + super(message); + this.errorCode = errorCode; + this.businessErrorValues = Map.of(); + } + + public DynamicMarginCalculationException(DynamicMarginCalculationBusinessErrorCode errorCode, String message, Map businessErrorValues) { + super(Objects.requireNonNull(message, "message must not be null")); + this.errorCode = Objects.requireNonNull(errorCode, "errorCode must not be null"); + this.businessErrorValues = businessErrorValues != null ? Map.copyOf(businessErrorValues) : Map.of(); + } + +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/error/DynamicMarginCalculationExceptionHandler.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/error/DynamicMarginCalculationExceptionHandler.java new file mode 100644 index 0000000..27b4bf1 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/error/DynamicMarginCalculationExceptionHandler.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.dynamicmargincalculation.server.error; + +import com.powsybl.ws.commons.error.AbstractBusinessExceptionHandler; +import com.powsybl.ws.commons.error.PowsyblWsProblemDetail; +import com.powsybl.ws.commons.error.ServerNameProvider; +import jakarta.servlet.http.HttpServletRequest; +import lombok.NonNull; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +/** + * @author Thang PHAM + */ +@ControllerAdvice +public class DynamicMarginCalculationExceptionHandler extends AbstractBusinessExceptionHandler { + + protected DynamicMarginCalculationExceptionHandler(ServerNameProvider serverNameProvider) { + super(serverNameProvider); + } + + @Override + protected @NonNull DynamicMarginCalculationBusinessErrorCode getBusinessCode(DynamicMarginCalculationException e) { + return e.getBusinessErrorCode(); + } + + protected HttpStatus mapStatus(DynamicMarginCalculationBusinessErrorCode businessErrorCode) { + return switch (businessErrorCode) { + case PROVIDER_NOT_FOUND, + LOAD_FILTERS_NOT_FOUND -> HttpStatus.NOT_FOUND; + }; + } + + @ExceptionHandler(DynamicMarginCalculationException.class) + public ResponseEntity handleDynamicMarginCalculationException(DynamicMarginCalculationException exception, HttpServletRequest request) { + return super.handleDomainException(exception, request); + } + +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/repositories/DynamicMarginCalculationParametersRepository.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/repositories/DynamicMarginCalculationParametersRepository.java new file mode 100644 index 0000000..6c9f254 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/repositories/DynamicMarginCalculationParametersRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.repositories; + +import org.gridsuite.dynamicmargincalculation.server.entities.parameters.DynamicMarginCalculationParametersEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +/** + * @author Thang PHAM + */ +@Repository +public interface DynamicMarginCalculationParametersRepository extends JpaRepository { + + @Query("SELECT params.provider FROM DynamicMarginCalculationParametersEntity params WHERE params.id = :id") + Optional findProviderById(@Param("id") UUID id); +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/repositories/DynamicMarginCalculationStatusRepository.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/repositories/DynamicMarginCalculationStatusRepository.java index 4db5b2c..da638bb 100644 --- a/src/main/java/org/gridsuite/dynamicmargincalculation/server/repositories/DynamicMarginCalculationStatusRepository.java +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/repositories/DynamicMarginCalculationStatusRepository.java @@ -9,6 +9,9 @@ import org.gridsuite.dynamicmargincalculation.server.entities.DynamicMarginCalculationStatusEntity; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.Optional; @@ -23,4 +26,9 @@ public interface DynamicMarginCalculationStatusRepository extends JpaRepository< Optional findByResultUuid(UUID resultUuid); void deleteByResultUuid(UUID resultUuid); + + @Modifying + @Query("UPDATE DynamicMarginCalculationStatusEntity r SET r.debugFileLocation = :debugFileLocation WHERE r.resultUuid = :resultUuid") + int updateDebugFileLocation(@Param("resultUuid") UUID resultUuid, @Param("debugFileLocation") String debugFileLocation); + } diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/repositories/MarginCalculationResultRepository.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/repositories/MarginCalculationResultRepository.java new file mode 100644 index 0000000..f470adf --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/repositories/MarginCalculationResultRepository.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.repositories; + +import org.gridsuite.dynamicmargincalculation.server.entities.result.MarginCalculationResultEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +/** + * @author Thang PHAM + */ +@Repository +public interface MarginCalculationResultRepository extends JpaRepository { + + Optional findByResultUuid(UUID resultUuid); + + void deleteByResultUuid(UUID resultUuid); +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/DynamicMarginCalculationObserver.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/DynamicMarginCalculationObserver.java new file mode 100644 index 0000000..84b0117 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/DynamicMarginCalculationObserver.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.service; + +import com.powsybl.dynawo.contingency.results.Status; +import com.powsybl.dynawo.margincalculation.results.MarginCalculationResult; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.observation.ObservationRegistry; +import lombok.NonNull; +import org.gridsuite.computation.service.AbstractComputationObserver; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.DynamicMarginCalculationParametersInfos; +import org.springframework.stereotype.Service; + +/** + * @author Thang PHAM + */ +@Service +public class DynamicMarginCalculationObserver extends AbstractComputationObserver { + + private static final String COMPUTATION_TYPE = "dynamicmargincalculation"; + + public DynamicMarginCalculationObserver(@NonNull ObservationRegistry observationRegistry, @NonNull MeterRegistry meterRegistry) { + super(observationRegistry, meterRegistry); + } + + @Override + protected String getComputationType() { + return COMPUTATION_TYPE; + } + + @Override + protected String getResultStatus(MarginCalculationResult res) { + return res != null && res.getLoadIncreaseResults().stream() + .noneMatch(loadIncreaseResult -> loadIncreaseResult.status() == Status.EXECUTION_PROBLEM) ? "OK" : "NOK"; + } +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/DynamicMarginCalculationResultService.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/DynamicMarginCalculationResultService.java index 244bb7f..a26959c 100644 --- a/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/DynamicMarginCalculationResultService.java +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/DynamicMarginCalculationResultService.java @@ -7,15 +7,18 @@ package org.gridsuite.dynamicmargincalculation.server.service; -import jakarta.transaction.Transactional; +import com.powsybl.dynawo.margincalculation.results.MarginCalculationResult; import org.gridsuite.computation.error.ComputationException; import org.gridsuite.computation.service.AbstractComputationResultService; import org.gridsuite.dynamicmargincalculation.server.dto.DynamicMarginCalculationStatus; import org.gridsuite.dynamicmargincalculation.server.entities.DynamicMarginCalculationStatusEntity; +import org.gridsuite.dynamicmargincalculation.server.entities.result.MarginCalculationResultEntity; import org.gridsuite.dynamicmargincalculation.server.repositories.DynamicMarginCalculationStatusRepository; +import org.gridsuite.dynamicmargincalculation.server.repositories.MarginCalculationResultRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Objects; @@ -33,9 +36,12 @@ public class DynamicMarginCalculationResultService extends AbstractComputationRe public static final String MSG_RESULT_UUID_NOT_FOUND = "Result uuid not found: "; private final DynamicMarginCalculationStatusRepository statusRepository; + private final MarginCalculationResultRepository resultRepository; - public DynamicMarginCalculationResultService(DynamicMarginCalculationStatusRepository statusRepository) { + public DynamicMarginCalculationResultService(DynamicMarginCalculationStatusRepository statusRepository, + MarginCalculationResultRepository resultRepository) { this.statusRepository = statusRepository; + this.resultRepository = resultRepository; } @Override @@ -56,32 +62,65 @@ public List updateStatus(List resultUuids, DynamicMarginCalculationS return statusRepository.saveAllAndFlush(resultEntities).stream().map(DynamicMarginCalculationStatusEntity::getResultUuid).toList(); } - @Transactional - public void updateStatus(UUID resultUuid, DynamicMarginCalculationStatus status) { + private void doUpdateStatus(UUID resultUuid, DynamicMarginCalculationStatus status) { LOGGER.debug("Update margin calculation status [resultUuid={}, status={}", resultUuid, status); DynamicMarginCalculationStatusEntity resultEntity = statusRepository.findByResultUuid(resultUuid) .orElseThrow(() -> new ComputationException(RESULT_NOT_FOUND, MSG_RESULT_UUID_NOT_FOUND + resultUuid)); resultEntity.setStatus(status); } + @Transactional + public void updateStatus(UUID resultUuid, DynamicMarginCalculationStatus status) { + doUpdateStatus(resultUuid, status); + } + + @Override + @Transactional + public void saveDebugFileLocation(UUID resultUuid, String debugFilePath) { + statusRepository.findById(resultUuid).ifPresentOrElse( + (var resultEntity) -> statusRepository.updateDebugFileLocation(resultUuid, debugFilePath), + () -> statusRepository.save(new DynamicMarginCalculationStatusEntity(resultUuid, DynamicMarginCalculationStatus.NOT_DONE, debugFilePath)) + ); + } + @Override @Transactional public void delete(UUID resultUuid) { Objects.requireNonNull(resultUuid); statusRepository.deleteByResultUuid(resultUuid); + resultRepository.deleteByResultUuid(resultUuid); } @Override @Transactional public void deleteAll() { statusRepository.deleteAll(); + resultRepository.deleteAll(); } @Override + @Transactional public DynamicMarginCalculationStatus findStatus(UUID resultUuid) { Objects.requireNonNull(resultUuid); return statusRepository.findByResultUuid(resultUuid) .map(DynamicMarginCalculationStatusEntity::getStatus) .orElse(null); } + + @Transactional + public void insertResult(UUID resultUuid, MarginCalculationResult result, DynamicMarginCalculationStatus status) { + doUpdateStatus(resultUuid, status); + MarginCalculationResultEntity resultEntity = MarginCalculationResultEntity.fromDomain(resultUuid, result); + resultRepository.save(resultEntity); + } + + @Override + @Transactional(readOnly = true) + public String findDebugFileLocation(UUID resultUuid) { + Objects.requireNonNull(resultUuid); + return statusRepository.findById(resultUuid) + .map(DynamicMarginCalculationStatusEntity::getDebugFileLocation) + .orElse(null); + } + } diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/DynamicMarginCalculationService.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/DynamicMarginCalculationService.java new file mode 100644 index 0000000..c31ee3f --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/DynamicMarginCalculationService.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.dynamicmargincalculation.server.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.powsybl.dynawo.margincalculation.MarginCalculation; +import com.powsybl.network.store.client.NetworkStoreService; +import org.gridsuite.computation.s3.ComputationS3Service; +import org.gridsuite.computation.service.AbstractComputationService; +import org.gridsuite.computation.service.NotificationService; +import org.gridsuite.computation.service.UuidGeneratorService; +import org.gridsuite.dynamicmargincalculation.server.dto.DynamicMarginCalculationStatus; +import org.gridsuite.dynamicmargincalculation.server.service.contexts.DynamicMarginCalculationResultContext; +import org.gridsuite.dynamicmargincalculation.server.service.contexts.DynamicMarginCalculationRunContext; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.messaging.Message; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; + +/** + * @author Thang PHAM + */ +@Service +@ComponentScan(basePackageClasses = {NetworkStoreService.class, NotificationService.class}) +public class DynamicMarginCalculationService extends AbstractComputationService { + public static final String COMPUTATION_TYPE = "dynamic margin calculation"; + + public DynamicMarginCalculationService( + NotificationService notificationService, + ObjectMapper objectMapper, + UuidGeneratorService uuidGeneratorService, + DynamicMarginCalculationResultService dynamicSecurityAnalysisResultService, + ComputationS3Service computationS3Service, + @Value("${dynamic-margin-calculation.default-provider}") String defaultProvider) { + super(notificationService, dynamicSecurityAnalysisResultService, computationS3Service, objectMapper, uuidGeneratorService, defaultProvider); + } + + @Override + public UUID runAndSaveResult(DynamicMarginCalculationRunContext runContext) { + // insert a new result entity with running status + UUID resultUuid = uuidGeneratorService.generate(); + resultService.insertStatus(List.of(resultUuid), DynamicMarginCalculationStatus.RUNNING); + + // emit a message to launch the dynamic security analysis by the worker service + Message message = new DynamicMarginCalculationResultContext(resultUuid, runContext).toMessage(objectMapper); + notificationService.sendRunMessage(message); + return resultUuid; + } + + public List getProviders() { + return List.of(MarginCalculation.getRunner().getName()); + } +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/DynamicMarginCalculationWorkerService.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/DynamicMarginCalculationWorkerService.java new file mode 100644 index 0000000..b306f0a --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/DynamicMarginCalculationWorkerService.java @@ -0,0 +1,235 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.dynamicmargincalculation.server.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.powsybl.commons.report.ReportNode; +import com.powsybl.computation.ComputationManager; +import com.powsybl.contingency.ContingenciesProvider; +import com.powsybl.contingency.Contingency; +import com.powsybl.dynamicsimulation.DynamicModelsSupplier; +import com.powsybl.dynawo.DynawoSimulationParameters; +import com.powsybl.dynawo.contingency.results.Status; +import com.powsybl.dynawo.margincalculation.MarginCalculation; +import com.powsybl.dynawo.margincalculation.MarginCalculationParameters; +import com.powsybl.dynawo.margincalculation.MarginCalculationRunParameters; +import com.powsybl.dynawo.margincalculation.loadsvariation.LoadsVariation; +import com.powsybl.dynawo.margincalculation.loadsvariation.supplier.LoadsVariationSupplier; +import com.powsybl.dynawo.margincalculation.results.MarginCalculationResult; +import com.powsybl.dynawo.suppliers.dynamicmodels.DynamicModelConfig; +import com.powsybl.dynawo.suppliers.dynamicmodels.DynawoModelsSupplier; +import com.powsybl.iidm.network.Network; +import com.powsybl.network.store.client.NetworkStoreService; +import org.apache.commons.collections4.CollectionUtils; +import org.gridsuite.computation.s3.ComputationS3Service; +import org.gridsuite.computation.service.*; +import org.gridsuite.dynamicmargincalculation.server.PropertyServerNameProvider; +import org.gridsuite.dynamicmargincalculation.server.dto.DynamicMarginCalculationStatus; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.DynamicMarginCalculationParametersInfos; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.DynamicSecurityAnalysisParametersValues; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.DynamicSimulationParametersValues; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.LoadsVariationInfos; +import org.gridsuite.dynamicmargincalculation.server.service.client.DynamicSecurityAnalysisClient; +import org.gridsuite.dynamicmargincalculation.server.service.client.DynamicSimulationClient; +import org.gridsuite.dynamicmargincalculation.server.service.contexts.DynamicMarginCalculationResultContext; +import org.gridsuite.dynamicmargincalculation.server.service.contexts.DynamicMarginCalculationRunContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.messaging.Message; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import static org.gridsuite.dynamicmargincalculation.server.service.DynamicMarginCalculationService.COMPUTATION_TYPE; + +/** + * @author Thang PHAM + */ +@ComponentScan(basePackageClasses = {NetworkStoreService.class, NotificationService.class}) +@Service +public class DynamicMarginCalculationWorkerService extends AbstractWorkerService { + + private static final Logger LOGGER = LoggerFactory.getLogger(DynamicMarginCalculationWorkerService.class); + + private final DynamicSimulationClient dynamicSimulationClient; + private final DynamicSecurityAnalysisClient dynamicSecurityAnalysisClient; + private final ParametersService parametersService; + + public DynamicMarginCalculationWorkerService(NetworkStoreService networkStoreService, + NotificationService notificationService, + ReportService reportService, + ExecutionService executionService, + DynamicMarginCalculationObserver observer, + ObjectMapper objectMapper, + DynamicMarginCalculationResultService dynamicSecurityAnalysisResultService, + ComputationS3Service computationS3Service, + DynamicSimulationClient dynamicSimulationClient, + DynamicSecurityAnalysisClient dynamicSecurityAnalysisClient, + ParametersService parametersService, + PropertyServerNameProvider propertyServerNameProvider) { + super(networkStoreService, notificationService, reportService, dynamicSecurityAnalysisResultService, computationS3Service, executionService, observer, objectMapper, propertyServerNameProvider); + this.dynamicSimulationClient = Objects.requireNonNull(dynamicSimulationClient); + this.dynamicSecurityAnalysisClient = Objects.requireNonNull(dynamicSecurityAnalysisClient); + this.parametersService = Objects.requireNonNull(parametersService); + } + + /** + * Use this method to mock with DockerLocalComputationManager in case of integration tests with test container + * + * @return a computation manager + */ + public ComputationManager getComputationManager() { + return executionService.getComputationManager(); + } + + @Override + protected DynamicMarginCalculationResultContext fromMessage(Message message) { + return DynamicMarginCalculationResultContext.fromMessage(message, objectMapper); + } + + public void updateResult(UUID resultUuid, MarginCalculationResult result) { + Objects.requireNonNull(resultUuid); + DynamicMarginCalculationStatus status = result.getLoadIncreaseResults().stream() + .anyMatch(loadIncreaseResult -> loadIncreaseResult.status() == Status.EXECUTION_PROBLEM) ? + DynamicMarginCalculationStatus.FAILED : + DynamicMarginCalculationStatus.SUCCEED; + + resultService.insertResult(resultUuid, result, status); + } + + @Override + protected void saveResult(Network network, AbstractResultContext resultContext, MarginCalculationResult result) { + updateResult(resultContext.getResultUuid(), result); + } + + @Override + protected String getComputationType() { + return COMPUTATION_TYPE; + } + + // open the visibility from protected to public to mock in a test where the stop arrives early + @Override + public void preRun(DynamicMarginCalculationRunContext runContext) { + super.preRun(runContext); + + // get evaluated contingencies from the dynamic security analysis server + DynamicSecurityAnalysisParametersValues dynamicSecurityAnalysisParametersValues = + dynamicSecurityAnalysisClient.getParametersValues(runContext.getDynamicSecurityAnalysisParametersUuid(), + runContext.getNetworkUuid(), runContext.getVariantId()); + List contingencies = dynamicSecurityAnalysisParametersValues.getContingencies(); + + // get evaluated parameters values from the dynamic simulation server + DynamicSimulationParametersValues dynamicSimulationParametersValues = + dynamicSimulationClient.getParametersValues(runContext.getDynamicSimulationParametersJson(), + runContext.getNetworkUuid(), runContext.getVariantId()); + + // get dynamic model list from dynamic simulation server + List dynamicModel = dynamicSimulationParametersValues.getDynamicModel(); + + // get dynawo parameters from the dynamic simulation server + DynawoSimulationParameters dynawoParameters = dynamicSimulationParametersValues.getDynawoParameters(); + + DynamicMarginCalculationParametersInfos parametersInfos = runContext.getParameters(); + // create new margin calculation parameters + MarginCalculationParameters.Builder parametersBuilder = MarginCalculationParameters.builder(); + if (runContext.getDebugDir() != null) { + parametersBuilder.setDebugDir(runContext.getDebugDir().toString()); + } + parametersBuilder.setDynawoParameters(dynawoParameters); + + // set start and stop times + parametersBuilder.setStartTime(parametersInfos.getStartTime()); + parametersBuilder.setStopTime(parametersInfos.getStopTime()); + // set margin calculation start time + parametersBuilder.setMarginCalculationStartTime(parametersInfos.getMarginCalculationStartTime()); + // set load increase start and stop times + parametersBuilder.setLoadIncreaseStartTime(parametersInfos.getLoadIncreaseStartTime()); + parametersBuilder.setLoadIncreaseStopTime(parametersInfos.getLoadIncreaseStopTime()); + // set other parameters + parametersBuilder.setCalculationType(parametersInfos.getCalculationType()); + parametersBuilder.setAccuracy(parametersInfos.getAccuracy()); + parametersBuilder.setLoadModelsRule(parametersInfos.getLoadModelsRule()); + + // set contingency start time + parametersBuilder.setContingenciesStartTime(dynamicSecurityAnalysisParametersValues.getContingenciesStartTime()); + + // evaluate loads variation list + List loadsVariationInfosList = parametersInfos.getLoadsVariations(); + List loadsVariations = parametersService.getLoadsVariations(loadsVariationInfosList, runContext.getNetwork()); + + // enrich runContext + runContext.setDynamicModel(dynamicModel); + runContext.setMarginCalculationParameters(parametersBuilder.build()); + runContext.setContingencies(contingencies); + runContext.setLoadsVariations(loadsVariations); + } + + @Override + public CompletableFuture getCompletableFuture(DynamicMarginCalculationRunContext runContext, String provider, UUID resultUuid) { + + DynamicModelsSupplier dynamicModelsSupplier = new DynawoModelsSupplier(runContext.getDynamicModel()); + + List contingencies = runContext.getContingencies(); + ContingenciesProvider contingenciesProvider = network -> contingencies; + + LoadsVariationSupplier loadsVariationSupplier = (n, r) -> runContext.getLoadsVariations(); + + MarginCalculationParameters parameters = runContext.getMarginCalculationParameters(); + LOGGER.info("Run margin calculation on network {}, startTime {}, stopTime {}, marginCalculationStartTime {}", + runContext.getNetworkUuid(), parameters.getStartTime(), + parameters.getStopTime(), + parameters.getMarginCalculationStartTime()); + + MarginCalculationRunParameters runParameters = new MarginCalculationRunParameters() + .setComputationManager(getComputationManager()) + .setMarginCalculationParameters(parameters) + .setReportNode(runContext.getReportNode()); + + MarginCalculation.Runner runner = MarginCalculation.getRunner(); + + return runner.runAsync(runContext.getNetwork(), + dynamicModelsSupplier, + contingenciesProvider, + loadsVariationSupplier, + runParameters + ); + } + + @Override + protected void handleNonCancellationException(AbstractResultContext resultContext, Exception exception, AtomicReference rootReporter) { + super.handleNonCancellationException(resultContext, exception, rootReporter); + // try to get report nodes at powsybl level + List computationReportNodes = Optional.ofNullable(resultContext.getRunContext().getReportNode()).map(ReportNode::getChildren).orElse(null); + if (CollectionUtils.isNotEmpty(computationReportNodes)) { // means computing has started at powsybl level + // re-inject result table since it has been removed by handling exception in the super + resultService.insertStatus(List.of(resultContext.getResultUuid()), DynamicMarginCalculationStatus.FAILED); + // continue sending report for tracing reason + super.postRun(resultContext.getRunContext(), rootReporter, null); + } + } + + @Bean + @Override + public Consumer> consumeRun() { + return super.consumeRun(); + } + + @Bean + @Override + public Consumer> consumeCancel() { + return super.consumeCancel(); + } + +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/FilterService.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/FilterService.java new file mode 100644 index 0000000..e320ff6 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/FilterService.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.dynamicmargincalculation.server.service; + +import com.powsybl.network.store.client.NetworkStoreService; +import org.gridsuite.computation.service.AbstractFilterService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.stereotype.Service; + +/** + * @author Thang PHAM + */ +@Service +public class FilterService extends AbstractFilterService { + + public FilterService(RestTemplateBuilder restTemplateBuilder, + NetworkStoreService networkStoreService, + @Value("${gridsuite.services.filter-server.base-uri:http://filter-server/}") String filterServerBaseUri) { + super(restTemplateBuilder, networkStoreService, filterServerBaseUri); + } +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/ParametersService.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/ParametersService.java new file mode 100644 index 0000000..d5cef81 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/ParametersService.java @@ -0,0 +1,260 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.dynamicmargincalculation.server.service; + +import com.powsybl.dynawo.margincalculation.MarginCalculation; +import com.powsybl.dynawo.margincalculation.MarginCalculationParameters; +import com.powsybl.dynawo.margincalculation.loadsvariation.LoadsVariation; +import com.powsybl.iidm.network.Load; +import com.powsybl.iidm.network.Network; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.gridsuite.computation.dto.ReportInfos; +import org.gridsuite.computation.error.ComputationException; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.DynamicMarginCalculationParametersInfos; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.IdNameInfos; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.LoadsVariationInfos; +import org.gridsuite.dynamicmargincalculation.server.entities.parameters.DynamicMarginCalculationParametersEntity; +import org.gridsuite.dynamicmargincalculation.server.error.DynamicMarginCalculationException; +import org.gridsuite.dynamicmargincalculation.server.repositories.DynamicMarginCalculationParametersRepository; +import org.gridsuite.dynamicmargincalculation.server.service.client.DirectoryClient; +import org.gridsuite.dynamicmargincalculation.server.service.contexts.DynamicMarginCalculationRunContext; +import org.gridsuite.filter.AbstractFilter; +import org.gridsuite.filter.expertfilter.ExpertFilter; +import org.gridsuite.filter.expertfilter.expertrule.FilterUuidExpertRule; +import org.gridsuite.filter.utils.EquipmentType; +import org.gridsuite.filter.utils.FiltersUtils; +import org.gridsuite.filter.utils.expertfilter.FieldType; +import org.gridsuite.filter.utils.expertfilter.OperatorType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +import static org.gridsuite.computation.error.ComputationBusinessErrorCode.PARAMETERS_NOT_FOUND; +import static org.gridsuite.dynamicmargincalculation.server.error.DynamicMarginCalculationBusinessErrorCode.LOAD_FILTERS_NOT_FOUND; +import static org.gridsuite.dynamicmargincalculation.server.error.DynamicMarginCalculationBusinessErrorCode.PROVIDER_NOT_FOUND; + +/** + * @author Thang PHAM + */ +@Service +public class ParametersService { + + public static final String MSG_PARAMETERS_UUID_NOT_FOUND = "Parameters uuid not found: "; + + private final String defaultProvider; + + private final DynamicMarginCalculationParametersRepository dynamicMarginCalculationParametersRepository; + + private final DirectoryClient directoryClient; + + private final FilterService filterService; + + @Autowired + public ParametersService(@Value("${dynamic-margin-calculation.default-provider}") String defaultProvider, + DynamicMarginCalculationParametersRepository dynamicMarginCalculationParametersRepository, + DirectoryClient directoryClient, + FilterService filterService) { + this.defaultProvider = defaultProvider; + this.dynamicMarginCalculationParametersRepository = dynamicMarginCalculationParametersRepository; + this.directoryClient = directoryClient; + this.filterService = filterService; + } + + @Transactional(readOnly = true) + public DynamicMarginCalculationRunContext createRunContext(UUID networkUuid, String variantId, String receiver, + String provider, ReportInfos reportInfos, String userId, + // should be UUID dynamicSimulationParametersUuid after moving dynamic simulation parameters to its server, + String dynamicSimulationParametersJson, + UUID dynamicSecurityAnalysisParametersUuid, + UUID dynamicMarginCalculationParametersUuid, + boolean debug) { + + // get parameters from the local database + DynamicMarginCalculationParametersInfos dynamicMarginCalculationParametersInfos = doGetParameters(dynamicMarginCalculationParametersUuid, null); + // take only active load variations + dynamicMarginCalculationParametersInfos.setLoadsVariations(dynamicMarginCalculationParametersInfos.getLoadsVariations() + .stream() + .filter(LoadsVariationInfos::getActive).toList()); + + // build run context + DynamicMarginCalculationRunContext runContext = DynamicMarginCalculationRunContext.builder() + .networkUuid(networkUuid) + .variantId(variantId) + .receiver(receiver) + .reportInfos(reportInfos) + .userId(userId) + .parameters(dynamicMarginCalculationParametersInfos) + .debug(debug) + .build(); + runContext.setDynamicSimulationParametersJson(dynamicSimulationParametersJson); + runContext.setDynamicSecurityAnalysisParametersUuid(dynamicSecurityAnalysisParametersUuid); + + // set provider for run context + String providerToUse = provider; + if (providerToUse == null) { + providerToUse = Optional.ofNullable(runContext.getParameters().getProvider()).orElse(defaultProvider); + } + + runContext.setProvider(providerToUse); + + // check provider + if (!MarginCalculation.getRunner().getName().equals(runContext.getProvider())) { + throw new DynamicMarginCalculationException(PROVIDER_NOT_FOUND, "Dynamic margin calculation provider not found: " + runContext.getProvider()); + } + + return runContext; + } + + // --- Dynamic security analysis parameters related methods --- // + + @Transactional(readOnly = true) + public DynamicMarginCalculationParametersInfos getParameters(UUID parametersUuid, String userId) { + return doGetParameters(parametersUuid, userId); + } + + private DynamicMarginCalculationParametersInfos doGetParameters(UUID parametersUuid, String userId) { + DynamicMarginCalculationParametersEntity entity = dynamicMarginCalculationParametersRepository.findById(parametersUuid) + .orElseThrow(() -> new ComputationException(PARAMETERS_NOT_FOUND, MSG_PARAMETERS_UUID_NOT_FOUND + parametersUuid)); + + DynamicMarginCalculationParametersInfos parameters = entity.toDto(false); + + // enrich parameters with names of directory elements inside parameters if userId is provided + if (StringUtils.isNotBlank(userId)) { + Map elementsUuidToName = directoryClient.getElementNames( + parameters.getLoadsVariations().stream() + .flatMap(elem -> elem.getLoadFilters().stream().map(IdNameInfos::getId)) + .distinct().toList(), + userId); + // enrich load filters with name + parameters.getLoadsVariations().forEach(loadsVariation -> + loadsVariation.getLoadFilters().forEach(loadFilter -> + loadFilter.setName(elementsUuidToName.get(loadFilter.getId()))) + ); + } + + return parameters; + } + + @Transactional(readOnly = true) + public String getProvider(UUID parametersUuid) { + return dynamicMarginCalculationParametersRepository.findProviderById(parametersUuid) + .orElseThrow(() -> new ComputationException(PARAMETERS_NOT_FOUND, MSG_PARAMETERS_UUID_NOT_FOUND + parametersUuid)); + } + + @Transactional + public UUID createParameters(DynamicMarginCalculationParametersInfos parametersInfos) { + return doCreateParameters(parametersInfos); + } + + private UUID doCreateParameters(DynamicMarginCalculationParametersInfos parametersInfos) { + return dynamicMarginCalculationParametersRepository.save(new DynamicMarginCalculationParametersEntity(parametersInfos)).getId(); + } + + @Transactional + public UUID createDefaultParameters() { + DynamicMarginCalculationParametersInfos defaultParametersInfos = getDefaultParametersValues(defaultProvider); + return doCreateParameters(defaultParametersInfos); + } + + public DynamicMarginCalculationParametersInfos getDefaultParametersValues(String provider) { + MarginCalculationParameters defaultConfigParameters = MarginCalculationParameters.load(); + return DynamicMarginCalculationParametersInfos.builder() + .provider(provider) + .startTime(defaultConfigParameters.getStartTime()) + .stopTime(defaultConfigParameters.getStopTime()) + .marginCalculationStartTime(defaultConfigParameters.getMarginCalculationStartTime()) + .loadIncreaseStartTime(defaultConfigParameters.getLoadIncreaseStartTime()) + .loadIncreaseStopTime(defaultConfigParameters.getLoadIncreaseStopTime()) + .calculationType(defaultConfigParameters.getCalculationType()) + .accuracy(defaultConfigParameters.getAccuracy()) + .loadModelsRule(defaultConfigParameters.getLoadModelsRule()) + .build(); + } + + @Transactional + public UUID duplicateParameters(UUID sourceParametersUuid) { + DynamicMarginCalculationParametersEntity entity = dynamicMarginCalculationParametersRepository.findById(sourceParametersUuid) + .orElseThrow(() -> new ComputationException(PARAMETERS_NOT_FOUND, MSG_PARAMETERS_UUID_NOT_FOUND + sourceParametersUuid)); + DynamicMarginCalculationParametersInfos duplicatedParametersInfos = entity.toDto(true); + duplicatedParametersInfos.setId(null); + return doCreateParameters(duplicatedParametersInfos); + } + + public List getAllParameters() { + return dynamicMarginCalculationParametersRepository.findAll().stream() + .map(paramsEntity -> paramsEntity.toDto(false)) + .toList(); + } + + @Transactional + public void updateParameters(UUID parametersUuid, DynamicMarginCalculationParametersInfos parametersInfos) { + DynamicMarginCalculationParametersEntity entity = dynamicMarginCalculationParametersRepository.findById(parametersUuid) + .orElseThrow(() -> new ComputationException(PARAMETERS_NOT_FOUND, MSG_PARAMETERS_UUID_NOT_FOUND + parametersUuid)); + if (parametersInfos == null) { + // if the parameter is null, it means it's a reset to defaultValues, but we need to keep the provider because it's updated separately + entity.update(getDefaultParametersValues(Optional.ofNullable(entity.getProvider()).orElse(defaultProvider))); + } else { + entity.update(parametersInfos); + } + } + + @Transactional + public void deleteParameters(UUID parametersUuid) { + dynamicMarginCalculationParametersRepository.deleteById(parametersUuid); + } + + @Transactional + public void updateProvider(UUID parametersUuid, String provider) { + DynamicMarginCalculationParametersEntity entity = dynamicMarginCalculationParametersRepository.findById(parametersUuid) + .orElseThrow(() -> new ComputationException(PARAMETERS_NOT_FOUND, MSG_PARAMETERS_UUID_NOT_FOUND + parametersUuid)); + entity.setProvider(provider != null ? provider : defaultProvider); + } + + public List getLoadsVariations(List loadsVariationInfosList, Network network) { + if (CollectionUtils.isEmpty(loadsVariationInfosList)) { + return Collections.emptyList(); + } + + // check none-existing load filters + List loadFilerUuids = loadsVariationInfosList.stream().flatMap(loadsVariationInfos -> loadsVariationInfos.getLoadFilters().stream()) + .distinct() + .map(IdNameInfos::getId) + .toList(); + List loadFilters = filterService.getFilters(loadFilerUuids); + Map filterByUuidMap = loadFilters.stream().collect(Collectors.toMap(AbstractFilter::getId, filter -> filter)); + List missingFilterUuids = loadFilerUuids.stream().filter(uuid -> filterByUuidMap.get(uuid) == null).map(Objects::toString).toList(); + if (CollectionUtils.isNotEmpty(missingFilterUuids)) { + throw new DynamicMarginCalculationException(LOAD_FILTERS_NOT_FOUND, "Some load filters do not exist", Map.of("filterUuids", " [" + String.join(", ", missingFilterUuids) + "]")); + } + + List loadsVariations = loadsVariationInfosList.stream().map(loadsVariationInfos -> { + // build as a unique IS_PART_OF expert-filter then evaluate + ExpertFilter filter = ExpertFilter.builder() + .equipmentType(EquipmentType.LOAD) + .rules(FilterUuidExpertRule.builder() + .field(FieldType.ID) + .operator(OperatorType.IS_PART_OF) + .values(loadsVariationInfos.getLoadFilters().stream() + .map(IdNameInfos::getId) + .map(UUID::toString).collect(Collectors.toSet())) + .build()) + .build(); + + List loads = FiltersUtils.getIdentifiables(filter, network, filterService::getFilters).stream() + .map(Load.class::cast).toList(); + return new LoadsVariation(loads, loadsVariationInfos.getVariation()); + }).toList(); + + return loadsVariations; + } + +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/client/AbstractRestClient.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/client/AbstractRestClient.java new file mode 100644 index 0000000..a440479 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/client/AbstractRestClient.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.service.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.client.RestTemplate; + +/** + * @author Thang PHAM + */ +public abstract class AbstractRestClient { + + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Getter + private final RestTemplate restTemplate; + + @Getter + private final String baseUri; + + @Getter + private final ObjectMapper objectMapper; + + protected AbstractRestClient(String baseUri, RestTemplate restTemplate, ObjectMapper objectMapper) { + this.baseUri = baseUri; + this.restTemplate = restTemplate; + this.objectMapper = objectMapper; + } + +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/client/DirectoryClient.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/client/DirectoryClient.java new file mode 100644 index 0000000..27bb674 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/client/DirectoryClient.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.service.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.gridsuite.dynamicmargincalculation.server.dto.ElementAttributes; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.gridsuite.computation.service.NotificationService.HEADER_USER_ID; +import static org.gridsuite.dynamicmargincalculation.server.service.client.utils.UrlUtils.buildEndPointUrl; + +/** + * @author Thang PHAM + */ +@Service +public class DirectoryClient extends AbstractRestClient { + public static final String API_VERSION = "v1"; + public static final String ELEMENT_END_POINT_INFOS = "elements"; + public static final String QUERY_PARAM_IDS = "ids"; + public static final String QUERY_PARAM_STRICT_MODE = "strictMode"; + + protected DirectoryClient( + @Value("${gridsuite.services.directory-server.base-uri:http://directory-server/}") String baseUri, + RestTemplate restTemplate, + ObjectMapper objectMapper + ) { + super(baseUri, restTemplate, objectMapper); + } + + public Map getElementNames(List ids, String userId) { + if (CollectionUtils.isEmpty(ids)) { + return Collections.emptyMap(); + } + + String endPointUrl = buildEndPointUrl(getBaseUri(), API_VERSION, ELEMENT_END_POINT_INFOS); + + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(endPointUrl); + uriComponentsBuilder.queryParam(QUERY_PARAM_IDS, ids); + uriComponentsBuilder.queryParam(QUERY_PARAM_STRICT_MODE, false); // to ignore non existing elements error + + HttpHeaders headers = new HttpHeaders(); + if (StringUtils.isNotBlank(userId)) { + headers.set(HEADER_USER_ID, userId); + } + + List elementAttributes = getRestTemplate() + .exchange( + uriComponentsBuilder.build().toUriString(), + HttpMethod.GET, + new HttpEntity<>(headers), + new ParameterizedTypeReference>() { } + ).getBody(); + + return elementAttributes == null ? + Collections.emptyMap() : + elementAttributes.stream().collect(Collectors.toMap(ElementAttributes::getElementUuid, ElementAttributes::getElementName)); + } +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/client/DynamicSecurityAnalysisClient.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/client/DynamicSecurityAnalysisClient.java new file mode 100644 index 0000000..e30b3e0 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/client/DynamicSecurityAnalysisClient.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.service.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.DynamicSecurityAnalysisParametersValues; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.UUID; + +import static org.gridsuite.computation.service.AbstractResultContext.VARIANT_ID_HEADER; +import static org.gridsuite.dynamicmargincalculation.server.service.client.utils.UrlUtils.buildEndPointUrl; + +/** + * @author Thang PHAM + */ +@Service +public class DynamicSecurityAnalysisClient extends AbstractRestClient { + + public static final String API_VERSION = "v1"; + public static final String DYNAMIC_SECURITY_ANALYSIS_REST_API_CALLED_SUCCESSFULLY_MESSAGE = "Dynamic security analysis REST API called successfully {}"; + + public static final String DYNAMIC_SECURITY_ANALYSIS_END_POINT_PARAMETERS = "parameters"; + + @Autowired + public DynamicSecurityAnalysisClient(@Value("${gridsuite.services.dynamic-security-analysis-server.base-uri:http://dynamic-security-analysis-server/}") String baseUri, + RestTemplate restTemplate, ObjectMapper objectMapper) { + super(baseUri, restTemplate, objectMapper); + } + + public DynamicSecurityAnalysisParametersValues getParametersValues(UUID dynamicSecurityAnalysisParametersUuid, UUID networkUuid, String variant) { + String endPointUrl = buildEndPointUrl(getBaseUri(), API_VERSION, DYNAMIC_SECURITY_ANALYSIS_END_POINT_PARAMETERS); + + UriComponents uriComponents = UriComponentsBuilder.fromUriString(endPointUrl + "/{parametersUuid}/values") + .queryParam("networkUuid", networkUuid) + .queryParam(VARIANT_ID_HEADER, variant) + .buildAndExpand(dynamicSecurityAnalysisParametersUuid); + + // call dynamic security analysis REST API + String url = uriComponents.toUriString(); + DynamicSecurityAnalysisParametersValues result = getRestTemplate().getForObject(url, DynamicSecurityAnalysisParametersValues.class); + logger.debug(DYNAMIC_SECURITY_ANALYSIS_REST_API_CALLED_SUCCESSFULLY_MESSAGE, url); + return result; + } + +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/client/DynamicSimulationClient.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/client/DynamicSimulationClient.java new file mode 100644 index 0000000..80bfe31 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/client/DynamicSimulationClient.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.service.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.DynamicSimulationParametersValues; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.UUID; + +import static org.gridsuite.computation.service.AbstractResultContext.VARIANT_ID_HEADER; +import static org.gridsuite.dynamicmargincalculation.server.service.client.utils.UrlUtils.buildEndPointUrl; + +/** + * @author Thang PHAM + */ +@Service +public class DynamicSimulationClient extends AbstractRestClient { + + public static final String API_VERSION = "v1"; + public static final String DYNAMIC_SIMULATION_REST_API_CALLED_SUCCESSFULLY_MESSAGE = "Dynamic simulation REST API called successfully {}"; + + public static final String DYNAMIC_SIMULATION_END_POINT_PARAMETERS = "parameters"; + + @Autowired + public DynamicSimulationClient(@Value("${gridsuite.services.dynamic-simulation-server.base-uri:http://dynamic-simulation-server/}") String baseUri, + RestTemplate restTemplate, ObjectMapper objectMapper) { + super(baseUri, restTemplate, objectMapper); + } + + public DynamicSimulationParametersValues getParametersValues(String dynamicSimulationParametersJson, UUID networkUuid, String variant) { + String endPointUrl = buildEndPointUrl(getBaseUri(), API_VERSION, DYNAMIC_SIMULATION_END_POINT_PARAMETERS); + + // TODO should use GET instead of POST after moving dynamic simulation parameters to its server + UriComponents uriComponents = UriComponentsBuilder.fromUriString(endPointUrl + "/values") + .queryParam("networkUuid", networkUuid) + .queryParam(VARIANT_ID_HEADER, variant) + .build(); + + // call dynamic simulation REST API + String url = uriComponents.toUriString(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity requestEntity = new HttpEntity<>(dynamicSimulationParametersJson, headers); + + ResponseEntity result = getRestTemplate().exchange(url, HttpMethod.POST, requestEntity, DynamicSimulationParametersValues.class); + + logger.debug(DYNAMIC_SIMULATION_REST_API_CALLED_SUCCESSFULLY_MESSAGE, url); + return result.getBody(); + } +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/client/utils/UrlUtils.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/client/utils/UrlUtils.java new file mode 100644 index 0000000..4dd6392 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/client/utils/UrlUtils.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.service.client.utils; + +import com.powsybl.commons.exceptions.UncheckedUriSyntaxException; +import org.apache.logging.log4j.util.Strings; + +import java.net.URI; +import java.net.URISyntaxException; + +/** + * @author Thang PHAM + */ +public final class UrlUtils { + + public static final String URL_DELIMITER = "/"; + + private UrlUtils() { + throw new AssertionError("Utility class should not be instantiated"); + } + + /** + * Build endpoint url + * @param baseUri base uri with "http://domain:port" or empty + * @param apiVersion for example "v1" or empty + * @param endPoint root endpoint + * @return a normalized completed url to endpoint + */ + public static String buildEndPointUrl(String baseUri, String apiVersion, String endPoint) { + try { + var sb = new StringBuilder(baseUri); + if (Strings.isNotBlank(apiVersion)) { + sb.append(URL_DELIMITER).append(apiVersion); + } + if (Strings.isNotBlank(endPoint)) { + sb.append(URL_DELIMITER).append(endPoint); + } + var url = sb.toString(); + + // normalize before return + return new URI(url).normalize().toString(); + } catch (URISyntaxException e) { + throw new UncheckedUriSyntaxException(e); + } + } +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/contexts/DynamicMarginCalculationResultContext.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/contexts/DynamicMarginCalculationResultContext.java new file mode 100644 index 0000000..b145b70 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/contexts/DynamicMarginCalculationResultContext.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.service.contexts; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.gridsuite.computation.dto.ReportInfos; +import org.gridsuite.computation.service.AbstractResultContext; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.DynamicMarginCalculationParametersInfos; +import org.gridsuite.dynamicmargincalculation.server.utils.GZipUtils; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; + +import java.io.UncheckedIOException; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +import static org.gridsuite.computation.service.NotificationService.*; +import static org.gridsuite.computation.utils.MessageUtils.getNonNullHeader; + +/** + * @author Thang PHAM + */ +public class DynamicMarginCalculationResultContext extends AbstractResultContext { + + private static final String HEADER_DYNAMIC_SIMULATION_PARAMETERS_JSON_UUID = "dynamicSimulationParametersJson"; + private static final String HEADER_DYNAMIC_SECURITY_ANALYSIS_PARAMETERS_UUID = "dynamicSecurityAnalysisParametersUuid"; + + public DynamicMarginCalculationResultContext(UUID resultUuid, DynamicMarginCalculationRunContext runContext) { + super(resultUuid, runContext); + } + + public static DynamicMarginCalculationResultContext fromMessage(Message message, ObjectMapper objectMapper) { + Objects.requireNonNull(message); + + // decode the parameters values + DynamicMarginCalculationParametersInfos parametersInfos; + try { + parametersInfos = objectMapper.readValue(message.getPayload(), DynamicMarginCalculationParametersInfos.class); + } catch (JsonProcessingException e) { + throw new UncheckedIOException(e); + } + + MessageHeaders headers = message.getHeaders(); + UUID resultUuid = UUID.fromString(getNonNullHeader(headers, RESULT_UUID_HEADER)); + String provider = getNonNullHeader(headers, HEADER_PROVIDER); + String receiver = (String) headers.get(HEADER_RECEIVER); + UUID networkUuid = UUID.fromString(getNonNullHeader(headers, NETWORK_UUID_HEADER)); + String variantId = (String) headers.get(VARIANT_ID_HEADER); + String reportUuidStr = (String) headers.get(REPORT_UUID_HEADER); + UUID reportUuid = reportUuidStr != null ? UUID.fromString(reportUuidStr) : null; + String reporterId = (String) headers.get(REPORTER_ID_HEADER); + String reportType = (String) headers.get(REPORT_TYPE_HEADER); + String userId = (String) headers.get(HEADER_USER_ID); + Boolean debug = (Boolean) headers.get(HEADER_DEBUG); + + DynamicMarginCalculationRunContext runContext = DynamicMarginCalculationRunContext.builder() + .networkUuid(networkUuid) + .variantId(variantId) + .receiver(receiver) + .provider(provider) + .reportInfos(ReportInfos.builder().reportUuid(reportUuid).reporterId(reporterId).computationType(reportType).build()) + .userId(userId) + .parameters(parametersInfos) + .debug(debug) + .build(); + + // specific headers + UUID dynamicSecurityAnalysisParametersUuid = UUID.fromString(getNonNullHeader(headers, HEADER_DYNAMIC_SECURITY_ANALYSIS_PARAMETERS_UUID)); + runContext.setDynamicSecurityAnalysisParametersUuid(dynamicSecurityAnalysisParametersUuid); + // TODO : using directly uuid after moving dynamic simulation parameters to its server + String compressedJson = headers.get(HEADER_DYNAMIC_SIMULATION_PARAMETERS_JSON_UUID).toString(); + runContext.setDynamicSimulationParametersJson(GZipUtils.decompress(compressedJson)); + + return new DynamicMarginCalculationResultContext(resultUuid, runContext); + } + + @Override + public Map getSpecificMsgHeaders(ObjectMapper objectMapper) { + // TODO : using directly uuid after moving dynamic simulation parameters to its server + String compressedJson = GZipUtils.compress(getRunContext().getDynamicSimulationParametersJson()); + return Map.of(HEADER_DYNAMIC_SECURITY_ANALYSIS_PARAMETERS_UUID, getRunContext().getDynamicSecurityAnalysisParametersUuid().toString(), + HEADER_DYNAMIC_SIMULATION_PARAMETERS_JSON_UUID, compressedJson); + } +} diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/contexts/DynamicMarginCalculationRunContext.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/contexts/DynamicMarginCalculationRunContext.java new file mode 100644 index 0000000..5c74bc5 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/service/contexts/DynamicMarginCalculationRunContext.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.dynamicmargincalculation.server.service.contexts; + +import com.powsybl.contingency.Contingency; +import com.powsybl.dynawo.margincalculation.MarginCalculationParameters; +import com.powsybl.dynawo.margincalculation.loadsvariation.LoadsVariation; +import com.powsybl.dynawo.suppliers.dynamicmodels.DynamicModelConfig; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.gridsuite.computation.dto.ReportInfos; +import org.gridsuite.computation.service.AbstractComputationRunContext; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.DynamicMarginCalculationParametersInfos; + +import java.util.List; +import java.util.UUID; + +/** + * @author Thang PHAM + */ +@Getter +@Setter +public class DynamicMarginCalculationRunContext extends AbstractComputationRunContext { + + private String dynamicSimulationParametersJson; + private UUID dynamicSecurityAnalysisParametersUuid; + + // --- Fields which are enriched in worker service --- // + + private List dynamicModel; + private MarginCalculationParameters marginCalculationParameters; + private List contingencies; + private List loadsVariations; + + @Builder + public DynamicMarginCalculationRunContext(UUID networkUuid, String variantId, String receiver, String provider, + ReportInfos reportInfos, String userId, DynamicMarginCalculationParametersInfos parameters, Boolean debug) { + super(networkUuid, variantId, receiver, reportInfos, userId, provider, parameters, debug); + } +} + diff --git a/src/main/java/org/gridsuite/dynamicmargincalculation/server/utils/GZipUtils.java b/src/main/java/org/gridsuite/dynamicmargincalculation/server/utils/GZipUtils.java new file mode 100644 index 0000000..fef3cc2 --- /dev/null +++ b/src/main/java/org/gridsuite/dynamicmargincalculation/server/utils/GZipUtils.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.utils; + +import org.apache.logging.log4j.util.Strings; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +/** + * @author Thang PHAM + */ +public final class GZipUtils { + private GZipUtils() { + throw new AssertionError("Utility class should not be instantiated"); + } + + /** + * Compresses a string using GZIP and encodes it to Base64. + */ + public static String compress(String str) { + if (Strings.isEmpty(str)) { + return str; + } + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + GZIPOutputStream gzip = new GZIPOutputStream(out)) { + gzip.write(str.getBytes(StandardCharsets.UTF_8)); + gzip.finish(); + return Base64.getEncoder().encodeToString(out.toByteArray()); + } catch (IOException e) { + throw new UncheckedIOException("Failed to compress string", e); + } + } + + /** + * Decodes a Base64 string and decompresses it using GZIP. + */ + public static String decompress(String compressedStr) { + if (Strings.isEmpty(compressedStr)) { + return compressedStr; + } + try { + byte[] compressed = Base64.getDecoder().decode(compressedStr); + try (ByteArrayInputStream in = new ByteArrayInputStream(compressed); + GZIPInputStream gzip = new GZIPInputStream(in)) { + return new String(gzip.readAllBytes(), StandardCharsets.UTF_8); + } + } catch (IOException e) { + throw new UncheckedIOException("Failed to decompress string", e); + } + } +} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 13f935c..84573ae 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -4,6 +4,9 @@ server: spring: rabbitmq: addresses: localhost + cloud: + aws: + endpoint: http://localhost:19000 powsybl-ws: database: @@ -16,7 +19,13 @@ powsybl: gridsuite: services: - dynamic-mapping-server: - base-uri: http://localhost:5036 + directory-server: + base-uri: http://localhost:5026 + filter-server: + base-uri: http://localhost:5027 report-server: base-uri: http://localhost:5028 + dynamic-simulation-server: + base-uri: http://localhost:5032 + dynamic-security-analysis-server: + base-uri: http://localhost:5040 diff --git a/src/main/resources/config/application.yaml b/src/main/resources/config/application.yaml index 43bcde6..f71f747 100644 --- a/src/main/resources/config/application.yaml +++ b/src/main/resources/config/application.yaml @@ -15,6 +15,8 @@ spring: max-attempts: 1 publishRun-out-0: destination: ${powsybl-ws.rabbitmq.destination.prefix:}dmc.run + publishDebug-out-0: + destination: ${powsybl-ws.rabbitmq.destination.prefix:}dmc.debug publishResult-out-0: destination: ${powsybl-ws.rabbitmq.destination.prefix:}dmc.result consumeCancel-in-0: @@ -25,7 +27,7 @@ spring: destination: ${powsybl-ws.rabbitmq.destination.prefix:}dmc.stopped publishCancelFailed-out-0: destination: ${powsybl-ws.rabbitmq.destination.prefix:}dmc.cancelfailed - output-bindings: publishRun-out-0;publishResult-out-0;publishCancel-out-0;publishStopped-out-0;publishCancelFailed-out-0 + output-bindings: publishRun-out-0;publishDebug-out-0;publishResult-out-0;publishCancel-out-0;publishStopped-out-0;publishCancelFailed-out-0 rabbit: bindings: consumeRun-in-0: @@ -38,6 +40,12 @@ spring: enabled: true delivery-limit: 2 +computation: + s3: + enabled: true + +debug-subpath: debug + powsybl-ws: database: name: dynamicmargincalculation diff --git a/src/main/resources/db/changelog/changesets/changelog_20251223T095053Z.xml b/src/main/resources/db/changelog/changesets/changelog_20251223T095053Z.xml new file mode 100644 index 0000000..65cdf52 --- /dev/null +++ b/src/main/resources/db/changelog/changesets/changelog_20251223T095053Z.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 462efc8..2d37ab3 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -3,3 +3,7 @@ databaseChangeLog: - include: file: changesets/changelog_20250922T104750Z.xml relativeToChangelogFile: true + + - include: + file: changesets/changelog_20251223T095053Z.xml + relativeToChangelogFile: true diff --git a/src/test/java/com/powsybl/dynawo/suppliers/dynamicmodels/DynamicModelConfigsJsonSerializerTest.java b/src/test/java/com/powsybl/dynawo/suppliers/dynamicmodels/DynamicModelConfigsJsonSerializerTest.java new file mode 100644 index 0000000..8139536 --- /dev/null +++ b/src/test/java/com/powsybl/dynawo/suppliers/dynamicmodels/DynamicModelConfigsJsonSerializerTest.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package com.powsybl.dynawo.suppliers.dynamicmodels; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Thang PHAM + */ +class DynamicModelConfigsJsonSerializerTest { + + @Test + void testModelConfigDeserializerSerializer() throws IOException { + + ObjectMapper objectMapper = DynamicModelConfigJsonUtils.createObjectMapper(); + List dynamicModelConfigs = objectMapper.readValue(getClass().getResourceAsStream("/data/dynamicModels.json"), new TypeReference<>() { }); + assertEquals(2, dynamicModelConfigs.size()); + + String dynamicModelConfigsJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(dynamicModelConfigs); + List dynamicModelConfigs2 = objectMapper.readValue(dynamicModelConfigsJson, new TypeReference<>() { }); + + Assertions.assertThat(dynamicModelConfigs2).usingRecursiveComparison().isEqualTo(dynamicModelConfigs); + } +} diff --git a/src/test/java/org/gridsuite/dynamicmargincalculation/server/controller/AbstractDynamicMarginCalculationControllerTest.java b/src/test/java/org/gridsuite/dynamicmargincalculation/server/controller/AbstractDynamicMarginCalculationControllerTest.java new file mode 100644 index 0000000..4dadee0 --- /dev/null +++ b/src/test/java/org/gridsuite/dynamicmargincalculation/server/controller/AbstractDynamicMarginCalculationControllerTest.java @@ -0,0 +1,156 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.dynamicmargincalculation.server.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.powsybl.network.store.client.NetworkStoreService; +import lombok.SneakyThrows; +import org.gridsuite.computation.service.ReportService; +import org.gridsuite.dynamicmargincalculation.server.DynamicMarginCalculationApplication; +import org.gridsuite.dynamicmargincalculation.server.controller.utils.TestUtils; +import org.gridsuite.dynamicmargincalculation.server.dto.DynamicMarginCalculationStatus; +import org.gridsuite.dynamicmargincalculation.server.repositories.DynamicMarginCalculationParametersRepository; +import org.gridsuite.dynamicmargincalculation.server.service.DynamicMarginCalculationWorkerService; +import org.gridsuite.dynamicmargincalculation.server.service.FilterService; +import org.gridsuite.dynamicmargincalculation.server.service.ParametersService; +import org.gridsuite.dynamicmargincalculation.server.service.client.DirectoryClient; +import org.gridsuite.dynamicmargincalculation.server.service.client.DynamicSecurityAnalysisClient; +import org.gridsuite.dynamicmargincalculation.server.service.client.DynamicSimulationClient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.mockito.Mockito; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.stream.binder.test.OutputDestination; +import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Shared setup for DynamicMarginCalculationController integration tests (MockMvc + test binder + Dynawo computation manager). + * + * @author Thang PHAM + */ +@AutoConfigureMockMvc +@SpringBootTest +@ContextConfiguration(classes = {DynamicMarginCalculationApplication.class, TestChannelBinderConfiguration.class}) +public abstract class AbstractDynamicMarginCalculationControllerTest extends AbstractDynawoTest { + + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + + protected final String dmcDebugDestination = "dmc.debug.destination"; + protected final String dmcResultDestination = "dmc.result.destination"; + protected final String dmcStoppedDestination = "dmc.stopped.destination"; + protected final String dmcCancelFailedDestination = "dmc.cancelfailed.destination"; + + @Autowired + protected MockMvc mockMvc; + + @Autowired + protected ObjectMapper objectMapper; + + @MockitoBean + protected ReportService reportService; + + @MockitoBean + protected DirectoryClient directoryClient; + + @MockitoBean + protected FilterService filterClient; + + @MockitoBean + protected DynamicSecurityAnalysisClient dynamicSecurityAnalysisClient; + + @MockitoBean + protected DynamicSimulationClient dynamicSimulationClient; + + @MockitoBean + protected NetworkStoreService networkStoreClient; + + @MockitoBean + protected DynamicMarginCalculationParametersRepository dynamicMarginCalculationParametersRepository; + + @MockitoSpyBean + protected ParametersService parametersService; + + @MockitoSpyBean + protected DynamicMarginCalculationWorkerService dynamicMarginCalculationWorkerService; + + @BeforeEach + @Override + public void setUp() throws IOException { + super.setUp(); + initDynamicMarginCalculationWorkerServiceSpy(); + initParametersRepositoryMock(); + initExternalClientsMocks(); + } + + @SneakyThrows + @AfterEach + @Override + public void tearDown() { + super.tearDown(); + + // delete all results + mockMvc.perform(delete("/v1/results")) + .andExpect(status().isOk()); + + // ensure queues are empty then clear + OutputDestination output = getOutputDestination(); + List destinations = List.of(dmcDebugDestination, dmcResultDestination, dmcStoppedDestination, dmcCancelFailedDestination); + TestUtils.assertQueuesEmptyThenClear(destinations, output); + } + + protected abstract OutputDestination getOutputDestination(); + + /** + * Provide a computation manager for the worker service so that computation can run. + */ + protected void initDynamicMarginCalculationWorkerServiceSpy() { + Mockito.when(dynamicMarginCalculationWorkerService.getComputationManager()).thenReturn(computationManager); + } + + /** + * Concrete test classes must set up repository responses for parameters UUIDs they use. + */ + protected abstract void initParametersRepositoryMock(); + + /** + * Concrete test classes may stub directory/filter calls if their parameters require it. + */ + protected abstract void initExternalClientsMocks(); + + // --- utility methods --- // + + protected void assertResultStatus(UUID runUuid, DynamicMarginCalculationStatus expectedStatus) throws Exception { + MvcResult result = mockMvc.perform(get("/v1/results/{resultUuid}/status", runUuid)) + .andExpect(status().isOk()) + .andReturn(); + + DynamicMarginCalculationStatus statusValue = null; + if (!result.getResponse().getContentAsString().isEmpty()) { + statusValue = objectMapper.readValue(result.getResponse().getContentAsString(), DynamicMarginCalculationStatus.class); + } + + assertThat(statusValue).isSameAs(expectedStatus); + } +} diff --git a/src/test/java/org/gridsuite/dynamicmargincalculation/server/controller/AbstractDynawoTest.java b/src/test/java/org/gridsuite/dynamicmargincalculation/server/controller/AbstractDynawoTest.java new file mode 100644 index 0000000..2dffe17 --- /dev/null +++ b/src/test/java/org/gridsuite/dynamicmargincalculation/server/controller/AbstractDynawoTest.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.dynamicmargincalculation.server.controller; + +import com.powsybl.computation.ComputationManager; +import com.powsybl.computation.local.test.ComputationDockerConfig; +import com.powsybl.computation.local.test.DockerLocalComputationManager; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Path; + +/** + * Base class to run Dynawo-based computations in integration tests (docker local computation manager). + * + * @author Thang PHAM + */ +public abstract class AbstractDynawoTest { + + private static final String JAVA_DYNAWO_VERSION = "3.1.0"; + private static final String DOCKER_IMAGE_ID = "powsybl/java-dynawo:" + JAVA_DYNAWO_VERSION; + + @TempDir + public Path localDir; + + protected ComputationManager computationManager; + + @BeforeEach + public void setUp() throws IOException { + Path dockerDir = Path.of("/home/powsybl"); + ComputationDockerConfig config = new ComputationDockerConfig() + .setDockerImageId(DOCKER_IMAGE_ID); + computationManager = new DockerLocalComputationManager(localDir, dockerDir, config); + } + + @AfterEach + public void tearDown() { + computationManager.close(); + } +} diff --git a/src/test/java/org/gridsuite/dynamicmargincalculation/server/controller/DynamicMarginCalculationControllerIEEE14Test.java b/src/test/java/org/gridsuite/dynamicmargincalculation/server/controller/DynamicMarginCalculationControllerIEEE14Test.java new file mode 100644 index 0000000..1a002fc --- /dev/null +++ b/src/test/java/org/gridsuite/dynamicmargincalculation/server/controller/DynamicMarginCalculationControllerIEEE14Test.java @@ -0,0 +1,211 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.dynamicmargincalculation.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.powsybl.commons.datasource.ReadOnlyDataSource; +import com.powsybl.commons.datasource.ResourceDataSource; +import com.powsybl.commons.datasource.ResourceSet; +import com.powsybl.contingency.Contingency; +import com.powsybl.dynamicsimulation.DynamicSimulationParameters; +import com.powsybl.dynawo.DynawoSimulationParameters; +import com.powsybl.dynawo.suppliers.dynamicmodels.DynamicModelConfig; +import com.powsybl.iidm.network.Importers; +import com.powsybl.iidm.network.Network; +import com.powsybl.iidm.network.VariantManagerConstants; +import com.powsybl.network.store.client.PreloadingStrategy; +import org.gridsuite.dynamicmargincalculation.server.controller.utils.DynamicModelConfigJsonUtils; +import org.gridsuite.dynamicmargincalculation.server.dto.DynamicMarginCalculationStatus; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.*; +import org.gridsuite.dynamicmargincalculation.server.entities.parameters.DynamicMarginCalculationParametersEntity; +import org.gridsuite.filter.expertfilter.ExpertFilter; +import org.gridsuite.filter.expertfilter.expertrule.CombinatorExpertRule; +import org.gridsuite.filter.expertfilter.expertrule.StringExpertRule; +import org.gridsuite.filter.utils.EquipmentType; +import org.gridsuite.filter.utils.expertfilter.CombinatorType; +import org.gridsuite.filter.utils.expertfilter.FieldType; +import org.gridsuite.filter.utils.expertfilter.OperatorType; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.stream.binder.test.OutputDestination; +import org.springframework.messaging.Message; +import org.springframework.test.web.servlet.MvcResult; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.gridsuite.computation.service.AbstractResultContext.VARIANT_ID_HEADER; +import static org.gridsuite.computation.service.NotificationService.HEADER_RESULT_UUID; +import static org.gridsuite.computation.service.NotificationService.HEADER_USER_ID; +import static org.gridsuite.dynamicmargincalculation.server.controller.utils.TestUtils.RESOURCE_PATH_DELIMITER; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * IEEE14 controller test for DynamicMarginCalculationController + * + * This test focuses on: + * - calling the /v1/networks/{uuid}/run endpoint + * - asserting a result notification is emitted + * - asserting the persisted status ends as SUCCEED + * + * @author Thang PHAM + */ +public class DynamicMarginCalculationControllerIEEE14Test extends AbstractDynamicMarginCalculationControllerTest { + + // directories + public static final String DATA_IEEE14_BASE_DIR = RESOURCE_PATH_DELIMITER + "data" + RESOURCE_PATH_DELIMITER + "ieee14"; + public static final String INPUT = "input"; + + public static final String DYNAMIC_MODEL_DUMP_FILE = "dynamicModel.dmp"; + public static final String DYNAMIC_SIMULATION_PARAMETERS_DUMP_FILE = "dynamicSimulationParameters.dmp"; + + public static final String NETWORK_FILE = "IEEE14.iidm"; + private static final UUID NETWORK_UUID = UUID.fromString("43b0f54e-e8fc-4607-8f53-e1cab1075ab5"); + private static final String VARIANT_1_ID = "variant_1"; + + private static final UUID DSA_PARAMETERS_UUID = UUID.fromString("2745666a-0abe-4c3a-8b91-a719c1d1f753"); + private static final UUID PARAMETERS_UUID = UUID.fromString("e786c4ca-64e7-4f44-b6a2-8f23b8b4334a"); + private static final UUID FILTER_UUID = UUID.fromString("b234ce92-23f2-422c-b239-ec69abc399bd"); + + @Autowired + private OutputDestination output; + + @Override + public OutputDestination getOutputDestination() { + return output; + } + + @Override + protected void initParametersRepositoryMock() { + // Use defaults from service to keep test stable across parameter schema changes + DynamicMarginCalculationParametersInfos params = parametersService.getDefaultParametersValues("Dynawo"); + params.setLoadsVariations(List.of( + LoadsVariationInfos.builder() + .loadFilters(List.of(IdNameInfos.builder().id(FILTER_UUID).build())) + .variation(10.0) + .active(true) + .build() + )); + + DynamicMarginCalculationParametersEntity entity = new DynamicMarginCalculationParametersEntity(params); + given(dynamicMarginCalculationParametersRepository.findById(PARAMETERS_UUID)).willReturn(Optional.of(entity)); + } + + @Override + protected void initExternalClientsMocks() { + // Mock for network + ReadOnlyDataSource dataSource = new ResourceDataSource("IEEE14", + new ResourceSet(DATA_IEEE14_BASE_DIR, NETWORK_FILE)); + Network network = Importers.importData("XIIDM", dataSource, null); + network.getVariantManager().cloneVariant(VariantManagerConstants.INITIAL_VARIANT_ID, VARIANT_1_ID); + given(networkStoreClient.getNetwork(NETWORK_UUID, PreloadingStrategy.COLLECTION)).willReturn(network); + + // Mock for the dynamic simulation client + initDynamicSimulationClientsMock(); + + // Mock for the dynamic security analysis client + initDynamicSecurityAnalysisClientMock(); + + // Mock for the filer client + when(filterClient.getFilters(List.of(FILTER_UUID))) + .thenReturn( + List.of( + ExpertFilter.builder() + .id(FILTER_UUID) + .rules(CombinatorExpertRule.builder() + .combinator(CombinatorType.AND) + .rules(List.of( + StringExpertRule.builder() + .field(FieldType.ID) + .operator(OperatorType.IS) + .value("_LOAD__11_EC") + .build() + )) + .build()) + .equipmentType(EquipmentType.LOAD) + .build()) + ); + + } + + private void initDynamicSimulationClientsMock() { + + try { + String inputDir = DATA_IEEE14_BASE_DIR + RESOURCE_PATH_DELIMITER + INPUT; + + // load dynamicModel.dmp + String dynamicModelFilePath = inputDir + RESOURCE_PATH_DELIMITER + DYNAMIC_MODEL_DUMP_FILE; + InputStream dynamicModelIS = getClass().getResourceAsStream(dynamicModelFilePath); + assert dynamicModelIS != null; + // temporal : use custom ObjectMapper provided by powsybl-dynawo to deserialize dynamic model + ObjectMapper dynamicModelConfigObjectMapper = DynamicModelConfigJsonUtils.createObjectMapper(); + List dynamicModel = dynamicModelConfigObjectMapper.readValue(dynamicModelIS, new TypeReference<>() { }); + + // load dynamicSimulationParameters.dmp + String dynamicSimulationParametersFilePath = inputDir + RESOURCE_PATH_DELIMITER + DYNAMIC_SIMULATION_PARAMETERS_DUMP_FILE; + InputStream dynamicSimulationParametersIS = getClass().getResourceAsStream(dynamicSimulationParametersFilePath); + assert dynamicSimulationParametersIS != null; + DynamicSimulationParameters dynamicSimulationParameters = objectMapper.readValue(dynamicSimulationParametersIS, DynamicSimulationParameters.class); + + // Mock for dynamic simulation server + when(dynamicSimulationClient.getParametersValues(anyString(), eq(NETWORK_UUID), any())) + .thenReturn(DynamicSimulationParametersValues.builder() + .dynamicModel(dynamicModel) + .dynawoParameters(dynamicSimulationParameters.getExtension(DynawoSimulationParameters.class)) + .build()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private void initDynamicSecurityAnalysisClientMock() { + when(dynamicSecurityAnalysisClient.getParametersValues(eq(DSA_PARAMETERS_UUID), eq(NETWORK_UUID), any())) + .thenReturn(DynamicSecurityAnalysisParametersValues.builder() + .contingenciesStartTime(105d) + .contingencies(List.of(Contingency.load("_LOAD__11_EC"))) + .build()); + } + + @Test + void test01IEEE14() throws Exception { + // The controller requires a request body string: dynamicSimulationParametersJson. + String dynamicSimulationParametersJson = "{}"; + + // run dynamic margin calculation on a specific variant + MvcResult result = mockMvc.perform( + post("/v1/networks/{networkUuid}/run", NETWORK_UUID.toString()) + .param(VARIANT_ID_HEADER, VARIANT_1_ID) + .param("dynamicSecurityAnalysisParametersUuid", DSA_PARAMETERS_UUID.toString()) + .param("parametersUuid", PARAMETERS_UUID.toString()) + .contentType(APPLICATION_JSON) + .content(dynamicSimulationParametersJson) + .header(HEADER_USER_ID, "testUserId") + ) + .andExpect(status().isOk()) + .andReturn(); + + UUID runUuid = objectMapper.readValue(result.getResponse().getContentAsString(), UUID.class); + + Message messageSwitch = output.receive(2000, dmcResultDestination); + assertThat(messageSwitch).isNotNull(); + assertThat(messageSwitch.getHeaders()).containsEntry(HEADER_RESULT_UUID, runUuid.toString()); + + // --- CHECK result --- // + assertResultStatus(runUuid, DynamicMarginCalculationStatus.SUCCEED); + } +} diff --git a/src/test/java/org/gridsuite/dynamicmargincalculation/server/controller/DynamicMarginCalculationControllerTest.java b/src/test/java/org/gridsuite/dynamicmargincalculation/server/controller/DynamicMarginCalculationControllerTest.java new file mode 100644 index 0000000..18418b4 --- /dev/null +++ b/src/test/java/org/gridsuite/dynamicmargincalculation/server/controller/DynamicMarginCalculationControllerTest.java @@ -0,0 +1,480 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.dynamicmargincalculation.server.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.powsybl.commons.datasource.ReadOnlyDataSource; +import com.powsybl.commons.datasource.ResourceDataSource; +import com.powsybl.commons.datasource.ResourceSet; +import com.powsybl.contingency.Contingency; +import com.powsybl.dynawo.contingency.results.FailedCriterion; +import com.powsybl.dynawo.contingency.results.ScenarioResult; +import com.powsybl.dynawo.margincalculation.MarginCalculation; +import com.powsybl.dynawo.margincalculation.results.LoadIncreaseResult; +import com.powsybl.dynawo.margincalculation.results.MarginCalculationResult; +import com.powsybl.iidm.network.Importers; +import com.powsybl.iidm.network.Network; +import com.powsybl.iidm.network.VariantManagerConstants; +import com.powsybl.network.store.client.PreloadingStrategy; +import org.gridsuite.computation.service.NotificationService; +import org.gridsuite.dynamicmargincalculation.server.dto.DynamicMarginCalculationStatus; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.DynamicMarginCalculationParametersInfos; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.DynamicSecurityAnalysisParametersValues; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.DynamicSimulationParametersValues; +import org.gridsuite.dynamicmargincalculation.server.entities.parameters.DynamicMarginCalculationParametersEntity; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.stream.binder.test.OutputDestination; +import org.springframework.messaging.Message; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.web.servlet.MvcResult; +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; + +import java.io.ByteArrayInputStream; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import static com.powsybl.dynawo.contingency.results.Status.CONVERGENCE; +import static com.powsybl.dynawo.contingency.results.Status.CRITERIA_NON_RESPECTED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.gridsuite.computation.s3.ComputationS3Service.METADATA_FILE_NAME; +import static org.gridsuite.computation.service.AbstractResultContext.REPORTER_ID_HEADER; +import static org.gridsuite.computation.service.AbstractResultContext.VARIANT_ID_HEADER; +import static org.gridsuite.computation.service.NotificationService.*; +import static org.gridsuite.dynamicmargincalculation.server.controller.utils.TestUtils.RESOURCE_PATH_DELIMITER; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Thang PHAM + */ +public class DynamicMarginCalculationControllerTest extends AbstractDynamicMarginCalculationControllerTest { + + // directories + public static final String DATA_IEEE14_BASE_DIR = RESOURCE_PATH_DELIMITER + "data" + RESOURCE_PATH_DELIMITER + "ieee14"; + + private static final UUID NETWORK_UUID = UUID.fromString("508a9a3a-cc8d-4bb2-97fa-1d38a5dd4ac1"); + private static final String VARIANT_1_ID = "variant_1"; + private static final String NETWORK_FILE = "IEEE14.iidm"; + + private static final UUID PARAMETERS_UUID = UUID.fromString("34f54d14-92ab-4616-a705-e9711275fc3b"); + private static final UUID DSA_PARAMETERS_UUID = UUID.fromString("bb9f39cf-7146-4892-8005-cd7c3fa48333"); + + private static final String LINE_ID = "_BUS____1-BUS____5-1_AC"; + private static final String GEN_ID = "_GEN____2_SM"; + + @Autowired + private OutputDestination output; + + @Autowired + ObjectMapper objectMapper; + + @MockitoSpyBean + private NotificationService notificationService; + + @MockitoSpyBean + private S3Client s3Client; + + @Override + public OutputDestination getOutputDestination() { + return output; + } + + @Override + protected void initParametersRepositoryMock() { + DynamicMarginCalculationParametersInfos params = parametersService.getDefaultParametersValues("Dynawo"); + params.setLoadsVariations(List.of()); // keep test independent from directory/filter enrichment + + DynamicMarginCalculationParametersEntity entity = new DynamicMarginCalculationParametersEntity(params); + given(dynamicMarginCalculationParametersRepository.findById(PARAMETERS_UUID)).willReturn(Optional.of(entity)); + } + + @Override + protected void initExternalClientsMocks() { + // Mock for network + ReadOnlyDataSource dataSource = new ResourceDataSource("IEEE14", + new ResourceSet(DATA_IEEE14_BASE_DIR, NETWORK_FILE)); + Network network = Importers.importData("XIIDM", dataSource, null); + network.getVariantManager().cloneVariant(VariantManagerConstants.INITIAL_VARIANT_ID, VARIANT_1_ID); + given(networkStoreClient.getNetwork(NETWORK_UUID, PreloadingStrategy.COLLECTION)).willReturn(network); + + // Mock for dynamic security analysis + when(dynamicSecurityAnalysisClient.getParametersValues(eq(DSA_PARAMETERS_UUID), eq(NETWORK_UUID), any())) + .thenReturn(DynamicSecurityAnalysisParametersValues.builder() + .contingenciesStartTime(105d) + .contingencies(List.of(Contingency.load("_LOAD__11_EC"))) + .build()); + + // Mock for dynamic simulation server + when(dynamicSimulationClient.getParametersValues(anyString(), eq(NETWORK_UUID), any())) + .thenReturn(DynamicSimulationParametersValues.builder().build()); + } + + @Test + void testResult() throws Exception { + + // mock DynamicMarginCalculationWorkerService + doReturn(CompletableFuture.completedFuture(MarginCalculationResult.empty())) + .when(dynamicMarginCalculationWorkerService).getCompletableFuture(any(), any(), any()); + + // mock s3 client for run with debug + doReturn(PutObjectResponse.builder().build()) + .when(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class)); + + doReturn(new ResponseInputStream<>( + GetObjectResponse.builder() + .metadata(Map.of(METADATA_FILE_NAME, "debugFile")) + .contentLength(100L).build(), + AbortableInputStream.create(new ByteArrayInputStream("s3 debug file content".getBytes())) + )).when(s3Client).getObject(any(GetObjectRequest.class)); + + // run with debug (body is the dynamicSimulationParametersJson string) + String dynamicSimulationParametersJson = "{}"; + MvcResult result = mockMvc.perform( + post("/v1/networks/{networkUuid}/run", NETWORK_UUID) + .param(VARIANT_ID_HEADER, VARIANT_1_ID) + .param("dynamicSecurityAnalysisParametersUuid", DSA_PARAMETERS_UUID.toString()) + .param("parametersUuid", PARAMETERS_UUID.toString()) + .param(HEADER_DEBUG, "true") + .contentType(APPLICATION_JSON) + .content(dynamicSimulationParametersJson) + .header(HEADER_USER_ID, "testUserId") + ) + .andExpect(status().isOk()) + .andReturn(); + + UUID runUuid = objectMapper.readValue(result.getResponse().getContentAsString(), UUID.class); + + // check notification of result + Message messageSwitch = output.receive(10_000, dmcResultDestination); + assertThat(messageSwitch).isNotNull(); + assertThat(messageSwitch.getHeaders()).containsEntry(HEADER_RESULT_UUID, runUuid.toString()); + + // check notification of debug + messageSwitch = output.receive(10_000, dmcDebugDestination); + assertThat(messageSwitch).isNotNull(); + assertThat(messageSwitch.getHeaders()).containsEntry(HEADER_RESULT_UUID, runUuid.toString()); + + // download debug zip file is ok + mockMvc.perform(get("/v1/results/{resultUuid}/download-debug-file", runUuid)) + .andExpect(status().isOk()); + + // check interaction with s3 client + verify(s3Client, times(1)).putObject(any(PutObjectRequest.class), any(RequestBody.class)); + verify(s3Client, times(1)).getObject(any(GetObjectRequest.class)); + + // run on implicit default variant (variantId omitted) + result = mockMvc.perform( + post("/v1/networks/{networkUuid}/run", NETWORK_UUID) + .param("dynamicSecurityAnalysisParametersUuid", DSA_PARAMETERS_UUID.toString()) + .param("parametersUuid", PARAMETERS_UUID.toString()) + .contentType(APPLICATION_JSON) + .content(dynamicSimulationParametersJson) + .header(HEADER_USER_ID, "testUserId") + ) + .andExpect(status().isOk()) + .andReturn(); + + runUuid = objectMapper.readValue(result.getResponse().getContentAsString(), UUID.class); + + messageSwitch = output.receive(10_000, dmcResultDestination); + assertThat(messageSwitch).isNotNull(); + assertThat(messageSwitch.getHeaders()).containsEntry(HEADER_RESULT_UUID, runUuid.toString()); + + // get status (depending on timing, could be RUNNING or SUCCEED) + result = mockMvc.perform(get("/v1/results/{resultUuid}/status", runUuid)) + .andExpect(status().isOk()) + .andReturn(); + + DynamicMarginCalculationStatus statusValue = objectMapper.readValue( + result.getResponse().getContentAsString(), + DynamicMarginCalculationStatus.class + ); + assertThat(statusValue).isIn(DynamicMarginCalculationStatus.SUCCEED, DynamicMarginCalculationStatus.RUNNING); + + // status of non-existing result => empty (mapped to null in helper) + assertResultStatus(UUID.randomUUID(), null); + + // invalidate status => set NOT_DONE + mockMvc.perform(put("/v1/results/invalidate-status") + .param("resultUuid", runUuid.toString())) + .andExpect(status().isOk()); + + result = mockMvc.perform(get("/v1/results/{resultUuid}/status", runUuid)) + .andExpect(status().isOk()) + .andReturn(); + + DynamicMarginCalculationStatus statusAfterInvalidate = objectMapper.readValue( + result.getResponse().getContentAsString(), + DynamicMarginCalculationStatus.class + ); + assertThat(statusAfterInvalidate).isSameAs(DynamicMarginCalculationStatus.NOT_DONE); + + // invalidate status for unknown result => 404 (controller returns notFound when update list is empty) + mockMvc.perform(put("/v1/results/invalidate-status") + .param("resultUuid", UUID.randomUUID().toString())) + .andExpect(status().isNotFound()); + + // delete one result + mockMvc.perform(delete("/v1/results/{resultUuid}", runUuid)) + .andExpect(status().isOk()); + + // verify deleted => status becomes empty (null) + assertResultStatus(runUuid, null); + + // delete non-existing => ok + mockMvc.perform(delete("/v1/results/{resultUuid}", UUID.randomUUID())) + .andExpect(status().isOk()); + + // delete all results => ok + mockMvc.perform(delete("/v1/results")) + .andExpect(status().isOk()); + } + + @Test + void testRunWithSynchronousExceptions() throws Exception { + String dynamicSimulationParametersJson = "{}"; + + // provider not found + mockMvc.perform( + post("/v1/networks/{networkUuid}/run", NETWORK_UUID) + .param(HEADER_PROVIDER, "notFoundProvider") + .param(VARIANT_ID_HEADER, VARIANT_1_ID) + .param("dynamicSecurityAnalysisParametersUuid", DSA_PARAMETERS_UUID.toString()) + .param("parametersUuid", PARAMETERS_UUID.toString()) + .contentType(APPLICATION_JSON) + .content(dynamicSimulationParametersJson) + .header(HEADER_USER_ID, "testUserId") + ) + .andExpect(status().isNotFound()); + + // parameters not found + mockMvc.perform( + post("/v1/networks/{networkUuid}/run", NETWORK_UUID) + .param(VARIANT_ID_HEADER, VARIANT_1_ID) + .param("dynamicSecurityAnalysisParametersUuid", DSA_PARAMETERS_UUID.toString()) + .param("parametersUuid", UUID.randomUUID().toString()) + .contentType(APPLICATION_JSON) + .content(dynamicSimulationParametersJson) + .header(HEADER_USER_ID, "testUserId") + ) + .andExpect(status().isNotFound()); + } + + @Test + void testRunWithResult() throws Exception { + doAnswer(invocation -> null).when(reportService).deleteReport(any()); + doAnswer(invocation -> null).when(reportService).sendReport(any(), any()); + + doReturn(CompletableFuture.completedFuture(new MarginCalculationResult(List.of( + new LoadIncreaseResult(100, CRITERIA_NON_RESPECTED, List.of(), + List.of(new FailedCriterion("total load power = 207.704MW > 200MW (criteria id: Risque protection)", 56.929320))), + new LoadIncreaseResult(0, CONVERGENCE, + List.of(new ScenarioResult(LINE_ID, CONVERGENCE), + new ScenarioResult(GEN_ID, CONVERGENCE))) + )))) + .when(dynamicMarginCalculationWorkerService).getCompletableFuture(any(), any(), any()); + String dynamicSimulationParametersJson = "{}"; + + MvcResult result = mockMvc.perform( + post("/v1/networks/{networkUuid}/run", NETWORK_UUID) + .param(VARIANT_ID_HEADER, VARIANT_1_ID) + .param("dynamicSecurityAnalysisParametersUuid", DSA_PARAMETERS_UUID.toString()) + .param("parametersUuid", PARAMETERS_UUID.toString()) + .param("reportUuid", UUID.randomUUID().toString()) + .param(REPORTER_ID_HEADER, "dmc") + .contentType(APPLICATION_JSON) + .content(dynamicSimulationParametersJson) + .header(HEADER_USER_ID, "testUserId") + ) + .andExpect(status().isOk()) + .andReturn(); + + UUID runUuid = objectMapper.readValue(result.getResponse().getContentAsString(), UUID.class); + + Message messageSwitch = output.receive(1000, dmcResultDestination); + assertThat(messageSwitch).isNotNull(); + assertThat(messageSwitch.getHeaders()).containsEntry(HEADER_RESULT_UUID, runUuid.toString()); + } + + // --- BEGIN Test cancelling a running computation ---// + + private void mockSendRunMessage(Supplier> runAsyncMock) { + // In test environment, the test binder calls consumers directly in the caller thread, i.e. the controller thread. + // By consequence, a real asynchronous Producer/Consumer can not be simulated like prod. + // So mocking producer in a separated thread differing from the controller thread (same pattern as the selected test). + doAnswer(invocation -> CompletableFuture.runAsync(() -> { + // static mock must be in the same thread of the consumer + // see : https://stackoverflow.com/questions/76406935/mock-static-method-in-spring-boot-integration-test + try (MockedStatic marginCalculationMockedStatic = mockStatic(MarginCalculation.class)) { + MarginCalculation.Runner runner = mock(MarginCalculation.Runner.class); + marginCalculationMockedStatic.when(MarginCalculation::getRunner).thenReturn(runner); + + // This gives us deterministic control over "long" vs "short" computations. + doAnswer(invocation2 -> runAsyncMock.get()) + .when(runner).runAsync(any(), any(), any(), any(), any()); + + // call real method sendRunMessage + try { + invocation.callRealMethod(); + } catch (Throwable e) { + throw new RuntimeException("Error while wrapping sendRunMessage in a separated thread", e); + } + } + })) + .when(notificationService).sendRunMessage(any()); + } + + private UUID runAndCancel(CountDownLatch cancelLatch, int cancelDelayMs) throws Exception { + String dynamicSimulationParametersJson = "{}"; + + // run the dynamic margin calculation + MvcResult result = mockMvc.perform( + post("/v1/networks/{networkUuid}/run", NETWORK_UUID) + .param(VARIANT_ID_HEADER, VARIANT_1_ID) + .param("dynamicSecurityAnalysisParametersUuid", DSA_PARAMETERS_UUID.toString()) + .param("parametersUuid", PARAMETERS_UUID.toString()) + .contentType(APPLICATION_JSON) + .content(dynamicSimulationParametersJson) + .header(HEADER_USER_ID, "testUserId")) + .andExpect(status().isOk()) + .andReturn(); + + UUID runUuid = objectMapper.readValue(result.getResponse().getContentAsString(), UUID.class); + + // Should be running quickly after creation + assertResultStatus(runUuid, DynamicMarginCalculationStatus.RUNNING); + + // stop, with a timeout to avoid test hangs if an exception occurs before latch countdown + boolean completed = cancelLatch.await(5, TimeUnit.SECONDS); + if (!completed) { + throw new AssertionError("Timed out waiting for cancelLatch, something might have crashed before latch countdown happens."); + } + + // optional extra wait (simulate user cancelling early/late) + await().pollDelay(cancelDelayMs, TimeUnit.MILLISECONDS).until(() -> true); + + mockMvc.perform(put("/v1/results/{resultUuid}/stop", runUuid)) + .andExpect(status().isOk()); + + return runUuid; + } + + @Test + void testStopOnTime() throws Exception { + CountDownLatch cancelLatch = new CountDownLatch(1); + + // Emit messages in separate threads, like in production. + mockSendRunMessage(() -> { + // trigger stop at the beginning of computation + cancelLatch.countDown(); + + // fake a long computation (1s) + return CompletableFuture.supplyAsync( + () -> null, + CompletableFuture.delayedExecutor(1000, TimeUnit.MILLISECONDS) + ); + }); + + UUID runUuid = runAndCancel(cancelLatch, 0); + + // Must have a cancel message in the stop queue + Message message = output.receive(1000, dmcStoppedDestination); + assertThat(message).isNotNull(); + assertThat(message.getHeaders()) + .containsEntry(HEADER_RESULT_UUID, runUuid.toString()) + .containsKey(HEADER_MESSAGE); + + // result has been deleted by cancel so status is empty (null) + assertResultStatus(runUuid, null); + } + + @Test + void testStopEarly() throws Exception { + CountDownLatch cancelLatch = new CountDownLatch(1); + + // Emit messages in separate threads, like in production. + mockSendRunMessage(() -> CompletableFuture.supplyAsync(() -> null)); + + // Delay before the computation starts to simulate "cancel too early" + doAnswer(invocation -> { + Object object = invocation.callRealMethod(); + + cancelLatch.countDown(); + + await().pollDelay(1000, TimeUnit.MILLISECONDS).until(() -> true); + return object; + }).when(dynamicMarginCalculationWorkerService).preRun(any()); + + UUID runUuid = runAndCancel(cancelLatch, 0); + + // Must have a cancel failed message + Message message = output.receive(1000, dmcCancelFailedDestination); + assertThat(message).isNotNull(); + assertThat(message.getHeaders()) + .containsEntry(HEADER_RESULT_UUID, runUuid.toString()) + .containsKey(HEADER_MESSAGE); + + // cancel failed so result still exists (status remains RUNNING in this behaviour) + assertResultStatus(runUuid, DynamicMarginCalculationStatus.RUNNING); + } + + @Test + void testStopLately() throws Exception { + CountDownLatch cancelLatch = new CountDownLatch(1); + + // Emit messages in separate threads, like in production. + mockSendRunMessage(() -> { + // using latch to trigger stop dynamic margin calculation at the beginning of computation + cancelLatch.countDown(); + + // fake a short computation + return CompletableFuture.supplyAsync(MarginCalculationResult::empty); + }); + + // test run then cancel + UUID runUuid = runAndCancel(cancelLatch, 1000); + + // check result + // Computation finished quickly => must have a result message + Message message = output.receive(1000, dmcResultDestination); + assertThat(message).isNotNull(); + assertThat(message.getHeaders()) + .containsEntry(HEADER_RESULT_UUID, runUuid.toString()); + + // Stop arrives after end => cancel failed message + message = output.receive(1000, dmcCancelFailedDestination); + assertThat(message).isNotNull(); + assertThat(message.getHeaders()) + .containsEntry(HEADER_RESULT_UUID, runUuid.toString()) + .containsKey(HEADER_MESSAGE); + + // cancel failed so results are not deleted + assertResultStatus(runUuid, DynamicMarginCalculationStatus.SUCCEED); + } + + // --- END Test cancelling a running computation ---// +} diff --git a/src/test/java/org/gridsuite/dynamicmargincalculation/server/controller/DynamicMarginCalculationParametersControllerTest.java b/src/test/java/org/gridsuite/dynamicmargincalculation/server/controller/DynamicMarginCalculationParametersControllerTest.java new file mode 100644 index 0000000..8431b22 --- /dev/null +++ b/src/test/java/org/gridsuite/dynamicmargincalculation/server/controller/DynamicMarginCalculationParametersControllerTest.java @@ -0,0 +1,274 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.dynamicmargincalculation.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.gridsuite.dynamicmargincalculation.server.DynamicMarginCalculationApplication; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.DynamicMarginCalculationParametersInfos; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.IdNameInfos; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.LoadsVariationInfos; +import org.gridsuite.dynamicmargincalculation.server.entities.parameters.DynamicMarginCalculationParametersEntity; +import org.gridsuite.dynamicmargincalculation.server.repositories.DynamicMarginCalculationParametersRepository; +import org.gridsuite.dynamicmargincalculation.server.service.FilterService; +import org.gridsuite.dynamicmargincalculation.server.service.ParametersService; +import org.gridsuite.dynamicmargincalculation.server.service.client.DirectoryClient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.gridsuite.computation.service.NotificationService.HEADER_USER_ID; +import static org.mockito.Mockito.*; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Thang PHAM + */ +@AutoConfigureMockMvc +@SpringBootTest +@ContextConfiguration(classes = {DynamicMarginCalculationApplication.class}) +class DynamicMarginCalculationParametersControllerTest { + + private static final String USER_ID = "userId"; + private static final UUID LOAD_FILTER_UUID_1 = UUID.fromString("fff118fa-12ff-4fe1-965d-1d81a45d8ef8"); + private static final UUID LOAD_FILTER_UUID_2 = UUID.fromString("96d0097b-ec80-4ac9-827a-4a38095972a0"); + + @Autowired + MockMvc mockMvc; + + @Autowired + ObjectMapper objectMapper; + + @Autowired + ParametersService parametersService; + + @Autowired + DynamicMarginCalculationParametersRepository parametersRepository; + + @MockitoBean + DirectoryClient directoryClient; + + @MockitoBean + FilterService filterService; + + @AfterEach + void tearDown() { + parametersRepository.deleteAll(); + reset(directoryClient, filterService); + } + + private DynamicMarginCalculationParametersInfos newParametersInfos() { + // Keep the DTO minimal and stable for CRUD tests (no need to involve filter evaluation). + DynamicMarginCalculationParametersInfos infos = parametersService.getDefaultParametersValues("Dynawo"); + infos.setLoadsVariations(List.of()); // make explicit to avoid null handling differences + return infos; + } + + private DynamicMarginCalculationParametersInfos newParametersInfosWithLoadFilters() { + DynamicMarginCalculationParametersInfos infos = parametersService.getDefaultParametersValues("Dynawo"); + + LoadsVariationInfos loadsVariationInfos = LoadsVariationInfos.builder() + .active(true) + .variation(10.0) + .loadFilters(List.of( + IdNameInfos.builder().id(LOAD_FILTER_UUID_1).build(), + IdNameInfos.builder().id(LOAD_FILTER_UUID_2).build() + )) + .build(); + + infos.setLoadsVariations(List.of(loadsVariationInfos)); + return infos; + } + + @Test + void testCreateParameters() throws Exception { + DynamicMarginCalculationParametersInfos parametersInfos = newParametersInfos(); + + MvcResult result = mockMvc.perform(post("/v1/parameters") + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(parametersInfos))) + .andExpect(status().isOk()) + .andReturn(); + + UUID parametersUuid = objectMapper.readValue(result.getResponse().getContentAsString(), UUID.class); + + Optional entityOpt = parametersRepository.findById(parametersUuid); + assertThat(entityOpt).isPresent(); + + DynamicMarginCalculationParametersInfos resultParametersInfos = entityOpt.get().toDto(true); + + assertThat(resultParametersInfos).usingRecursiveComparison().isEqualTo(parametersInfos); + } + + @Test + void testCreateDefaultParameters() throws Exception { + DynamicMarginCalculationParametersInfos defaultParametersInfos = newParametersInfos(); + + MvcResult result = mockMvc.perform(post("/v1/parameters/default")) + .andExpect(status().isOk()) + .andReturn(); + + UUID parametersUuid = objectMapper.readValue(result.getResponse().getContentAsString(), UUID.class); + + Optional entityOpt = parametersRepository.findById(parametersUuid); + assertThat(entityOpt).isPresent(); + assertThat(entityOpt.get().getProvider()).isEqualTo("Dynawo"); + DynamicMarginCalculationParametersInfos resultParametersInfos = entityOpt.get().toDto(true); + assertThat(resultParametersInfos).usingRecursiveComparison().isEqualTo(defaultParametersInfos); + } + + @Test + void testDuplicateParameters() throws Exception { + DynamicMarginCalculationParametersInfos originalInfos = newParametersInfos(); + UUID originalUuid = parametersRepository.save(new DynamicMarginCalculationParametersEntity(originalInfos)).getId(); + + MvcResult result = mockMvc.perform(post("/v1/parameters") + .param("duplicateFrom", originalUuid.toString())) + .andExpect(status().isOk()) + .andReturn(); + + UUID duplicatedUuid = objectMapper.readValue(result.getResponse().getContentAsString(), UUID.class); + + Optional duplicatedOpt = parametersRepository.findById(duplicatedUuid); + assertThat(duplicatedOpt).isPresent(); + + DynamicMarginCalculationParametersInfos duplicatedInfos = duplicatedOpt.get().toDto(true); + + assertThat(duplicatedInfos).usingRecursiveComparison().isEqualTo(originalInfos); + } + + @Test + void testGetParametersRequiresUserHeaderAndEnrichesLoadFilterNames() throws Exception { + // --- Setup: persist parameters with load filters (names missing before enrichment) --- // + DynamicMarginCalculationParametersInfos infos = newParametersInfosWithLoadFilters(); + UUID parametersUuid = parametersRepository.save(new DynamicMarginCalculationParametersEntity(infos)).getId(); + + // service enrichment uses DirectoryClient only when userId is provided + when(directoryClient.getElementNames(List.of(LOAD_FILTER_UUID_1, LOAD_FILTER_UUID_2), USER_ID)) + .thenReturn(Map.of( + LOAD_FILTER_UUID_1, "Filter 1", + LOAD_FILTER_UUID_2, "Filter 2" + )); + + // --- Execute --- // + MvcResult result = mockMvc.perform(get("/v1/parameters/{uuid}", parametersUuid) + .header(HEADER_USER_ID, USER_ID)) + .andExpect(status().isOk()) + .andReturn(); + + DynamicMarginCalculationParametersInfos returned = + objectMapper.readValue(result.getResponse().getContentAsString(), DynamicMarginCalculationParametersInfos.class); + + // --- Verify --- // + assertThat(returned.getId()).isEqualTo(parametersUuid); + assertThat(returned.getLoadsVariations()).hasSize(1); + assertThat(returned.getLoadsVariations().getFirst().getLoadFilters()).hasSize(2); + + assertThat(returned.getLoadsVariations().getFirst().getLoadFilters().getFirst().getName()).isEqualTo("Filter 1"); + assertThat(returned.getLoadsVariations().getFirst().getLoadFilters().get(1).getName()).isEqualTo("Filter 2"); + + verify(directoryClient, times(1)).getElementNames(List.of(LOAD_FILTER_UUID_1, LOAD_FILTER_UUID_2), USER_ID); + } + + @Test + void testGetAllParameters() throws Exception { + DynamicMarginCalculationParametersInfos infos = newParametersInfos(); + parametersRepository.saveAll(List.of( + new DynamicMarginCalculationParametersEntity(infos), + new DynamicMarginCalculationParametersEntity(infos) + )); + + MvcResult result = mockMvc.perform(get("/v1/parameters")) + .andExpect(status().isOk()) + .andReturn(); + + List returned = objectMapper.readValue( + result.getResponse().getContentAsString(), + new TypeReference<>() { } + ); + + assertThat(returned).hasSize(2); + assertThat(returned.get(0).getId()).isNotNull(); + assertThat(returned.get(1).getId()).isNotNull(); + } + + @Test + void testUpdateParameters() throws Exception { + DynamicMarginCalculationParametersInfos infos = newParametersInfos(); + UUID parametersUuid = parametersRepository.save(new DynamicMarginCalculationParametersEntity(infos)).getId(); + + DynamicMarginCalculationParametersInfos updatedInfos = newParametersInfos(); + updatedInfos.setAccuracy(2); // change a field to ensure update persists + + mockMvc.perform(put("/v1/parameters/{uuid}", parametersUuid) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updatedInfos))) + .andExpect(status().isOk()); + + Optional entityOpt = parametersRepository.findById(parametersUuid); + assertThat(entityOpt).isPresent(); + + DynamicMarginCalculationParametersInfos persisted = entityOpt.get().toDto(false); + assertThat(persisted.getAccuracy()).isEqualTo(2); + } + + @Test + void testDeleteParameters() throws Exception { + DynamicMarginCalculationParametersInfos infos = newParametersInfos(); + UUID parametersUuid = parametersRepository.save(new DynamicMarginCalculationParametersEntity(infos)).getId(); + + mockMvc.perform(delete("/v1/parameters/{uuid}", parametersUuid)) + .andExpect(status().isOk()); + + assertThat(parametersRepository.findById(parametersUuid)).isEmpty(); + } + + @Test + void testGetProvider() throws Exception { + DynamicMarginCalculationParametersInfos infos = newParametersInfos(); + infos.setProvider("Dynawo"); + UUID parametersUuid = parametersRepository.save(new DynamicMarginCalculationParametersEntity(infos)).getId(); + + MvcResult result = mockMvc.perform(get("/v1/parameters/{uuid}/provider", parametersUuid)) + .andExpect(status().isOk()) + .andReturn(); + String provider = result.getResponse().getContentAsString(); + assertThat(provider).isEqualTo("Dynawo"); + } + + @Test + void testUpdateProvider() throws Exception { + DynamicMarginCalculationParametersInfos infos = newParametersInfos(); + UUID parametersUuid = parametersRepository.save(new DynamicMarginCalculationParametersEntity(infos)).getId(); + + String newProvider = "Dynawo"; + + mockMvc.perform(put("/v1/parameters/{uuid}/provider", parametersUuid) + .contentType(TEXT_PLAIN_VALUE) + .content(newProvider)) + .andExpect(status().isOk()); + + Optional entityOpt = parametersRepository.findById(parametersUuid); + assertThat(entityOpt).isPresent(); + assertThat(entityOpt.get().getProvider()).isEqualTo(newProvider); + } +} diff --git a/src/test/java/org/gridsuite/dynamicmargincalculation/server/controller/utils/DynamicModelConfigJsonUtils.java b/src/test/java/org/gridsuite/dynamicmargincalculation/server/controller/utils/DynamicModelConfigJsonUtils.java new file mode 100644 index 0000000..b156540 --- /dev/null +++ b/src/test/java/org/gridsuite/dynamicmargincalculation/server/controller/utils/DynamicModelConfigJsonUtils.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.controller.utils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.powsybl.commons.json.JsonUtil; +import com.powsybl.dynawo.suppliers.dynamicmodels.DynamicModelConfigsJsonDeserializer; + +import java.util.List; + +/** + * @author Thang PHAM + */ +public final class DynamicModelConfigJsonUtils { + + private DynamicModelConfigJsonUtils() { + throw new AssertionError("Utility class should not be instantiated"); + } + + public static ObjectMapper createObjectMapper() { + ObjectMapper mapper = JsonUtil.createObjectMapper(); + + SimpleModule module = new SimpleModule("dynamic-model-configs"); + module.addDeserializer(List.class, new DynamicModelConfigsJsonDeserializer()); + + mapper.registerModule(module); + return mapper; + } +} diff --git a/src/test/java/org/gridsuite/dynamicmargincalculation/server/controller/utils/TestUtils.java b/src/test/java/org/gridsuite/dynamicmargincalculation/server/controller/utils/TestUtils.java new file mode 100644 index 0000000..b9a6e46 --- /dev/null +++ b/src/test/java/org/gridsuite/dynamicmargincalculation/server/controller/utils/TestUtils.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.controller.utils; + +import org.springframework.cloud.stream.binder.test.OutputDestination; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.testcontainers.shaded.org.awaitility.Awaitility.await; + +/** + * @author Thang PHAM + */ +public final class TestUtils { + + public static final String RESOURCE_PATH_DELIMITER = "/"; + + private static final long TIMEOUT = 100; + + private TestUtils() { + throw new AssertionError("Utility class should not be instantiated"); + } + + public static void assertQueuesEmptyThenClear(List destinations, OutputDestination output) { + await().pollDelay(TIMEOUT, TimeUnit.MILLISECONDS).until(() -> { + try { + destinations.forEach(destination -> assertThat(output.receive(0, destination)).as("Should not be any messages in queue " + destination + " : ").isNull()); + } catch (NullPointerException e) { + // Ignoring + } finally { + output.clear(); // purge in order to not fail the other tests + } + return true; + }); + } +} diff --git a/src/test/java/org/gridsuite/dynamicmargincalculation/server/service/client/AbstractRestClientTest.java b/src/test/java/org/gridsuite/dynamicmargincalculation/server/service/client/AbstractRestClientTest.java new file mode 100644 index 0000000..e1dd6b6 --- /dev/null +++ b/src/test/java/org/gridsuite/dynamicmargincalculation/server/service/client/AbstractRestClientTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.service.client; + +import com.github.tomakehurst.wiremock.WireMockServer; +import org.gridsuite.dynamicmargincalculation.server.DynamicMarginCalculationApplication; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInstance; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; + +/** + * @author Thang PHAM + */ +@SpringBootTest +@ContextHierarchy({@ContextConfiguration(classes = {DynamicMarginCalculationApplication.class})}) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public abstract class AbstractRestClientTest { + + public static final String ERROR_MESSAGE = "Something wrong in remote server"; + public static final String ERROR_MESSAGE_JSON = """ + {"message": "%s"} + """.formatted(ERROR_MESSAGE); + + public final Logger getLogger() { + return LoggerFactory.getLogger(this.getClass()); + } + + protected WireMockServer wireMockServer; + + protected String initMockWebServer(WireMockServer server) { + wireMockServer = server; + + wireMockServer.start(); + getLogger().info("Mock server started at port = {}", wireMockServer.port()); + + // get base URL + return wireMockServer.baseUrl(); + } + + @BeforeEach + void setup() { + // Clear stubs/requests between tests while keeping the same server instance + wireMockServer.resetAll(); + } + + @AfterAll + public void tearDown() { + try { + wireMockServer.shutdown(); + } catch (Exception e) { + getLogger().info("Can not shutdown the mock server {}", this.getClass().getSimpleName()); + } + } +} diff --git a/src/test/java/org/gridsuite/dynamicmargincalculation/server/service/client/DirectoryClientTest.java b/src/test/java/org/gridsuite/dynamicmargincalculation/server/service/client/DirectoryClientTest.java new file mode 100644 index 0000000..5d54891 --- /dev/null +++ b/src/test/java/org/gridsuite/dynamicmargincalculation/server/service/client/DirectoryClientTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.service.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import org.gridsuite.dynamicmargincalculation.server.dto.ElementAttributes; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestTemplate; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.havingExactly; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowableOfType; +import static org.gridsuite.computation.service.NotificationService.HEADER_USER_ID; +import static org.gridsuite.dynamicmargincalculation.server.service.client.DirectoryClient.*; +import static org.gridsuite.dynamicmargincalculation.server.service.client.utils.UrlUtils.buildEndPointUrl; + +/** + * @author Thang PHAM + */ +class DirectoryClientTest extends AbstractRestClientTest { + + private DirectoryClient directoryClient; + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private ObjectMapper objectMapper; + + private String getBaseUrl() { + return buildEndPointUrl("", API_VERSION, ELEMENT_END_POINT_INFOS); + } + + @BeforeAll + void init() { + directoryClient = new DirectoryClient( + // use new WireMockServer(DIRECTORY_PORT) to test with local server if needed + initMockWebServer(new WireMockServer(wireMockConfig().dynamicPort())), + restTemplate, + objectMapper + ); + } + + @Test + void testGetElementNamesEmptyInput() { + Map result = directoryClient.getElementNames(List.of(), "userId"); + assertThat(result).isEmpty(); + } + + @Test + void testGetElementNames() throws Exception { + + // --- Setup --- // + UUID id1 = UUID.fromString("0d740ec6-15ca-4d81-9963-ffed732c3d36"); + UUID id2 = UUID.fromString("ad1f5f93-67ad-4ea8-a85a-b82a52cce0a2"); + String userId = "testUserId"; + + List responseBody = List.of( + new ElementAttributes(id1, "Element 1"), + new ElementAttributes(id2, "Element 2") + ); + + wireMockServer.stubFor(WireMock.get(WireMock.urlPathEqualTo(getBaseUrl())) + .withQueryParam(QUERY_PARAM_IDS, havingExactly(id1.toString(), id2.toString())) + .withQueryParam(QUERY_PARAM_STRICT_MODE, equalTo("false")) + .withHeader(HEADER_USER_ID, equalTo(userId)) + .willReturn(WireMock.ok() + .withBody(objectMapper.writeValueAsString(responseBody)) + .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) + )); + + // --- Execute --- // + Map result = directoryClient.getElementNames(List.of(id1, id2), userId); + + // --- Verify --- // + assertThat(result).containsExactlyInAnyOrderEntriesOf(Map.of( + id1, "Element 1", + id2, "Element 2" + )); + } + + @Test + void testGetElementNamesGivenException() { + + // --- Setup --- // + UUID id1 = UUID.fromString("a5efc25c-c836-423d-a8ba-0c6e74a2f8ae"); + + wireMockServer.stubFor(WireMock.get(WireMock.urlPathEqualTo(getBaseUrl())) + .withQueryParam(QUERY_PARAM_IDS, havingExactly(id1.toString())) + .withQueryParam(QUERY_PARAM_STRICT_MODE, equalTo("false")) + .willReturn(WireMock.serverError() + .withBody(ERROR_MESSAGE_JSON) + )); + + // --- Execute --- // + HttpServerErrorException exception = catchThrowableOfType(HttpServerErrorException.class, + () -> directoryClient.getElementNames(List.of(id1), null)); + + // --- Verify --- // + assertThat(exception.getMessage()).contains(ERROR_MESSAGE); + } +} diff --git a/src/test/java/org/gridsuite/dynamicmargincalculation/server/service/client/DynamicSecurityAnalysisClientTest.java b/src/test/java/org/gridsuite/dynamicmargincalculation/server/service/client/DynamicSecurityAnalysisClientTest.java new file mode 100644 index 0000000..c6b6630 --- /dev/null +++ b/src/test/java/org/gridsuite/dynamicmargincalculation/server/service/client/DynamicSecurityAnalysisClientTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.service.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.powsybl.contingency.Contingency; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.DynamicSecurityAnalysisParametersValues; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestTemplate; + +import java.util.List; +import java.util.UUID; + +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowableOfType; +import static org.gridsuite.computation.service.AbstractResultContext.VARIANT_ID_HEADER; +import static org.gridsuite.dynamicmargincalculation.server.service.client.DynamicSecurityAnalysisClient.API_VERSION; +import static org.gridsuite.dynamicmargincalculation.server.service.client.DynamicSecurityAnalysisClient.DYNAMIC_SECURITY_ANALYSIS_END_POINT_PARAMETERS; +import static org.gridsuite.dynamicmargincalculation.server.service.client.utils.UrlUtils.buildEndPointUrl; + +/** + * @author Thang PHAM + */ +class DynamicSecurityAnalysisClientTest extends AbstractRestClientTest { + + private DynamicSecurityAnalysisClient dynamicSecurityAnalysisClient; + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private ObjectMapper objectMapper; + + @BeforeAll + void init() { + // use new WireMockServer(DYNAMIC_SECURITY_ANALYSIS_PORT) to test with local server if needed + dynamicSecurityAnalysisClient = new DynamicSecurityAnalysisClient( + initMockWebServer(new WireMockServer(wireMockConfig().dynamicPort())), + restTemplate, + objectMapper + ); + } + + @Test + void testGetParametersValues() throws Exception { + + // --- Setup --- // + UUID parametersUuid = UUID.fromString("f1be5de3-b8e5-4ab5-9521-62a1f8a66228"); + UUID networkUuid = UUID.fromString("f1be5de3-b8e5-4ab5-9521-62a1f8a66228"); + String variantId = "variant_1"; + + DynamicSecurityAnalysisParametersValues expected = DynamicSecurityAnalysisParametersValues.builder() + .contingenciesStartTime(105d) + .contingencies(List.of(Contingency.load("_LOAD__11_EC"))) + .build(); + String bodyJson = objectMapper.writeValueAsString(expected); + + String baseEndpoint = buildEndPointUrl("", API_VERSION, DYNAMIC_SECURITY_ANALYSIS_END_POINT_PARAMETERS); + String urlPath = baseEndpoint + "/" + parametersUuid + "/values"; + + wireMockServer.stubFor(WireMock.get(WireMock.urlPathEqualTo(urlPath)) + .withQueryParam("networkUuid", equalTo(networkUuid.toString())) + .withQueryParam(VARIANT_ID_HEADER, equalTo(variantId)) + .willReturn(WireMock.ok() + .withBody(bodyJson) + .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) + )); + + // --- Execute --- // + DynamicSecurityAnalysisParametersValues result = + dynamicSecurityAnalysisClient.getParametersValues(parametersUuid, networkUuid, variantId); + + // --- Verify --- // + assertThat(result).usingRecursiveComparison().isEqualTo(expected); + } + + @Test + void testGetParametersValuesGivenException() { + + // --- Setup --- // + UUID parametersUuid = UUID.fromString("eb4a62e4-0927-4e1e-9256-56a8494b0786"); + UUID networkUuid = UUID.fromString("eb4a62e4-0927-4e1e-9256-56a8494b0786"); + String variantId = "variant_1"; + + String baseEndpoint = buildEndPointUrl("", API_VERSION, DYNAMIC_SECURITY_ANALYSIS_END_POINT_PARAMETERS); + String urlPath = baseEndpoint + "/" + parametersUuid + "/values"; + + wireMockServer.stubFor(WireMock.get(WireMock.urlPathEqualTo(urlPath)) + .withQueryParam("networkUuid", equalTo(networkUuid.toString())) + .withQueryParam(VARIANT_ID_HEADER, equalTo(variantId)) + .willReturn(WireMock.serverError() + .withBody(ERROR_MESSAGE_JSON) + )); + + // --- Execute --- // + HttpServerErrorException exception = catchThrowableOfType(HttpServerErrorException.class, + () -> dynamicSecurityAnalysisClient.getParametersValues(parametersUuid, networkUuid, variantId)); + + // --- Verify --- // + assertThat(exception.getMessage()).contains(ERROR_MESSAGE); + } +} diff --git a/src/test/java/org/gridsuite/dynamicmargincalculation/server/service/client/DynamicSimulationClientTest.java b/src/test/java/org/gridsuite/dynamicmargincalculation/server/service/client/DynamicSimulationClientTest.java new file mode 100644 index 0000000..063f5f1 --- /dev/null +++ b/src/test/java/org/gridsuite/dynamicmargincalculation/server/service/client/DynamicSimulationClientTest.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.dynamicmargincalculation.server.service.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.powsybl.dynawo.DynawoSimulationParameters; +import com.powsybl.dynawo.parameters.Parameter; +import com.powsybl.dynawo.parameters.ParameterType; +import com.powsybl.dynawo.parameters.ParametersSet; +import com.powsybl.dynawo.suppliers.PropertyBuilder; +import com.powsybl.dynawo.suppliers.PropertyType; +import com.powsybl.dynawo.suppliers.SetGroupType; +import com.powsybl.dynawo.suppliers.dynamicmodels.DynamicModelConfig; +import org.gridsuite.dynamicmargincalculation.server.dto.parameters.DynamicSimulationParametersValues; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestTemplate; + +import java.util.List; +import java.util.UUID; + +import static com.github.tomakehurst.wiremock.client.WireMock.containing; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowableOfType; +import static org.gridsuite.computation.service.AbstractResultContext.VARIANT_ID_HEADER; +import static org.gridsuite.dynamicmargincalculation.server.service.client.DynamicSimulationClient.API_VERSION; +import static org.gridsuite.dynamicmargincalculation.server.service.client.DynamicSimulationClient.DYNAMIC_SIMULATION_END_POINT_PARAMETERS; +import static org.gridsuite.dynamicmargincalculation.server.service.client.utils.UrlUtils.buildEndPointUrl; + +/** + * @author Thang PHAM + */ +class DynamicSimulationClientTest extends AbstractRestClientTest { + + private DynamicSimulationClient dynamicSimulationClient; + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private ObjectMapper objectMapper; + + @BeforeAll + void init() { + // use new WireMockServer(DYNAMIC_SIMULATION_PORT) to test with local server if needed + dynamicSimulationClient = new DynamicSimulationClient( + initMockWebServer(new WireMockServer(wireMockConfig().dynamicPort())), + restTemplate, + objectMapper + ); + } + + @Test + void testGetParametersValues() throws Exception { + + // --- Setup --- // + UUID networkUuid = UUID.fromString("e6516424-21c3-4bff-953d-9d0efc20ea08"); + String variantId = "variant_1"; + String dynamicSimulationParametersJson = "{}"; + + // prepare dynamic model + List dynamicModel = List.of(new DynamicModelConfig("LoadAlphaBeta", "_DM", SetGroupType.SUFFIX, List.of( + new PropertyBuilder() + .name("staticId") + .value("LOAD") + .type(PropertyType.STRING) + .build()))); + + // prepare dynawo simulation parameters + DynawoSimulationParameters dynawoParameters = DynawoSimulationParameters.load(); + + // network + ParametersSet networkParamsSet = new ParametersSet("network"); + networkParamsSet.addParameter(new Parameter("LoadBeta", ParameterType.DOUBLE, "2")); + networkParamsSet.addParameter(new Parameter("LoadAlpha", ParameterType.DOUBLE, "1")); + + dynawoParameters.setNetworkParameters(networkParamsSet); + + // model parameters set + dynawoParameters.setModelsParameters(List.of(new ParametersSet("LoadAlphaBeta"))); + + // solver parameters set + ParametersSet simSolverParamsSet = new ParametersSet("SIM"); + simSolverParamsSet.addParameter(new Parameter("HMin", ParameterType.DOUBLE, "0.001")); + simSolverParamsSet.addParameter(new Parameter("LinearSolverName", ParameterType.STRING, "KLU")); + + dynawoParameters.setSolverParameters(simSolverParamsSet); + + // build Dto to mock return + DynamicSimulationParametersValues expected = DynamicSimulationParametersValues.builder() + .dynamicModel(dynamicModel) + .dynawoParameters(dynawoParameters) + .build(); + + String bodyJson = objectMapper.writeValueAsString(expected); + + String baseEndpoint = buildEndPointUrl("", API_VERSION, DYNAMIC_SIMULATION_END_POINT_PARAMETERS); + String urlPath = baseEndpoint + "/values"; + + wireMockServer.stubFor(WireMock.post(WireMock.urlPathEqualTo(urlPath)) + .withQueryParam("networkUuid", equalTo(networkUuid.toString())) + .withQueryParam(VARIANT_ID_HEADER, equalTo(variantId)) + .withHeader(HttpHeaders.CONTENT_TYPE, containing(MediaType.APPLICATION_JSON_VALUE)) + .withRequestBody(WireMock.equalToJson(dynamicSimulationParametersJson)) + .willReturn(WireMock.ok() + .withBody(bodyJson) + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + )); + + // --- Execute --- // + DynamicSimulationParametersValues result = + dynamicSimulationClient.getParametersValues(dynamicSimulationParametersJson, networkUuid, variantId); + + // --- Verify --- // + assertThat(result).usingRecursiveComparison().isEqualTo(expected); + } + + @Test + void testGetParametersValuesGivenException() { + + // --- Setup --- // + UUID networkUuid = UUID.fromString("4b364ee1-2933-401a-ae3c-e79253b2de67"); + String variantId = "variant_1"; + String dynamicSimulationParametersJson = "{}"; + + String baseEndpoint = buildEndPointUrl("", API_VERSION, DYNAMIC_SIMULATION_END_POINT_PARAMETERS); + String urlPath = baseEndpoint + "/values"; + + wireMockServer.stubFor(WireMock.post(WireMock.urlPathEqualTo(urlPath)) + .withQueryParam("networkUuid", equalTo(networkUuid.toString())) + .withQueryParam(VARIANT_ID_HEADER, equalTo(variantId)) + .willReturn(WireMock.serverError() + .withBody(ERROR_MESSAGE_JSON) + )); + + // --- Execute --- // + HttpServerErrorException exception = catchThrowableOfType(HttpServerErrorException.class, + () -> dynamicSimulationClient.getParametersValues(dynamicSimulationParametersJson, networkUuid, variantId)); + + // --- Verify --- // + assertThat(exception.getMessage()).contains(ERROR_MESSAGE); + } +} diff --git a/src/test/resources/com/powsybl/config/test/config.yml b/src/test/resources/com/powsybl/config/test/config.yml new file mode 100644 index 0000000..ab45183 --- /dev/null +++ b/src/test/resources/com/powsybl/config/test/config.yml @@ -0,0 +1,9 @@ +dynawo: + # home dir references to directory inside the container java-dynawo + homeDir: /dynaflow-launcher + debug: true + +dynawo-algorithms: + # home dir references to directory inside the container java-dynawo + homeDir: /dynaflow-launcher + debug: true diff --git a/src/test/resources/com/powsybl/config/test/filelist.txt b/src/test/resources/com/powsybl/config/test/filelist.txt new file mode 100644 index 0000000..e9abc7f --- /dev/null +++ b/src/test/resources/com/powsybl/config/test/filelist.txt @@ -0,0 +1 @@ +config.yml \ No newline at end of file diff --git a/src/test/resources/data/dynamicModels.json b/src/test/resources/data/dynamicModels.json new file mode 100644 index 0000000..71605fc --- /dev/null +++ b/src/test/resources/data/dynamicModels.json @@ -0,0 +1,37 @@ +{ + "models":[ + { + "model":"LoadAlphaBeta", + "group": "_DM", + "groupType": "SUFFIX", + "properties":[ + { + "name":"staticId", + "value":"LOAD", + "type":"STRING" + } + ] + }, + { + "model": "TapChangerBlockingAutomationSystem", + "group": "tcb_par", + "properties":[ + { + "name": "dynamicModelId", + "value": "TCB1", + "type": "STRING" + }, + { + "name": "transformers", + "values": ["NGEN_NHV1", "NHV2_NLOAD"], + "type": "STRING" + }, + { + "name": "uMeasurements", + "arrays": [["OldNGen", "NGEN"], ["NHV1", "NHV2"]], + "type": "STRING" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/data/ieee14/IEEE14.iidm b/src/test/resources/data/ieee14/IEEE14.iidm new file mode 100644 index 0000000..ef4c3fd --- /dev/null +++ b/src/test/resources/data/ieee14/IEEE14.iidm @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/data/ieee14/input/dynamicModel.dmp b/src/test/resources/data/ieee14/input/dynamicModel.dmp new file mode 100644 index 0000000..73a7557 --- /dev/null +++ b/src/test/resources/data/ieee14/input/dynamicModel.dmp @@ -0,0 +1 @@ +{"models":[{"model":"GeneratorSynchronousThreeWindingsProportionalRegulations","group":"IEEE14","groupType":"PREFIX","properties":[{"name":"staticId","value":"_GEN____6_SM","type":"STRING"}]},{"model":"GeneratorSynchronousThreeWindingsProportionalRegulations","group":"IEEE14","groupType":"PREFIX","properties":[{"name":"staticId","value":"_GEN____8_SM","type":"STRING"}]},{"model":"GeneratorSynchronousFourWindingsProportionalRegulations","group":"IEEE14","groupType":"PREFIX","properties":[{"name":"staticId","value":"_GEN____1_SM","type":"STRING"}]},{"model":"GeneratorSynchronousFourWindingsProportionalRegulations","group":"IEEE14","groupType":"PREFIX","properties":[{"name":"staticId","value":"_GEN____2_SM","type":"STRING"}]},{"model":"GeneratorSynchronousFourWindingsProportionalRegulations","group":"IEEE14","groupType":"PREFIX","properties":[{"name":"staticId","value":"_GEN____3_SM","type":"STRING"}]},{"model":"LoadAlphaBeta","group":"LAB","groupType":"FIXED","properties":[{"name":"staticId","value":"_LOAD__10_EC","type":"STRING"}]},{"model":"LoadAlphaBeta","group":"LAB","groupType":"FIXED","properties":[{"name":"staticId","value":"_LOAD__11_EC","type":"STRING"}]},{"model":"LoadAlphaBeta","group":"LAB","groupType":"FIXED","properties":[{"name":"staticId","value":"_LOAD__12_EC","type":"STRING"}]},{"model":"LoadAlphaBeta","group":"LAB","groupType":"FIXED","properties":[{"name":"staticId","value":"_LOAD__13_EC","type":"STRING"}]},{"model":"LoadAlphaBeta","group":"LAB","groupType":"FIXED","properties":[{"name":"staticId","value":"_LOAD__14_EC","type":"STRING"}]},{"model":"LoadAlphaBeta","group":"LAB","groupType":"FIXED","properties":[{"name":"staticId","value":"_LOAD___2_EC","type":"STRING"}]},{"model":"LoadAlphaBeta","group":"LAB","groupType":"FIXED","properties":[{"name":"staticId","value":"_LOAD___3_EC","type":"STRING"}]},{"model":"LoadAlphaBeta","group":"LAB","groupType":"FIXED","properties":[{"name":"staticId","value":"_LOAD___9_EC","type":"STRING"}]},{"model":"LoadAlphaBeta","group":"LAB","groupType":"FIXED","properties":[{"name":"staticId","value":"_LOAD___4_EC","type":"STRING"}]},{"model":"LoadAlphaBeta","group":"LAB","groupType":"FIXED","properties":[{"name":"staticId","value":"_LOAD___6_EC","type":"STRING"}]},{"model":"LoadAlphaBeta","group":"LAB","groupType":"FIXED","properties":[{"name":"staticId","value":"_LOAD___5_EC","type":"STRING"}]},{"model":"StaticVarCompensator","group":"SVarCT","groupType":"FIXED","properties":[{"name":"staticId","value":"SVC2","type":"STRING"}]},{"model":"OverloadManagementSystem","group":"CLA_2_4","groupType":"FIXED","properties":[{"name":"dynamicModelId","value":"CLA_1","type":"STRING"},{"name":"iMeasurement","value":"_BUS____2-BUS____4-1_AC","type":"STRING"},{"name":"iMeasurementSide","value":"TWO","type":"TWO_SIDES"},{"name":"controlledBranch","value":"_BUS____2-BUS____4-1_AC","type":"STRING"}]},{"model":"OverloadManagementSystem","group":"CLA_2_5","groupType":"FIXED","properties":[{"name":"dynamicModelId","value":"CLA_2","type":"STRING"},{"name":"iMeasurement","value":"_BUS____2-BUS____5-1_AC","type":"STRING"},{"name":"iMeasurementSide","value":"TWO","type":"TWO_SIDES"},{"name":"controlledBranch","value":"_BUS____2-BUS____5-1_AC","type":"STRING"}]},{"model":"TapChangerBlockingAutomaton","group":"TCB_2_4","groupType":"FIXED","properties":[{"name":"dynamicModelId","value":"TCB_1","type":"STRING"},{"name":"uMeasurements","values":["_BUS___11_TN","_BUS___12_TN"],"type":"STRING"},{"name":"transformers","values":["_LOAD__11_EC","_LOAD__12_EC"],"type":"STRING"}]},{"model":"TapChangerBlockingAutomaton","group":"TCB_2_5","groupType":"FIXED","properties":[{"name":"dynamicModelId","value":"TCB_2","type":"STRING"},{"name":"uMeasurements","value":"_BUS____4_TN","type":"STRING"},{"name":"transformers","value":"_BUS____4-BUS____9-1_PT","type":"STRING"}]}]} \ No newline at end of file diff --git a/src/test/resources/data/ieee14/input/dynamicSimulationParameters.dmp b/src/test/resources/data/ieee14/input/dynamicSimulationParameters.dmp new file mode 100644 index 0000000..d6a5299 --- /dev/null +++ b/src/test/resources/data/ieee14/input/dynamicSimulationParameters.dmp @@ -0,0 +1 @@ +{"version":"1.1","startTime":0.0,"stopTime":50.0,"extensions":{"DynawoSimulationParameters":{"networkParameters":{"id":"NETWORK","parameters":{"capacitor_no_reclosing_delay":{"name":"capacitor_no_reclosing_delay","type":"DOUBLE","value":"300.0"},"dangling_line_currentLimit_maxTimeOperation":{"name":"dangling_line_currentLimit_maxTimeOperation","type":"DOUBLE","value":"240.0"},"line_currentLimit_maxTimeOperation":{"name":"line_currentLimit_maxTimeOperation","type":"DOUBLE","value":"240.0"},"load_Tp":{"name":"load_Tp","type":"DOUBLE","value":"90.0"},"load_Tq":{"name":"load_Tq","type":"DOUBLE","value":"90.0"},"load_alpha":{"name":"load_alpha","type":"DOUBLE","value":"1.0"},"load_alphaLong":{"name":"load_alphaLong","type":"DOUBLE","value":"0.0"},"load_beta":{"name":"load_beta","type":"DOUBLE","value":"2.0"},"load_betaLong":{"name":"load_betaLong","type":"DOUBLE","value":"0.0"},"load_isControllable":{"name":"load_isControllable","type":"BOOL","value":"false"},"load_isRestorative":{"name":"load_isRestorative","type":"BOOL","value":"false"},"load_zPMax":{"name":"load_zPMax","type":"DOUBLE","value":"100.0"},"load_zQMax":{"name":"load_zQMax","type":"DOUBLE","value":"100.0"},"reactance_no_reclosing_delay":{"name":"reactance_no_reclosing_delay","type":"DOUBLE","value":"0.0"},"transformer_currentLimit_maxTimeOperation":{"name":"transformer_currentLimit_maxTimeOperation","type":"DOUBLE","value":"240.0"},"transformer_t1st_HT":{"name":"transformer_t1st_HT","type":"DOUBLE","value":"60.0"},"transformer_t1st_THT":{"name":"transformer_t1st_THT","type":"DOUBLE","value":"30.0"},"transformer_tNext_HT":{"name":"transformer_tNext_HT","type":"DOUBLE","value":"10.0"},"transformer_tNext_THT":{"name":"transformer_tNext_THT","type":"DOUBLE","value":"10.0"},"transformer_tolV":{"name":"transformer_tolV","type":"DOUBLE","value":"0.015"}},"references":[]},"solverParameters":{"id":"SIM","parameters":{"hMin":{"name":"hMin","type":"DOUBLE","value":"0.001"},"hMax":{"name":"hMax","type":"DOUBLE","value":"1.0"},"kReduceStep":{"name":"kReduceStep","type":"DOUBLE","value":"0.5"},"maxNewtonTry":{"name":"maxNewtonTry","type":"INT","value":"10"},"linearSolverName":{"name":"linearSolverName","type":"STRING","value":"KLU"},"fnormtol":{"name":"fnormtol","type":"DOUBLE","value":"0.001"},"initialaddtol":{"name":"initialaddtol","type":"DOUBLE","value":"1.0"},"scsteptol":{"name":"scsteptol","type":"DOUBLE","value":"0.001"},"mxnewtstep":{"name":"mxnewtstep","type":"DOUBLE","value":"10000.0"},"msbset":{"name":"msbset","type":"INT","value":"0"},"mxiter":{"name":"mxiter","type":"INT","value":"15"},"printfl":{"name":"printfl","type":"INT","value":"0"},"optimizeAlgebraicResidualsEvaluations":{"name":"optimizeAlgebraicResidualsEvaluations","type":"BOOL","value":"true"},"skipNRIfInitialGuessOK":{"name":"skipNRIfInitialGuessOK","type":"BOOL","value":"true"},"enableSilentZ":{"name":"enableSilentZ","type":"BOOL","value":"true"},"optimizeReinitAlgebraicResidualsEvaluations":{"name":"optimizeReinitAlgebraicResidualsEvaluations","type":"BOOL","value":"true"},"minimumModeChangeTypeForAlgebraicRestoration":{"name":"minimumModeChangeTypeForAlgebraicRestoration","type":"STRING","value":"ALGEBRAIC_J_UPDATE"},"minimumModeChangeTypeForAlgebraicRestorationInit":{"name":"minimumModeChangeTypeForAlgebraicRestorationInit","type":"STRING","value":"ALGEBRAIC_J_UPDATE"},"fnormtolAlg":{"name":"fnormtolAlg","type":"DOUBLE","value":"0.001"},"initialaddtolAlg":{"name":"initialaddtolAlg","type":"DOUBLE","value":"1.0"},"scsteptolAlg":{"name":"scsteptolAlg","type":"DOUBLE","value":"0.001"},"mxnewtstepAlg":{"name":"mxnewtstepAlg","type":"DOUBLE","value":"10000.0"},"msbsetAlg":{"name":"msbsetAlg","type":"INT","value":"5"},"mxiterAlg":{"name":"mxiterAlg","type":"INT","value":"30"},"printflAlg":{"name":"printflAlg","type":"INT","value":"0"},"fnormtolAlgJ":{"name":"fnormtolAlgJ","type":"DOUBLE","value":"0.001"},"initialaddtolAlgJ":{"name":"initialaddtolAlgJ","type":"DOUBLE","value":"1.0"},"scsteptolAlgJ":{"name":"scsteptolAlgJ","type":"DOUBLE","value":"0.001"},"mxnewtstepAlgJ":{"name":"mxnewtstepAlgJ","type":"DOUBLE","value":"10000.0"},"msbsetAlgJ":{"name":"msbsetAlgJ","type":"INT","value":"1"},"mxiterAlgJ":{"name":"mxiterAlgJ","type":"INT","value":"50"},"printflAlgJ":{"name":"printflAlgJ","type":"INT","value":"0"},"fnormtolAlgInit":{"name":"fnormtolAlgInit","type":"DOUBLE","value":"0.001"},"initialaddtolAlgInit":{"name":"initialaddtolAlgInit","type":"DOUBLE","value":"1.0"},"scsteptolAlgInit":{"name":"scsteptolAlgInit","type":"DOUBLE","value":"0.001"},"mxnewtstepAlgInit":{"name":"mxnewtstepAlgInit","type":"DOUBLE","value":"10000.0"},"msbsetAlgInit":{"name":"msbsetAlgInit","type":"INT","value":"1"},"mxiterAlgInit":{"name":"mxiterAlgInit","type":"INT","value":"50"},"printflAlgInit":{"name":"printflAlgInit","type":"INT","value":"0"},"maximumNumberSlowStepIncrease":{"name":"maximumNumberSlowStepIncrease","type":"INT","value":"40"},"minimalAcceptableStep":{"name":"minimalAcceptableStep","type":"DOUBLE","value":"0.001"}},"references":[]},"solverType":"SIM","mergeLoads":false,"modelSimplifiers":[],"dumpFileParameters":{"exportDumpFile":false,"useDumpFile":false,"dumpFileFolder":null,"dumpFile":null},"precision":1.0E-6,"timelineExportMode":"XML","logLevelFilter":"INFO","specificLogs":["NETWORK"],"criteriaFilePath":null,"additionalModelsPath":null,"modelsParameters":[{"id":"LAB","parameters":{"load_alpha":{"name":"load_alpha","type":"DOUBLE","value":"1"},"load_beta":{"name":"load_beta","type":"DOUBLE","value":"2"}},"references":[{"name":"load_P0Pu","type":"DOUBLE","origData":"IIDM","origName":"p_pu","componentId":null},{"name":"load_Q0Pu","type":"DOUBLE","origData":"IIDM","origName":"q_pu","componentId":null},{"name":"load_U0Pu","type":"DOUBLE","origData":"IIDM","origName":"v_pu","componentId":null},{"name":"load_UPhase0","type":"DOUBLE","origData":"IIDM","origName":"angle_pu","componentId":null}]},{"id":"IEEE14_GEN____6_SM","parameters":{"generator_UNom":{"name":"generator_UNom","type":"DOUBLE","value":"15"},"generator_SNom":{"name":"generator_SNom","type":"DOUBLE","value":"80"},"generator_PNomTurb":{"name":"generator_PNomTurb","type":"DOUBLE","value":"74.4"},"generator_PNomAlt":{"name":"generator_PNomAlt","type":"DOUBLE","value":"74.4"},"generator_ExcitationPu":{"name":"generator_ExcitationPu","type":"INT","value":"1"},"generator_MdPuEfd":{"name":"generator_MdPuEfd","type":"DOUBLE","value":"0"},"generator_H":{"name":"generator_H","type":"DOUBLE","value":"4.975"},"generator_DPu":{"name":"generator_DPu","type":"DOUBLE","value":"0"},"generator_SnTfo":{"name":"generator_SnTfo","type":"DOUBLE","value":"80"},"generator_UNomHV":{"name":"generator_UNomHV","type":"DOUBLE","value":"15"},"generator_UNomLV":{"name":"generator_UNomLV","type":"DOUBLE","value":"15"},"generator_UBaseHV":{"name":"generator_UBaseHV","type":"DOUBLE","value":"15"},"generator_UBaseLV":{"name":"generator_UBaseLV","type":"DOUBLE","value":"15"},"generator_RTfPu":{"name":"generator_RTfPu","type":"DOUBLE","value":"0.0"},"generator_XTfPu":{"name":"generator_XTfPu","type":"DOUBLE","value":"0.0"},"generator_RaPu":{"name":"generator_RaPu","type":"DOUBLE","value":"0.004"},"generator_XlPu":{"name":"generator_XlPu","type":"DOUBLE","value":"0.102"},"generator_XdPu":{"name":"generator_XdPu","type":"DOUBLE","value":"0.75"},"generator_XpdPu":{"name":"generator_XpdPu","type":"DOUBLE","value":"0.225"},"generator_XppdPu":{"name":"generator_XppdPu","type":"DOUBLE","value":"0.154"},"generator_Tpd0":{"name":"generator_Tpd0","type":"DOUBLE","value":"3"},"generator_Tppd0":{"name":"generator_Tppd0","type":"DOUBLE","value":"0.04"},"generator_XqPu":{"name":"generator_XqPu","type":"DOUBLE","value":"0.45"},"generator_XppqPu":{"name":"generator_XppqPu","type":"DOUBLE","value":"0.2"},"generator_Tppq0":{"name":"generator_Tppq0","type":"DOUBLE","value":"0.04"},"generator_md":{"name":"generator_md","type":"DOUBLE","value":"0.16"},"generator_mq":{"name":"generator_mq","type":"DOUBLE","value":"0.16"},"generator_nd":{"name":"generator_nd","type":"DOUBLE","value":"5.7"},"generator_nq":{"name":"generator_nq","type":"DOUBLE","value":"5.7"},"governor_KGover":{"name":"governor_KGover","type":"DOUBLE","value":"5"},"governor_PMin":{"name":"governor_PMin","type":"DOUBLE","value":"0"},"governor_PMax":{"name":"governor_PMax","type":"DOUBLE","value":"74.4"},"governor_PNom":{"name":"governor_PNom","type":"DOUBLE","value":"74.4"},"voltageRegulator_LagEfdMin":{"name":"voltageRegulator_LagEfdMin","type":"DOUBLE","value":"0"},"voltageRegulator_LagEfdMax":{"name":"voltageRegulator_LagEfdMax","type":"DOUBLE","value":"0"},"voltageRegulator_EfdMinPu":{"name":"voltageRegulator_EfdMinPu","type":"DOUBLE","value":"-5"},"voltageRegulator_EfdMaxPu":{"name":"voltageRegulator_EfdMaxPu","type":"DOUBLE","value":"5"},"voltageRegulator_UsRefMinPu":{"name":"voltageRegulator_UsRefMinPu","type":"DOUBLE","value":"0.8"},"voltageRegulator_UsRefMaxPu":{"name":"voltageRegulator_UsRefMaxPu","type":"DOUBLE","value":"1.2"},"voltageRegulator_Gain":{"name":"voltageRegulator_Gain","type":"DOUBLE","value":"20"},"URef_ValueIn":{"name":"URef_ValueIn","type":"DOUBLE","value":"0"},"Pm_ValueIn":{"name":"Pm_ValueIn","type":"DOUBLE","value":"0"},"generator_UseApproximation":{"name":"generator_UseApproximation","type":"BOOL","value":"true"}},"references":[{"name":"generator_P0Pu","type":"DOUBLE","origData":"IIDM","origName":"p_pu","componentId":null},{"name":"generator_Q0Pu","type":"DOUBLE","origData":"IIDM","origName":"q_pu","componentId":null},{"name":"generator_UPhase0","type":"DOUBLE","origData":"IIDM","origName":"angle_pu","componentId":null},{"name":"generator_U0Pu","type":"DOUBLE","origData":"IIDM","origName":"v_pu","componentId":null}]},{"id":"IEEE14_GEN____8_SM","parameters":{"generator_UNom":{"name":"generator_UNom","type":"DOUBLE","value":"18"},"generator_SNom":{"name":"generator_SNom","type":"DOUBLE","value":"250"},"generator_PNomTurb":{"name":"generator_PNomTurb","type":"DOUBLE","value":"228"},"generator_PNomAlt":{"name":"generator_PNomAlt","type":"DOUBLE","value":"228"},"generator_ExcitationPu":{"name":"generator_ExcitationPu","type":"INT","value":"1"},"generator_MdPuEfd":{"name":"generator_MdPuEfd","type":"DOUBLE","value":"0"},"generator_H":{"name":"generator_H","type":"DOUBLE","value":"2.748"},"generator_DPu":{"name":"generator_DPu","type":"DOUBLE","value":"0"},"generator_SnTfo":{"name":"generator_SnTfo","type":"DOUBLE","value":"250"},"generator_UNomHV":{"name":"generator_UNomHV","type":"DOUBLE","value":"13.8"},"generator_UNomLV":{"name":"generator_UNomLV","type":"DOUBLE","value":"18"},"generator_UBaseHV":{"name":"generator_UBaseHV","type":"DOUBLE","value":"13.8"},"generator_UBaseLV":{"name":"generator_UBaseLV","type":"DOUBLE","value":"18"},"generator_RTfPu":{"name":"generator_RTfPu","type":"DOUBLE","value":"0.0"},"generator_XTfPu":{"name":"generator_XTfPu","type":"DOUBLE","value":"0.1"},"generator_RaPu":{"name":"generator_RaPu","type":"DOUBLE","value":"0.004"},"generator_XlPu":{"name":"generator_XlPu","type":"DOUBLE","value":"0.11"},"generator_XdPu":{"name":"generator_XdPu","type":"DOUBLE","value":"1.53"},"generator_XpdPu":{"name":"generator_XpdPu","type":"DOUBLE","value":"0.31"},"generator_XppdPu":{"name":"generator_XppdPu","type":"DOUBLE","value":"0.275"},"generator_Tpd0":{"name":"generator_Tpd0","type":"DOUBLE","value":"8.4"},"generator_Tppd0":{"name":"generator_Tppd0","type":"DOUBLE","value":"0.096"},"generator_XqPu":{"name":"generator_XqPu","type":"DOUBLE","value":"0.99"},"generator_XppqPu":{"name":"generator_XppqPu","type":"DOUBLE","value":"0.58"},"generator_Tppq0":{"name":"generator_Tppq0","type":"DOUBLE","value":"0.56"},"generator_md":{"name":"generator_md","type":"DOUBLE","value":"0"},"generator_mq":{"name":"generator_mq","type":"DOUBLE","value":"0"},"generator_nd":{"name":"generator_nd","type":"DOUBLE","value":"0"},"generator_nq":{"name":"generator_nq","type":"DOUBLE","value":"0"},"governor_KGover":{"name":"governor_KGover","type":"DOUBLE","value":"5"},"governor_PMin":{"name":"governor_PMin","type":"DOUBLE","value":"0"},"governor_PMax":{"name":"governor_PMax","type":"DOUBLE","value":"228"},"governor_PNom":{"name":"governor_PNom","type":"DOUBLE","value":"228"},"voltageRegulator_LagEfdMin":{"name":"voltageRegulator_LagEfdMin","type":"DOUBLE","value":"0"},"voltageRegulator_LagEfdMax":{"name":"voltageRegulator_LagEfdMax","type":"DOUBLE","value":"0"},"voltageRegulator_EfdMinPu":{"name":"voltageRegulator_EfdMinPu","type":"DOUBLE","value":"-5"},"voltageRegulator_EfdMaxPu":{"name":"voltageRegulator_EfdMaxPu","type":"DOUBLE","value":"5"},"voltageRegulator_UsRefMinPu":{"name":"voltageRegulator_UsRefMinPu","type":"DOUBLE","value":"0.8"},"voltageRegulator_UsRefMaxPu":{"name":"voltageRegulator_UsRefMaxPu","type":"DOUBLE","value":"1.2"},"voltageRegulator_Gain":{"name":"voltageRegulator_Gain","type":"DOUBLE","value":"20"},"URef_ValueIn":{"name":"URef_ValueIn","type":"DOUBLE","value":"0"},"Pm_ValueIn":{"name":"Pm_ValueIn","type":"DOUBLE","value":"0"},"generator_UseApproximation":{"name":"generator_UseApproximation","type":"BOOL","value":"true"}},"references":[{"name":"generator_P0Pu","type":"DOUBLE","origData":"IIDM","origName":"p_pu","componentId":null},{"name":"generator_Q0Pu","type":"DOUBLE","origData":"IIDM","origName":"q_pu","componentId":null},{"name":"generator_UPhase0","type":"DOUBLE","origData":"IIDM","origName":"angle_pu","componentId":null},{"name":"generator_U0Pu","type":"DOUBLE","origData":"IIDM","origName":"v_pu","componentId":null}]},{"id":"IEEE14_GEN____1_SM","parameters":{"generator_UNom":{"name":"generator_UNom","type":"DOUBLE","value":"24"},"generator_SNom":{"name":"generator_SNom","type":"DOUBLE","value":"1211"},"generator_PNomTurb":{"name":"generator_PNomTurb","type":"DOUBLE","value":"1090"},"generator_PNomAlt":{"name":"generator_PNomAlt","type":"DOUBLE","value":"1090"},"generator_ExcitationPu":{"name":"generator_ExcitationPu","type":"INT","value":"1"},"generator_MdPuEfd":{"name":"generator_MdPuEfd","type":"DOUBLE","value":"0"},"generator_H":{"name":"generator_H","type":"DOUBLE","value":"5.4"},"generator_DPu":{"name":"generator_DPu","type":"DOUBLE","value":"0"},"generator_SnTfo":{"name":"generator_SnTfo","type":"DOUBLE","value":"1211"},"generator_UNomHV":{"name":"generator_UNomHV","type":"DOUBLE","value":"69"},"generator_UNomLV":{"name":"generator_UNomLV","type":"DOUBLE","value":"24"},"generator_UBaseHV":{"name":"generator_UBaseHV","type":"DOUBLE","value":"69"},"generator_UBaseLV":{"name":"generator_UBaseLV","type":"DOUBLE","value":"24"},"generator_RTfPu":{"name":"generator_RTfPu","type":"DOUBLE","value":"0.0"},"generator_XTfPu":{"name":"generator_XTfPu","type":"DOUBLE","value":"0.1"},"generator_RaPu":{"name":"generator_RaPu","type":"DOUBLE","value":"0.002796"},"generator_XlPu":{"name":"generator_XlPu","type":"DOUBLE","value":"0.202"},"generator_XdPu":{"name":"generator_XdPu","type":"DOUBLE","value":"2.22"},"generator_XpdPu":{"name":"generator_XpdPu","type":"DOUBLE","value":"0.384"},"generator_XppdPu":{"name":"generator_XppdPu","type":"DOUBLE","value":"0.264"},"generator_Tpd0":{"name":"generator_Tpd0","type":"DOUBLE","value":"8.094"},"generator_Tppd0":{"name":"generator_Tppd0","type":"DOUBLE","value":"0.08"},"generator_XqPu":{"name":"generator_XqPu","type":"DOUBLE","value":"2.22"},"generator_XppqPu":{"name":"generator_XppqPu","type":"DOUBLE","value":"0.262"},"generator_Tppq0":{"name":"generator_Tppq0","type":"DOUBLE","value":"0.084"},"generator_md":{"name":"generator_md","type":"DOUBLE","value":"0.215"},"generator_mq":{"name":"generator_mq","type":"DOUBLE","value":"0.215"},"generator_nd":{"name":"generator_nd","type":"DOUBLE","value":"6.995"},"generator_nq":{"name":"generator_nq","type":"DOUBLE","value":"6.995"},"generator_XpqPu":{"name":"generator_XpqPu","type":"DOUBLE","value":"0.393"},"generator_Tpq0":{"name":"generator_Tpq0","type":"DOUBLE","value":"1.572"},"governor_KGover":{"name":"governor_KGover","type":"DOUBLE","value":"5"},"governor_PMin":{"name":"governor_PMin","type":"DOUBLE","value":"0"},"governor_PMax":{"name":"governor_PMax","type":"DOUBLE","value":"1090"},"governor_PNom":{"name":"governor_PNom","type":"DOUBLE","value":"1090"},"voltageRegulator_LagEfdMin":{"name":"voltageRegulator_LagEfdMin","type":"DOUBLE","value":"0"},"voltageRegulator_LagEfdMax":{"name":"voltageRegulator_LagEfdMax","type":"DOUBLE","value":"0"},"voltageRegulator_EfdMinPu":{"name":"voltageRegulator_EfdMinPu","type":"DOUBLE","value":"-5"},"voltageRegulator_EfdMaxPu":{"name":"voltageRegulator_EfdMaxPu","type":"DOUBLE","value":"5"},"voltageRegulator_UsRefMinPu":{"name":"voltageRegulator_UsRefMinPu","type":"DOUBLE","value":"0.8"},"voltageRegulator_UsRefMaxPu":{"name":"voltageRegulator_UsRefMaxPu","type":"DOUBLE","value":"1.2"},"voltageRegulator_Gain":{"name":"voltageRegulator_Gain","type":"DOUBLE","value":"20"},"URef_ValueIn":{"name":"URef_ValueIn","type":"DOUBLE","value":"0"},"Pm_ValueIn":{"name":"Pm_ValueIn","type":"DOUBLE","value":"0"},"generator_UseApproximation":{"name":"generator_UseApproximation","type":"BOOL","value":"true"}},"references":[{"name":"generator_P0Pu","type":"DOUBLE","origData":"IIDM","origName":"p_pu","componentId":null},{"name":"generator_Q0Pu","type":"DOUBLE","origData":"IIDM","origName":"q_pu","componentId":null},{"name":"generator_UPhase0","type":"DOUBLE","origData":"IIDM","origName":"angle_pu","componentId":null},{"name":"generator_U0Pu","type":"DOUBLE","origData":"IIDM","origName":"v_pu","componentId":null}]},{"id":"IEEE14_GEN____2_SM","parameters":{"generator_UNom":{"name":"generator_UNom","type":"DOUBLE","value":"24"},"generator_SNom":{"name":"generator_SNom","type":"DOUBLE","value":"1120"},"generator_PNomTurb":{"name":"generator_PNomTurb","type":"DOUBLE","value":"1008"},"generator_PNomAlt":{"name":"generator_PNomAlt","type":"DOUBLE","value":"1008"},"generator_ExcitationPu":{"name":"generator_ExcitationPu","type":"INT","value":"1"},"generator_MdPuEfd":{"name":"generator_MdPuEfd","type":"DOUBLE","value":"0"},"generator_H":{"name":"generator_H","type":"DOUBLE","value":"6.3"},"generator_DPu":{"name":"generator_DPu","type":"DOUBLE","value":"0"},"generator_SnTfo":{"name":"generator_SnTfo","type":"DOUBLE","value":"1120"},"generator_UNomHV":{"name":"generator_UNomHV","type":"DOUBLE","value":"69"},"generator_UNomLV":{"name":"generator_UNomLV","type":"DOUBLE","value":"24"},"generator_UBaseHV":{"name":"generator_UBaseHV","type":"DOUBLE","value":"69"},"generator_UBaseLV":{"name":"generator_UBaseLV","type":"DOUBLE","value":"24"},"generator_RTfPu":{"name":"generator_RTfPu","type":"DOUBLE","value":"0.0"},"generator_XTfPu":{"name":"generator_XTfPu","type":"DOUBLE","value":"0.1"},"generator_RaPu":{"name":"generator_RaPu","type":"DOUBLE","value":"0.00357"},"generator_XlPu":{"name":"generator_XlPu","type":"DOUBLE","value":"0.219"},"generator_XdPu":{"name":"generator_XdPu","type":"DOUBLE","value":"2.57"},"generator_XpdPu":{"name":"generator_XpdPu","type":"DOUBLE","value":"0.407"},"generator_XppdPu":{"name":"generator_XppdPu","type":"DOUBLE","value":"0.3"},"generator_Tpd0":{"name":"generator_Tpd0","type":"DOUBLE","value":"9.651"},"generator_Tppd0":{"name":"generator_Tppd0","type":"DOUBLE","value":"0.058"},"generator_XqPu":{"name":"generator_XqPu","type":"DOUBLE","value":"2.57"},"generator_XppqPu":{"name":"generator_XppqPu","type":"DOUBLE","value":"0.301"},"generator_Tppq0":{"name":"generator_Tppq0","type":"DOUBLE","value":"0.06"},"generator_md":{"name":"generator_md","type":"DOUBLE","value":"0.084"},"generator_mq":{"name":"generator_mq","type":"DOUBLE","value":"0.084"},"generator_nd":{"name":"generator_nd","type":"DOUBLE","value":"5.57"},"generator_nq":{"name":"generator_nq","type":"DOUBLE","value":"5.57"},"generator_XpqPu":{"name":"generator_XpqPu","type":"DOUBLE","value":"0.454"},"generator_Tpq0":{"name":"generator_Tpq0","type":"DOUBLE","value":"1.009"},"governor_KGover":{"name":"governor_KGover","type":"DOUBLE","value":"5"},"governor_PMin":{"name":"governor_PMin","type":"DOUBLE","value":"0"},"governor_PMax":{"name":"governor_PMax","type":"DOUBLE","value":"1008"},"governor_PNom":{"name":"governor_PNom","type":"DOUBLE","value":"1008"},"voltageRegulator_LagEfdMin":{"name":"voltageRegulator_LagEfdMin","type":"DOUBLE","value":"0"},"voltageRegulator_LagEfdMax":{"name":"voltageRegulator_LagEfdMax","type":"DOUBLE","value":"0"},"voltageRegulator_EfdMinPu":{"name":"voltageRegulator_EfdMinPu","type":"DOUBLE","value":"-5"},"voltageRegulator_EfdMaxPu":{"name":"voltageRegulator_EfdMaxPu","type":"DOUBLE","value":"5"},"voltageRegulator_UsRefMinPu":{"name":"voltageRegulator_UsRefMinPu","type":"DOUBLE","value":"0.8"},"voltageRegulator_UsRefMaxPu":{"name":"voltageRegulator_UsRefMaxPu","type":"DOUBLE","value":"1.2"},"voltageRegulator_Gain":{"name":"voltageRegulator_Gain","type":"DOUBLE","value":"20"},"URef_ValueIn":{"name":"URef_ValueIn","type":"DOUBLE","value":"0"},"Pm_ValueIn":{"name":"Pm_ValueIn","type":"DOUBLE","value":"0"},"generator_UseApproximation":{"name":"generator_UseApproximation","type":"BOOL","value":"true"}},"references":[{"name":"generator_P0Pu","type":"DOUBLE","origData":"IIDM","origName":"p_pu","componentId":null},{"name":"generator_Q0Pu","type":"DOUBLE","origData":"IIDM","origName":"q_pu","componentId":null},{"name":"generator_UPhase0","type":"DOUBLE","origData":"IIDM","origName":"angle_pu","componentId":null},{"name":"generator_U0Pu","type":"DOUBLE","origData":"IIDM","origName":"v_pu","componentId":null}]},{"id":"IEEE14_GEN____3_SM","parameters":{"generator_UNom":{"name":"generator_UNom","type":"DOUBLE","value":"20"},"generator_SNom":{"name":"generator_SNom","type":"DOUBLE","value":"1650"},"generator_PNomTurb":{"name":"generator_PNomTurb","type":"DOUBLE","value":"1485"},"generator_PNomAlt":{"name":"generator_PNomAlt","type":"DOUBLE","value":"1485"},"generator_ExcitationPu":{"name":"generator_ExcitationPu","type":"INT","value":"1"},"generator_MdPuEfd":{"name":"generator_MdPuEfd","type":"DOUBLE","value":"0"},"generator_H":{"name":"generator_H","type":"DOUBLE","value":"5.625"},"generator_DPu":{"name":"generator_DPu","type":"DOUBLE","value":"0"},"generator_SnTfo":{"name":"generator_SnTfo","type":"DOUBLE","value":"1650"},"generator_UNomHV":{"name":"generator_UNomHV","type":"DOUBLE","value":"69"},"generator_UNomLV":{"name":"generator_UNomLV","type":"DOUBLE","value":"20"},"generator_UBaseHV":{"name":"generator_UBaseHV","type":"DOUBLE","value":"69"},"generator_UBaseLV":{"name":"generator_UBaseLV","type":"DOUBLE","value":"20"},"generator_RTfPu":{"name":"generator_RTfPu","type":"DOUBLE","value":"0.0"},"generator_XTfPu":{"name":"generator_XTfPu","type":"DOUBLE","value":"0.1"},"generator_RaPu":{"name":"generator_RaPu","type":"DOUBLE","value":"0.00316"},"generator_XlPu":{"name":"generator_XlPu","type":"DOUBLE","value":"0.256"},"generator_XdPu":{"name":"generator_XdPu","type":"DOUBLE","value":"2.81"},"generator_XpdPu":{"name":"generator_XpdPu","type":"DOUBLE","value":"0.509"},"generator_XppdPu":{"name":"generator_XppdPu","type":"DOUBLE","value":"0.354"},"generator_Tpd0":{"name":"generator_Tpd0","type":"DOUBLE","value":"10.041"},"generator_Tppd0":{"name":"generator_Tppd0","type":"DOUBLE","value":"0.065"},"generator_XqPu":{"name":"generator_XqPu","type":"DOUBLE","value":"2.62"},"generator_XppqPu":{"name":"generator_XppqPu","type":"DOUBLE","value":"0.377"},"generator_Tppq0":{"name":"generator_Tppq0","type":"DOUBLE","value":"0.094"},"generator_md":{"name":"generator_md","type":"DOUBLE","value":"0.05"},"generator_mq":{"name":"generator_mq","type":"DOUBLE","value":"0.05"},"generator_nd":{"name":"generator_nd","type":"DOUBLE","value":"9.285"},"generator_nq":{"name":"generator_nq","type":"DOUBLE","value":"9.285"},"generator_XpqPu":{"name":"generator_XpqPu","type":"DOUBLE","value":"0.601"},"generator_Tpq0":{"name":"generator_Tpq0","type":"DOUBLE","value":"1.22"},"governor_KGover":{"name":"governor_KGover","type":"DOUBLE","value":"5"},"governor_PMin":{"name":"governor_PMin","type":"DOUBLE","value":"0"},"governor_PMax":{"name":"governor_PMax","type":"DOUBLE","value":"1485"},"governor_PNom":{"name":"governor_PNom","type":"DOUBLE","value":"1485"},"voltageRegulator_LagEfdMin":{"name":"voltageRegulator_LagEfdMin","type":"DOUBLE","value":"0"},"voltageRegulator_LagEfdMax":{"name":"voltageRegulator_LagEfdMax","type":"DOUBLE","value":"0"},"voltageRegulator_EfdMinPu":{"name":"voltageRegulator_EfdMinPu","type":"DOUBLE","value":"-5"},"voltageRegulator_EfdMaxPu":{"name":"voltageRegulator_EfdMaxPu","type":"DOUBLE","value":"5"},"voltageRegulator_UsRefMinPu":{"name":"voltageRegulator_UsRefMinPu","type":"DOUBLE","value":"0.8"},"voltageRegulator_UsRefMaxPu":{"name":"voltageRegulator_UsRefMaxPu","type":"DOUBLE","value":"1.2"},"voltageRegulator_Gain":{"name":"voltageRegulator_Gain","type":"DOUBLE","value":"20"},"URef_ValueIn":{"name":"URef_ValueIn","type":"DOUBLE","value":"0"},"Pm_ValueIn":{"name":"Pm_ValueIn","type":"DOUBLE","value":"0"},"generator_UseApproximation":{"name":"generator_UseApproximation","type":"BOOL","value":"true"}},"references":[{"name":"generator_P0Pu","type":"DOUBLE","origData":"IIDM","origName":"p_pu","componentId":null},{"name":"generator_Q0Pu","type":"DOUBLE","origData":"IIDM","origName":"q_pu","componentId":null},{"name":"generator_UPhase0","type":"DOUBLE","origData":"IIDM","origName":"angle_pu","componentId":null},{"name":"generator_U0Pu","type":"DOUBLE","origData":"IIDM","origName":"v_pu","componentId":null}]},{"id":"GPQ","parameters":{"generator_AlphaPuPNom":{"name":"generator_AlphaPuPNom","type":"DOUBLE","value":"25"}},"references":[{"name":"generator_PNom","type":"DOUBLE","origData":"IIDM","origName":"pMax","componentId":null},{"name":"generator_PMin","type":"DOUBLE","origData":"IIDM","origName":"pMin","componentId":null},{"name":"generator_PMax","type":"DOUBLE","origData":"IIDM","origName":"pMax","componentId":null},{"name":"generator_P0Pu","type":"DOUBLE","origData":"IIDM","origName":"p_pu","componentId":null},{"name":"generator_Q0Pu","type":"DOUBLE","origData":"IIDM","origName":"q_pu","componentId":null},{"name":"generator_U0Pu","type":"DOUBLE","origData":"IIDM","origName":"v_pu","componentId":null},{"name":"generator_UPhase0","type":"DOUBLE","origData":"IIDM","origName":"angle_pu","componentId":null}]},{"id":"SVarCT","parameters":{"SVarC_BMaxPu":{"name":"SVarC_BMaxPu","type":"DOUBLE","value":"1.0678"},"SVarC_BMinPu":{"name":"SVarC_BMinPu","type":"DOUBLE","value":"-1.0466"},"SVarC_BShuntPu":{"name":"SVarC_BShuntPu","type":"DOUBLE","value":"0"},"SVarC_IMaxPu":{"name":"SVarC_IMaxPu","type":"DOUBLE","value":"1"},"SVarC_IMinPu":{"name":"SVarC_IMinPu","type":"DOUBLE","value":"-1"},"SVarC_KCurrentLimiter":{"name":"SVarC_KCurrentLimiter","type":"DOUBLE","value":"8"},"SVarC_Kp":{"name":"SVarC_Kp","type":"DOUBLE","value":"1.75"},"SVarC_Lambda":{"name":"SVarC_Lambda","type":"DOUBLE","value":"0.01"},"SVarC_SNom":{"name":"SVarC_SNom","type":"DOUBLE","value":"225"},"SVarC_Ti":{"name":"SVarC_Ti","type":"DOUBLE","value":"0.003428"},"SVarC_UBlock":{"name":"SVarC_UBlock","type":"DOUBLE","value":"5"},"SVarC_UNom":{"name":"SVarC_UNom","type":"DOUBLE","value":"250"},"SVarC_URefDown":{"name":"SVarC_URefDown","type":"DOUBLE","value":"220"},"SVarC_URefUp":{"name":"SVarC_URefUp","type":"DOUBLE","value":"230"},"SVarC_UThresholdDown":{"name":"SVarC_UThresholdDown","type":"DOUBLE","value":"218"},"SVarC_UThresholdUp":{"name":"SVarC_UThresholdUp","type":"DOUBLE","value":"240"},"SVarC_UUnblockDown":{"name":"SVarC_UUnblockDown","type":"DOUBLE","value":"180"},"SVarC_UUnblockUp":{"name":"SVarC_UUnblockUp","type":"DOUBLE","value":"270"},"SVarC_tThresholdDown":{"name":"SVarC_tThresholdDown","type":"DOUBLE","value":"0"},"SVarC_tThresholdUp":{"name":"SVarC_tThresholdUp","type":"DOUBLE","value":"60"}},"references":[{"name":"SVarC_Mode0","type":"INT","origData":"IIDM","origName":"regulatingMode","componentId":null},{"name":"SVarC_P0Pu","type":"DOUBLE","origData":"IIDM","origName":"p_pu","componentId":null},{"name":"SVarC_Q0Pu","type":"DOUBLE","origData":"IIDM","origName":"q_pu","componentId":null},{"name":"SVarC_U0Pu","type":"DOUBLE","origData":"IIDM","origName":"v_pu","componentId":null},{"name":"SVarC_UPhase0","type":"DOUBLE","origData":"IIDM","origName":"angle_pu","componentId":null}]},{"id":"CLA_2_4","parameters":{"currentLimitAutomaton_OrderToEmit":{"name":"currentLimitAutomaton_OrderToEmit","type":"INT","value":"3"},"currentLimitAutomaton_Running":{"name":"currentLimitAutomaton_Running","type":"BOOL","value":"true"},"currentLimitAutomaton_IMax":{"name":"currentLimitAutomaton_IMax","type":"DOUBLE","value":"1000"},"currentLimitAutomaton_tLagBeforeActing":{"name":"currentLimitAutomaton_tLagBeforeActing","type":"DOUBLE","value":"10"}},"references":[]},{"id":"CLA_2_5","parameters":{"currentLimitAutomaton_OrderToEmit":{"name":"currentLimitAutomaton_OrderToEmit","type":"INT","value":"1"},"currentLimitAutomaton_Running":{"name":"currentLimitAutomaton_Running","type":"BOOL","value":"true"},"currentLimitAutomaton_IMax":{"name":"currentLimitAutomaton_IMax","type":"DOUBLE","value":"600"},"currentLimitAutomaton_tLagBeforeActing":{"name":"currentLimitAutomaton_tLagBeforeActing","type":"DOUBLE","value":"5"}},"references":[]},{"id":"TCB_2_4","parameters":{"tapChangerBlocking_UMin":{"name":"tapChangerBlocking_UMin","type":"DOUBLE","value":"14"},"tapChangerBlocking_tLagBeforeBlocked":{"name":"tapChangerBlocking_tLagBeforeBlocked","type":"DOUBLE","value":"104"},"tapChangerBlocking_tLagTransBlockedD":{"name":"tapChangerBlocking_tLagTransBlockedD","type":"DOUBLE","value":"1004"},"tapChangerBlocking_tLagTransBlockedT":{"name":"tapChangerBlocking_tLagTransBlockedT","type":"DOUBLE","value":"14"}},"references":[]},{"id":"TCB_2_5","parameters":{"tapChangerBlocking_UMin":{"name":"tapChangerBlocking_UMin","type":"DOUBLE","value":"15"},"tapChangerBlocking_tLagBeforeBlocked":{"name":"tapChangerBlocking_tLagBeforeBlocked","type":"DOUBLE","value":"105"},"tapChangerBlocking_tLagTransBlockedD":{"name":"tapChangerBlocking_tLagTransBlockedD","type":"DOUBLE","value":"1005"},"tapChangerBlocking_tLagTransBlockedT":{"name":"tapChangerBlocking_tLagTransBlockedT","type":"DOUBLE","value":"15"}},"references":[]}]}}} \ No newline at end of file