diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c2cfe9b0..aa6bab355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Enhancing `VoltageLevel` with `equals` method [#1063](https://github.com/ie3-institute/PowerSystemDataModel/issues/1063) - `ConnectorValidationUtils` checks if parallel devices is > 0 [#1077](https://github.com/ie3-institute/PowerSystemDataModel/issues/1077) +- `GridContainerValidationUtils` checks the connectivity for all defined operation time intervals [#1091](https://github.com/ie3-institute/PowerSystemDataModel/issues/1091) ### Fixed - Fixed `MappingEntryies` not getting processed by adding `Getter` methods for record fields [#1084](https://github.com/ie3-institute/PowerSystemDataModel/issues/1084) diff --git a/docs/readthedocs/io/ValidationUtils.md b/docs/readthedocs/io/ValidationUtils.md index 34771c745..8dc2ce73d 100644 --- a/docs/readthedocs/io/ValidationUtils.md +++ b/docs/readthedocs/io/ValidationUtils.md @@ -11,6 +11,7 @@ The methods in ValidationUtils and subclasses can be used to check that objects The general validation checks: - if assigned values are valid, e.g. lines are not allowed to have negative lengths or the rated power factor of any unit must be between 0 and 1 - furthermore, several connections are checked, e.g. that lines only connect nodes of the same voltage level or that the voltage levels indicated for the transformer sides match the voltage levels of the nodes they are connected to. +- the connectivity of the given grid for all defined operation intervals The uniqueness validation checks if a collection of given objects are unique in either: - a specific field diff --git a/src/main/java/edu/ie3/datamodel/utils/validation/GridContainerValidationUtils.java b/src/main/java/edu/ie3/datamodel/utils/validation/GridContainerValidationUtils.java index 9c0df8194..2b0245466 100644 --- a/src/main/java/edu/ie3/datamodel/utils/validation/GridContainerValidationUtils.java +++ b/src/main/java/edu/ie3/datamodel/utils/validation/GridContainerValidationUtils.java @@ -8,19 +8,33 @@ import static edu.ie3.datamodel.utils.validation.UniquenessValidationUtils.checkAssetUniqueness; import static edu.ie3.datamodel.utils.validation.UniquenessValidationUtils.checkUniqueEntities; -import edu.ie3.datamodel.exceptions.*; +import edu.ie3.datamodel.exceptions.DuplicateEntitiesException; +import edu.ie3.datamodel.exceptions.InvalidEntityException; +import edu.ie3.datamodel.exceptions.InvalidGridException; +import edu.ie3.datamodel.exceptions.ValidationException; +import edu.ie3.datamodel.models.OperationTime; import edu.ie3.datamodel.models.input.AssetInput; import edu.ie3.datamodel.models.input.MeasurementUnitInput; import edu.ie3.datamodel.models.input.NodeInput; -import edu.ie3.datamodel.models.input.connector.*; +import edu.ie3.datamodel.models.input.connector.ConnectorInput; +import edu.ie3.datamodel.models.input.connector.LineInput; +import edu.ie3.datamodel.models.input.connector.Transformer3WInput; import edu.ie3.datamodel.models.input.container.*; import edu.ie3.datamodel.models.input.graphics.GraphicInput; import edu.ie3.datamodel.models.input.system.SystemParticipantInput; import edu.ie3.datamodel.utils.ContainerUtils; import edu.ie3.datamodel.utils.Try; -import edu.ie3.datamodel.utils.Try.*; +import edu.ie3.datamodel.utils.Try.Failure; +import edu.ie3.datamodel.utils.Try.Success; +import java.time.ZonedDateTime; import java.util.*; +import java.util.function.Predicate; +import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jgrapht.Graph; +import org.jgrapht.alg.connectivity.ConnectivityInspector; +import org.jgrapht.graph.DefaultEdge; +import org.jgrapht.graph.SimpleGraph; public class GridContainerValidationUtils extends ValidationUtils { @@ -155,9 +169,105 @@ private GridContainerValidationUtils() { exceptions.add(MeasurementUnitValidationUtils.check(measurement)); }); + exceptions.addAll(checkConnectivity(rawGridElements)); + return exceptions; } + /** + * Checks the connectivity of the given grid for all defined {@link OperationTime}s. If every + * {@link AssetInput} is set to {@link OperationTime#notLimited()}, the connectivity is only + * checked once. + * + * @param rawGridElements to check + * @return a try + */ + protected static List> checkConnectivity( + RawGridElements rawGridElements) { + Set times = + rawGridElements.allEntitiesAsList().stream() + .map(AssetInput::getOperationTime) + .filter(OperationTime::isLimited) + .map(OperationTime::getOperationLimit) + .filter(Optional::isPresent) + .map(Optional::get) + .map(interval -> Set.of(interval.getLower(), interval.getUpper())) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + + if (times.isEmpty()) { + return List.of(checkConnectivity(rawGridElements, Optional.empty())); + } else { + return times.stream() + .sorted() + .map(time -> checkConnectivity(rawGridElements, Optional.of(time))) + .toList(); + } + } + + /** + * Checks if the given {@link RawGridElements} from a connected grid. + * + * @param rawGridElements to check + * @param time for operation filtering + * @return a try + */ + protected static Try checkConnectivity( + RawGridElements rawGridElements, Optional time) { + + Predicate isInOperation = + assetInput -> time.map(assetInput::inOperationOn).orElse(true); + + // build graph + Graph graph = new SimpleGraph<>(DefaultEdge.class); + + rawGridElements.getNodes().stream() + .filter(isInOperation) + .forEach(node -> graph.addVertex(node.getUuid())); + rawGridElements.getLines().stream() + .filter(isInOperation) + .forEach( + connector -> + graph.addEdge(connector.getNodeA().getUuid(), connector.getNodeB().getUuid())); + rawGridElements.getTransformer2Ws().stream() + .filter(isInOperation) + .forEach( + connector -> + graph.addEdge(connector.getNodeA().getUuid(), connector.getNodeB().getUuid())); + rawGridElements.getTransformer3Ws().stream() + .filter(isInOperation) + .forEach( + connector -> + graph.addEdge(connector.getNodeA().getUuid(), connector.getNodeB().getUuid())); + rawGridElements.getSwitches().stream() + .filter(isInOperation) + .forEach( + connector -> + graph.addEdge(connector.getNodeA().getUuid(), connector.getNodeB().getUuid())); + + ConnectivityInspector inspector = new ConnectivityInspector<>(graph); + + if (inspector.isConnected()) { + return Success.empty(); + } else { + List> sets = inspector.connectedSets(); + + List unconnected = + sets.stream() + .max(Comparator.comparing(Set::size)) + .map(set -> graph.vertexSet().stream().filter(v -> !set.contains(v)).toList()) + .orElse(List.of()); + + String message = "The grid contains unconnected elements"; + + if (time.isPresent()) { + message += " for time " + time.get(); + } + + return Failure.of(new InvalidGridException(message + ": " + unconnected)); + } + } + /** * Checks the validity of each and every system participant. Moreover, it checks, if the systems * are connected to a node that is not in the provided set diff --git a/src/test/groovy/edu/ie3/datamodel/utils/validation/GridContainerValidationUtilsTest.groovy b/src/test/groovy/edu/ie3/datamodel/utils/validation/GridContainerValidationUtilsTest.groovy new file mode 100644 index 000000000..013bad1a4 --- /dev/null +++ b/src/test/groovy/edu/ie3/datamodel/utils/validation/GridContainerValidationUtilsTest.groovy @@ -0,0 +1,106 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ +package edu.ie3.datamodel.utils.validation + +import edu.ie3.datamodel.exceptions.InvalidGridException +import edu.ie3.datamodel.models.OperationTime +import edu.ie3.datamodel.models.input.container.RawGridElements +import edu.ie3.datamodel.utils.Try +import edu.ie3.test.common.GridTestData as GTD +import spock.lang.Shared +import spock.lang.Specification + +import java.time.ZonedDateTime + +class GridContainerValidationUtilsTest extends Specification { + @Shared + private ZonedDateTime start + + @Shared + private RawGridElements limitedElements + + def setupSpec() { + start = ZonedDateTime.now() + + def operationTimeFrame1 = OperationTime.builder().withStart(start).withEnd(start.plusHours(2)).build() + def operationTimeFrame2 = OperationTime.builder().withStart(start.plusHours(1)).withEnd(start.plusHours(3)).build() + + def nodes = [ + GTD.nodeC, + GTD.nodeD, + GTD.nodeE, + GTD.nodeF, + GTD.nodeG + ] as Set + + def lines = [ + GTD.lineCtoD.copy().operationTime(operationTimeFrame1).build(), + GTD.lineFtoG.copy().operationTime(operationTimeFrame2).build() + ] as Set + + def transformers = [ + GTD.transformerCtoF.copy().operationTime(operationTimeFrame1).build(), + GTD.transformerCtoE.copy().operationTime(operationTimeFrame2).build() + ] as Set + + limitedElements = new RawGridElements(nodes, lines, transformers, [] as Set, [] as Set, [] as Set) + } + + def "The GridContainerValidationUtils should check the connectivity for all operation intervals correctly"() { + when: + def actual = GridContainerValidationUtils.checkConnectivity(limitedElements) + + then: + actual.size() == 4 + actual.get(0).failure + actual.get(1).success + actual.get(2).success + actual.get(3).failure + + actual.get(0).exception.get().message == "The grid contains unconnected elements for time " + start + ": " + [ + GTD.nodeE.uuid, + GTD.nodeG.uuid + ] + actual.get(3).exception.get().message == "The grid contains unconnected elements for time " + start.plusHours(3) + ": " + [ + GTD.nodeD.uuid, + GTD.nodeF.uuid, + GTD.nodeG.uuid + ] + } + + def "The GridContainerValidationUtils should check the connectivity correctly"() { + when: + def actual = GridContainerValidationUtils.checkConnectivity(limitedElements, time as Optional) + + then: + actual == expectedResult + + where: + time || expectedResult + Optional.empty() || Try.Success.empty() + Optional.of(start.plusHours(1)) || Try.Success.empty() + } + + def "The GridContainerValidationUtils should return an exception if the grid is not properly connected"() { + when: + def actual = GridContainerValidationUtils.checkConnectivity(limitedElements, time as Optional) + + then: + actual.exception.get().message == expectedException.message + + where: + time || expectedException + Optional.of(start) || new InvalidGridException("The grid contains unconnected elements for time " + start + ": " + [ + GTD.nodeE.uuid, + GTD.nodeG.uuid + ]) + Optional.of(start.plusHours(3)) || new InvalidGridException("The grid contains unconnected elements for time " + start.plusHours(3) + ": " + [ + GTD.nodeD.uuid, + GTD.nodeF.uuid, + GTD.nodeG.uuid + ]) + } +}