diff --git a/pom.xml b/pom.xml index bf616a14..51ec30f2 100644 --- a/pom.xml +++ b/pom.xml @@ -224,6 +224,10 @@ powsybl-iidm-impl runtime + + com.powsybl + powsybl-case-datasource-client + diff --git a/src/main/java/org/gridsuite/securityanalysis/server/SecurityAnalysisOnCaseController.java b/src/main/java/org/gridsuite/securityanalysis/server/SecurityAnalysisOnCaseController.java new file mode 100644 index 00000000..500f974d --- /dev/null +++ b/src/main/java/org/gridsuite/securityanalysis/server/SecurityAnalysisOnCaseController.java @@ -0,0 +1,58 @@ +/** + * 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.securityanalysis.server; + +import com.powsybl.security.SecurityAnalysisResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +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.securityanalysis.server.service.SecurityAnalysisOnCaseService; +import org.gridsuite.securityanalysis.server.service.SecurityAnalysisParametersService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.UUID; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +/** + * @author Franck Lecuyer + */ +@RestController +@RequestMapping(value = "/" + SecurityAnalysisApi.API_VERSION + "/cases") +@Tag(name = "Security analysis server on case") +public class SecurityAnalysisOnCaseController { + private final SecurityAnalysisOnCaseService securityAnalysisOnCaseService; + + public SecurityAnalysisOnCaseController(SecurityAnalysisOnCaseService securityAnalysisOnCaseService, SecurityAnalysisParametersService securityAnalysisParametersService) { + this.securityAnalysisOnCaseService = securityAnalysisOnCaseService; + } + + @PostMapping(value = "/{caseUuid}/run-and-save", produces = APPLICATION_JSON_VALUE, consumes = APPLICATION_JSON_VALUE) + @Operation(summary = "Run a security analysis on a case and store the result in the database") + @ApiResponses(value = {@ApiResponse(responseCode = "200", + description = "The security analysis has been performed and results have been saved to database", + content = {@Content(mediaType = APPLICATION_JSON_VALUE, + schema = @Schema(implementation = SecurityAnalysisResult.class))})}) + public ResponseEntity runAndSave(@Parameter(description = "Case UUID") @PathVariable("caseUuid") UUID caseUuid, + @Parameter(description = "Execution UUID") @RequestParam(name = "executionUuid", required = false) UUID executionUuid, + @Parameter(description = "Contingency list name") @RequestParam(name = "contingencyListName", required = false) List contigencyListNames, + @Parameter(description = "parametersUuid") @RequestParam(name = "parametersUuid", required = false) UUID parametersUuid, + @Parameter(description = "loadFlow parameters uuid") @RequestParam(name = "loadFlowParametersUuid", required = false) UUID loadFlowParametersUuid) { + securityAnalysisOnCaseService.runAndSaveResult(caseUuid, executionUuid, contigencyListNames, parametersUuid, loadFlowParametersUuid); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/org/gridsuite/securityanalysis/server/service/CaseResultInfos.java b/src/main/java/org/gridsuite/securityanalysis/server/service/CaseResultInfos.java new file mode 100644 index 00000000..2ee9a041 --- /dev/null +++ b/src/main/java/org/gridsuite/securityanalysis/server/service/CaseResultInfos.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.securityanalysis.server.service; + +import lombok.Getter; + +import java.util.UUID; + +/** + * @author Franck Lecuyer + */ +@Getter +public class CaseResultInfos { + private final UUID caseResultUuid; + + private final UUID executionUuid; + + private final UUID reportUuid; + + private final UUID resultUuid; + + private final String stepType; + + private final String status; + + public CaseResultInfos(UUID caseResultUuid, UUID executionUuid, UUID reportUuid, UUID resultUuid, String stepType, String status) { + this.caseResultUuid = caseResultUuid; + this.executionUuid = executionUuid; + this.reportUuid = reportUuid; + this.resultUuid = resultUuid; + this.stepType = stepType; + this.status = status; + } +} diff --git a/src/main/java/org/gridsuite/securityanalysis/server/service/NetworkConversionService.java b/src/main/java/org/gridsuite/securityanalysis/server/service/NetworkConversionService.java new file mode 100644 index 00000000..9767187c --- /dev/null +++ b/src/main/java/org/gridsuite/securityanalysis/server/service/NetworkConversionService.java @@ -0,0 +1,52 @@ +/** + * 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.securityanalysis.server.service; + +import com.powsybl.cases.datasource.CaseDataSourceClient; +import com.powsybl.commons.PowsyblException; +import com.powsybl.commons.report.ReportNode; +import com.powsybl.computation.local.LocalComputationManager; +import com.powsybl.iidm.network.Importer; +import com.powsybl.iidm.network.Network; +import com.powsybl.iidm.network.NetworkFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.Properties; +import java.util.UUID; + +/** + * @author Franck Lecuyer + */ +@Service +public class NetworkConversionService { + private static final Logger LOGGER = LoggerFactory.getLogger(NetworkConversionService.class); + + private final RestTemplate caseServerRest; + + public NetworkConversionService(@Value("${powsybl.services.case-server.base-uri:http://case-server/}") String caseServerBaseUri, + RestTemplateBuilder restTemplateBuilder) { + this.caseServerRest = restTemplateBuilder.rootUri(caseServerBaseUri).build(); + } + + public Network createNetwork(UUID caseUuid, ReportNode reporter) { + LOGGER.info("Creating network"); + + CaseDataSourceClient dataSource = new CaseDataSourceClient(caseServerRest, caseUuid); + + Importer importer = Importer.find(dataSource, LocalComputationManager.getDefault()); + if (importer == null) { + throw new PowsyblException("No importer found"); + } else { + return importer.importData(dataSource, NetworkFactory.findDefault(), new Properties(), reporter); + } + } +} diff --git a/src/main/java/org/gridsuite/securityanalysis/server/service/NotificationOnCaseService.java b/src/main/java/org/gridsuite/securityanalysis/server/service/NotificationOnCaseService.java new file mode 100644 index 00000000..1162cfb4 --- /dev/null +++ b/src/main/java/org/gridsuite/securityanalysis/server/service/NotificationOnCaseService.java @@ -0,0 +1,33 @@ +/** + * 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.securityanalysis.server.service; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.stream.function.StreamBridge; +import org.springframework.messaging.Message; +import org.springframework.stereotype.Service; + +/** + * @author Franck Lecuyer message, String bindingName) { + publisher.send(publishPrefix + bindingName, message); + } +} diff --git a/src/main/java/org/gridsuite/securityanalysis/server/service/ReportOnCaseService.java b/src/main/java/org/gridsuite/securityanalysis/server/service/ReportOnCaseService.java new file mode 100644 index 00000000..ce63efdb --- /dev/null +++ b/src/main/java/org/gridsuite/securityanalysis/server/service/ReportOnCaseService.java @@ -0,0 +1,68 @@ +/** + * 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.securityanalysis.server.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.InjectableValues; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.powsybl.commons.PowsyblException; +import com.powsybl.commons.report.ReportNode; +import com.powsybl.commons.report.ReportNodeDeserializer; +import com.powsybl.commons.report.ReportNodeJsonModule; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.UUID; + +/** + * @author Franck Lecuyer (objectMapper.writeValueAsString(reportNode), headers), ReportNode.class); + } catch (JsonProcessingException error) { + throw new PowsyblException("error creating report", error); + } + } +} diff --git a/src/main/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisCaseContext.java b/src/main/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisCaseContext.java new file mode 100644 index 00000000..5663dabe --- /dev/null +++ b/src/main/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisCaseContext.java @@ -0,0 +1,59 @@ +/** + * 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.securityanalysis.server.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; + +import java.io.UncheckedIOException; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +/** + * @author Franck Lecuyer + */ +@Getter +public class SecurityAnalysisCaseContext { + public final UUID caseUuid; + public final UUID executionUuid; + public final List contigencyListNames; + public final UUID parametersUuid; + public final UUID loadFlowParametersUuid; + + public SecurityAnalysisCaseContext(UUID caseUuid, UUID executionUuid, List contigencyListNames, UUID parametersUuid, UUID loadFlowParametersUuid) { + this.caseUuid = caseUuid; + this.executionUuid = executionUuid; + this.contigencyListNames = contigencyListNames; + this.parametersUuid = parametersUuid; + this.loadFlowParametersUuid = loadFlowParametersUuid; + } + + public static SecurityAnalysisCaseContext fromMessage(Message message, ObjectMapper objectMapper) { + Objects.requireNonNull(message); + SecurityAnalysisCaseContext context; + try { + context = objectMapper.readValue(message.getPayload(), SecurityAnalysisCaseContext.class); + } catch (JsonProcessingException e) { + throw new UncheckedIOException(e); + } + return context; + } + + public Message toMessage(ObjectMapper objectMapper) { + String json; + try { + json = objectMapper.writeValueAsString(this); + } catch (JsonProcessingException e) { + throw new UncheckedIOException(e); + } + return MessageBuilder.withPayload(json).build(); + } +} diff --git a/src/main/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisOnCaseService.java b/src/main/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisOnCaseService.java new file mode 100644 index 00000000..8d5c3a88 --- /dev/null +++ b/src/main/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisOnCaseService.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.securityanalysis.server.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.UUID; + +/** + * @author Franck Lecuyer + */ +@Service +public class SecurityAnalysisOnCaseService { + private final SecurityAnalysisResultService securityAnalysisResultService; + private final NotificationOnCaseService notificationOnCaseService; + private final ObjectMapper objectMapper; + private final String defaultProvider; + + public SecurityAnalysisOnCaseService(SecurityAnalysisResultService securityAnalysisResultService, + NotificationOnCaseService notificationOnCaseService, + ObjectMapper objectMapper, + @Value("${security-analysis.default-provider}") String defaultProvider) { + this.securityAnalysisResultService = securityAnalysisResultService; + this.notificationOnCaseService = notificationOnCaseService; + this.objectMapper = objectMapper; + this.defaultProvider = defaultProvider; + } + + @Transactional + public void runAndSaveResult(UUID caseUuid, UUID executionUuid, List contigencyListNames, UUID parametersUuid, UUID loadFlowParametersUuid) { + notificationOnCaseService.sendMessage( + new SecurityAnalysisCaseContext(caseUuid, executionUuid, contigencyListNames, parametersUuid, loadFlowParametersUuid).toMessage(objectMapper), + "CaseRun-out-0"); + } +} diff --git a/src/main/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisOnCaseWorkerService.java b/src/main/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisOnCaseWorkerService.java new file mode 100644 index 00000000..5efe8f6e --- /dev/null +++ b/src/main/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisOnCaseWorkerService.java @@ -0,0 +1,164 @@ +/** + * 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.securityanalysis.server.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.powsybl.commons.datasource.MemDataSource; +import com.powsybl.commons.report.ReportNode; +import com.powsybl.contingency.Contingency; +import com.powsybl.contingency.LineContingency; +import com.powsybl.iidm.network.Network; +import com.powsybl.loadflow.LoadFlowResult; +import com.powsybl.security.SecurityAnalysis; +import com.powsybl.security.SecurityAnalysisReport; +import com.powsybl.security.SecurityAnalysisResult; +import com.powsybl.security.SecurityAnalysisRunParameters; +import org.gridsuite.securityanalysis.server.dto.SecurityAnalysisStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; + +/** + * @author Franck Lecuyer + */ +@Service +public class SecurityAnalysisOnCaseWorkerService { + private static final Logger LOGGER = LoggerFactory.getLogger(SecurityAnalysisOnCaseWorkerService.class); + + private final NetworkConversionService networkConversionService; + private final NotificationOnCaseService notificationOnCaseService; + private final ReportOnCaseService reportOnCaseService; + private final SecurityAnalysisResultService resultService; + + private final ObjectMapper objectMapper; + private final RestTemplate restTemplate; + private final String caseExportFormat = "XIIDM"; + + public SecurityAnalysisOnCaseWorkerService(NetworkConversionService networkConversionService, + NotificationOnCaseService notificationOnCaseService, + SecurityAnalysisResultService resultService, + ReportOnCaseService reportOnCaseService, + ObjectMapper objectMapper, + RestTemplateBuilder restTemplateBuilder, + @Value("${powsybl.services.case-server.base-uri:http://case-server}") String caseServerBaseUri) { + this.networkConversionService = networkConversionService; + this.notificationOnCaseService = notificationOnCaseService; + this.reportOnCaseService = reportOnCaseService; + this.resultService = resultService; + this.objectMapper = objectMapper; + this.restTemplate = restTemplateBuilder.rootUri(caseServerBaseUri).build(); + } + + private void saveResult(Network network, UUID resultUuid, SecurityAnalysisResult result) { + resultService.insert(network, + resultUuid, + result, + result.getPreContingencyResult().getStatus() == LoadFlowResult.ComponentResult.Status.CONVERGED + ? SecurityAnalysisStatus.CONVERGED + : SecurityAnalysisStatus.DIVERGED); + } + + private Network loadNetworkFromCase(UUID caseUuid, ReportNode reportNode) { + return networkConversionService.createNetwork(caseUuid, reportNode); + } + + private UUID save(Resource resource) { + String uri = "/v1/cases"; + + MultiValueMap body = new LinkedMultiValueMap<>(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + body.add("file", resource); + HttpEntity> request = new HttpEntity<>(body, headers); + + return restTemplate.postForObject(uri, request, UUID.class); + } + + private UUID save(Network network) throws IOException { + MemDataSource memDataSource = new MemDataSource(); + network.write(this.caseExportFormat, null, memDataSource); + + Set listNames = memDataSource.listNames(".*"); + String caseFileName = "security-analysis-output." + this.caseExportFormat.toLowerCase(); + return save(new ByteArrayResource(memDataSource.getData(listNames.toArray()[0].toString())) { + @Override + public String getFilename() { + return caseFileName; + } + }); + } + + @Bean + public Consumer> consumeCaseRun() { + return message -> { + UUID executionUuid = null; + UUID resultCaseUuid = null; + UUID reportUuid = null; + UUID resultUuid = null; + String status = "COMPLETED"; + + try { + SecurityAnalysisCaseContext context = SecurityAnalysisCaseContext.fromMessage(message, objectMapper); + UUID caseUuid = context.getCaseUuid(); + executionUuid = context.getExecutionUuid(); + List contingencyListNames = context.getContigencyListNames(); + + ReportNode rootReport = ReportNode.newRootReportNode() + .withAllResourceBundlesFromClasspath() + .withMessageTemplate("security.analysis.server.caseUuid") + .withUntypedValue("caseUuid", caseUuid.toString()) + .build(); + + LOGGER.info("Run security analysis on case {}", caseUuid); + + // create network from case + Network network = loadNetworkFromCase(caseUuid, rootReport); + + // run security analysis + List contingencyList = contingencyListNames.stream().map(id -> new Contingency(id, new LineContingency(id))).toList(); + SecurityAnalysisRunParameters runParameters = new SecurityAnalysisRunParameters().setReportNode(rootReport); + SecurityAnalysisReport saReport = SecurityAnalysis.find("OpenLoadFlow").run(network, contingencyList, runParameters); + SecurityAnalysisResult result = saReport.getResult(); + + // save result + resultUuid = UUID.randomUUID(); + saveResult(network, resultUuid, result); + + // send report to report server + reportUuid = UUID.randomUUID(); + reportOnCaseService.sendReport(reportUuid, rootReport); + + // save network in case server + resultCaseUuid = save(network); + } catch (Exception e) { + status = "FAILED"; + } finally { + // send notification + notificationOnCaseService.sendMessage(MessageBuilder.withPayload(new CaseResultInfos(resultCaseUuid, executionUuid, reportUuid, resultUuid, "SECURITY_ANALYSIS", status)).build(), "CaseResult-out-0"); + } + }; + } +} diff --git a/src/main/resources/application-local.yaml b/src/main/resources/application-local.yaml index dda0a5f9..e26f59f6 100644 --- a/src/main/resources/application-local.yaml +++ b/src/main/resources/application-local.yaml @@ -13,6 +13,8 @@ powsybl: services: network-store-server: base-uri: http://localhost:8080 + case-server: + base-uri: http://localhost:5000 gridsuite: services: diff --git a/src/main/resources/config/application.yaml b/src/main/resources/config/application.yaml index ffdc9313..cc9082e9 100644 --- a/src/main/resources/config/application.yaml +++ b/src/main/resources/config/application.yaml @@ -12,7 +12,7 @@ spring: cloud: function: - definition: consumeRun;consumeCancel + definition: consumeRun;consumeCancel;consumeCaseRun stream: bindings: consumeRun-in-0: @@ -33,7 +33,13 @@ spring: destination: ${powsybl-ws.rabbitmq.destination.prefix:}sa.stopped publishCancelFailed-out-0: destination: ${powsybl-ws.rabbitmq.destination.prefix:}sa.cancelfailed - output-bindings: publishRun-out-0;publishResult-out-0;publishCancel-out-0;publishStopped-out-0;publishCancelFailed-out-0 + publishCaseRun-out-0: + destination: ${powsybl-ws.rabbitmq.destination.prefix:}sa.case.run + consumeCaseRun-in-0: + destination: ${powsybl-ws.rabbitmq.destination.prefix:}sa.case.run + publishCaseResult-out-0: + destination: ${powsybl-ws.rabbitmq.destination.prefix:}sa.case.result + output-bindings: publishRun-out-0;publishResult-out-0;publishCancel-out-0;publishStopped-out-0;publishCancelFailed-out-0;publishCaseRun-out-0;publishCaseResult-out-0 rabbit: bindings: consumeRun-in-0: @@ -46,7 +52,6 @@ spring: enabled: true delivery-limit: 2 - powsybl-ws: database: queryBegin: '&' diff --git a/src/main/resources/org/gridsuite/securityanalysis/server/reports.properties b/src/main/resources/org/gridsuite/securityanalysis/server/reports.properties index ee8d5a57..ebd4247d 100644 --- a/src/main/resources/org/gridsuite/securityanalysis/server/reports.properties +++ b/src/main/resources/org/gridsuite/securityanalysis/server/reports.properties @@ -2,3 +2,4 @@ security.analysis.server.notFoundEquipments = Equipments not found security.analysis.server.contingencyEquipmentNotFound = Cannot find the following equipments ${elementsIds} in contingency ${contingencyId} security.analysis.server.notConnectedEquipments = Equipments not connected security.analysis.server.contingencyEquipmentNotConnected = The following equipments ${elementsIds} in contingency ${contingencyId} are not connected +security.analysis.server.caseUuid = ${caseUuid}