Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export sensitivity results as csv #60

Merged
merged 20 commits into from
Jan 29, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2024, 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.sensitivityanalysis.server;

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 Seddik Yengui <seddik.yengui at rte-france.com>
*/

@ControllerAdvice
public class RestResponseEntityExceptionHandler {
@ExceptionHandler(SensibilityAnalysisException.class)
protected ResponseEntity<Object> handleSensibilityAnalysisException(SensibilityAnalysisException exception) {
return switch (exception.getType()) {
case RESULT_NOT_FOUND -> ResponseEntity.status(HttpStatus.NOT_FOUND).body(exception.getMessage());
case FILE_EXPORT_ERROR -> ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(exception.getMessage());
case INVALID_EXPORT_PARAMS -> ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exception.getMessage());
default -> ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
thangqp marked this conversation as resolved.
Show resolved Hide resolved
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2024, 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.sensitivityanalysis.server;

import java.util.Objects;

/**
* @author Seddik Yengui <seddik.yengui at rte-france.com>
*/

public class SensibilityAnalysisException extends RuntimeException {
public enum Type {
RESULT_NOT_FOUND,
INVALID_EXPORT_PARAMS,
FILE_EXPORT_ERROR,
}

private final Type type;

public SensibilityAnalysisException(Type type) {
super(Objects.requireNonNull(type.name()));
this.type = type;
}

public SensibilityAnalysisException(Type type, String message) {
super(message);
this.type = type;
}

public Type getType() {
return type;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
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.gridsuite.sensitivityanalysis.server.dto.SensitivityAnalysisCsvFileInfos;
import org.gridsuite.sensitivityanalysis.server.dto.SensitivityAnalysisInputData;
import org.gridsuite.sensitivityanalysis.server.dto.SensitivityAnalysisStatus;
import org.gridsuite.sensitivityanalysis.server.dto.SensitivityResultFilterOptions;
Expand All @@ -26,6 +27,7 @@
import org.gridsuite.sensitivityanalysis.server.service.SensitivityAnalysisRunContext;
import org.gridsuite.sensitivityanalysis.server.service.SensitivityAnalysisService;
import org.gridsuite.sensitivityanalysis.server.service.SensitivityAnalysisWorkerService;
import org.springframework.http.HttpHeaders;
import org.gridsuite.sensitivityanalysis.server.service.nonevacuatedenergy.NonEvacuatedEnergyRunContext;
import org.gridsuite.sensitivityanalysis.server.service.nonevacuatedenergy.NonEvacuatedEnergyService;
import org.gridsuite.sensitivityanalysis.server.dto.SensitivityFactorsIdsByGroup;
Expand All @@ -40,6 +42,7 @@

import static org.gridsuite.sensitivityanalysis.server.service.NotificationService.HEADER_USER_ID;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM;
import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE;

/**
Expand Down Expand Up @@ -212,6 +215,20 @@ public ResponseEntity<String> getDefaultProvider() {
return ResponseEntity.ok().body(service.getDefaultProvider());
}

@PostMapping(value = "/results/{resultUuid}/csv")
@Operation(summary = "export sensitivity results as csv file")
@ApiResponses(@ApiResponse(responseCode = "200", description = "Sensitivity results successfully exported as csv file"))
public ResponseEntity<byte[]> exportSensitivityResultsAsCsv(@Parameter(description = "Result UUID") @PathVariable("resultUuid") UUID resultUuid,
@RequestBody SensitivityAnalysisCsvFileInfos sensitivityAnalysisCsvFileInfos) {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(APPLICATION_OCTET_STREAM);
httpHeaders.setContentDispositionFormData("attachment", "sensitivity_results.zip");
byte[] csv = service.exportSensitivityResultsAsCsv(resultUuid, sensitivityAnalysisCsvFileInfos);
return ResponseEntity.ok()
.headers(httpHeaders)
.body(csv);
}

@PostMapping(value = "/networks/{networkUuid}/non-evacuated-energy/run-and-save", produces = APPLICATION_JSON_VALUE, consumes = APPLICATION_JSON_VALUE)
@Operation(summary = "Run a non evacuated energy sensitivity analysis on a network and save results in the database")
@ApiResponses(@ApiResponse(responseCode = "200", description = "The non evacuated energy sensitivity analysis default provider has been found"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2024, 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.sensitivityanalysis.server.dto;

import com.powsybl.sensitivity.SensitivityFunctionType;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import org.gridsuite.sensitivityanalysis.server.dto.resultselector.ResultTab;

import java.util.List;

/**
* @author Seddik Yengui <seddik.yengui at rte-france.com>
*/

@SuperBuilder
@NoArgsConstructor
@Getter
@Setter
public class SensitivityAnalysisCsvFileInfos {
private SensitivityFunctionType sensitivityFunctionType;
private ResultTab resultTab;
private List<String> csvHeaders;
}
Original file line number Diff line number Diff line change
Expand Up @@ -301,15 +301,14 @@ private void complete(SensitivityRunQueryResult.SensitivityRunQueryResultBuilder
}

private String getSort(SortKey sortKey) {
switch (sortKey) {
case FUNCTION : return "factor.functionId";
case SENSITIVITY : return "value";
case REFERENCE : return "functionReference";
case VARIABLE : return "factor.variableId";
case CONTINGENCY : return "contingency.contingencyId";
case POST_REFERENCE : return "functionReferenceAfter";
case POST_SENSITIVITY : return "valueAfter";
default: return null;
}
return switch (sortKey) {
case FUNCTION -> "factor.functionId";
case SENSITIVITY -> "value";
case REFERENCE -> "functionReference";
case VARIABLE -> "factor.variableId";
case CONTINGENCY -> "contingency.contingencyId";
case POST_REFERENCE -> "functionReferenceAfter";
case POST_SENSITIVITY -> "valueAfter";
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import com.powsybl.sensitivity.SensitivityAnalysisProvider;
import com.univocity.parsers.csv.CsvWriter;
import com.univocity.parsers.csv.CsvWriterSettings;
import org.gridsuite.sensitivityanalysis.server.SensibilityAnalysisException;
import org.gridsuite.sensitivityanalysis.server.dto.SensitivityAnalysisCsvFileInfos;
import org.gridsuite.sensitivityanalysis.server.dto.SensitivityWithContingency;
import org.gridsuite.sensitivityanalysis.server.dto.resultselector.ResultTab;
import org.gridsuite.sensitivityanalysis.server.dto.SensitivityFactorsIdsByGroup;
import org.gridsuite.sensitivityanalysis.server.dto.resultselector.ResultsSelector;
import org.gridsuite.sensitivityanalysis.server.dto.SensitivityAnalysisStatus;
Expand All @@ -16,12 +22,21 @@
import org.gridsuite.sensitivityanalysis.server.repositories.SensitivityAnalysisResultRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import static org.gridsuite.sensitivityanalysis.server.SensibilityAnalysisException.Type.FILE_EXPORT_ERROR;
import static org.gridsuite.sensitivityanalysis.server.SensibilityAnalysisException.Type.INVALID_EXPORT_PARAMS;
import static org.gridsuite.sensitivityanalysis.server.SensibilityAnalysisException.Type.RESULT_NOT_FOUND;

/**
* @author Franck Lecuyer <franck.lecuyer at rte-france.com>
Expand Down Expand Up @@ -146,4 +161,62 @@ private Integer getContingenciesCount(List<UUID> ids, UUID networkUuid, String v
public String getDefaultProvider() {
return defaultProvider;
}

public byte[] exportSensitivityResultsAsCsv(UUID resultUuid, SensitivityAnalysisCsvFileInfos sensitivityAnalysisCsvFileInfos) {
if (sensitivityAnalysisCsvFileInfos == null ||
sensitivityAnalysisCsvFileInfos.getSensitivityFunctionType() == null ||
sensitivityAnalysisCsvFileInfos.getResultTab() == null ||
CollectionUtils.isEmpty(sensitivityAnalysisCsvFileInfos.getCsvHeaders())) {
throw new SensibilityAnalysisException(INVALID_EXPORT_PARAMS, "Missing information to export sensitivity result as csv : Sensitivity result tab, sensitivity function type and csv file headers must be provided");
}
ResultsSelector selector = ResultsSelector.builder()
.functionType(sensitivityAnalysisCsvFileInfos.getSensitivityFunctionType())
.tabSelection(sensitivityAnalysisCsvFileInfos.getResultTab())
.build();

SensitivityRunQueryResult result = getRunResult(resultUuid, selector);
if (result == null) {
throw new SensibilityAnalysisException(RESULT_NOT_FOUND, "The sensitivity analysis result '" + resultUuid + "' does not exist");
}

try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
zipOutputStream.putNextEntry(new ZipEntry("sensitivity_result.csv"));

CsvWriterSettings settings = new CsvWriterSettings();
CsvWriter csvWriter = new CsvWriter(zipOutputStream, settings);
csvWriter.writeHeaders(sensitivityAnalysisCsvFileInfos.getCsvHeaders());
if (selector.getTabSelection() == ResultTab.N) {
result.getSensitivities()
.forEach(sensitivity -> csvWriter.writeRow(
sensitivity.getFuncId(),
sensitivity.getVarId(),
nullIfNan(sensitivity.getFunctionReference()),
nullIfNan(sensitivity.getValue())
));
} else if (selector.getTabSelection() == ResultTab.N_K) {
result.getSensitivities()
.stream()
.map(SensitivityWithContingency.class::cast)
.forEach(sensitivityWithContingency -> csvWriter.writeRow(
sensitivityWithContingency.getFuncId(),
sensitivityWithContingency.getVarId(),
sensitivityWithContingency.getContingencyId(),
nullIfNan(sensitivityWithContingency.getFunctionReference()),
nullIfNan(sensitivityWithContingency.getValue()),
nullIfNan(sensitivityWithContingency.getFunctionReferenceAfter()),
nullIfNan(sensitivityWithContingency.getValueAfter())
));
}

csvWriter.close();
return outputStream.toByteArray();
} catch (IOException e) {
throw new SensibilityAnalysisException(FILE_EXPORT_ERROR, e.getMessage());
}
}

private static Double nullIfNan(double d) {
return Double.isNaN(d) ? null : d;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
Expand All @@ -84,6 +86,7 @@
import static org.gridsuite.sensitivityanalysis.server.service.NotificationService.CANCEL_MESSAGE;
import static org.gridsuite.sensitivityanalysis.server.service.NotificationService.FAIL_MESSAGE;
import static org.gridsuite.sensitivityanalysis.server.service.NotificationService.HEADER_USER_ID;
import static org.gridsuite.sensitivityanalysis.server.util.TestUtils.unzip;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
Expand Down Expand Up @@ -624,6 +627,43 @@ public void runAndSaveTest() throws Exception {
assertEquals(6, (long) resNK.getTotalSensitivitiesCount());
assertEquals(2, resNK.getSensitivities().size());

// export results as csv
SensitivityAnalysisCsvFileInfos sensitivityAnalysisCsvFileInfos = SensitivityAnalysisCsvFileInfos.builder()
.sensitivityFunctionType(SensitivityFunctionType.BRANCH_ACTIVE_POWER_1)
.resultTab(ResultTab.N)
.csvHeaders(List.of("functionId", "variableId", "functionReference", "value"))
.build();

UUID randomUuid = UUID.randomUUID();
mockMvc.perform(post("/" + VERSION + "/results/{resultUuid}/csv", randomUuid)
.contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(sensitivityAnalysisCsvFileInfos)))
.andExpect(status().isNotFound());

mockMvc.perform(post("/" + VERSION + "/results/{resultUuid}/csv", RESULT_UUID)
.contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(SensitivityAnalysisCsvFileInfos.builder().build())))
.andExpect(status().isBadRequest());

result = mockMvc.perform(post("/" + VERSION + "/results/{resultUuid}/csv", RESULT_UUID)
.contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(sensitivityAnalysisCsvFileInfos)))
.andExpect(status().isOk())
.andReturn();

byte[] zipFile = result.getResponse().getContentAsByteArray();
byte[] csvFile = unzip(zipFile);
String csvFileAsString = new String(csvFile, StandardCharsets.UTF_8);
List<String> actualCsvLines = Arrays.asList(csvFileAsString.split("\n"));
List<String> expectedCsvLines = new ArrayList<>(List.of("functionId,variableId,functionReference,value",
"l1,GEN,2.9,500.1",
"l2,GEN,2.8,500.2"));

actualCsvLines.sort(String::compareTo);
expectedCsvLines.sort(String::compareTo);
assertEquals(expectedCsvLines, actualCsvLines);

// test filter options
ResultsSelector filterOptionsSelector = ResultsSelector.builder().tabSelection(ResultTab.N_K)
.functionType(SensitivityFunctionType.BRANCH_ACTIVE_POWER_1).build();
result = mockMvc.perform(get("/" + VERSION + "/results/{resultUuid}/filter-options?selector={selector}", RESULT_UUID,
Expand Down
Loading
Loading