diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 59caf0233..bbc9cb281 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -149,6 +149,7 @@ set(GIT_HEADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/namespaces.h ${CMAKE_CURRENT_SOURCE_DIR}/parentedentity_p.h ${CMAKE_CURRENT_SOURCE_DIR}/reset_p.h + ${CMAKE_CURRENT_SOURCE_DIR}/unionfind.h ${CMAKE_CURRENT_SOURCE_DIR}/units_p.h ${CMAKE_CURRENT_SOURCE_DIR}/utilities.h ${CMAKE_CURRENT_SOURCE_DIR}/variable_p.h diff --git a/src/internaltypes.h b/src/internaltypes.h index e21e35ed6..dcb67919b 100644 --- a/src/internaltypes.h +++ b/src/internaltypes.h @@ -42,13 +42,14 @@ using UniqueNames = std::set; /**< Type definition for a set of uni using NodeAttributeNamespaceInfo = std::vector>; /**< Type definition for attribute namespace information. */ // VariableMap +using VariableStdPair = std::pair; /**< Type definition for Variable pointer pair using standard libary. */ using VariableMap = std::vector; /**< Type definition for vector of VariablePair.*/ using VariableMapIterator = VariableMap::const_iterator; /**< Type definition of const iterator for vector of VariablePair.*/ // ComponentMap -using ComponentPair = std::pair; /**< Type definition for Component pointer pair.*/ -using ComponentMap = std::vector; /**< Type definition for vector of ComponentPair.*/ -using ComponentMapIterator = ComponentMap::const_iterator; /**< Type definition of const iterator for vector of ComponentPair.*/ +using ComponentStdPair = std::pair; /**< Type definition for Component pointer pair using standard library.*/ +using ComponentMap = std::vector; /**< Type definition for vector of ComponentStdPair.*/ +using ComponentMapIterator = ComponentMap::const_iterator; /**< Type definition of const iterator for vector of ComponentStdPair.*/ using VariablePtrs = std::vector; /**< Type definition for list of variables. */ @@ -79,6 +80,25 @@ using UnitsConstPtr = std::shared_ptr; /**< Type definition for sha using ConnectionMap = std::map; /**< Type definition for a connection map.*/ using NamePairList = std::vector; /**< Type definition for a list of a pair of names. */ +using ComponentRawPtrPair = std::pair; +using ConnectionIdMap = std::map; + +struct ComponentPair { + + bool operator==(const ComponentPair& other) const { + return c1 == other.c1 && c2 == other.c2; + } + + ComponentPtr c1; + ComponentPtr c2; +}; + +struct ComponentPairHash { + size_t operator()(const ComponentPair& p) const { + return std::hash()(p.c1) ^ std::hash()(p.c2); + } +}; + /** * @brief Class for defining an epoch in the history of a @ref Component or @ref Units. * diff --git a/src/printer.cpp b/src/printer.cpp index 6a39d3a5f..d88bed4e1 100644 --- a/src/printer.cpp +++ b/src/printer.cpp @@ -77,7 +77,7 @@ std::string printConnections(const ComponentMap &componentMap, const VariableMap for (auto iterPair = componentMap.begin(); iterPair < componentMap.end(); ++iterPair) { ComponentPtr currentComponent1 = iterPair->first; ComponentPtr currentComponent2 = iterPair->second; - ComponentPair currentComponentPair = std::make_pair(currentComponent1, currentComponent2); + ComponentStdPair currentComponentPair = std::make_pair(currentComponent1, currentComponent2); // Check whether this set of connections has already been serialised. bool pairFound = false; for (const auto &serialisedIterPair : serialisedComponentMap) { @@ -178,7 +178,7 @@ void buildMapsForComponentsVariables(const ComponentPtr &component, ComponentMap ComponentPtr component1 = owningComponent(variable); ComponentPtr component2 = owningComponent(equivalentVariable); // Also create a component map pair corresponding with the variable map pair. - ComponentPair componentPair = std::make_pair(component1, component2); + ComponentStdPair componentPair = std::make_pair(component1, component2); componentMap.push_back(componentPair); } } diff --git a/src/unionfind.h b/src/unionfind.h new file mode 100644 index 000000000..f421fb350 --- /dev/null +++ b/src/unionfind.h @@ -0,0 +1,60 @@ +/* +Copyright libCellML Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include + +template +class UnionFind { +public: + T find(const T& x) { + auto it = parent.find(x); + if (it == parent.end()) { + parent[x] = x; + rank[x] = 0; + return x; + } + + if (it->second != x) { + it->second = find(it->second); // Path compression + } + return it->second; + } + + void unite(const T& a, const T& b) { + T rootA = find(a); + T rootB = find(b); + + if (rootA == rootB) return; + + // Union by rank + if (rank[rootA] < rank[rootB]) { + parent[rootA] = rootB; + } else if (rank[rootA] > rank[rootB]) { + parent[rootB] = rootA; + } else { + parent[rootB] = rootA; + rank[rootA]++; + } + } + + bool connected(const T& a, const T& b) { + return find(a) == find(b); + } + +private: + std::unordered_map parent; + std::unordered_map rank; +}; diff --git a/src/validator.cpp b/src/validator.cpp index 55a1db118..976740f51 100644 --- a/src/validator.cpp +++ b/src/validator.cpp @@ -35,6 +35,7 @@ limitations under the License. #include "issue_p.h" #include "logger_p.h" #include "namespaces.h" +#include "unionfind.h" #include "utilities.h" #include "xmldoc.h" #include "xmlutils.h" @@ -639,8 +640,9 @@ class Validator::ValidatorImpl: public LoggerImpl * @param component The component to check. * @param idMap The IdMap object to construct. * @param reportedConnections A set of connection identifiers to prevent duplicate reporting. + * @param connectionIds A map of connection identifiers to prevent duplicate reporting of connections. */ - void buildComponentIdMap(const ComponentPtr &component, IdMap &idMap, std::set &reportedConnections); + void buildComponentIdMap(const ComponentPtr &component, IdMap &idMap, std::set &reportedConnections, const ConnectionIdMap &connectionIds); /** @brief Utility function to add an item to the idMap. * @@ -2692,11 +2694,102 @@ void Validator::ValidatorImpl::addIdMapItem(const std::string &id, const std::st } } +void gatherComponents(const ComponentPtr &component, std::vector &allComponents) +{ + allComponents.push_back(component); + for (size_t c = 0; c < component->componentCount(); ++c) { + gatherComponents(component->component(c), allComponents); + } +} + IdMap Validator::ValidatorImpl::buildModelIdMap(const ModelPtr &model) { + UnionFind uf; + + // Traverse all components and variables + for (size_t c = 0; c < model->componentCount(); ++c) { + auto component = model->component(c); + + for (size_t i = 0; i < component->variableCount(); ++i) { + auto v = component->variable(i); + + for (size_t e = 0; e < v->equivalentVariableCount(); ++e) { + auto equiv = v->equivalentVariable(e); + + if (equiv != nullptr) { + uf.unite(v, equiv); + } + } + } + } + + std::unordered_map> groups; + for (size_t c = 0; c < model->componentCount(); ++c) { + auto component = model->component(c); + + for (size_t i = 0; i < component->variableCount(); ++i) { + auto v = component->variable(i); + auto root = uf.find(v); + + groups[root].push_back(v); + } + } + + std::unordered_map, ComponentPairHash> connectionMap; + // using VarPair>, ComponentPairHash> connectionMap; + + // for (auto& [root, vars] : groups) { + // for (size_t i = 0; i < vars.size(); ++i) { + // for (size_t j = i + 1; j < vars.size(); ++j) { + // auto v1 = vars[i]; + // auto v2 = vars[j]; + + // auto c1 = owningComponent(v1); + // auto c2 = owningComponent(v2); + + // if (!c1 || !c2 || c1 == c2) continue; + + // // Normalize ordering + // ComponentPair key = (c1 < c2) + // ? ComponentPair{c1, c2} + // : ComponentPair{c2, c1}; + + // connectionMap[key].emplace_back(v1, v2); + // } + // } + // } + IdMap idMap; std::string info; std::set reportedConnections; + + std::vector allComponents; + for (size_t c = 0; c < model->componentCount(); ++c) { + gatherComponents(model->component(c), allComponents); + } + + ConnectionIdMap connectionIds; + for (const auto &comp : allComponents) { + for (size_t i = 0; i < comp->variableCount(); ++i) { + auto item = comp->variable(i); + for (size_t e = 0; e < item->equivalentVariableCount(); ++e) { + auto equiv = item->equivalentVariable(e); + auto equivParent = owningComponent(equiv); + if (equivParent != nullptr) { + // Normalize the key order (min pointer first, max pointer second) + auto key = comp.get() < equivParent.get() + ? std::make_pair(comp.get(), equivParent.get()) + : std::make_pair(equivParent.get(), comp.get()); + + // If we haven't processed this component connection yet, do it once + if (connectionIds.find(key) == connectionIds.end()) { + connectionIds[key] = ""; //Variable::equivalenceConnectionId(item, equiv); + } + } + } + } + } + // Model. if (!model->id().empty()) { info = " - model '" + model->name() + "'"; @@ -2748,12 +2841,12 @@ IdMap Validator::ValidatorImpl::buildModelIdMap(const ModelPtr &model) // Start recursion through encapsulation hierarchy. for (size_t c = 0; c < model->componentCount(); ++c) { - buildComponentIdMap(model->component(c), idMap, reportedConnections); + buildComponentIdMap(model->component(c), idMap, reportedConnections, connectionIds); } return idMap; } -void Validator::ValidatorImpl::buildComponentIdMap(const ComponentPtr &component, IdMap &idMap, std::set &reportedConnections) +void Validator::ValidatorImpl::buildComponentIdMap(const ComponentPtr &component, IdMap &idMap, std::set &reportedConnections, const ConnectionIdMap &connectionIds) { std::string info; @@ -2807,7 +2900,12 @@ void Validator::ValidatorImpl::buildComponentIdMap(const ComponentPtr &component addIdMapItem(mappingId, info, idMap); } // Connections. - auto connectionId = Variable::equivalenceConnectionId(item, equiv); + auto key = component.get() < equivParent.get() + ? std::make_pair(component.get(), equivParent.get()) + : std::make_pair(equivParent.get(), component.get()); + + auto connectionId = connectionIds.at(key); + // auto connectionId = Variable::equivalenceConnectionId(item, equiv); std::string connection = component->name() < equivParent->name() ? component->name() + equivParent->name() : equivParent->name() + component->name(); if ((s1 < s2) && !connectionId.empty() && (reportedConnections.count(connection) == 0)) { std::string connectionDescription = @@ -2879,7 +2977,7 @@ void Validator::ValidatorImpl::buildComponentIdMap(const ComponentPtr &component // Child components. for (size_t c = 0; c < component->componentCount(); ++c) { - buildComponentIdMap(component->component(c), idMap, reportedConnections); + buildComponentIdMap(component->component(c), idMap, reportedConnections, connectionIds); } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1dbf26da8..b66e31cdc 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -47,6 +47,7 @@ include(units/tests.cmake) include(validator/tests.cmake) include(variable/tests.cmake) include(version/tests.cmake) +include(investigations/tests.cmake) set(TEST_EXPORTDEFINITIONS_H "${CMAKE_CURRENT_BINARY_DIR}/test_exportdefinitions.h") diff --git a/tests/investigations/investigations.cpp b/tests/investigations/investigations.cpp new file mode 100644 index 000000000..d5490872b --- /dev/null +++ b/tests/investigations/investigations.cpp @@ -0,0 +1,172 @@ +/* +Copyright libCellML Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "gtest/gtest.h" +#include + +#include + +#include "test_utils.h" + +const char* BENCHMARKING_MODEL_ROOT = std::getenv("BENCHMARKING_MODEL_ROOT"); + +TEST(Investigations, exponentialTimeConsumption11) +{ + const std::string modelPath = std::string(BENCHMARKING_MODEL_ROOT) + "image_to_model_11_vessels/image_to_model.cellml"; + const std::string modelImportPath = std::string(BENCHMARKING_MODEL_ROOT) + "image_to_model_11_vessels/"; + auto importer = libcellml::Importer::create(false); + + auto parser = libcellml::Parser::create(false); + auto originalModel = parser->parseModel(fileContents(modelPath, true)); + EXPECT_EQ(size_t(1), parser->issueCount()); + Debug() << modelPath; + Debug() << parser->issue(0)->description(); + Debug() << originalModel->name(); + + + importer->resolveImports(originalModel, modelImportPath); + + EXPECT_EQ(size_t(3), importer->issueCount()); + + auto flatModel = importer->flattenModel(originalModel); + EXPECT_EQ(size_t(0), importer->issueCount()); + EXPECT_NE(nullptr, flatModel); + + auto analyser = libcellml::Analyser::create(); + analyser->analyseModel(flatModel); + EXPECT_EQ(size_t(0), analyser->issueCount()); + printIssues(analyser); +} + +TEST(Investigations, DISABLED_exponentialTimeConsumption246) +{ + const std::string modelPath = std::string(BENCHMARKING_MODEL_ROOT) + "image_to_model_246_vessels/image_to_model.cellml"; + const std::string modelImportPath = std::string(BENCHMARKING_MODEL_ROOT) + "image_to_model_246_vessels/"; + auto importer = libcellml::Importer::create(false); + + auto parser = libcellml::Parser::create(false); + auto originalModel = parser->parseModel(fileContents(modelPath, true)); + EXPECT_EQ(size_t(1), parser->issueCount()); + Debug() << parser->issue(0)->description(); + Debug() << originalModel->name(); + + + importer->resolveImports(originalModel, modelImportPath); + + EXPECT_EQ(size_t(3), importer->issueCount()); + + auto flatModel = importer->flattenModel(originalModel); + EXPECT_EQ(size_t(0), importer->issueCount()); + EXPECT_NE(nullptr, flatModel); + + auto analyser = libcellml::Analyser::create(); + analyser->analyseModel(flatModel); + EXPECT_EQ(size_t(0), analyser->issueCount()); + printIssues(analyser); +} + +TEST(Investigations, DISABLED_exponentialTimeConsumption380) +{ + const std::string modelPath = std::string(BENCHMARKING_MODEL_ROOT) + "image_to_model_380_vessels/image_to_model.cellml"; + const std::string modelImportPath = std::string(BENCHMARKING_MODEL_ROOT) + "image_to_model_380_vessels/"; + auto importer = libcellml::Importer::create(false); + + auto parser = libcellml::Parser::create(false); + auto originalModel = parser->parseModel(fileContents(modelPath, true)); + EXPECT_EQ(size_t(1), parser->issueCount()); + Debug() << modelPath; + Debug() << parser->issue(0)->description(); + Debug() << originalModel->name(); + + + importer->resolveImports(originalModel, modelImportPath); + + EXPECT_EQ(size_t(3), importer->issueCount()); + + auto flatModel = importer->flattenModel(originalModel); + EXPECT_EQ(size_t(0), importer->issueCount()); + EXPECT_NE(nullptr, flatModel); + + auto analyser = libcellml::Analyser::create(); + analyser->analyseModel(flatModel); + EXPECT_EQ(size_t(0), analyser->issueCount()); + printIssues(analyser); +} + +TEST(Investigations, DISABLED_exponentialTimeConsumption524) +{ + const std::string modelPath = std::string(BENCHMARKING_MODEL_ROOT) + "image_to_model_524_vessels/image_to_model.cellml"; + const std::string modelImportPath = std::string(BENCHMARKING_MODEL_ROOT) + "image_to_model_524_vessels/"; + auto importer = libcellml::Importer::create(false); + + auto parser = libcellml::Parser::create(false); + auto originalModel = parser->parseModel(fileContents(modelPath, true)); + EXPECT_EQ(size_t(1), parser->issueCount()); + Debug() << modelPath; + Debug() << parser->issue(0)->description(); + Debug() << originalModel->name(); + + + importer->resolveImports(originalModel, modelImportPath); + + EXPECT_EQ(size_t(3), importer->issueCount()); + + auto flatModel = importer->flattenModel(originalModel); + EXPECT_EQ(size_t(0), importer->issueCount()); + EXPECT_NE(nullptr, flatModel); + + auto analyser = libcellml::Analyser::create(); + analyser->analyseModel(flatModel); + EXPECT_EQ(size_t(0), analyser->issueCount()); + printIssues(analyser); +} + + +TEST(Investigations, DISABLED_exponentialTimeConsumptionOthers) +{ + const std::vector vesselCounts = { 246, 380, 524 }; + + auto importer = libcellml::Importer::create(false); + auto parser = libcellml::Parser::create(false); + auto printer = libcellml::Printer::create(); + auto analyser = libcellml::Analyser::create(); + auto generator = libcellml::Generator::create(); + + for (int vesselCount : vesselCounts) { + SCOPED_TRACE("Vessel count: " + std::to_string(vesselCount)); + std::string modelPath = std::string(BENCHMARKING_MODEL_ROOT) + "image_to_model_" + std::to_string(vesselCount) + "_vessels/image_to_model.cellml"; + std::string modelImportPath = std::string(BENCHMARKING_MODEL_ROOT) + "image_to_model_" + std::to_string(vesselCount) + "_vessels/"; + auto originalModel = parser->parseModel(fileContents(modelPath, true)); + EXPECT_EQ(size_t(1), parser->issueCount()); + + importer->resolveImports(originalModel, modelImportPath); + + EXPECT_EQ(size_t(0), importer->issueCount()); + for (size_t i = 0; i < importer->issueCount(); ++i) { + Debug() << "[" << i << "]: " << importer->issue(i)->description(); + } + + auto flatModel = importer->flattenModel(originalModel); + EXPECT_EQ(size_t(0), importer->issueCount()); + EXPECT_NE(nullptr, flatModel); + + analyser->analyseModel(flatModel); + EXPECT_EQ(size_t(0), analyser->issueCount()); + + const auto implementationCode = generator->implementationCode(analyser->analyserModel()); + EXPECT_NE(size_t(0), implementationCode.length()); + } +} diff --git a/tests/investigations/tests.cmake b/tests/investigations/tests.cmake new file mode 100644 index 000000000..1b01f81fa --- /dev/null +++ b/tests/investigations/tests.cmake @@ -0,0 +1,18 @@ + +# Set the test name, 'test_' will be prepended to the +# name set here +set(CURRENT_TEST investigations) +# Set a category name to enable running commands like: +# ctest -R +# which will run the tests matching this category-label. +# Can be left empty (or just not set) +set(${CURRENT_TEST}_CATEGORY misc) +list(APPEND LIBCELLML_TESTS ${CURRENT_TEST}) +# Using absolute path relative to this file +set(${CURRENT_TEST}_SRCS + ${CMAKE_CURRENT_LIST_DIR}/investigations.cpp +) +set(${CURRENT_TEST}_HDRS +) + + diff --git a/tests/test_utils.cpp b/tests/test_utils.cpp index 21f57ed54..27fa619c3 100644 --- a/tests/test_utils.cpp +++ b/tests/test_utils.cpp @@ -35,12 +35,18 @@ std::string resourcePath(const std::string &resourceRelativePath) return TESTS_RESOURCE_LOCATION + "/" + resourceRelativePath; } -std::string fileContents(const std::string &fileName) +std::string fileContents(const std::string &fileName, bool absoulte) { - std::ifstream file(resourcePath(fileName)); + std::ifstream file; + if (absoulte) { + file.open(fileName); + } else { + file.open(resourcePath(fileName)); + } std::stringstream buffer; buffer << file.rdbuf(); + file.close(); return buffer.str(); } diff --git a/tests/test_utils.h b/tests/test_utils.h index 119913868..c9c6347e9 100644 --- a/tests/test_utils.h +++ b/tests/test_utils.h @@ -82,7 +82,7 @@ std::chrono::steady_clock::time_point TEST_EXPORT timeNow(); int TEST_EXPORT elapsedTime(const std::chrono::steady_clock::time_point &startTime); std::string TEST_EXPORT resourcePath(const std::string &resourceRelativePath = ""); -std::string TEST_EXPORT fileContents(const std::string &fileName); +std::string TEST_EXPORT fileContents(const std::string &fileName, bool absoulte=false); void TEST_EXPORT printIssues(const libcellml::LoggerPtr &l, bool headings = false, bool cellmlElementTypes = false, bool rule = false); void TEST_EXPORT printModel(const libcellml::ModelPtr &model, bool includeMaths = true);