-
Notifications
You must be signed in to change notification settings - Fork 43
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
CGMES. Import input with multiple grid models as a network with subnetworks #2775
Changes from 11 commits
ccac6f5
7134949
7d68f13
ae21520
b7de144
08f490b
2acae88
0845d63
592f1b2
59a206c
9f64755
1245d6b
0e39a5f
5531769
1b5803c
a0cc2eb
8e9f4c1
d064b8a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,11 +9,11 @@ | |
|
||
import com.google.auto.service.AutoService; | ||
import com.google.common.io.ByteStreams; | ||
import com.powsybl.cgmes.model.CgmesModel; | ||
import com.powsybl.cgmes.model.CgmesModelFactory; | ||
import com.powsybl.cgmes.model.CgmesOnDataSource; | ||
import com.powsybl.cgmes.model.*; | ||
import com.powsybl.commons.PowsyblException; | ||
import com.powsybl.commons.config.PlatformConfig; | ||
import com.powsybl.commons.datasource.DataSource; | ||
import com.powsybl.commons.datasource.DataSourceUtil; | ||
import com.powsybl.commons.datasource.GenericReadOnlyDataSource; | ||
import com.powsybl.commons.datasource.ReadOnlyDataSource; | ||
import com.powsybl.commons.parameters.Parameter; | ||
|
@@ -30,6 +30,10 @@ | |
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import javax.xml.stream.XMLInputFactory; | ||
import javax.xml.stream.XMLStreamConstants; | ||
import javax.xml.stream.XMLStreamException; | ||
import javax.xml.stream.XMLStreamReader; | ||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.io.OutputStream; | ||
|
@@ -38,18 +42,26 @@ | |
import java.nio.file.Path; | ||
import java.nio.file.Paths; | ||
import java.util.*; | ||
import java.util.function.Predicate; | ||
import java.util.stream.Collectors; | ||
|
||
import static java.util.function.Predicate.not; | ||
|
||
/** | ||
* @author Luma Zamarreño {@literal <zamarrenolm at aia.es>} | ||
*/ | ||
@AutoService(Importer.class) | ||
public class CgmesImport implements Importer { | ||
|
||
enum FictitiousSwitchesCreationMode { | ||
public enum FictitiousSwitchesCreationMode { | ||
ALWAYS, | ||
ALWAYS_EXCEPT_SWITCHES, | ||
NEVER; | ||
NEVER | ||
} | ||
|
||
public enum AssembledSeparatingBy { | ||
NAME, | ||
MODELING_AUTHORITY | ||
} | ||
|
||
public CgmesImport(PlatformConfig platformConfig, List<CgmesImportPreProcessor> preProcessors, List<CgmesImportPostProcessor> postProcessors) { | ||
|
@@ -134,11 +146,198 @@ public Network importData(ReadOnlyDataSource ds, NetworkFactory networkFactory, | |
Objects.requireNonNull(ds); | ||
Objects.requireNonNull(networkFactory); | ||
Objects.requireNonNull(reporter); | ||
if (Parameter.readBoolean(getFormat(), p, IMPORT_ASSEMBLED_AS_SUBNETWORKS_PARAMETER, defaultValueConfig)) { | ||
AssembledSeparatingBy separatingBy = AssembledSeparatingBy.valueOf(Parameter.readString(getFormat(), | ||
p, IMPORT_ASSEMBLED_AS_SUBNETWORKS_SEPARATING_BY_PARAMETER, defaultValueConfig)); | ||
Set<ReadOnlyDataSource> dss = new AssembledChecker(ds).separate(separatingBy); | ||
if (dss.size() > 1) { | ||
return Network.merge(dss.stream() | ||
.map(ds1 -> importData1(ds1, networkFactory, p, reporter)) | ||
.toArray(Network[]::new)); | ||
} | ||
} | ||
return importData1(ds, networkFactory, p, reporter); | ||
} | ||
|
||
private Network importData1(ReadOnlyDataSource ds, NetworkFactory networkFactory, Properties p, Reporter reporter) { | ||
CgmesModel cgmes = readCgmes(ds, p, reporter); | ||
Reporter conversionReporter = reporter.createSubReporter("CGMESConversion", "Importing CGMES file(s)"); | ||
return new Conversion(cgmes, config(ds, p), activatedPreProcessors(p), activatedPostProcessors(p), networkFactory).convert(conversionReporter); | ||
} | ||
|
||
static class FilteredReadOnlyDataSource implements ReadOnlyDataSource { | ||
private final ReadOnlyDataSource ds; | ||
private final Predicate<String> filter; | ||
|
||
FilteredReadOnlyDataSource(ReadOnlyDataSource ds, Predicate<String> filter) { | ||
this.ds = ds; | ||
this.filter = filter; | ||
} | ||
|
||
@Override | ||
public String getBaseName() { | ||
return ds.getBaseName(); | ||
} | ||
|
||
@Override | ||
public boolean exists(String suffix, String ext) throws IOException { | ||
return ds.exists(suffix, ext) && filter.test(DataSourceUtil.getFileName(getBaseName(), suffix, ext)); | ||
} | ||
|
||
@Override | ||
public boolean exists(String fileName) throws IOException { | ||
return ds.exists(fileName) && filter.test(fileName); | ||
} | ||
|
||
@Override | ||
public InputStream newInputStream(String suffix, String ext) throws IOException { | ||
if (filter.test(DataSourceUtil.getFileName(getBaseName(), suffix, ext))) { | ||
return ds.newInputStream(suffix, ext); | ||
} | ||
throw new IOException(DataSourceUtil.getFileName(getBaseName(), suffix, ext) + " not found"); | ||
} | ||
|
||
@Override | ||
public InputStream newInputStream(String fileName) throws IOException { | ||
if (filter.test(fileName)) { | ||
return ds.newInputStream(fileName); | ||
} | ||
throw new IOException(fileName + " not found"); | ||
} | ||
|
||
@Override | ||
public Set<String> listNames(String regex) throws IOException { | ||
return ds.listNames(regex).stream().filter(filter).collect(Collectors.toSet()); | ||
} | ||
} | ||
|
||
static class AssembledChecker { | ||
private final ReadOnlyDataSource dataSource; | ||
private XMLInputFactory xmlInputFactory; | ||
|
||
AssembledChecker(ReadOnlyDataSource dataSource) { | ||
this.dataSource = dataSource; | ||
} | ||
|
||
Set<ReadOnlyDataSource> separate(AssembledSeparatingBy separatingBy) { | ||
// If it is a CGM, create a filtered dataset for each IGM. | ||
// In the dataset for each IGM we must include: | ||
// - Its own files. | ||
// - The boundaries (we will read the boundaries multiple times, one for each IGM). | ||
// - Any other shared instance files (files that do not contain the name of any IGMs identified). | ||
// An example of shared file is the unique SV from a CGM solved case | ||
// Shared files will be also loaded multiple times, one for each IGM | ||
return switch (separatingBy) { | ||
case MODELING_AUTHORITY -> separateByModelingAuthority(); | ||
case NAME -> separateByIgmName(); | ||
}; | ||
} | ||
|
||
private Set<ReadOnlyDataSource> separateByModelingAuthority() { | ||
xmlInputFactory = XMLInputFactory.newInstance(); | ||
Map<String, List<String>> igmNames = new CgmesOnDataSource(dataSource).names().stream() | ||
// We consider IGMs only the modeling authorities that have an EQ file | ||
// The CGM SV should have the MA of the merging agent | ||
.filter(CgmesSubset.EQUIPMENT::isValidName) | ||
.map(name -> readModelingAuthority(name).map(ma -> Map.entry(ma, name))) | ||
.flatMap(Optional::stream) | ||
.collect(Collectors.toMap(Map.Entry::getKey, e -> new ArrayList<>(List.of(e.getValue())))); | ||
if (LOGGER.isInfoEnabled() && !igmNames.isEmpty()) { | ||
LOGGER.info("IGM EQ files identified by Modeling Authority:"); | ||
igmNames.forEach((k, v) -> LOGGER.info(" {} {}", k, v.get(0))); | ||
} | ||
// If we only have found one IGM there is no need to partition | ||
if (igmNames.size() == 1) { | ||
return Set.of(dataSource); | ||
} | ||
Set<String> shared = new HashSet<>(); | ||
new CgmesOnDataSource(dataSource).names().stream() | ||
// We read the modeling authorities present in the rest of instance files | ||
// and mark the instance name as linked to an IGM or as shared | ||
.filter(not(CgmesSubset.EQUIPMENT::isValidName)) | ||
.filter(not(AssembledChecker::isBoundary)) | ||
.forEach(name -> { | ||
Optional<String> ma = readModelingAuthority(name); | ||
if (ma.isPresent() && igmNames.containsKey(ma.get())) { | ||
igmNames.get(ma.get()).add(name); | ||
} else { | ||
shared.add(name); | ||
} | ||
}); | ||
// Build one data source for each IGM found | ||
if (LOGGER.isInfoEnabled() && !igmNames.isEmpty()) { | ||
LOGGER.info("IGM files identified by Modeling Authority:"); | ||
igmNames.forEach((k, v) -> LOGGER.info(" {} {}", k, String.join(",", v))); | ||
if (!shared.isEmpty()) { | ||
LOGGER.info("Shared files:"); | ||
shared.forEach(name -> LOGGER.info(" {}", name)); | ||
} | ||
LOGGER.info("Boundaries:"); | ||
try { | ||
dataSource.listNames(".*").stream().filter(name -> isBoundary(name)).forEach(name -> LOGGER.info(" {}", name)); | ||
} catch (IOException e) { | ||
throw new PowsyblException(e); | ||
} | ||
} | ||
return igmNames.keySet().stream() | ||
.map(ma -> new FilteredReadOnlyDataSource(dataSource, | ||
name -> isBoundary(name) || igmNames.get(ma).contains(name) || shared.contains(name))) | ||
.collect(Collectors.toSet()); | ||
} | ||
|
||
private Optional<String> readModelingAuthority(String name) { | ||
String modellingAuthority = null; | ||
try (InputStream is = dataSource.newInputStream(name)) { | ||
XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(is); | ||
boolean stopReading = false; | ||
while (reader.hasNext() && !stopReading) { | ||
int token = reader.next(); | ||
if (token == XMLStreamConstants.START_ELEMENT && reader.getLocalName().equals(CgmesNames.MODELING_AUTHORITY_SET)) { | ||
modellingAuthority = reader.getElementText(); | ||
stopReading = true; | ||
} else if (token == XMLStreamConstants.END_ELEMENT && reader.getLocalName().equals(CgmesNames.FULL_MODEL)) { | ||
// Try to finish parsing the input file as soon as we can | ||
// If we do not have found a modelling authority set inside the FullModel object, exit with unknown | ||
stopReading = true; | ||
} | ||
} | ||
reader.close(); | ||
} catch (IOException | XMLStreamException e) { | ||
throw new PowsyblException(e); | ||
} | ||
return Optional.ofNullable(modellingAuthority); | ||
} | ||
|
||
private Set<ReadOnlyDataSource> separateByIgmName() { | ||
// Here we obtain the IGM name from the EQ filenames, | ||
// and rely on it to find related SSH, TP files, | ||
Set<String> igmNames = new CgmesOnDataSource(dataSource).names().stream() | ||
.filter(CgmesSubset.EQUIPMENT::isValidName) | ||
// We rely on the CIMXML pattern: | ||
// <effectiveDateTime>_<businessProcess>_<sourcingActor>_<modelPart>_<fileVersion> | ||
// we define igmName := sourcingActor | ||
.map(name -> name.split("_")[2]) | ||
.collect(Collectors.toSet()); | ||
return igmNames.stream() | ||
.map(igmName -> new FilteredReadOnlyDataSource(dataSource, name -> name.contains(igmName) | ||
|| isBoundary(name) | ||
|| isShared(name, igmNames))) | ||
.collect(Collectors.toSet()); | ||
} | ||
|
||
private static boolean isBoundary(String name) { | ||
return CgmesSubset.EQUIPMENT_BOUNDARY.isValidName(name) || CgmesSubset.TOPOLOGY_BOUNDARY.isValidName(name); | ||
} | ||
|
||
private static boolean isShared(String name, Set<String> allIgmNames) { | ||
// The name does not contain the name of one the IGMs | ||
return allIgmNames.stream() | ||
.filter(name::contains) | ||
.findAny() | ||
.isEmpty(); | ||
} | ||
} | ||
|
||
public CgmesModel readCgmes(ReadOnlyDataSource ds, Properties p, Reporter reporter) { | ||
TripleStoreOptions options = new TripleStoreOptions(); | ||
String sourceForIidmIds = Parameter.readString(getFormat(), p, SOURCE_FOR_IIDM_ID_PARAMETER, defaultValueConfig); | ||
|
@@ -357,6 +556,8 @@ private void copyStream(ReadOnlyDataSource from, DataSource to, String fromName, | |
public static final String STORE_CGMES_CONVERSION_CONTEXT_AS_NETWORK_EXTENSION = "iidm.import.cgmes.store-cgmes-conversion-context-as-network-extension"; | ||
public static final String IMPORT_NODE_BREAKER_AS_BUS_BREAKER = "iidm.import.cgmes.import-node-breaker-as-bus-breaker"; | ||
public static final String DISCONNECT_DANGLING_LINE_IF_BOUNDARY_SIDE_IS_DISCONNECTED = "iidm.import.cgmes.disconnect-dangling-line-if-boundary-side-is-disconnected"; | ||
public static final String IMPORT_ASSEMBLED_AS_SUBNETWORKS = "iidm.import.cgmes.assembled-as-subnetworks"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See above, for me There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see you already made the change |
||
public static final String IMPORT_ASSEMBLED_AS_SUBNETWORKS_SEPARATING_BY = "iidm.import.cgmes.assembled-as-subnetworks-separating-by"; | ||
|
||
public static final String SOURCE_FOR_IIDM_ID_MRID = "mRID"; | ||
public static final String SOURCE_FOR_IIDM_ID_RDFID = "rdfID"; | ||
|
@@ -471,6 +672,17 @@ private void copyStream(ReadOnlyDataSource from, DataSource to, String fromName, | |
ParameterType.BOOLEAN, | ||
"Force disconnection of dangling line network side if boundary side is disconnected", | ||
Boolean.TRUE); | ||
private static final Parameter IMPORT_ASSEMBLED_AS_SUBNETWORKS_PARAMETER = new Parameter( | ||
IMPORT_ASSEMBLED_AS_SUBNETWORKS, | ||
ParameterType.BOOLEAN, | ||
"Import assembled models as subnetworks", | ||
Boolean.TRUE); | ||
private static final Parameter IMPORT_ASSEMBLED_AS_SUBNETWORKS_SEPARATING_BY_PARAMETER = new Parameter( | ||
IMPORT_ASSEMBLED_AS_SUBNETWORKS_SEPARATING_BY, | ||
ParameterType.STRING, | ||
"Choose how subnetworks from assembled input must be separated: based on filenames or by modeling authority", | ||
AssembledSeparatingBy.MODELING_AUTHORITY.name(), | ||
Arrays.stream(AssembledSeparatingBy.values()).map(Enum::name).collect(Collectors.toList())); | ||
|
||
private static final List<Parameter> STATIC_PARAMETERS = List.of( | ||
ALLOW_UNSUPPORTED_TAP_CHANGERS_PARAMETER, | ||
|
@@ -491,7 +703,9 @@ private void copyStream(ReadOnlyDataSource from, DataSource to, String fromName, | |
DECODE_ESCAPED_IDENTIFIERS_PARAMETER, | ||
CREATE_FICTITIOUS_SWITCHES_FOR_DISCONNECTED_TERMINALS_MODE_PARAMETER, | ||
IMPORT_NODE_BREAKER_AS_BUS_BREAKER_PARAMETER, | ||
DISCONNECT_DANGLING_LINE_IF_BOUNDARY_SIDE_IS_DISCONNECTED_PARAMETER); | ||
DISCONNECT_DANGLING_LINE_IF_BOUNDARY_SIDE_IS_DISCONNECTED_PARAMETER, | ||
IMPORT_ASSEMBLED_AS_SUBNETWORKS_PARAMETER, | ||
IMPORT_ASSEMBLED_AS_SUBNETWORKS_SEPARATING_BY_PARAMETER); | ||
|
||
private final Parameter boundaryLocationParameter; | ||
private final Parameter preProcessorsParameter; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,6 +25,7 @@ class CgmesConversionContextExtensionTest { | |
void test() { | ||
Properties properties = new Properties(); | ||
properties.put(CgmesImport.STORE_CGMES_CONVERSION_CONTEXT_AS_NETWORK_EXTENSION, "true"); | ||
properties.put(CgmesImport.IMPORT_ASSEMBLED_AS_SUBNETWORKS, "false"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you run all the test with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. some expected results in the unit tests were only valid if we import with the "single network" configuration. I do not think we have to adapt old unit tests to the new feature. We have already added specific unit tests for the new feature |
||
Network network = new CgmesImport().importData(CgmesConformity1Catalog.microGridBaseCaseBE().dataSource(), NetworkFactory.findDefault(), properties); | ||
CgmesConversionContextExtension extension = network.getExtension(CgmesConversionContextExtension.class); | ||
assertNotNull(extension); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I said in the previous PR, "assembled" is a name choice hard to understand for the community and for non CGMES expert. In Powsybl, assembled is a network. Here the question is just : do we want to add subnetworks in a network and how do we define a subnetwork.
NAME
is too global, maybe something likeFILE_NAME
(and you can keepMODELING_AUTHORITY
).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree with your proposal