diff --git a/connectable-switches-collapser/pom.xml b/connectable-switches-collapser/pom.xml
new file mode 100644
index 00000000..9ae2f34d
--- /dev/null
+++ b/connectable-switches-collapser/pom.xml
@@ -0,0 +1,55 @@
+
+
+
+ 4.0.0
+
+
+ eu.itesla_project
+ itesla-parent
+ 0.4.0-SNAPSHOT
+
+
+ connectable-switches-collapser
+
+ Connectable switches collapser post processor
+
+
+
+
+ com.google.auto.service
+ auto-service
+
+
+ com.powsybl
+ powsybl-iidm-api
+
+
+ com.powsybl
+ powsybl-iidm-converter-api
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+ com.powsybl
+ powsybl-iidm-impl
+ test
+
+
+ junit
+ junit
+ test
+
+
+
diff --git a/connectable-switches-collapser/src/main/java/com/powsybl/iidm/postprocessor/ConnectableSwitchesCollapserPostProcessor.java b/connectable-switches-collapser/src/main/java/com/powsybl/iidm/postprocessor/ConnectableSwitchesCollapserPostProcessor.java
new file mode 100644
index 00000000..23ad67c9
--- /dev/null
+++ b/connectable-switches-collapser/src/main/java/com/powsybl/iidm/postprocessor/ConnectableSwitchesCollapserPostProcessor.java
@@ -0,0 +1,321 @@
+/**
+ * Copyright (c) 2018, 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.iidm.postprocessor;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.auto.service.AutoService;
+import com.google.common.collect.Maps;
+import com.powsybl.computation.ComputationManager;
+import com.powsybl.iidm.import_.ImportPostProcessor;
+import com.powsybl.iidm.network.Bus;
+import com.powsybl.iidm.network.BusbarSection;
+import com.powsybl.iidm.network.DanglingLine;
+import com.powsybl.iidm.network.Generator;
+import com.powsybl.iidm.network.HvdcConverterStation;
+import com.powsybl.iidm.network.Line;
+import com.powsybl.iidm.network.Load;
+import com.powsybl.iidm.network.Network;
+import com.powsybl.iidm.network.ShuntCompensator;
+import com.powsybl.iidm.network.StaticVarCompensator;
+import com.powsybl.iidm.network.Switch;
+import com.powsybl.iidm.network.SwitchKind;
+import com.powsybl.iidm.network.Terminal;
+import com.powsybl.iidm.network.ThreeWindingsTransformer;
+import com.powsybl.iidm.network.TopologyVisitor;
+import com.powsybl.iidm.network.TwoWindingsTransformer;
+import com.powsybl.iidm.network.VoltageLevel;
+
+/**
+ * @author Giovanni Ferrari
+ */
+@AutoService(ImportPostProcessor.class)
+public class ConnectableSwitchesCollapserPostProcessor implements ImportPostProcessor {
+
+ private static final Logger LOGGER = LoggerFactory
+ .getLogger(ConnectableSwitchesCollapserPostProcessor.class);
+
+ @Override
+ public String getName() {
+ return "connectableSwitchesCollapser";
+ }
+
+ @Override
+ public void process(Network network, ComputationManager cm) throws Exception {
+ Objects.requireNonNull(network);
+ LOGGER.info("Execute {} post processor on network {}", getName(), network.getId());
+ removeBreakers(network);
+ }
+
+ private int removeBreakers(Network network) {
+ AtomicInteger removedSwitches = new AtomicInteger(0);
+
+ network.getVoltageLevelStream().forEach(vl -> {
+
+ int removed = removeBreakers(network, vl);
+ removedSwitches.addAndGet(removed);
+
+ while (removed > 0) {
+ removed = removeBreakers(network, vl);
+ removedSwitches.addAndGet(removed);
+ }
+ removed = removeDoubleSwitchBuses(network, vl);
+ removedSwitches.addAndGet(removed);
+
+ while (removed > 0) {
+ removed = removeDoubleSwitchBuses(network, vl);
+ removedSwitches.addAndGet(removed);
+ }
+ });
+ LOGGER.info("Removed switches: " + removedSwitches.get());
+ return removedSwitches.get();
+ }
+
+ private int removeBreakers(Network network, VoltageLevel vl) {
+ AtomicInteger removed = new AtomicInteger();
+
+ Map> busMap = getBusSwitchMap(vl);
+
+ List singleSwitchBuses = busMap.entrySet().stream()
+ .filter(e -> e.getValue().size() == 1).map(r -> r.getKey()).collect(Collectors.toList());
+
+ List switchList = busMap.entrySet().stream()
+ .filter(e -> e.getValue().size() == 1).map(e -> vl.getBusBreakerView().getSwitch(e.getValue().get(0))).distinct().collect(Collectors.toList());
+
+ switchList.forEach(sw -> {
+ Bus b1 = vl.getBusBreakerView().getBus1(sw.getId());
+ Bus b2 = vl.getBusBreakerView().getBus2(sw.getId());
+ Bus toRemove = null;
+ Bus toKeep = null;
+ if (singleSwitchBuses.contains(b1.getId())
+ && singleSwitchBuses.contains(b2.getId())) {
+ if (compareBusToRemove(b1, b2)) {
+ toRemove = b1;
+ toKeep = b2;
+ } else {
+ toRemove = b2;
+ toKeep = b1;
+ }
+ } else if (singleSwitchBuses.contains(b1.getId())) {
+ toRemove = b1;
+ toKeep = b2;
+ } else if (singleSwitchBuses.contains(b2.getId())) {
+ toRemove = b2;
+ toKeep = b1;
+ }
+ if (toRemove != null) {
+ if (this.removeSwitchandBus(vl, sw, toRemove, toKeep)) {
+ removed.incrementAndGet();
+ }
+ if (toKeep.getConnectedTerminalCount() == 0) {
+ try {
+ vl.getBusBreakerView().removeBus(toKeep.getId());
+ } catch (Exception ex) {
+ LOGGER.debug("Cannot remove bus " + toKeep.getId());
+ }
+ }
+ }
+ });
+ return removed.get();
+ }
+
+ private int removeDoubleSwitchBuses(Network network, VoltageLevel vl) {
+ AtomicInteger removed = new AtomicInteger();
+ Map> busMap = getBusSwitchMap(vl);
+
+ Map> doubleSwitchBuses = busMap.entrySet().stream()
+ .filter(e -> e.getValue().size() == 2)
+ .collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue()));
+
+ doubleSwitchBuses.forEach((b, swl) -> {
+ Bus bus = vl.getBusBreakerView().getBus(b);
+ Switch s1 = vl.getBusBreakerView().getSwitch(swl.get(0));
+ Switch s2 = vl.getBusBreakerView().getSwitch(swl.get(1));
+
+ List terminals = getTerminals(bus);
+
+ if (s1 != null && s2 != null && terminals.size() == 0) {
+ Switch switchToKeep;
+ Switch switchToRemove;
+
+ if (compareSwitchToRemove(vl, bus, s1, s2)) {
+ switchToRemove = s1;
+ switchToKeep = s2;
+ } else {
+ switchToRemove = s2;
+ switchToKeep = s1;
+ }
+
+ Bus busToKeep = vl.getBusBreakerView().getBus1(switchToRemove.getId()).equals(bus)
+ ? vl.getBusBreakerView().getBus2(switchToRemove.getId())
+ : vl.getBusBreakerView().getBus1(switchToRemove.getId());
+
+ boolean reconnectBus1 = vl.getBusBreakerView().getBus1(switchToKeep.getId())
+ .equals(bus) ? true : false;
+
+ vl.getBusBreakerView().removeSwitch(switchToRemove.getId());
+
+ String bus1 = reconnectBus1 ? busToKeep.getId()
+ : vl.getBusBreakerView().getBus1(switchToKeep.getId()).getId();
+ String bus2 = reconnectBus1
+ ? vl.getBusBreakerView().getBus2(switchToKeep.getId()).getId()
+ : busToKeep.getId();
+
+ vl.getBusBreakerView().removeSwitch(switchToKeep.getId());
+ vl.getBusBreakerView().removeBus(bus.getId());
+ vl.getBusBreakerView().newSwitch().setId(switchToKeep.getId())
+ .setName(switchToKeep.getName()).setFictitious(switchToKeep.isFictitious())
+ .setOpen(switchToKeep.isOpen() || switchToRemove.isOpen()).setBus1(bus1)
+ .setBus2(bus2).add();
+ removed.incrementAndGet();
+ }
+ });
+ return removed.get();
+ }
+
+ private Map> getBusSwitchMap(VoltageLevel vl) {
+
+ return vl.getBusBreakerView().getSwitchStream().flatMap(s -> Stream.of(
+ Maps.immutableEntry(vl.getBusBreakerView().getBus1(s.getId()).getId(), s.getId()),
+ Maps.immutableEntry(vl.getBusBreakerView().getBus2(s.getId()).getId(), s.getId())))
+ .collect(Collectors.toMap(a -> a.getKey(),
+ a -> new ArrayList<>(Collections.singletonList(a.getValue())), (o, n) -> {
+ o.addAll(n);
+ return o;
+ }, HashMap::new));
+ }
+
+ private boolean compareSwitchToRemove(VoltageLevel vl, Bus bus, Switch s1, Switch s2) {
+
+ if (s1.isFictitious()) {
+ return true;
+ }
+ if (s2.isFictitious()) {
+ return false;
+ }
+ if (s1.getKind() != s2.getKind()) {
+ return s1.getKind() == SwitchKind.BREAKER;
+ }
+ Bus b1 = vl.getBusBreakerView().getBus1(s1.getId()).equals(bus)
+ ? vl.getBusBreakerView().getBus2(s1.getId())
+ : vl.getBusBreakerView().getBus1(s1.getId());
+ Bus b2 = vl.getBusBreakerView().getBus1(s2.getId()).equals(bus)
+ ? vl.getBusBreakerView().getBus2(s2.getId())
+ : vl.getBusBreakerView().getBus1(s2.getId());
+
+ if (b1.getAngle() != b2.getAngle()) {
+ return b1.getAngle() < b2.getAngle();
+ }
+ return s1.getId().compareTo(s2.getId()) < 0;
+ }
+
+ private boolean compareBusToRemove(Bus b1, Bus b2) {
+ if (b1.getConnectedTerminalCount() == b2.getConnectedTerminalCount()) {
+ long score1 = b1.getTwoWindingTransformerStream().count()
+ + b1.getThreeWindingTransformerStream().count() + b1.getLineStream().count();
+ long score2 = b2.getTwoWindingTransformerStream().count()
+ + b2.getThreeWindingTransformerStream().count() + b2.getLineStream().count();
+ return score1 <= score2;
+ } else {
+ return b1.getConnectedTerminalCount() < b2.getConnectedTerminalCount();
+ }
+ }
+
+ private List getTerminals(Bus b) {
+ List terminals = new ArrayList();
+
+ b.visitConnectedOrConnectableEquipments(new TopologyVisitor() {
+ @Override
+ public void visitBusbarSection(BusbarSection section) {
+ terminals.add(section.getTerminal());
+ }
+
+ @Override
+ public void visitLine(Line line, Line.Side side) {
+ terminals.add(line.getTerminal(side));
+ }
+
+ @Override
+ public void visitTwoWindingsTransformer(TwoWindingsTransformer transformer,
+ TwoWindingsTransformer.Side side) {
+ terminals.add(transformer.getTerminal(side));
+ }
+
+ @Override
+ public void visitThreeWindingsTransformer(ThreeWindingsTransformer transformer,
+ ThreeWindingsTransformer.Side side) {
+ terminals.add(transformer.getTerminal(side));
+ }
+
+ @Override
+ public void visitGenerator(Generator generator) {
+ terminals.add(generator.getTerminal());
+ }
+
+ @Override
+ public void visitLoad(Load load) {
+ terminals.add(load.getTerminal());
+ }
+
+ @Override
+ public void visitShuntCompensator(ShuntCompensator sc) {
+ terminals.add(sc.getTerminal());
+ }
+
+ @Override
+ public void visitDanglingLine(DanglingLine danglingLine) {
+ terminals.add(danglingLine.getTerminal());
+ }
+
+ @Override
+ public void visitStaticVarCompensator(StaticVarCompensator staticVarCompensator) {
+ terminals.add(staticVarCompensator.getTerminal());
+ }
+
+ @Override
+ public void visitHvdcConverterStation(HvdcConverterStation> converterStation) {
+ terminals.add(converterStation.getTerminal());
+ }
+ });
+ return terminals;
+ }
+
+ private boolean removeSwitchandBus(VoltageLevel vl, Switch s, Bus toRemove, Bus toKeep) {
+ List terminals = getTerminals(toRemove);
+ if (terminals.size() <= 1) {
+ terminals.forEach(t -> {
+ boolean reconnect = false;
+ if (t.isConnected()) {
+ t.disconnect();
+ reconnect = true;
+ }
+ t.getBusBreakerView().setConnectableBus(toKeep.getId());
+
+ if (!s.isOpen() && reconnect) {
+ t.connect();
+ }
+ });
+ vl.getBusBreakerView().removeSwitch(s.getId());
+ vl.getBusBreakerView().removeBus(toRemove.getId());
+ return true;
+ }
+ return false;
+ }
+
+
+}
diff --git a/connectable-switches-collapser/src/test/java/eu/itesla_project/iidm/postprocessor/ConnectableSwitchesCollapserTest.java b/connectable-switches-collapser/src/test/java/eu/itesla_project/iidm/postprocessor/ConnectableSwitchesCollapserTest.java
new file mode 100644
index 00000000..0e87e684
--- /dev/null
+++ b/connectable-switches-collapser/src/test/java/eu/itesla_project/iidm/postprocessor/ConnectableSwitchesCollapserTest.java
@@ -0,0 +1,219 @@
+/**
+ * Copyright (c) 2018, 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 eu.itesla_project.iidm.postprocessor;
+
+import java.util.Optional;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.powsybl.iidm.network.Bus;
+import com.powsybl.iidm.network.Country;
+import com.powsybl.iidm.network.Generator;
+import com.powsybl.iidm.network.Network;
+import com.powsybl.iidm.network.NetworkFactory;
+import com.powsybl.iidm.network.StaticVarCompensator;
+import com.powsybl.iidm.network.Substation;
+import com.powsybl.iidm.network.TopologyKind;
+import com.powsybl.iidm.network.VoltageLevel;
+import com.powsybl.iidm.postprocessor.ConnectableSwitchesCollapserPostProcessor;
+
+/**
+ * @author Giovanni Ferrari
+ */
+public class ConnectableSwitchesCollapserTest {
+
+ private static final String BUS_1_ID = "bus1";
+ private static final String BUS_2_ID = "bus2";
+ private static final String BUS_3_ID = "bus3";
+ private static final String BUS_4_ID = "bus4";
+ private static final String BUS_5_ID = "bus5";
+ private static final String GEN_ID = "g1";
+ private static final String GEN_2_ID = "g2";
+ private static final String AUX_ID = "ld1";
+ private static final String AUX_2_ID = "ld2";
+ private static final String AUX_3_ID = "ld3";
+ private static final String AUX_5_ID = "ld5";
+ private static final String SWITCH_ID = "sw1";
+ private static final String SWITCH_2_ID = "sw2";
+ private static final String SWITCH_3_ID = "sw3";
+ private static final String VL1 = "vl1";
+ private static final String VL2 = "vl2";
+
+ private Network createNetwork() {
+ Network n = NetworkFactory.create("UNIT_TEST", "MANUAL");
+ Substation s = n.newSubstation()
+ .setId("s1")
+ .setCountry(Country.FR)
+ .add();
+ VoltageLevel vl = s.newVoltageLevel()
+ .setId(VL1)
+ .setNominalV(380f)
+ .setTopologyKind(TopologyKind.BUS_BREAKER)
+ .add();
+ Bus b1 = vl.getBusBreakerView().newBus()
+ .setId(BUS_1_ID)
+ .add();
+ b1.setV(412.3f);
+ b1.setAngle(0f);
+ Bus b2 = vl.getBusBreakerView().newBus()
+ .setId(BUS_2_ID)
+ .add();
+ b2.setV(412.3f);
+ b2.setAngle(0f);
+ Generator g1 = vl.newGenerator()
+ .setId(GEN_ID)
+ .setBus(BUS_1_ID)
+ .setConnectableBus(BUS_1_ID)
+ .setMinP(220f)
+ .setMaxP(910f)
+ .setTargetP(910f)
+ .setTargetQ(79f)
+ .setTargetV(412.3f)
+ .setVoltageRegulatorOn(true)
+ .add();
+ g1.getTerminal().setP(910f).setQ(79f);
+ g1.newReactiveCapabilityCurve()
+ .beginPoint()
+ .setP(220f)
+ .setMinQ(-520f)
+ .setMaxQ(300f)
+ .endPoint()
+ .beginPoint()
+ .setP(910f)
+ .setMinQ(-600f)
+ .setMaxQ(300f)
+ .endPoint()
+ .add();
+ vl.newLoad()
+ .setId(AUX_ID)
+ .setBus(BUS_2_ID)
+ .setConnectableBus(BUS_2_ID)
+ .setP0(2)
+ .setQ0(3)
+ .add();
+
+ vl.newLoad()
+ .setId(AUX_2_ID)
+ .setBus(BUS_2_ID)
+ .setConnectableBus(BUS_2_ID)
+ .setP0(2)
+ .setQ0(3)
+ .add();
+
+ vl.getBusBreakerView().newSwitch()
+ .setId(SWITCH_ID)
+ .setBus1(BUS_1_ID)
+ .setBus2(BUS_2_ID)
+ .add();
+
+ Substation sub2 = n.newSubstation()
+ .setId("sub2")
+ .setCountry(Country.FR)
+ .add();
+ VoltageLevel vl2 = sub2.newVoltageLevel()
+ .setId(VL2)
+ .setNominalV(380f)
+ .setTopologyKind(TopologyKind.BUS_BREAKER)
+ .add();
+ Bus b3 = vl2.getBusBreakerView().newBus()
+ .setId(BUS_3_ID)
+ .add();
+ b3.setV(412.3f);
+ b3.setAngle(0f);
+ Bus b4 = vl2.getBusBreakerView().newBus()
+ .setId(BUS_4_ID)
+ .add();
+ b4.setV(412.3f);
+ b4.setAngle(0f);
+ Bus b5 = vl2.getBusBreakerView().newBus()
+ .setId(BUS_5_ID)
+ .add();
+ b5.setV(412.3f);
+ b5.setAngle(30f);
+ vl2.newLoad()
+ .setId(AUX_3_ID)
+ .setBus(BUS_3_ID)
+ .setConnectableBus(BUS_3_ID)
+ .setP0(2)
+ .setQ0(3)
+ .add();
+
+ vl2.newLoad()
+ .setId(AUX_5_ID)
+ .setBus(BUS_5_ID)
+ .setConnectableBus(BUS_5_ID)
+ .setP0(2)
+ .setQ0(3)
+ .add();
+
+ vl2.getBusBreakerView().newSwitch()
+ .setId(SWITCH_2_ID)
+ .setBus1(BUS_3_ID)
+ .setBus2(BUS_4_ID)
+ .add();
+ vl2.getBusBreakerView().newSwitch()
+ .setId(SWITCH_3_ID)
+ .setBus1(BUS_4_ID)
+ .setBus2(BUS_5_ID)
+ .add();
+ Generator g2 = vl2.newGenerator()
+ .setId(GEN_2_ID)
+ .setBus(BUS_3_ID)
+ .setConnectableBus(BUS_3_ID)
+ .setMinP(220f)
+ .setMaxP(910f)
+ .setTargetP(910f)
+ .setTargetQ(79f)
+ .setTargetV(412.3f)
+ .setVoltageRegulatorOn(true)
+ .add();
+ g2.getTerminal().setP(910f).setQ(79f);
+ g2.newReactiveCapabilityCurve()
+ .beginPoint()
+ .setP(220f)
+ .setMinQ(-520f)
+ .setMaxQ(300f)
+ .endPoint()
+ .beginPoint()
+ .setP(910f)
+ .setMinQ(-600f)
+ .setMaxQ(300f)
+ .endPoint()
+ .add();
+ vl2.newStaticVarCompensator()
+ .setId("SVC2")
+ .setConnectableBus(BUS_5_ID)
+ .setBus(BUS_5_ID)
+ .setBmin(0.0002)
+ .setBmax(0.0008)
+ .setRegulationMode(StaticVarCompensator.RegulationMode.VOLTAGE)
+ .setVoltageSetPoint(390)
+ .add();
+ return n;
+ }
+
+ @Test
+ public void test() {
+ ConnectableSwitchesCollapserPostProcessor remover = new ConnectableSwitchesCollapserPostProcessor();
+ Network network = createNetwork();
+ try {
+ remover.process(network, null);
+ } catch (Exception e) {
+ Assert.fail(e.getMessage());
+ }
+ Assert.assertNull(network.getSwitch(SWITCH_ID));
+ Assert.assertNull(network.getVoltageLevel(VL1).getBusBreakerView().getBus(BUS_1_ID));
+ Optional gen = network.getVoltageLevel(VL1).getGeneratorStream().filter(g -> g.getId().equals(GEN_ID)).findFirst();
+ Assert.assertTrue(gen.get().getTerminals().get(0).getBusBreakerView().getBus().getId().equals(BUS_2_ID));
+
+ Assert.assertNull(network.getSwitch(SWITCH_2_ID));
+ Assert.assertNull(network.getVoltageLevel(VL2).getBusBreakerView().getBus(BUS_4_ID));
+ }
+
+
+}
diff --git a/pom.xml b/pom.xml
index 71f63b00..b1cbdc96 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,6 +22,7 @@
case-projector
case-repository
+ connectable-switches-collapser
distribution
dymola-integration
entsoe-case-repository