From 0390b757d90f5b53f373d60afe6faad0b24a6a19 Mon Sep 17 00:00:00 2001 From: Christopher Pace Robinson Date: Fri, 17 Jul 2026 01:04:37 -0700 Subject: [PATCH 1/2] Add growth-to-STDP integration regression test and related serialization improvements - Introduced a new regression test for the growth-to-STDP integration, which serializes a grown network and deserializes it for STDP simulation. - Added configuration files for both the growth simulation () and the STDP simulation (). - Implemented a new method in the class to replace the connections subgraph during deserialization. - Updated the class to handle the import of the grown topology into a new connections object. - Enhanced the testing workflow to include the new regression test and its verification process. - Added expected output files for the STDP simulation results. - Updated documentation to reflect the new testing capabilities and usage instructions. --- .github/workflows/tests.yml | 13 ++ Simulator/Core/Model.cpp | 6 + Simulator/Core/Model.h | 7 + Simulator/Core/Serializer.cpp | 78 ++++++++++ .../GoodOutput/Cpu/test-growth-stdp-out.xml | 37 +++++ .../configfiles/test-growth-stdp-source.xml | 114 +++++++++++++++ .../configfiles/test-growth-stdp.xml | 136 ++++++++++++++++++ Testing/RunTests.sh | 32 +++++ docs/Developer/UnitTests.md | 36 +++++ 9 files changed, 459 insertions(+) create mode 100644 Testing/RegressionTesting/GoodOutput/Cpu/test-growth-stdp-out.xml create mode 100644 Testing/RegressionTesting/configfiles/test-growth-stdp-source.xml create mode 100644 Testing/RegressionTesting/configfiles/test-growth-stdp.xml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9bac1793f..56fdd29f4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -101,3 +101,16 @@ jobs: run: ./cgraphitti -c ../Testing/RegressionTesting/configfiles/test-medium-connected-long.xml - name: verify test-medium-connected-long run: ../Testing/RegressionTesting/compare_matrices ../Testing/RegressionTesting/GoodOutput/Cpu/test-medium-connected-long-out.xml ../Testing/RegressionTesting/TestOutput/test-medium-connected-long-out.xml + + # Growth-to-STDP integration: serialize a grown network, then deserialize it as the + # input for an STDP simulation and verify the STDP output. This is a sequential, + # dependent pipeline (the STDP run consumes the growth run's checkpoint), so it cannot + # be part of the parallel single-simulation tests above. + - id: gs_source + name: run growth source for growth-to-STDP + run: ./cgraphitti -c ../Testing/RegressionTesting/configfiles/test-growth-stdp-source.xml -s ../Testing/RegressionTesting/TestOutput/test-growth-stdp-checkpoint.xml + - id: gs_stdp + name: run STDP from grown network + run: ./cgraphitti -c ../Testing/RegressionTesting/configfiles/test-growth-stdp.xml -d ../Testing/RegressionTesting/TestOutput/test-growth-stdp-checkpoint.xml + - name: verify growth-to-STDP + run: ../Testing/RegressionTesting/compare_matrices ../Testing/RegressionTesting/GoodOutput/Cpu/test-growth-stdp-out.xml ../Testing/RegressionTesting/TestOutput/test-growth-stdp-out.xml diff --git a/Simulator/Core/Model.cpp b/Simulator/Core/Model.cpp index efa8a51cd..9d610bf45 100644 --- a/Simulator/Core/Model.cpp +++ b/Simulator/Core/Model.cpp @@ -172,6 +172,12 @@ Connections &Model::getConnections() const return *connections_; } +/// Replaces the Connections subgraph with a new instance, taking ownership. +void Model::setConnections(unique_ptr connections) +{ + connections_ = std::move(connections); +} + /// Get the Layout class object. /// @return Pointer to the Layout class object. Layout &Model::getLayout() const diff --git a/Simulator/Core/Model.h b/Simulator/Core/Model.h index 460e0bf80..185df4f97 100644 --- a/Simulator/Core/Model.h +++ b/Simulator/Core/Model.h @@ -42,6 +42,13 @@ class Model { /// Returns reference to Connections Connections &getConnections() const; + /// Replaces the Connections subgraph with a new instance, taking ownership. + /// + /// Used when deserializing a growth checkpoint into a different (e.g. STDP) model: + /// after the checkpoint is loaded, the grown topology is imported into a freshly + /// constructed Connections object and installed here. See Serializer::deserialize(). + void setConnections(unique_ptr connections); + /// Returns reference to Layout Layout &getLayout() const; diff --git a/Simulator/Core/Serializer.cpp b/Simulator/Core/Serializer.cpp index 1af504e2e..890f3d8f3 100644 --- a/Simulator/Core/Serializer.cpp +++ b/Simulator/Core/Serializer.cpp @@ -24,9 +24,14 @@ */ #include "Serializer.h" +#include "AllEdges.h" #include "ConnGrowth.h" +#include "Connections.h" +#include "Factory.h" #include "GPUModel.h" +#include "Model.h" #include "OperationManager.h" +#include "ParameterManager.h" #include // About CEREAL_XML_STRING_VALUE @@ -36,6 +41,67 @@ #include #include +namespace { + + /// Imports the grown network topology from a deserialized ConnGrowth checkpoint into a + /// freshly constructed Connections object of the type requested by the current run's + /// configuration file (for example ConnStatic with AllSTDPSynapses). + /// + /// This enables the output network of a growth simulation to be used as the starting + /// point ("input") for a subsequent STDP simulation: only the edge source, destination, + /// weight, and type are carried over. The restored vertices/layout and global simulation + /// state (RNG, simulation step) are left untouched. + /// + /// @param connectionClassName Connections class named in the current configuration file. + void importGrowthTopology(const string &connectionClassName) + { + Simulator &simulator = Simulator::getInstance(); + Model &model = simulator.getModel(); + + // Edges grown during the checkpointed growth simulation (still owned by the model). + AllEdges &grownEdges = model.getConnections().getEdges(); + + // Build the Connections/Edges objects requested by the current configuration file. + unique_ptr importedConnections + = Factory::getInstance().createType(connectionClassName); + if (importedConnections == nullptr) { + throw runtime_error("Deserialization topology import: unknown Connections class '" + + connectionClassName + "'"); + } + + AllEdges &importedEdges = importedConnections->getEdges(); + importedEdges.setupEdges(); + // Populate per-edge parameters (e.g. STDP constants) from the configuration file so that + // addEdge()/createEdge() initialize the new edges with the correct values. + importedEdges.loadParameters(); + + BGFLOAT deltaT = simulator.getDeltaT(); + BGSIZE importedCount = 0; + for (BGSIZE iEdg = 0; iEdg < grownEdges.inUse_.size(); iEdg++) { + if (grownEdges.inUse_[iEdg] == 0) { + continue; + } + int srcVertex = grownEdges.sourceVertexIndex_[iEdg]; + int destVertex = grownEdges.destVertexIndex_[iEdg]; + edgeType type = grownEdges.type_[iEdg]; + BGSIZE newEdg = importedEdges.addEdge(type, srcVertex, destVertex, deltaT); + importedEdges.W_[newEdg] = grownEdges.W_[iEdg]; + ++importedCount; + } + + // Install the new connection subgraph (destroys the checkpoint's ConnGrowth) and rebuild + // its edge index map from the imported edges. + model.setConnections(std::move(importedConnections)); + model.getConnections().createEdgeIndexMap(); + + log4cplus::Logger consoleLogger = log4cplus::Logger::getInstance(LOG4CPLUS_TEXT("console")); + LOG4CPLUS_INFO(consoleLogger, "Imported " << importedCount << " grown edges into a " + << connectionClassName + << " network for the current simulation."); + } + +} // namespace + /// Deserializes all member variables of the /// Connections, Layout, Edges, Vertices, and associated helper classes. /// @@ -66,6 +132,18 @@ bool Serializer::deserialize() return false; } + // If a growth checkpoint is being loaded into a non-growth (e.g. STDP) configuration, + // carry over only the grown topology rather than resuming the growth model. This is what + // enables using a growth simulation's output network as the input for an STDP simulation. + string connectionClassName; + ParameterManager::getInstance().getStringByXpath("//ConnectionsParams/@class", + connectionClassName); + bool checkpointIsGrowth + = dynamic_cast(&simulator.getModel().getConnections()) != nullptr; + if (checkpointIsGrowth && connectionClassName != "ConnGrowth") { + importGrowthTopology(connectionClassName); + } + // Deserialization rebuilds Connections/Layout subgraphs (and nested edges_/vertices_ // unique_ptrs). Constructors register OperationManager callbacks via std::bind(this, ...), // but destroyed objects leave stale entries that segfault on the next executeOperation(). diff --git a/Testing/RegressionTesting/GoodOutput/Cpu/test-growth-stdp-out.xml b/Testing/RegressionTesting/GoodOutput/Cpu/test-growth-stdp-out.xml new file mode 100644 index 000000000..9c9868ce3 --- /dev/null +++ b/Testing/RegressionTesting/GoodOutput/Cpu/test-growth-stdp-out.xml @@ -0,0 +1,37 @@ + + + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 + + + 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 + + + 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 1 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 1 2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 2 2 1 2 2 2 1 2 2 2 2 2 2 2 + + + 102695 104216 126232 162426 188819 190770 + + + 100007 116938 135037 151070 172143 185912 + + + 100749 109075 116463 118598 122407 138333 140142 141758 148424 151342 168901 183717 191945 + + + 102754 108353 110406 125856 162410 173350 179751 182987 186910 + + + 109677 114613 117544 119338 132427 138054 143270 150357 158933 164963 173002 174424 183694 186887 189155 190274 194910 + + + 107554 115269 116826 120492 122455 130386 132229 134708 141253 147423 149068 156490 158588 163903 167668 169617 171811 174424 175946 178047 179248 184349 186009 187713 195563 + + + 190761 193391 199116 + + + 103180 105295 116474 118786 123459 130782 132936 134262 137969 149350 152549 153678 155363 161192 162146 167142 170557 172113 174395 187966 189555 192201 195336 198036 199650 + + + 109044 111981 113415 116004 117654 127890 129016 129973 132523 139506 144408 145703 150069 154906 155673 158397 161765 163873 165357 169303 170739 172134 180880 187211 189009 192370 + diff --git a/Testing/RegressionTesting/configfiles/test-growth-stdp-source.xml b/Testing/RegressionTesting/configfiles/test-growth-stdp-source.xml new file mode 100644 index 000000000..861996a62 --- /dev/null +++ b/Testing/RegressionTesting/configfiles/test-growth-stdp-source.xml @@ -0,0 +1,114 @@ + + + + + ../configfiles/graphs/test-small.graphml + + 10.0 + 1 + + + 200 + 200 + + + 1 + 1 + + + + + + + 13.5e-09 + 13.5e-09 + + + 1.0e-09 + 1.5e-09 + + + 15.0e-03 + 15.0e-03 + + + 0.0 + 0.0 + + + 13.5e-03 + 13.5e-03 + + + 13.0e-03 + 13.0e-03 + + + 13.565e-3 + 13.655e-3 + + + 13.0e-3 + 13.0e-3 + + + + + + 6e-3 + 6e-3 + 3e-3 + 3e-3 + + + 0.8e-3 + 0.8e-3 + 0.8e-3 + 1.5e-3 + + + 0.32 + 0.25 + 0.05 + 0.5 + + + 0.144 + 0.7 + 0.125 + 1.1 + + + 0.06 + 0.02 + 1.2 + 0.05 + + + + + + + 0.60 + 0.10 + 0.0001 + 1.0 + 0.1 + 0.6 + + + + + + + + + ../Testing/RegressionTesting/TestOutput/test-growth-stdp-source-out.xml + + + + diff --git a/Testing/RegressionTesting/configfiles/test-growth-stdp.xml b/Testing/RegressionTesting/configfiles/test-growth-stdp.xml new file mode 100644 index 000000000..df6e8aebc --- /dev/null +++ b/Testing/RegressionTesting/configfiles/test-growth-stdp.xml @@ -0,0 +1,136 @@ + + + + + ../configfiles/graphs/test-small.graphml + + 10.0 + 1 + + + 200 + 200 + + + 1 + 1 + + + + + + + 13.5e-09 + 13.5e-09 + + + 1.0e-09 + 1.5e-09 + + + 15.0e-03 + 15.0e-03 + + + 0.0 + 0.0 + + + 13.5e-03 + 13.5e-03 + + + 13.0e-03 + 13.0e-03 + + + 13.565e-3 + 13.655e-3 + + + 13.0e-3 + 13.0e-3 + + + + + + 6e-3 + 6e-3 + 3e-3 + 3e-3 + + + 0.8e-3 + 0.8e-3 + 0.8e-3 + 1.5e-3 + + + 0.32 + 0.25 + 0.05 + 0.5 + + + 0.144 + 0.7 + 0.125 + 1.1 + + + 0.06 + 0.02 + 1.2 + 0.05 + + 2e-3 + + 6e-3 + 6e-3 + + + 6e-3 + 6e-3 + + + 6e-3 + 6e-3 + + + 6e-3 + 6e-3 + + + 6e-3 + 6e-3 + + + 6e-3 + 6e-3 + + + 6e-3 + 6e-3 + + + + + + + + + + + + ../Testing/RegressionTesting/TestOutput/test-growth-stdp-out.xml + + + + diff --git a/Testing/RunTests.sh b/Testing/RunTests.sh index 00b7c655b..1eadac5c7 100644 --- a/Testing/RunTests.sh +++ b/Testing/RunTests.sh @@ -110,6 +110,32 @@ function verify_outputs() { done } +# This function runs the growth-to-STDP integration regression test. It is a dependent, +# sequential pipeline (the STDP run consumes the growth run's serialized checkpoint), so it +# runs after the parallel single-simulation tests rather than alongside them. +function run_growth_to_stdp_test() { + local checkpoint=${TEST_OUT_DIR}/test-growth-stdp-checkpoint.xml + local stdp_out=${TEST_OUT_DIR}/test-growth-stdp-out.xml + local good_out=${GOOD_OUT_DIR}/test-growth-stdp-out.xml + + echo -e "${BLUE}[ RUN TEST ]${NC} Growth-to-STDP: serializing grown network" + ${GRAPHITTI} -c ${CONFIG_DIR}/test-growth-stdp-source.xml -s ${checkpoint} > /dev/null + + echo -e "${BLUE}[ RUN TEST ]${NC} Growth-to-STDP: running STDP from grown network" + ${GRAPHITTI} -c ${CONFIG_DIR}/test-growth-stdp.xml -d ${checkpoint} > /dev/null + + echo -e "${BLUE}[--------]${NC}Verifying growth-to-STDP simulation output...${NC}" + if (cmp -s ${stdp_out} ${good_out}); then + echo -e "${GREEN}[ ]${NC} Output file: ${stdp_out}" + echo -e "${GREEN}[ AND ]${NC} Good output: ${good_out}" + echo -e "${GREEN}[ PASSED ]${NC} Are equal" + else + echo -e "${RED}[ ]${NC} Output file: ${stdp_out}" + echo -e "${RED}[ AND ]${NC} Good output: ${good_out}" + echo -e "${RED}[ FAILED ]${NC} Are NOT equal" + fi +} + ############################################################################################ # SCRIPT STARTS HERE # ############################################################################################ @@ -153,3 +179,9 @@ wait echo echo -e "${BLUE}[========]${NC} Start verification" verify_outputs + +echo +echo -e "${BLUE}============================================================================${NC}" +echo -e "${BLUE}| GROWTH-TO-STDP INTEGRATION TEST |${NC}" +echo -e "${BLUE}============================================================================${NC}" +run_growth_to_stdp_test diff --git a/docs/Developer/UnitTests.md b/docs/Developer/UnitTests.md index 70d1e06cc..a9c1e6baa 100644 --- a/docs/Developer/UnitTests.md +++ b/docs/Developer/UnitTests.md @@ -25,5 +25,41 @@ To run the tests against the GPU implementation, inside the `Testing` directory **Note**: Currently, the GPU regresssion tests fail because the random numbers generated are different from the ones generated during the CPU execution, causing the result files to be different to the CPU known good results. +### Growth-to-STDP Integration Regression Test + +Serialization lets the output network of a growth simulation be used as the input for an STDP +simulation. When a growth checkpoint (which stores a `ConnGrowth`/`AllDSSynapses` network) is +deserialized into a configuration whose `ConnectionsParams` is a non-growth class (for example +`ConnStatic` with `AllSTDPSynapses`), the deserializer imports only the grown topology — each +edge's source, destination, weight, and type — into the network described by the current +configuration file. The restored vertices, layout, and global simulation state (RNG, simulation +step) are left as loaded from the checkpoint. This behavior lives in `Serializer::deserialize()`. + +A dedicated regression test exercises this end to end: + +- `Testing/RegressionTesting/configfiles/test-growth-stdp-source.xml` — a short growth run whose + start radius is large enough to grow a non-trivial set of edges on the `test-small` grid. It is + run with `-s` to serialize the grown network to a checkpoint. +- `Testing/RegressionTesting/configfiles/test-growth-stdp.xml` — an STDP run (empty `ConnStatic`, + `AllSTDPSynapses`, edge-free graph) that is run with `-d` pointing at that checkpoint, so every + edge in the run originates from the imported grown network. Its output is compared against + `Testing/RegressionTesting/GoodOutput/Cpu/test-growth-stdp-out.xml`. + +Because the STDP run depends on the growth run's checkpoint, this is a sequential pipeline. It runs +after the parallel single-simulation regression tests in both `RunTests.sh` and the +`tests.yml` GitHub action. To run it manually from the `build` directory: + + ./cgraphitti -c ../Testing/RegressionTesting/configfiles/test-growth-stdp-source.xml \ + -s ../Testing/RegressionTesting/TestOutput/test-growth-stdp-checkpoint.xml + ./cgraphitti -c ../Testing/RegressionTesting/configfiles/test-growth-stdp.xml \ + -d ../Testing/RegressionTesting/TestOutput/test-growth-stdp-checkpoint.xml + ../Testing/RegressionTesting/compare_matrices \ + ../Testing/RegressionTesting/GoodOutput/Cpu/test-growth-stdp-out.xml \ + ../Testing/RegressionTesting/TestOutput/test-growth-stdp-out.xml + +If the growth model, the STDP model, or the topology-import path changes in a way that alters the +grown network or how it is imported, this test's output diverges from the known-good file and the +test fails. + --------- [<< Go back to the Graphitti home page](../index.md) From 95be61c6e956d6921f4e7ea287836e8e83bcc1f2 Mon Sep 17 00:00:00 2001 From: Christopher Pace Robinson Date: Fri, 31 Jul 2026 17:18:19 -0700 Subject: [PATCH 2/2] Add unit tests for the growth-to-STDP topology import The regression test compares simulation output, which tells you that something changed but not whether the import mechanism itself is still correct. These tests serialize both stages of the pipeline and assert on the checkpoints: the STDP run must keep the classes named in its own configuration file rather than the ones restored from the growth checkpoint, and must receive every edge the growth run produced. Each stage is a separate executable because running two simulations against the same singleton instances segfaults, following the existing serialization tests. run_growth_stdp_test.sh sequences them, and CI runs it before the growth-to-STDP regression pipeline. --- .github/workflows/tests.yml | 7 ++ .gitignore | 2 + CMakeLists.txt | 24 +++++ Testing/UnitTesting/GrowthToStdpHelper.cpp | 102 ++++++++++++++++++ .../UnitTesting/GrowthToStdpImportTest.cpp | 72 +++++++++++++ .../UnitTesting/GrowthToStdpSourceTest.cpp | 53 +++++++++ build/run_growth_stdp_test.sh | 36 +++++++ docs/Developer/UnitTests.md | 24 +++++ 8 files changed, 320 insertions(+) create mode 100644 Testing/UnitTesting/GrowthToStdpHelper.cpp create mode 100644 Testing/UnitTesting/GrowthToStdpImportTest.cpp create mode 100644 Testing/UnitTesting/GrowthToStdpSourceTest.cpp create mode 100755 build/run_growth_stdp_test.sh diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 56fdd29f4..a2e88e52c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -106,6 +106,13 @@ jobs: # input for an STDP simulation and verify the STDP output. This is a sequential, # dependent pipeline (the STDP run consumes the growth run's checkpoint), so it cannot # be part of the parallel single-simulation tests above. + # The unit tests below check that the import mechanism itself behaves (the STDP run keeps + # its own classes and receives every grown edge); the regression run after them checks that + # the resulting simulation output has not changed. + - id: gs_unit + name: run growth-to-STDP unit tests + run: ./run_growth_stdp_test.sh + - id: gs_source name: run growth source for growth-to-STDP run: ./cgraphitti -c ../Testing/RegressionTesting/configfiles/test-growth-stdp-source.xml -s ../Testing/RegressionTesting/TestOutput/test-growth-stdp-checkpoint.xml diff --git a/.gitignore b/.gitignore index 9d0e23327..05f968f19 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,8 @@ serialFullTest serialFirstHalfTest serialSecondHalfTest serialFileAccessTest +growthStdpSourceTest +growthStdpImportTest core # core is generated by GDB during debugging diff --git a/CMakeLists.txt b/CMakeLists.txt index be871596c..a714b6f25 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -535,6 +535,30 @@ target_link_libraries(serialFirstHalfTest combinedLib) target_link_libraries(serialSecondHalfTest stdc++fs) target_link_libraries(serialSecondHalfTest combinedLib) +#------- GROWTH-TO-STDP INTEGRATION TESTS -------------------- +# Like the serialization tests above, each of these runs a simulation from start to finish and so +# needs its own executable. growthStdpImportTest consumes the checkpoint written by +# growthStdpSourceTest, so they must run in that order; run_growth_stdp_test.sh sequences them. + +add_executable(growthStdpSourceTest + Testing/RunTests.cpp + Testing/UnitTesting/GrowthToStdpSourceTest.cpp) + +add_executable(growthStdpImportTest + Testing/RunTests.cpp + Testing/UnitTesting/GrowthToStdpImportTest.cpp) + +# Links the Googletest framework to each of the growth-to-STDP test executables. +target_link_libraries(growthStdpSourceTest gtest gtest_main) +target_link_libraries(growthStdpImportTest gtest gtest_main) + +# Link the combined library and filesystem support to the respective test executables. +target_link_libraries(growthStdpSourceTest stdc++fs) +target_link_libraries(growthStdpSourceTest combinedLib) + +target_link_libraries(growthStdpImportTest stdc++fs) +target_link_libraries(growthStdpImportTest combinedLib) + # commenting out serialFileAccessTest until issue-754 is resolved # add_executable(serialFileAccessTest # Testing/RunTests.cpp diff --git a/Testing/UnitTesting/GrowthToStdpHelper.cpp b/Testing/UnitTesting/GrowthToStdpHelper.cpp new file mode 100644 index 000000000..a35a0654a --- /dev/null +++ b/Testing/UnitTesting/GrowthToStdpHelper.cpp @@ -0,0 +1,102 @@ +/** + * @file GrowthToStdpHelper.cpp + * + * @brief Helper functions shared by the growth-to-STDP integration tests. + * + * These tests verify that the network produced by a growth simulation can be used as the input + * network for a subsequent STDP simulation (see Serializer::deserialize()). Both stages assert on + * the contents of Cereal checkpoint files, so the helper here extracts the connection class, the + * edge class, and the active edge count from a checkpoint. + * + * @ingroup Testing/UnitTesting + */ + +#include "SerializationHelper.cpp" + +using namespace std; + +/// The parts of a Cereal checkpoint that the growth-to-STDP tests assert on. +struct CheckpointInfo { + string connectionsClass; + string edgesClass; + int totalEdgeCount = -1; +}; + +namespace { + + /// Recursively finds the first descendant element with the given name. + TiXmlElement *findFirstElement(TiXmlElement *parent, const string &name) + { + if (!parent) { + return nullptr; + } + for (TiXmlElement *child = parent->FirstChildElement(); child; + child = child->NextSiblingElement()) { + if (name == child->Value()) { + return child; + } + if (TiXmlElement *found = findFirstElement(child, name)) { + return found; + } + } + return nullptr; + } + + /// Returns the text of a direct child element, or an empty string when it is absent. + string childText(TiXmlElement *parent, const string &name) + { + if (!parent) { + return ""; + } + TiXmlElement *child = parent->FirstChildElement(name.c_str()); + const char *text = child ? child->GetText() : nullptr; + return text ? text : ""; + } + +} // namespace + +/// Reads the connection class, edge class, and active edge count from a Cereal checkpoint. +/// +/// Checkpoints nest the edges inside the connections, so the edge lookups are scoped to the +/// connections subtree instead of searching the whole document. +/// +/// @param path Path to a checkpoint written by Graphitti's `-s` option. +/// @param info Populated when every field is found. +/// @return true on success, false if the file cannot be read or a field is missing. +bool readCheckpointInfo(const string &path, CheckpointInfo &info) +{ + TiXmlDocument document; + if (!document.LoadFile(path.c_str())) { + cerr << "Failed to load checkpoint file: " << path << endl; + return false; + } + + TiXmlElement *connections = findFirstElement(document.RootElement(), "connections"); + if (!connections) { + cerr << "Checkpoint has no connections element: " << path << endl; + return false; + } + info.connectionsClass = childText(connections, "polymorphic_name"); + + TiXmlElement *edges = findFirstElement(connections, "edges"); + if (!edges) { + cerr << "Checkpoint has no edges element: " << path << endl; + return false; + } + info.edgesClass = childText(edges, "polymorphic_name"); + + TiXmlElement *edgeCount = findFirstElement(edges, "totalEdgeCount"); + const char *countText = edgeCount ? edgeCount->GetText() : nullptr; + if (!countText) { + cerr << "Checkpoint has no totalEdgeCount element: " << path << endl; + return false; + } + try { + info.totalEdgeCount = stoi(countText); + } catch (const exception &e) { + cerr << "Could not parse totalEdgeCount in " << path << ": " << e.what() << endl; + return false; + } + + return !info.connectionsClass.empty() && !info.edgesClass.empty(); +} diff --git a/Testing/UnitTesting/GrowthToStdpImportTest.cpp b/Testing/UnitTesting/GrowthToStdpImportTest.cpp new file mode 100644 index 000000000..a7b2aff65 --- /dev/null +++ b/Testing/UnitTesting/GrowthToStdpImportTest.cpp @@ -0,0 +1,72 @@ +/** + * @file GrowthToStdpImportTest.cpp + * + * @brief Second stage of the growth-to-STDP integration test: start an STDP simulation from a + * serialized growth network. + * + * STAGE 1: Run a short growth simulation (ConnGrowth with AllDSSynapses) and save its + * serialized output. See GrowthToStdpSourceTest.cpp. + * STAGE 2: Run an STDP simulation (ConnStatic with AllSTDPSynapses) that deserializes the + * checkpoint from stage 1, and verify that the grown topology was imported into the + * STDP network. Reference file: /Testing/RegressionTesting/configfiles/ + * test-growth-stdp.xml + * + * @note This test covers stage 2 and consumes the checkpoint written by stage 1, so the two + * executables must run in order; `run_growth_stdp_test.sh` in the `build` directory does + * that. + * + * The STDP simulation is asked to serialize its own final state as well. Comparing the two + * checkpoints is what makes the import observable: the classes must be the ones named in the STDP + * configuration file, while the edge count must be the one carried over from the growth run. + * + * @ingroup Testing/UnitTesting + */ + +#include "GrowthToStdpHelper.cpp" +#include "gtest/gtest.h" + +using namespace std; + +// Deserialize a grown network into an STDP simulation and verify the topology was imported +TEST(GrowthToStdpTest, ImportGrownNetworkIntoStdpSimulation) +{ + string executable = "./cgraphitti"; + + // Configuration file for the STDP simulation + string configFile = "../Testing/RegressionTesting/configfiles/test-growth-stdp.xml"; + + // Serialized grown network written by GrowthToStdpSourceTest + string growthCheckpoint = "../Testing/UnitTesting/TestOutput/growth-network-checkpoint.xml"; + + // Path to save the serialized output of the STDP simulation + string stdpCheckpoint = "../Testing/UnitTesting/TestOutput/stdp-from-growth-checkpoint.xml"; + + ASSERT_TRUE(fileExists(growthCheckpoint)) + << "Growth checkpoint does not exist. Run growthStdpSourceTest first."; + + CheckpointInfo grown; + ASSERT_TRUE(readCheckpointInfo(growthCheckpoint, grown)) << "Could not read growth checkpoint."; + ASSERT_GT(grown.totalEdgeCount, 0) << "Growth checkpoint contains no edges to import."; + + // Command-line arguments for the simulation + string arguments = "-c " + configFile + " -d " + growthCheckpoint + " -s " + stdpCheckpoint; + + // Run simulation + ASSERT_TRUE(runSimulation(executable, arguments)) + << "STDP simulation from the grown network failed."; + + // Check that the serialized file was created + ASSERT_TRUE(fileExists(stdpCheckpoint)) << "STDP checkpoint file does not exist."; + + CheckpointInfo imported; + ASSERT_TRUE(readCheckpointInfo(stdpCheckpoint, imported)) << "Could not read STDP checkpoint."; + + // Deserialization restores whatever classes the checkpoint holds, which used to overwrite the + // classes named in the configuration file and silently continue the growth simulation. + EXPECT_EQ("ConnStatic", imported.connectionsClass); + EXPECT_EQ("AllSTDPSynapses", imported.edgesClass); + + // The STDP network's edges come from the grown topology rather than from its own (edge-free) + // graph file, so every grown edge must survive the import. + EXPECT_EQ(grown.totalEdgeCount, imported.totalEdgeCount); +} diff --git a/Testing/UnitTesting/GrowthToStdpSourceTest.cpp b/Testing/UnitTesting/GrowthToStdpSourceTest.cpp new file mode 100644 index 000000000..ab2af7707 --- /dev/null +++ b/Testing/UnitTesting/GrowthToStdpSourceTest.cpp @@ -0,0 +1,53 @@ +/** + * @file GrowthToStdpSourceTest.cpp + * + * @brief First stage of the growth-to-STDP integration test: grow a network and serialize it. + * + * STAGE 1: Run a short growth simulation (ConnGrowth with AllDSSynapses) and save its + * serialized output. Reference file: /Testing/RegressionTesting/configfiles/ + * test-growth-stdp-source.xml + * STAGE 2: Run an STDP simulation (ConnStatic with AllSTDPSynapses) that deserializes the + * checkpoint from stage 1, and verify that the grown topology was imported into the + * STDP network. See GrowthToStdpImportTest.cpp. + * + * @note This test covers stage 1. The checkpoint it writes is the input to stage 2, so the two + * executables must run in order; `run_growth_stdp_test.sh` in the `build` directory does + * that. + * + * @ingroup Testing/UnitTesting + */ + +#include "GrowthToStdpHelper.cpp" +#include "gtest/gtest.h" + +using namespace std; + +// Run the growth simulation that produces the input network for the STDP simulation +TEST(GrowthToStdpTest, SerializeGrownNetwork) +{ + string executable = "./cgraphitti"; + + // Configuration file for the growth simulation + string configFile = "../Testing/RegressionTesting/configfiles/test-growth-stdp-source.xml"; + + // Path to save the serialized grown network + string growthCheckpoint = "../Testing/UnitTesting/TestOutput/growth-network-checkpoint.xml"; + + // Command-line arguments for the simulation + string arguments = "-c " + configFile + " -s " + growthCheckpoint; + + // Run simulation + ASSERT_TRUE(runSimulation(executable, arguments)) << "Growth simulation failed."; + + // Check that the serialized file was created + ASSERT_TRUE(fileExists(growthCheckpoint)) << "Growth checkpoint file does not exist."; + + CheckpointInfo grown; + ASSERT_TRUE(readCheckpointInfo(growthCheckpoint, grown)) << "Could not read growth checkpoint."; + + EXPECT_EQ("ConnGrowth", grown.connectionsClass); + + // Stage 2 has nothing to import unless the growth simulation actually grew edges, so guard + // against a configuration change that would quietly make this test vacuous. + EXPECT_GT(grown.totalEdgeCount, 0) << "Growth simulation produced no edges to import."; +} diff --git a/build/run_growth_stdp_test.sh b/build/run_growth_stdp_test.sh new file mode 100755 index 000000000..21812ec87 --- /dev/null +++ b/build/run_growth_stdp_test.sh @@ -0,0 +1,36 @@ +#! /bin/bash +############################################################################################ +# Script for running the Graphitti CPU growth-to-STDP integration tests. +# +# This script runs two tests, in order: +# 1. A growth simulation that serializes the network it grows. +# 2. An STDP simulation that deserializes that network and imports its topology. +# +# The second test consumes the checkpoint written by the first, so they cannot be reordered +# or run concurrently. They are separate executables because each one runs a simulation from +# start to finish, and running two simulations against the same singleton instances results +# in a segmentation fault. +# +# If either of the tests fail, the script will exit with an error message. +# +############################################################################################ + +# Run the growth simulation that produces the input network +echo "Running growth simulation test..." +./growthStdpSourceTest +if [ $? -ne 0 ]; then + echo "Error: Growth simulation test failed." + exit 1 +fi + +# Run the STDP simulation that starts from the grown network +echo "Running STDP-from-growth import test..." +./growthStdpImportTest +if [ $? -ne 0 ]; then + echo "Error: STDP-from-growth import test failed." + exit 1 +fi + +# If all tests pass +echo "All tests completed successfully." +echo "We grew a network, serialized it, and started an STDP simulation from it, and verified that the STDP simulation used the classes from its own configuration file with every edge from the grown network." diff --git a/docs/Developer/UnitTests.md b/docs/Developer/UnitTests.md index a9c1e6baa..89216b240 100644 --- a/docs/Developer/UnitTests.md +++ b/docs/Developer/UnitTests.md @@ -61,5 +61,29 @@ If the growth model, the STDP model, or the topology-import path changes in a wa grown network or how it is imported, this test's output diverges from the known-good file and the test fails. +### Growth-to-STDP Unit Tests + +The regression test above tells you *that* the simulation output changed; the unit tests tell you +*whether the import mechanism itself* is still correct. They run the same two-stage pipeline, but +both stages serialize their final state and the tests assert on the checkpoints: + +- `Testing/UnitTesting/GrowthToStdpSourceTest.cpp` — runs the growth configuration with `-s` and + checks that the checkpoint holds a `ConnGrowth` network with at least one edge, so that the + second stage has something to import. +- `Testing/UnitTesting/GrowthToStdpImportTest.cpp` — runs the STDP configuration with `-d` on that + checkpoint and `-s` on a new one, then checks that the resulting network uses the classes named + in the STDP configuration file (`ConnStatic` and `AllSTDPSynapses`) rather than the checkpoint's + own classes, and that it contains exactly as many edges as the growth run produced. + +Both files share `Testing/UnitTesting/GrowthToStdpHelper.cpp`, which pulls those fields out of a +Cereal checkpoint. + +As with the serialization tests, each stage is a separate executable: a test runs a simulation from +start to finish, and running two simulations against the same singleton instances causes a +segmentation fault. The second stage consumes the first stage's checkpoint, so they must run in +order. From the `build` directory: + + ./run_growth_stdp_test.sh + --------- [<< Go back to the Graphitti home page](../index.md)