diff --git a/examples/mnist-client/CMakeLists.txt b/examples/mnist-client/CMakeLists.txt index 03560746..c0b66208 100644 --- a/examples/mnist-client/CMakeLists.txt +++ b/examples/mnist-client/CMakeLists.txt @@ -1,32 +1,24 @@ -#[[ -© 2024 AO Kaspersky Lab - -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. -]] - +#[[ +© 2024 AO Kaspersky Lab + +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. +]] + cmake_minimum_required(VERSION 3.25) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_EXTENSIONS OFF) -find_package(OpenCV) -if (NOT OpenCV_FOUND) - message(WARNING "OpenCV was not found. Skipping MNIST example...") - return() -endif() - -include_directories(${OpenCV_INCLUDE_DIRS}) - if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.30") # Suppress Boost warning. cmake_policy(SET CMP0167 OLD) @@ -45,4 +37,9 @@ add_executable(mnist-client process_data.cpp ) -target_link_libraries(mnist-client PRIVATE KNP::BaseFramework::Core ${OpenCV_LIBS} ${Boost_LIBRARIES}) +find_package(PkgConfig REQUIRED) +pkg_check_modules(GRAPHVIZ REQUIRED IMPORTED_TARGET libgvc libcgraph) + +target_include_directories(mnist-client PRIVATE ${GRAPHVIZ_INCLUDE_DIRS}) +target_link_libraries(mnist-client PRIVATE PkgConfig::GRAPHVIZ ) +target_link_libraries(mnist-client PRIVATE KNP::BaseFramework::Core ${Boost_LIBRARIES} ) diff --git a/examples/mnist-client/main.cpp b/examples/mnist-client/main.cpp index 6b25e0e0..ecbd43ee 100644 --- a/examples/mnist-client/main.cpp +++ b/examples/mnist-client/main.cpp @@ -74,15 +74,9 @@ int main(int argc, char **argv) // https://click.kaspersky.com/?hl=en-US&version=2.0&pid=KNP&link=online_help&helpid=235801 // https://click.kaspersky.com/?hl=en-US&version=2.0&pid=KNP&link=online_help&helpid=274991 knp::framework::Network network = knp::framework::sonata::load_network(network_path); - // Constructs a network graph. - const knp::framework::NetworkGraph net_graph(network); - // Prints descriptions of graph connections. - knp::framework::print_network_description(net_graph); - // Draws a subgraph in the OpenCV window. - // Press `Esc` to exit the OpenCV window. - knp::framework::position_network_test( - knp::framework::NetworkGraph(network), knp::framework::divide_graph_by_connectivity(net_graph)[0], - {1000, 700}); + + // knp::framework::set_saving_path("temp_test_dir"); + knp::framework::visualize_network(network); } // If `task=infer`, the function loads a network and runs inference. diff --git a/examples/mnist-client/process_data.cpp b/examples/mnist-client/process_data.cpp index a7cb5858..ccf1a9d5 100644 --- a/examples/mnist-client/process_data.cpp +++ b/examples/mnist-client/process_data.cpp @@ -27,9 +27,6 @@ #include #include -#include -#include - /** * @brief Filesystem namespace alias. @@ -37,28 +34,6 @@ namespace fs = std::filesystem; -/** - * @brief Visualize data instances. - * @param data vector of flattened images. - * @param size output image size. - */ -cv::Mat draw_data(const std::vector &data, const cv::Size &size) -{ - cv::Mat out_img(size, CV_8UC1); - for (int y = 0; y < size.height; ++y) - { - for (int x = 0; x < size.width; ++x) - { - out_img.at(cv::Point2i{x, y}) = data[size.width * y + x]; - } - } - cv::imshow("Data", out_img); - std::cout << cv::sum(out_img) << std::endl; - cv::waitKey(0); - return out_img; -} - - /** * @brief Turn data frames into spikes. * @param buf data frame. diff --git a/examples/mnist-learn/CMakeLists.txt b/examples/mnist-learn/CMakeLists.txt index b86476ae..67262070 100644 --- a/examples/mnist-learn/CMakeLists.txt +++ b/examples/mnist-learn/CMakeLists.txt @@ -26,6 +26,8 @@ if (NOT TARGET KNP::BaseFramework::Core) endif() find_package(Boost ${KNP_BOOST_MIN_VERSION} COMPONENTS program_options REQUIRED) +find_package(PkgConfig REQUIRED) +pkg_check_modules(GRAPHVIZ REQUIRED IMPORTED_TARGET libgvc libcgraph) add_executable(${PROJECT_NAME} main.cpp @@ -40,4 +42,7 @@ add_executable(${PROJECT_NAME} models/altai/prepare_network_for_inference.cpp models/altai/spike_generators.cpp) target_include_directories(${PROJECT_NAME} PRIVATE ${PROJECT_SOURCE_DIR}) -target_link_libraries(${PROJECT_NAME} PRIVATE KNP::BaseFramework::Core Boost::program_options) +target_link_libraries(${PROJECT_NAME} PRIVATE + KNP::BaseFramework::Core + Boost::program_options + ) diff --git a/examples/mnist-learn/main.cpp b/examples/mnist-learn/main.cpp index 1d3d8ac6..892ad412 100644 --- a/examples/mnist-learn/main.cpp +++ b/examples/mnist-learn/main.cpp @@ -19,8 +19,16 @@ * limitations under the License. */ +#include +#include +#include +#include +#include + #include +#include + #include "dataset.h" #include "evaluate_results.h" #include "inference.h" @@ -31,6 +39,7 @@ /** * @brief Run whole model. + * * @tparam Neuron Neuron type. * @param model_desc Model description. */ @@ -43,10 +52,16 @@ void run_model(const ModelDescription& model_desc) // Online Help link: https://click.kaspersky.com/?hl=en-US&version=2.0&pid=KNP&link=online_help&helpid=243548 knp::framework::BackendLoader backend_loader; - train_model(model_desc, dataset, network, backend_loader); + auto backend = train_model(model_desc, dataset, network, backend_loader); if (!model_desc.model_saving_path_.empty()) save_network(model_desc, network); + + // knp::framework::set_saving_path("temp_test_dir"); + visualize_network(network.network_); + visualize_network(network.network_, backend); + visualize_bus(network.network_, backend); + auto inference_spikes = infer_model(model_desc, dataset, network, backend_loader); evaluate_results(inference_spikes, dataset); @@ -55,8 +70,10 @@ void run_model(const ModelDescription& model_desc) /** * @brief Main function. + * * @param argc Argument count. * @param argv Arguments value. + * * @return Error code. */ int main(int argc, char** argv) diff --git a/examples/mnist-learn/models/blifat/construct_network.cpp b/examples/mnist-learn/models/blifat/construct_network.cpp index 5179c708..f5be84d9 100644 --- a/examples/mnist-learn/models/blifat/construct_network.cpp +++ b/examples/mnist-learn/models/blifat/construct_network.cpp @@ -48,19 +48,19 @@ using ResourceNeuronData = knp::neuron_traits::neuron_parameters -AnnotatedNetwork construct_network(const ModelDescription &model_desc) +AnnotatedNetwork construct_network(const ModelDescription& model_desc) { AnnotatedNetwork result; @@ -187,7 +184,6 @@ AnnotatedNetwork construct_network(const Model // Add input_pop as WTA sender. result.data_.wta_data_.emplace_back().first.push_back(pops.input_pop_.uid_); - create_projections(result, constructor, pops); } diff --git a/examples/mnist-learn/training.h b/examples/mnist-learn/training.h index 5d4c959b..39674a7c 100644 --- a/examples/mnist-learn/training.h +++ b/examples/mnist-learn/training.h @@ -26,10 +26,10 @@ #include #include -#include -#include #include #include +#include +#include #include #include #include @@ -162,7 +162,7 @@ void train_network( * @param backend_loader Backend loader. */ template -void train_model( +std::shared_ptr train_model( const ModelDescription& model_desc, const Dataset& dataset, AnnotatedNetwork& network, knp::framework::BackendLoader& backend_loader) { @@ -170,4 +170,5 @@ void train_model( train_network(training_backend, network, model_desc, dataset); prepare_network_for_inference(training_backend, model_desc, network); + return training_backend; } diff --git a/knp/base-framework/CMakeLists.txt b/knp/base-framework/CMakeLists.txt index 0e590a46..0d408941 100644 --- a/knp/base-framework/CMakeLists.txt +++ b/knp/base-framework/CMakeLists.txt @@ -34,7 +34,6 @@ project(knp-base-framework VERSION "${KNP_VERSION}" LANGUAGES C CXX set(${PROJECT_NAME}_PUBLIC_INCLUDE_DIR "knp/framework") find_package(Boost ${KNP_BOOST_MIN_VERSION} COMPONENTS filesystem REQUIRED) -find_package(OpenCV REQUIRED) include(GNUInstallDirs) include(knp-functions) @@ -77,13 +76,15 @@ knp_add_library("${PROJECT_NAME}-core" impl/inference_evaluation/classification/processor.cpp impl/observer.cpp impl/logging.cpp - impl/visualizer/graph_physics.cpp impl/visualizer/visualize_network.cpp + impl/visualizer/graph_helpers.cpp + impl/visualizer/dot_helpers.cpp + impl/visualizer/generate_files.cpp ${${PROJECT_NAME}_headers} ALIAS KNP::BaseFramework::Core LINK_PRIVATE spdlog::spdlog_header_only Boost::headers Boost::filesystem HighFive hdf5-static csv2 - ${OpenCV_LIBS} + ${GRAPHVIZ_LIBRARIES} # Hack to build with CLang. ${ADD_LIBS} LINK_PUBLIC diff --git a/knp/base-framework/impl/sonata/load_network.cpp b/knp/base-framework/impl/sonata/load_network.cpp index b0ac757d..be65fdcd 100644 --- a/knp/base-framework/impl/sonata/load_network.cpp +++ b/knp/base-framework/impl/sonata/load_network.cpp @@ -99,16 +99,16 @@ std::vector load_populations(const fs::path &pop_h5 for (size_t i = 0; i < num_populations; ++i) { - const std::string proj_name = group.getObjectName(i); + const std::string pop_name = group.getObjectName(i); const int neuron_type = - group.getGroup(proj_name).getDataSet("node_type_id").read>()[0]; // One type only. + group.getGroup(pop_name).getDataSet("node_type_id").read>()[0]; // One type only. // Check if type is in type_file. if (neuron_type == get_neuron_type_id()) - result.emplace_back(load_population(group, proj_name)); + result.emplace_back(load_population(group, pop_name)); else if (neuron_type == get_neuron_type_id()) - result.emplace_back(load_population(group, proj_name)); + result.emplace_back(load_population(group, pop_name)); else if (neuron_type == get_neuron_type_id()) - result.emplace_back(load_population(group, proj_name)); + result.emplace_back(load_population(group, pop_name)); // TODO: Add other supported types or better use a template. } return result; diff --git a/knp/base-framework/impl/sonata/types/resource_blifat_neuron.cpp b/knp/base-framework/impl/sonata/types/resource_blifat_neuron.cpp index b487cbca..2bf6ad1b 100644 --- a/knp/base-framework/impl/sonata/types/resource_blifat_neuron.cpp +++ b/knp/base-framework/impl/sonata/types/resource_blifat_neuron.cpp @@ -20,12 +20,17 @@ */ #include +#include #include +#include #include #include #include +#include +#include + #include #include "../csv_content.h" @@ -45,6 +50,35 @@ std::string get_neuron_type_name &population, + HighFive::NodeTraits &population_group) +{ + core::TagMap tags = population.get_tags(); + std::vector tags_values; + std::vector tags_keys; + + if (tags.exists("io_type")) + { + auto io_type_tag_value = std::any_cast(tags["io_type"]); + if (io_type_tag_value == knp::core::tags::IOType::input) + tags_values.push_back("input"); + else if (io_type_tag_value == knp::core::tags::IOType::output) + tags_values.push_back("output"); + else + SPDLOG_ERROR("Unknown io_type tag value"); + tags_keys.push_back("io_type"); + } + auto name_tag_value = std::any_cast(tags["name"]); + tags_values.push_back(name_tag_value); + tags_keys.push_back("name"); + population_group.createDataSet("tags_values", tags_values); + population_group.createDataSet("tags_keys", tags_keys); +} + + template <> void add_population_to_h5>( HighFive::File &file_h5, const core::Population &population) @@ -70,6 +104,8 @@ void add_population_to_h5(population.size(), get_neuron_type_id())); + + add_tags_to_h5(population, population_group); auto group0 = population_group.createGroup("0"); // TODO: Need to check if all parameters are the same. If not, then save them into h5. @@ -142,6 +178,45 @@ using ResourceNeuron = neuron_traits::SynapticResourceSTDPBLIFATNeuron; using ResourceNeuronParams = neuron_traits::neuron_parameters; +// Two arrays are uploaded: keys and tag values. +// They are tagged with the population in accordance with the TagMap structure. +void load_tags_to_population( + const HighFive::Group &nodes_group, const std::string &population_name, + core::Population &population) +{ + auto ds_tags_values = nodes_group.getGroup(population_name).getDataSet("tags_values"); + auto ds_tags_keys = nodes_group.getGroup(population_name).getDataSet("tags_keys"); + auto size = ds_tags_values.getElementCount(); + std::vector data_tags_values(size); + std::vector data_tags_keys(size); + ds_tags_values.read(data_tags_values); + ds_tags_keys.read(data_tags_keys); + + for (size_t i = 0; i < size; ++i) + { + auto key = data_tags_keys[i]; + auto value = data_tags_values[i]; + + if (key == "io_type") + { + if (value == "input") + { + population.get_tags()[key] = knp::core::tags::IOType::input; + break; + } + else if (value == "output") + { + population.get_tags()[key] = knp::core::tags::IOType::output; + break; + } + else + SPDLOG_ERROR("Unknown io_type tag value"); + continue; + } + population.get_tags()[key] = value; + } +} + template <> core::Population load_population( @@ -205,8 +280,12 @@ load_population( LOAD_NEURONS_PARAMETER(target, neuron_traits::BLIFATNeuron, additional_threshold_, dyn_group, group_size); const knp::core::UID uid{boost::lexical_cast(population_name)}; - return core::Population( + + auto result = core::Population( uid, [&target](size_t index) { return target[index]; }, group_size); -} + load_tags_to_population(nodes_group, population_name, result); + + return result; +} } // namespace knp::framework::sonata diff --git a/knp/base-framework/impl/visualizer/dot_helpers.cpp b/knp/base-framework/impl/visualizer/dot_helpers.cpp new file mode 100644 index 00000000..698b4e10 --- /dev/null +++ b/knp/base-framework/impl/visualizer/dot_helpers.cpp @@ -0,0 +1,336 @@ +/** + * @file dot_helpers.cpp + * @brief Help functions for writing a dot file. + * @kaspersky_support Kirill L. + * @date 05.08.2026 + * @license Apache 2.0 + * @copyright © 2026 AO Kaspersky Lab + * + * 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 "dot_helpers.h" + +#include + +#include + +#include +#include +#include +#include +#include + +#include "graph_helpers.h" + + +/** + * @brief Write header and styles to DOT file. + * + * @param out output file stream. + * + * @details commentsed - for a connected graph. + */ +void write_header_and_styles(std::ofstream& out) +{ + // Digraph - directed. + out << "digraph G {\n"; + + // for unconnected graphs this is needed: (+ change -> to --) + out << " layout=neato;\n"; // Change engine to neato (physical force model) + out << " pack=true;\n"; // Enable compact packing of independent components + out << " packmode=\"graph\";\n\n"; // Pack each subgroup as a separate mini-graph + + // Global design settings for the graph + out << " // Canvas settings.\n"; + out << " bgcolor=\"#FAFAFA\";\n"; // Light gray soft background page + // out << " rankdir=LR;\n"; // Graph goes left to right + out << " splines=true;\n"; // Beautiful smooth curves for arrows + // out << " nodesep=0.5;\n"; // Distance between nodes + // out << " ranksep=0.6;\n\n"; // Distance between levels (layers) of the graph + out << " overlap=false;\n"; + out << " sep=\"+30\";\n"; + + out << " // Default node styling.\n"; + out << " node [\n"; + out << " fontname=\"Helvetica,Arial,sans-serif\",\n"; // Modern font + out << " fontsize=11,\n"; + out << " shape=box,\n"; // Rectangular shape + out << " style=\"filled,rounded\",\n"; // Fill with color + rounded corners + out << " penwidth=0,\n"; // Remove rigid black border around node + out << " margin=\"0.2,0.1\"\n"; // Internal padding for text + out << " ];\n\n"; + + out << " // Default edge (arrow) styling.\n"; + out << " edge [\n"; + out << " fontname=\"Helvetica,Arial,sans-serif\",\n"; + out << " fontsize=9,\n"; + out << " fontcolor=\"#555555\",\n"; // Soft gray color for text above arrows + out << " color=\"#B0BEC5\",\n"; // Beautiful steel color for the arrow itself + out << " penwidth=1.5,\n"; // Make line slightly thicker than default + out << " arrowsize=0.8\n"; // Slightly reduce arrowhead size + out << " ];\n\n"; +} + + +/** + * @brief Write edge without source to DOT file. + * + * @param out output file stream. + * @param dst destination node UID. + * @param i counter for ghost node. + * @param edge_uid edge UID. + * @param size edge size. + * @param color edge color. + * + * @details A graphviz can't draw an arrow from nowhere. It is necessary to make an invisible vertex. + */ +void write_edge_without_src( + std::ofstream& out, knp::core::UID const& dst, int const& i, knp::core::UID const& edge_uid, + std::string const& size, std::string const& color) +{ + auto num = std::to_string(i); + auto short_name = std::string(edge_uid).substr(0, 8); + + out << " ghost_src_" << num << " [shape=point, style=invis" + << "];\n"; + + out << " \"" + << "ghost_src_" << num << "\" -> " << std::string(dst) << "[style=\"dotted\", " + << "label=\"projection :\\n" + << short_name << "\\nsize:" << size << "\", color=\"" << color << "\"];\n"; +} + + +/** + * @brief Write edge without destination to DOT file. + * + * @param out output file stream. + * @param src source node UID. + * @param i counter for ghost node. + * @param edge_uid edge UID. + * @param size edge size. + * @param color edge color. + * + * @details A graphviz can't draw an arrow to nowhere. It is necessary to make an invisible vertex. + */ +void write_edge_without_dst( + std::ofstream& out, knp::core::UID const& src, int const& i, knp::core::UID const& edge_uid, + std::string const& size, std::string const& color) +{ + auto num = std::to_string(i); + auto short_name = std::string(edge_uid).substr(0, 8); + + out << " ghost_dst_" << num << " [shape=point, style=invis" + << "];\n"; + + out << " \"" << std::string(src) << "\" -> " + << "ghost_dst_" << num << "[style=\"dotted\", " + << "label=\"projection :\\n" + << short_name << "\\nsize:" << size << "\", color=\"" << color << "\"];\n"; +} + + +/** + * @brief Write message nodes and edges to DOT file. + * + * @param out output file stream. + * @param message vector of message pairs (sender, receiver). + * @param graph network graph containing nodes and edges information. + * @param label label for the edge (e.g., "Spike Message" or "Synapse Message"). + * + * @details This function writes nodes and edges for message flow visualization. + * For Spike Messages, sender is a node and receiver is an edge. + * For Synapse Messages, sender is an edge and receiver is a node. + * The function extracts names and sizes from the graph for proper labeling. + */ +void writeMessageNodesAndEdge( + std::ostream& out, const std::vector>& messages, + knp::framework::NetworkGraph const& graph, const std::string& label) +{ + for (auto message : messages) + { + auto sender = message.first; + auto receiver = message.second; + auto sender_name = std::string(sender).substr(0, 8); + auto receiver_name = std::string(receiver).substr(0, 8); + std::string sender_size; + std::string receiver_size; + + if (label == "Spike Message") + { + auto node_it = std::find_if( + graph.nodes_.begin(), graph.nodes_.end(), [&sender](auto const& node) { return node.uid_ == sender; }); + if (node_it != graph.nodes_.end()) + { + sender_name = node_it->name_; + sender_size = std::to_string(node_it->size_); + } + auto edge_it = std::find_if( + graph.edges_.begin(), graph.edges_.end(), + [&receiver](const auto& edge) { return edge.uid_ == receiver; }); + if (edge_it != graph.edges_.end()) + { + receiver_name = edge_it->name_; + receiver_size = std::to_string(edge_it->size_); + } + } + else if (label == "Synaptic Impact Message") + { + auto edge_it = std::find_if( + graph.edges_.begin(), graph.edges_.end(), [&sender](const auto& edge) { return edge.uid_ == sender; }); + if (edge_it != graph.edges_.end()) + { + sender_name = edge_it->name_; + sender_size = std::to_string(edge_it->size_); + } + auto node_it = std::find_if( + graph.nodes_.begin(), graph.nodes_.end(), + [&receiver](const auto& node) { return node.uid_ == receiver; }); + if (node_it != graph.nodes_.end()) + { + receiver_name = node_it->name_; + receiver_size = std::to_string(node_it->size_); + } + } + + auto index_from = std::string(sender); + auto index_to = std::string(receiver); + + // Write node. + out << " \"" << index_from << "\" [label=\"" << sender_name << "\"];" + << "# " << sender_size << "\n"; + out << " \"" << index_to << "\" [label=\"" << receiver_name << "\"];" + << "# " << receiver_size << "\n"; + + // Write edge. + out << " \"" << index_from << "\" -> \"" << index_to << "\" [label=\"" << label << "\"];\n"; + } +} + + +/** + * @brief Write bus messages (spike messages and synaptic impact messages) to DOT file. + * + * @param out output file stream for DOT file. + * @param spike_messages vector of spike message pairs. + * @param synaptic_messages vector of synaptic message pairs. + * @param graph network graph. + */ +void write_bus_messages_to_dot( + std::ofstream& out, std::vector> const& spike_messages, + std::vector> const& synaptic_messages, + knp::framework::NetworkGraph const& graph) +{ + write_header_and_styles(out); + + writeMessageNodesAndEdge(out, spike_messages, graph, "Spike Message"); + writeMessageNodesAndEdge(out, synaptic_messages, graph, "Synaptic Impact Message"); + + out << "}\n"; +} + + +/** + * @brief Write projections and populations to DOT file. + * + * @param out output file stream. + * @param unique_nodes set of unique node UIDs. + * @param unique_edges set of unique edge UIDs. + * @param modificators set of modificator UIDs. + * @param dynamic_color color for dynamic elements. + * @param graph network graph. + * @param node_src map of node sources. + * @param node_dst map of node destinations. + */ +void write_projections_and_populations_to_dot( + std::ofstream& out, std::set const& unique_nodes, std::set const& unique_edges, + std::set& modificators, std::string& dynamic_color, knp::framework::NetworkGraph const& graph, + std::map& node_src, std::map& node_dst) +{ + write_header_and_styles(out); + + if (unique_nodes.empty() && unique_edges.empty()) + { + SPDLOG_WARN("No nodes or edges to visualize"); + out << "}\n"; + return; + } + + // Write nodes to dot. + for (const auto& unique_node : unique_nodes) + { + // check if it is modificator + if (modificators.find(unique_node) != modificators.end()) + { + write_node_to_dot( + out, std::string(unique_node), std::string(unique_node).substr(0, 8), "None", dynamic_color, + "Modificator"); + continue; + } + auto graph_node = get_graph_node_by_uid(unique_node, graph.nodes_); + auto node_name = get_node_name(unique_node, graph.nodes_); + auto graph_node_size = graph_node.size_; + write_node_to_dot(out, std::string(unique_node), node_name, graph_node_size); + } + + // Write edges to dot. + [[maybe_unused]] int num_invisible_src_nodes = 1; + [[maybe_unused]] int num_invisible_dst_nodes = 1; + for (const auto& unique_edge : unique_edges) + { + auto edge_size = get_proj_size(graph.edges_, unique_edge); + auto it1 = node_src.find(unique_edge); + if (it1 == node_src.end()) + { + SPDLOG_INFO("There is't node_src for this edge: {}", std::string(unique_edge)); + knp::core::UID dst; + try + { + dst = node_dst.at(unique_edge); + } + catch (const std::out_of_range& e) + { + SPDLOG_ERROR("ERROR: There is an edge without a source and a destination! Details: {}", e.what()); + } + // Write node to dot (ghost_i -> dst). + write_edge_without_src(out, dst, num_invisible_dst_nodes, unique_edge, edge_size); + ++num_invisible_src_nodes; + continue; + } + auto src = node_src.at(unique_edge); + + auto it2 = node_dst.find(unique_edge); + if (it2 == node_dst.end()) + { + SPDLOG_INFO("There is't node_dst for this edge: {}", std::string(unique_edge)); + // Write node to dot (src -> ghost_j). + write_edge_without_dst(out, src, num_invisible_dst_nodes, unique_edge, edge_size); + ++num_invisible_dst_nodes; + continue; + } + auto dst = node_dst.at(unique_edge); + + // Check if the source is a modifier (change color of edge). + if (modificators.find(src) != modificators.end()) + { + write_edge_to_dot( + out, std::string(src), std::string(dst), std::string(unique_edge).substr(0, 8), edge_size, + dynamic_color); + continue; + } + write_edge_to_dot(out, std::string(src), std::string(dst), std::string(unique_edge).substr(0, 8), edge_size); + } + out << "}\n"; +} diff --git a/knp/base-framework/impl/visualizer/dot_helpers.h b/knp/base-framework/impl/visualizer/dot_helpers.h new file mode 100644 index 00000000..cf006ce0 --- /dev/null +++ b/knp/base-framework/impl/visualizer/dot_helpers.h @@ -0,0 +1,182 @@ +/** + * @file dot_helpers.h + * @brief Help functions for writing a dot file. + * @kaspersky_support Kirill L. + * @date 05.08.2026 + * @license Apache 2.0 + * @copyright © 2026 AO Kaspersky Lab + * + * 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. + */ + + +#pragma once + +#include +#include + +#include + +#include +#include +#include +#include +#include +extern "C" +{ +#include +} + + +/** + * @brief Write header and styles to DOT file. + * + * @param out output file stream. + * + * @details commentsed - for a connected graph. + */ +void write_header_and_styles(std::ofstream& out); + + +/** + * @brief Write edge to DOT file. + * + * @param out output file stream. + * @param index_from source node UID. + * @param index_to target node UID. + * @param name edge name. + * @param size edge size. + * @param color edge color. + * + * @details need to visualize dynamic. + */ +template +void write_edge_to_dot( + std::ofstream& out, T index_from, T index_to, T name, T size, std::string const& color = "lightgray") +{ + // example of dot line: + // "bdd469c7-66f2-43d9-b751-fb1e20732d3d" -> "12fe2450-0f03-4d93-a1e0-8d2f7a9277ae" + // [label="Projection:\nfced41d5\nsize: 200", color="lightgray"]; + + out << " \"" << index_from << "\" -> \"" << index_to << "\" [label=\"Projection:\\n" + << name << "\\nsize: " << size << "\", color=\"" << color << "\"];\n"; +} + + +/** + * @brief Write node to DOT file. + * + * @param out output file stream. + * @param node_uid node UID. + * @param name node name. + * @param size node size. + * @param color node color. + * @param type_node node type. + * + * @details need to visualize dynamic. + */ +template +void write_node_to_dot( + std::ofstream& out, std::string const& node_uid, std::string const& name, T size, + std::string const& color = "lightgray", std::string const& type_node = "Population") +{ + //examples: + // "bdd469c7-66f2-43d9-b751-fb1e20732d3d" [label="Modificator:\nbdd469c7", color="yellow"]; + // "48bebcd2-80f6-488c-bbe8-0627cebc8af7" [label="Population:\nOUTPUT\nsize: 10", color="lightgray"]; + + if constexpr (std::is_same_v) + { + out << " \"" << node_uid << "\" [label=\"" << type_node << ":\\n" + << name << "\\nsize: " << size << "\", color=\"" << color << "\"];\n"; + return; + } + + out << " \"" << node_uid << "\" [label=\"" << type_node << ":\\n" << name << "\", color=\"" << color << "\"];\n"; +} + + +/** + * @brief Write edge without source to DOT file. + * + * @param out output file stream. + * @param dst destination node UID. + * @param i counter for ghost node. + * @param edge_uid edge UID. + * @param size edge size. + * @param color edge color. + * + * @details A graphviz can't draw an arrow from nowhere. It is necessary to make an invisible vertex. + */ +void write_edge_without_src( + std::ofstream& out, knp::core::UID const& dst, int const& i, knp::core::UID const& edge_uid, + std::string const& size, std::string const& color = "lightgray"); + + +/** + * @brief Write edge without destination to DOT file. + * + * @param out output file stream. + * @param src source node UID. + * @param i counter for ghost node. + * @param edge_uid edge UID. + * @param size edge size. + * @param color edge color. + * + * @details A graphviz can't draw an arrow to nowhere. It is necessary to make an invisible vertex. + */ +void write_edge_without_dst( + std::ofstream& out, knp::core::UID const& src, int const& i, knp::core::UID const& edge_uid, + std::string const& size, std::string const& color = "lightgray"); + + +/** + * @brief Convert DOT file to PNG image. + * + * @param path_to_dot_file path to input DOT file. + * @param path_to_png_file path to output PNG file. + * + * @return boolean indicating success + */ +bool convert_dot_to_png(const std::string& path_to_dot_file, const std::string& path_to_png_file); + + +/** + * @brief Write bus messages (spike messages and synaptic impact messages) to DOT file. + * + * @param out output file stream for DOT file. + * @param spike_messages vector of spike message pairs. + * @param synaptic_messages vector of synaptic message pairs. + * @param graph network graph. + */ +void write_bus_messages_to_dot( + std::ofstream& out, std::vector> const& spike_messages, + std::vector> const& synaptic_messages, + knp::framework::NetworkGraph const& graph); + + +/** + * @brief Write projections and populations to DOT file. + * + * @param out output file stream. + * @param unique_nodes set of unique node UIDs. + * @param unique_edges set of unique edge UIDs. + * @param modificators set of modificator UIDs. + * @param dynamic_color color for dynamic elements. + * @param graph network graph. + * @param node_src map of node sources. + * @param node_dst map of node destinations. + */ +void write_projections_and_populations_to_dot( + std::ofstream& out, std::set const& unique_nodes, std::set const& unique_edges, + std::set& modificators, std::string& dynamic_color, knp::framework::NetworkGraph const& graph, + std::map& node_src, std::map& node_dst); diff --git a/knp/base-framework/impl/visualizer/generate_files.cpp b/knp/base-framework/impl/visualizer/generate_files.cpp new file mode 100644 index 00000000..23fa7dc4 --- /dev/null +++ b/knp/base-framework/impl/visualizer/generate_files.cpp @@ -0,0 +1,287 @@ +/** + * @file generate_files.cpp + * @brief Functions for generate dot/png files. + * @kaspersky_support Kirill L. + * @date 05.08.2026 + * @license Apache 2.0 + * @copyright © 2026 AO Kaspersky Lab + * + * 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 "generate_files.h" + +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include "dot_helpers.h" +#include "graph_helpers.h" +extern "C" +{ +#include +} + + +/** + * @brief Create DOT file for static network. + * + * @param file_name output file name. + * @param graph network graph. + */ +void create_dot_file_for_static_network(std::string& file_name, knp::framework::NetworkGraph& graph) +{ + std::ofstream out(file_name); + + if (!out.is_open()) + { + SPDLOG_ERROR("Cannot open file {} for writing", file_name); + return; + } + + write_header_and_styles(out); + for (size_t i = 0; i < graph.nodes_.size(); ++i) + { + auto node = graph.nodes_[i]; + //write_node_to_dot(out, i, node.name_, node.size_); + out << " " << i << " [label=\"population:\\n" + << node.name_ << "\\nsize: " << node.size_ << "\", color=\"" + << "\"];\n"; + } + for (const auto& edge : graph.edges_) + { + if (edge.index_from_ == -1) + { + // write_edge_to_dot(out, "00000000", edge.index_to_, edge.name_); + // out << " " << "00000000" << " -> " << edge.index_to_ << " [label=\"projection :\\n" << edge.name_ << + // "\"];\n"; + continue; + } + // write_edge_to_dot(out, edge.index_from_, edge.index_to_, edge.name_); + out << " " << edge.index_from_ << " -> " << edge.index_to_ << " [label=\"projection :\\n" + << edge.name_ << "\"];\n"; + } + out << "}\n"; + if (out.good()) + { + SPDLOG_INFO("The DOT file is saved in: {}", std::filesystem::absolute(file_name).string()); + } + else + { + SPDLOG_ERROR("Failed to write to file {}", file_name); + } +} + + +/** + * @brief Create DOT file for bus messages. + * + * @param file_name output DOT file name. + * @param graph network graph. + * @param backend shared pointer to backend. + */ +void create_dot_file_for_bus( + const std::string& file_name, const knp::framework::NetworkGraph& graph, + std::shared_ptr& backend) +{ + auto [spike_messages, synaptic_messages] = get_bus(backend); + + std::ofstream out(file_name); + + write_bus_messages_to_dot(out, spike_messages, synaptic_messages, graph); // spike/synaptic impact messeges + + SPDLOG_INFO("The DOT file is saved in: {}", std::filesystem::absolute(file_name).string()); +} + + +/** + * @brief Create DOT file for dynamic network. + * + * @param path output path file name. + * @param graph network graph. + * @param backend shared pointer to backend. + */ +void create_dot_file_for_dynamic_network( + const std::string& path, const knp::framework::NetworkGraph& graph, std::shared_ptr& backend) +{ + const auto subs = backend->get_message_endpoint().get_endpoint_subscriptions(); + + constexpr size_t spike_idx = 0; // Must be 0 (Serial number of the type). + constexpr size_t synaptic_impact_idx = 1; // Must be 1 (Serial number of the type). + + std::map node_src; + std::map node_dst; + + std::set unique_senders_spike; + std::set unique_receivers_impact; + + std::set unique_nodes; + std::set unique_edges; + + std::vector> spike_messages; // [sender, receiver] + std::vector> synaptic_messages; // [sender, receiver] + + // take from bus nessosary maps + for (const auto& [key, sub_variant] : subs) + { + const auto& [type_idx, receiver_uid] = key; + + std::visit( + [&](const auto& sub) + { + const auto& senders = sub.get_senders(); + if (senders.empty()) return; + if (spike_idx == type_idx) + { + // Receiver: Projection, sender: *Population (* - or modificator) (0 -->). + for (const auto& sender : senders) + { + spike_messages.push_back({sender, receiver_uid}); + unique_senders_spike.insert(sender); + node_src[receiver_uid] = sender; + // edge_from[sender] = receiver_uid; + + unique_nodes.insert(sender); + unique_edges.insert(receiver_uid); + } + } + else if (type_idx == synaptic_impact_idx) + { + // Receiver: *Population, sender: Projection. (* - or modificator) (--> 0). + for (const auto& sender : senders) + { + synaptic_messages.push_back({sender, receiver_uid}); + unique_receivers_impact.insert(receiver_uid); + node_dst[sender] = receiver_uid; + // edge_to[receiver_uid] = sender; + unique_nodes.insert(receiver_uid); + unique_edges.insert(sender); + } + } + }, + sub_variant); + } + + std::set modificators; + + // Modificator exist in senders SPIKE. + // Modificator doesn't exist in receivers SYNAPTIC_IMPACT. + for (const auto& uid : unique_senders_spike) + { + if (unique_receivers_impact.find(uid) == unique_receivers_impact.end()) + { + modificators.insert(uid); + SPDLOG_INFO("Modificator: {}", std::string(uid)); + } + } + + std::ofstream out(path); + std::string dynamic_color = "yellow"; + write_projections_and_populations_to_dot( + out, unique_nodes, unique_edges, modificators, dynamic_color, graph, node_src, + node_dst); // projections/populations from bus + + SPDLOG_INFO("The DOT file is saved in: {}", std::filesystem::absolute(path).string()); +} + + +// Dot to png. +/** + * @brief Convert DOT file to PNG image. + * + * @param path_to_dot_file path to input DOT file. + * @param path_to_png_file path to output PNG file. + * + * @return boolean indicating success. + */ +bool convert_dot_to_png(const std::string& path_to_dot_file, const std::string& path_to_png_file) +{ + // 1. Initialize the Graphviz context + auto gvc = std::unique_ptr(gvContext(), gvFreeContext); + if (!gvc) return false; + + // 2. Open and parse the DOT file + // auto fp = std::unique_ptr(fopen(path_to_dot_file.c_str(), "r"), fclose); + struct FileDeleter + { + void operator()(FILE* fp) const + { + if (fp) + { + fclose(fp); + } + } + }; + std::unique_ptr fp(fopen(path_to_dot_file.c_str(), "r")); + + if (!fp) + { + SPDLOG_ERROR("Error: couldn't open the DOT file: {}", std::filesystem::absolute(path_to_dot_file).string()); + return false; + } + + // 3. Read the DOT file + auto g = std::unique_ptr(agread(fp.get(), nullptr), agclose); + if (!g) + { + SPDLOG_ERROR("Error: couldn't read DOT file: {}", std::filesystem::absolute(path_to_dot_file).string()); + return false; + } + + // 4. Compute the layout using the "dot" engine + // gvLayoutJobs(gvc.get(), g.get()); + int layout_res = gvLayoutJobs(gvc.get(), g.get()); + if (layout_res != 0) + { + SPDLOG_ERROR("Layout computation failed"); + return false; + } + // gvLayout(gvc.get(), g.get(), "dot"); + + // 5. Render the layout into a PNG file + int result = gvRenderFilename(gvc.get(), g.get(), "png", path_to_png_file.c_str()); + + + // 6. Check the result and print the path to the PNG file + if (0 == result) + { + SPDLOG_INFO("The PNG file is saved in: {}", std::filesystem::absolute(path_to_png_file).string()); + return true; + } + else + { + SPDLOG_ERROR("PNG file rendering error"); + return false; + } +} + + +/** + * @brief Create PNG file from DOT file. + * + * @param dot_file path to DOT file. + * @param png_file path to PNG file. + */ +void create_png_file(const std::string& dot_file, const std::string& png_file) +{ + // system("dot -Tpng ./store_log_file3/graph.dot -o ./store_log_file3/graph.png"); + if (!convert_dot_to_png(dot_file, png_file)) SPDLOG_ERROR("Dot file conversion failed."); +} diff --git a/knp/base-framework/impl/visualizer/generate_files.h b/knp/base-framework/impl/visualizer/generate_files.h new file mode 100644 index 00000000..80cf5372 --- /dev/null +++ b/knp/base-framework/impl/visualizer/generate_files.h @@ -0,0 +1,86 @@ +/** + * @file generate_files.h + * @brief Functions for generate dot/png files. + * @kaspersky_support Kirill L. + * @date 05.08.2026 + * @license Apache 2.0 + * @copyright © 2026 AO Kaspersky Lab + * + * 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. + */ + +#pragma once +#include +#include + +#include + +#include +#include + +extern "C" +{ +#include +} + + +/** + * @brief Create DOT file for static network. + * + * @param file_name output file name. + * @param graph network graph. + */ +void create_dot_file_for_static_network(std::string& file_name, knp::framework::NetworkGraph& graph); + + +/** + * @brief Create DOT file for bus messages. + * + * @param file_name output DOT file name. + * @param graph network graph. + * @param backend shared pointer to backend. + */ +void create_dot_file_for_bus( + const std::string& file_name, const knp::framework::NetworkGraph& graph, + std::shared_ptr& backend); + + +/** + * @brief Create DOT file for dynamic network. + * + * @param path output path file name. + * @param graph network graph. + * @param backend shared pointer to backend. + */ +void create_dot_file_for_dynamic_network( + const std::string& path, const knp::framework::NetworkGraph& graph, std::shared_ptr& backend); + + +/** + * @brief Convert DOT file to PNG image. + * + * @param path_to_dot_file path to input DOT file. + * @param path_to_png_file path to output PNG file. + * + * @return boolean indicating success. + */ +bool convert_dot_to_png(const std::string& path_to_dot_file, const std::string& path_to_png_file); + + +/** + * @brief Create PNG file from DOT file. + * + * @param dot_file path to DOT file. + * @param png_file path to PNG file. + */ +void create_png_file(const std::string& dot_file, const std::string& png_file); diff --git a/knp/base-framework/impl/visualizer/graph_helpers.cpp b/knp/base-framework/impl/visualizer/graph_helpers.cpp new file mode 100644 index 00000000..9dfb75b8 --- /dev/null +++ b/knp/base-framework/impl/visualizer/graph_helpers.cpp @@ -0,0 +1,152 @@ +/** + * @file graph_helpers.cpp + * @brief Help functions for graph vizualize. + * @kaspersky_support Kirill L. + * @date 05.08.2026 + * @license Apache 2.0 + * @copyright © 2026 AO Kaspersky Lab + * + * 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 "graph_helpers.h" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + + +/** + * @brief Get bus messages from backend subscriptions. + * + * @param backend shared pointer to backend. + * + * @return tuple of spike messages and synaptic messages. + */ +using SpikeMessages = std::vector>; +using SynapticMessages = std::vector>; +using BusMessages = std::tuple; + + +BusMessages get_bus(std::shared_ptr& backend) +{ + const auto subs = backend->get_message_endpoint().get_endpoint_subscriptions(); + + constexpr size_t spike_idx = 0; // Must be 0 (Serial number of the type). + constexpr size_t synaptic_impact_idx = 1; // Must be 1 (Serial number of the type). + + SpikeMessages spike_messages; // [sender, receiver] + SynapticMessages synaptic_messages; // [sender, receiver] + + + // take from bus nessosary maps + for (const auto& [key, sub_variant] : subs) + { + // const auto& [type_idx, receiver_uid] = key; + const auto& type_idx = key.first; + const auto& receiver_uid = key.second; + std::visit( + [&](const auto& sub) + { + const auto& senders = sub.get_senders(); + if (senders.empty()) return; + if (type_idx == spike_idx) + { + // Receiver: Projection, sender: *Population (* - or modificator) (0 -->). + std::transform( + senders.begin(), senders.end(), std::back_inserter(spike_messages), + [&receiver_uid](const auto& sender) { return std::make_pair(sender, receiver_uid); }); + } + else if (type_idx == synaptic_impact_idx) + { + // receiver: Population, sender: Projection (* - or modificator) (--> 0). + std::transform( + senders.begin(), senders.end(), std::back_inserter(synaptic_messages), + [&receiver_uid](const auto& sender) { return std::make_pair(sender, receiver_uid); }); + } + }, + sub_variant); + } + + return {spike_messages, synaptic_messages}; +} + + +/** + * @brief Get node name by UID from network graph nodes. + * + * @param node_uid UID of the node. + * @param nodes vector of network graph nodes. + * + * @return string node name. + */ +std::string get_node_name(knp::core::UID node_uid, std::vector const& nodes) +{ + std::string node_name = std::string(node_uid).substr(0, 8); + // nodes from netGraph + auto node_it = + std::find_if(nodes.begin(), nodes.end(), [&node_uid](const auto& node) { return node.uid_ == node_uid; }); + if (node_it != nodes.end()) node_name = node_it->name_; + return node_name; +} + + +/** + * @brief Get graph node by UID from network graph nodes. + * + * @param node_uid UID of the node. + * @param nodes vector of network graph nodes. + * + * @return NetworkGraph::Node object. + */ +knp::framework::NetworkGraph::Node get_graph_node_by_uid( + knp::core::UID node_uid, std::vector const& nodes) +{ + auto node_it = + std::find_if(nodes.begin(), nodes.end(), [&node_uid](const auto& node) { return node.uid_ == node_uid; }); + if (node_it != nodes.end()) + { + return *node_it; + } + + SPDLOG_INFO("Node with UID {} not found in graph", std::string(node_uid)); + return knp::framework::NetworkGraph::Node{}; +} + + +/** + * @brief Get projection size by UID from network graph edges. + * + * @param edges vector of network graph edges. + * @param edge_uid UID of the edge. + * + * @return string size of the edge. + */ +std::string get_proj_size(std::vector const& edges, const knp::core::UID& edge_uid) +{ + auto it = std::find_if( + edges.begin(), edges.end(), [&edge_uid](const auto& graph_edge) { return graph_edge.uid_ == edge_uid; }); + if (it != edges.end()) + { + return std::to_string(it->size_); + } + throw std::runtime_error("Edge not found"); +} diff --git a/knp/base-framework/impl/visualizer/graph_helpers.h b/knp/base-framework/impl/visualizer/graph_helpers.h new file mode 100644 index 00000000..9ccbc93c --- /dev/null +++ b/knp/base-framework/impl/visualizer/graph_helpers.h @@ -0,0 +1,113 @@ +/** + * @file graph_helpers.cpp + * @brief Help functions for graph vizualize. + * @kaspersky_support Kirill L. + * @date 05.08.2026 + * @license Apache 2.0 + * @copyright © 2026 AO Kaspersky Lab + * + * 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. + */ + +#pragma once +#include + +#include + +#include +#include +#include +#include +#include + +extern "C" +{ +#include +} + + +/** + * @brief Get bus messages from backend subscriptions. + * + * @param backend shared pointer to backend. + * + * @return tuple of spike messages and synaptic messages. + */ +std::tuple< + std::vector>, std::vector>> +get_bus(std::shared_ptr& backend); + + +/** + * @brief Get entity name for any network object. If there is no name it's constructed from UID. + * + * @param pop population object. + * + * @return string name of the population. + */ +template +std::string get_population_name(const Entity& pop) +{ + const size_t uid_part_size = 8; + knp::core::TagMap tags = std::visit([](const auto& p) { return p.get_tags(); }, pop); + knp::core::UID uid = std::visit([](const auto& p) { return p.get_uid(); }, pop); + std::string name; + auto tag = tags.get_tag("name"); + if (tag.has_value()) + { + try + { + name = std::any_cast(tag); + } + catch (std::bad_any_cast& exc) + { + SPDLOG_ERROR("Wrong name tag type."); + name = std::string{uid}.substr(0, uid_part_size); + } + } + if (name.empty()) name = std::string{uid}.substr(0, uid_part_size); + return name; +} + + +/** + * @brief Get node name by UID from network graph nodes. + * + * @param node_uid UID of the node. + * @param nodes vector of network graph nodes. + * + * @return string node name. + */ +std::string get_node_name(knp::core::UID node_uid, std::vector const& nodes); + +/** + * @brief Get graph node by UID from network graph nodes. + * + * @param node_uid UID of the node. + * @param nodes vector of network graph nodes. + * + * @return NetworkGraph::Node object. + */ +knp::framework::NetworkGraph::Node get_graph_node_by_uid( + knp::core::UID node_uid, std::vector const& nodes); + + +/** + * @brief Get projection size by UID from network graph edges. + * + * @param edges vector of network graph edges. + * @param edge_uid UID of the edge. + * + * @return string size of the edge. + */ +std::string get_proj_size(std::vector const& edges, const knp::core::UID& edge_uid); diff --git a/knp/base-framework/impl/visualizer/visualize_network.cpp b/knp/base-framework/impl/visualizer/visualize_network.cpp index 97cef63c..25c296f4 100644 --- a/knp/base-framework/impl/visualizer/visualize_network.cpp +++ b/knp/base-framework/impl/visualizer/visualize_network.cpp @@ -24,496 +24,241 @@ #include -#include -#include -#include +#include -#include -#include - -#include "graph_physics.h" - - -// TODO: Draw network as a set of subgraphs. -namespace knp::framework +extern "C" { +#include +} +#include -/** - * @brief Adjacency list is a representation of a graph where there is a list of adjacent nodes for each node. - * @see https://en.wikipedia.org/wiki/Adjacency_list. - */ -using AdjacencyList = std::vector>; +#include "generate_files.h" +#include "graph_helpers.h" -/** - * @brief Parameters for network drawing. - */ -struct DrawingParameters +namespace knp::framework { - /** - * @brief Nodes will be drawn using this color. - */ - const cv::Scalar node_color{0, 0, 0}; - /** - * @brief Background color. - */ - const cv::Scalar back_color{255, 255, 255}; - /** - * @brief Color for graph edges. - */ - const cv::Scalar edge_color{0, 0, 255}; - /** - * @brief Size of a circular arrow pointing from a node to the same node. - */ - const int self_arrow_radius = 30; - /** - * @brief Node circle radius. - */ - const int node_radius = 10; - /** - * @brief Length of an arrow showing the projection target. - */ - const int arrow_len = 20; - /** - * @brief An arrow head is a triangle. Arrow width is back side length divided by `2 * arrow_len` (`1.0` for a right - * angle). - */ - const double arrow_width = 0.3; - /** - * @brief Minimal number of pixels between image edge and text. - */ - const int text_margin = 5; -}; - - /** - * @brief Get entity name for any network object. If there is no name it's constructed from UID. + * @brief Set directory for saving visualization files. + * + * @param dir directory path for saving. + * + * @details Set directory for saving visualization files. + * Directory "visualization" is being created and it contains two subdirectories for dot files and png files. + * If the directory does not exist, it will be created. + * If the first character is /, then the path is absolute. (format: /path/to/absolute/dir) + * if not, relative to the current directory. format: path/to/relative/dir) */ -template -std::string get_name(const Entity &pop) +void set_saving_path(std::string dir) { - const size_t uid_part_size = 8; - knp::core::TagMap tags = std::visit([](const auto &p) { return p.get_tags(); }, pop); - knp::core::UID uid = std::visit([](const auto &p) { return p.get_uid(); }, pop); - std::string name; - auto tag = tags.get_tag("name"); - if (tag.has_value()) + if (!dir.empty() && dir[0] != '/') { try { - name = std::any_cast(tag); + if (dir.back() == '/') dir.pop_back(); + + std::filesystem::path current_dir = std::filesystem::current_path(); + ConfigVisualizePathes::default_path = (current_dir / dir).string(); + } + catch (const std::filesystem::filesystem_error& e) + { + SPDLOG_ERROR("Failed to get current path: {}", e.what()); + ConfigVisualizePathes::default_path = dir; } - catch (std::bad_any_cast &exc) + catch (...) { - SPDLOG_WARN("Wrong name tag type."); + SPDLOG_ERROR("Unknown error getting current path"); + ConfigVisualizePathes::default_path = dir; } } - if (name.empty()) name = std::string{uid}.substr(0, uid_part_size); - return name; + else + { + knp::framework::ConfigVisualizePathes::default_path = dir; + } } - /** * @brief Build network graph from a network. * @param network source network for a graph. * @details Online Help link: https://click.kaspersky.com/?hl=en-US&version=2.0&pid=KNP&link=online_help&helpid=235801 */ -NetworkGraph::NetworkGraph(const knp::framework::Network &network) +NetworkGraph::NetworkGraph(const knp::framework::Network& network) { // Add populations as nodes. - for (const auto &pop : network.get_populations()) + for (const auto& pop : network.get_populations()) { - size_t pop_size = std::visit([](const auto &p) { return p.size(); }, pop); - knp::core::UID uid = std::visit([](const auto &p) { return p.get_uid(); }, pop); - nodes_.push_back(Node{pop_size, uid, get_name(pop), pop.index()}); + size_t pop_size = std::visit([](const auto& p) { return p.size(); }, pop); + knp::core::UID uid = std::visit([](const auto& p) { return p.get_uid(); }, pop); + nodes_.push_back(Node{pop_size, uid, get_population_name(pop), pop.index()}); } // Add projections as edges. - for (const auto &proj : network.get_projections()) + for (const auto& proj : network.get_projections()) { - size_t proj_size = std::visit([](const auto &p) { return p.size(); }, proj); - knp::core::UID uid = std::visit([](const auto &p) { return p.get_uid(); }, proj); - knp::core::UID uid_from = std::visit([](const auto &p) { return p.get_presynaptic(); }, proj); - knp::core::UID uid_to = std::visit([](const auto &p) { return p.get_postsynaptic(); }, proj); + size_t proj_size = std::visit([](const auto& p) { return p.size(); }, proj); + knp::core::UID uid = std::visit([](const auto& p) { return p.get_uid(); }, proj); + knp::core::UID uid_from = std::visit([](const auto& p) { return p.get_presynaptic(); }, proj); + knp::core::UID uid_to = std::visit([](const auto& p) { return p.get_postsynaptic(); }, proj); int id_from = -1, id_to = -1; for (size_t i = 0; i < nodes_.size(); ++i) { if (uid_from == nodes_[i].uid_) id_from = i; if (uid_to == nodes_[i].uid_) id_to = i; } - edges_.push_back(Edge{proj_size, id_from, id_to, uid, get_name(proj), proj.index()}); + edges_.push_back(Edge{proj_size, id_from, id_to, uid, get_population_name(proj), proj.index()}); } } - - /** - * @brief Convert network graph into adjacency list form. - * @param graph network graph built from a `Network` object. - * @return graph as an adjacency list. - * @see https://en.wikipedia.org/wiki/Adjacency_list. - * @note There is no input node in `NetworkGraph`, but an adjacency list must have one as the last node. + * @brief Print node and edge connections of a network graph. + * + * @param graph network graph. + * + * @details The function writes a textual description of each node (population) and its incoming and outgoing edges to + * `stdout`. It is primarily useful for debugging the connectivity extraction logic. (needed to static network) */ -AdjacencyList build_adjacency_list(const NetworkGraph &graph) +void print_network_description(const NetworkGraph& graph) { - AdjacencyList adj_list; - adj_list.resize(graph.nodes_.size() + 1); - for (const auto &edge : graph.edges_) + // out to console + // for (size_t i = 0; i < graph.nodes_.size(); ++i) + // { + // std::cout << "Population #" << i << " of size " << graph.nodes_[i].size_ << ": receive from"; + + // std::vector edges_to_i; + // std::vector edges_from_i; + + // for (const auto& edge : graph.edges_) + // { + // if (edge.index_to_ == static_cast(i)) edges_to_i.push_back(edge.index_from_); + // if (edge.index_from_ == static_cast(i)) edges_from_i.push_back(edge.index_to_); + // } + // for (auto edge : edges_to_i) std::cout << " #" << edge; + // std::cout << "; send to"; + // for (auto edge : edges_from_i) std::cout << " #" << edge; + // std::cout << std::endl; + //} + //out to log info + for (size_t i = 0; i < graph.nodes_.size(); ++i) { - int index = edge.index_from_; - if (index < 0) index = static_cast(graph.nodes_.size()); - adj_list[index].push_back(edge.index_to_); - } - return adj_list; -} + std::string log_msg = fmt::format("Population #{} of size {},: receive from", i, graph.nodes_[i].size_); -/** - * @brief Draw a line with an arrow that is much nicer than basic OpenCV arrow. - * @param img image to modify. - * @param pt_from point from which the arrow starts. - * @param pt_to point to which the arrow points. - * @param len arrow length in pixels. - * @param width arrow width as a fraction of arrow length. - * @param margin arrow shift from the end of the line to the beginning by "margin" pixels. - * @param color line and arrow color. - */ -void draw_simple_arrow_line( - cv::Mat &img, const cv::Point2d &pt_from, const cv::Point2d &pt_to, int len, double width, int margin, - const cv::Scalar &color) -{ - cv::line(img, pt_from, pt_to, color); - cv::Point2d direction = pt_to - pt_from; - direction /= cv::norm(direction); - cv::Point2d op_direction{-direction.y, direction.x}; - - cv::Point2d pt_arrow_end = pt_to - direction * margin; - cv::Point2d pt_arrow_back = pt_to - direction * (len + margin); - cv::Point2d pt_back_pos_1 = pt_arrow_back + width * len * op_direction; - cv::Point2d pt_back_pos_2 = pt_arrow_back - width * len * op_direction; - cv::fillConvexPoly(img, std::vector{pt_back_pos_1, pt_arrow_end, pt_back_pos_2}, color); -} + std::vector edges_to_i; + std::vector edges_from_i; - -/** - * @brief Draw edges between selected nodes. - * @param out_img output image matrix. - * @param adj_list adjacency list for the whole network. - * @param nodes indexes of the nodes that were selected. - * @param points node positions. - * @param params drawing parameters. - */ -void draw_edges( - cv::Mat &out_img, const AdjacencyList &adj_list, const std::vector &nodes, - const std::vector &points, const DrawingParameters ¶ms) -{ - for (size_t i = 0; i < nodes.size(); ++i) - { - // This is a bit nonobvious. `adj_list` is a list for all nodes in the graph, - // while `i` is the position of node in subgraph. - // Position of node in the whole graph is `nodes[i]`. - for (size_t j = 0; j < adj_list[nodes[i]].size(); ++j) + for (const auto& edge : graph.edges_) { - // `i` is an index of a list element and index of a source element for the graph edge simultaneously. - auto target_node_iter = std::find(nodes.begin(), nodes.end(), adj_list[nodes[i]][j]); - if (target_node_iter == nodes.end()) continue; - - size_t target_point_index = target_node_iter - nodes.begin(); - if (target_point_index == i) - { - cv::circle( - out_img, points[i] - cv::Point2i(0, params.self_arrow_radius), params.self_arrow_radius, - params.edge_color); - continue; - } - const cv::Point2i &point_from = points[i]; - const cv::Point2i &point_to = points[target_point_index]; - draw_simple_arrow_line( - out_img, point_from, point_to, params.arrow_len, params.arrow_width, params.node_radius, - params.edge_color); + if (edge.index_to_ == static_cast(i)) edges_to_i.push_back(edge.index_from_); + if (edge.index_from_ == static_cast(i)) edges_from_i.push_back(edge.index_to_); } + for (auto edge : edges_to_i) log_msg += fmt::format(" #{}", edge); + log_msg += fmt::format("; send to"); + for (auto edge : edges_from_i) log_msg += fmt::format(" #{}", edge); + SPDLOG_INFO(log_msg); } } - /** - * @brief Draw graph with with node names or IDs as graph legends. - * @param graph whole network graph. - * @param adj_list whole network adjacency list. - */ -cv::Mat draw_annotated_subgraph( - const NetworkGraph &graph, const AdjacencyList &adj_list, const std::vector &nodes, - const std::vector &points, const std::vector &inputs, const cv::Size &img_size, - const DrawingParameters ¶ms = DrawingParameters{}) -{ - // TODO: Draw graph of selected nodes, while the nodes are considered external if they are not included. - cv::Mat out_img{img_size, CV_8UC3, params.back_color}; - // Draw inputs (black arrow from above). - for (auto input : inputs) - { - auto target_node_iter = std::find(nodes.begin(), nodes.end(), input); - if (target_node_iter == nodes.end()) continue; - cv::Point2i point = points[target_node_iter - nodes.begin()]; - draw_simple_arrow_line( - out_img, point - cv::Point2i(0, 2 * params.arrow_len + params.node_radius), point, params.arrow_len, - params.arrow_width, params.node_radius, params.node_color); - } - - draw_edges(out_img, adj_list, nodes, points, params); - - // Draw nodes. - for (size_t i = 0; i < nodes.size(); ++i) - { - // Drawing. - const auto &point = points[i]; - cv::circle( - out_img, {static_cast(point.x), static_cast(point.y)}, params.node_radius, params.node_color, -1); - cv::Point2i text_start = point; - std::string name = graph.nodes_[i].name_; - int baseline = 0; - auto text_size = cv::getTextSize(name, cv::FONT_HERSHEY_SIMPLEX, 0.7, 2, &baseline); - text_start.x = std::max(params.text_margin, text_start.x - text_size.width / 2); - if (text_start.x + text_size.width / 2 > img_size.width - params.text_margin) - text_start.x = img_size.width - text_size.width - params.text_margin; - text_start.y += params.node_radius + params.text_margin + text_size.height; - if (text_start.y > img_size.height) text_start.y = point.y - params.node_radius - params.text_margin; - cv::putText(out_img, name, text_start, cv::FONT_HERSHEY_SIMPLEX, 0.7, {0, 0, 0}, 2); - } - return out_img; -} - - -/** - * @brief Draw connected subgraph. It shouldn't have any connections to other parts of the graph, excluding inputs. - * @details The function draws a vertical black arrow if a node is connected to input. - * @param adj_list adjacency list of the whole graph. - * @param nodes nodes that are part of the subgraph. - * @param points node coordinates. - * @param inputs input nodes. - * @param img_size output image size. - * @param params graph drawing parameters. - * @return image of a drawn subgraph. + * @brief Visualize static network. + * + * @param network source network for visualization. + * + * @details Visualize network. + * The model is needed to get the names of static nodes. + * By default, it saves dot/png files of graph to the current directory (the visualization directory is being created). + * For the change save directory, use set_saving_path(directory). */ -cv::Mat draw_subgraph( - const AdjacencyList &adj_list, const std::vector &nodes, const std::vector &points, - const std::vector &inputs, const cv::Size &img_size, const DrawingParameters ¶ms = DrawingParameters{}) +void visualize_network(const knp::framework::Network& network) { - cv::Mat out_img{img_size, CV_8UC3, params.back_color}; - // Draw inputs (black arrow from above). - for (auto input : inputs) - { - auto target_node_iter = std::find(nodes.begin(), nodes.end(), input); - if (target_node_iter == nodes.end()) continue; - cv::Point2i point = points[target_node_iter - nodes.begin()]; - draw_simple_arrow_line( - out_img, point - cv::Point2i(0, 2 * params.arrow_len + params.node_radius), point, params.arrow_len, - params.arrow_width, params.node_radius, params.node_color); - } + knp::framework::NetworkGraph graph(network); + knp::framework::ConfigVisualizePathes file_info{}; - draw_edges(out_img, adj_list, nodes, points, params); - - // Draw nodes. - for (size_t i = 0; i < nodes.size(); ++i) - { - // Drawing. - const auto &point = points[i]; - cv::circle( - out_img, {static_cast(point.x), static_cast(point.y)}, params.node_radius, params.node_color, -1); - } - return out_img; + create_dot_file_for_static_network(file_info.name_dot_file, graph); + create_png_file(file_info.name_dot_file, file_info.name_png_file); + print_network_description(graph); // description of static network } - /** - * @brief Make a reverse adjacency list. It's used to quickly find "incoming" nodes. - * @param adj_list regular adjacency list. - * @return reversed adjacency list, where each node has a list of nodes it is adjacent to. + * @brief Visualize static network by model. + * + * @param model source model for visualization. + * + * @details Visualize network. + * The model is needed to get the names of static nodes. + * By default, it saves dot/png files of graph to the current directory (the visualization directory is being created). + * For the change save directory, use set_saving_path(directory). */ -AdjacencyList make_reverse_list(const AdjacencyList &adj_list) +void visualize_network(const knp::framework::Model& model) { - AdjacencyList rev_list; - rev_list.resize(adj_list.size()); - for (size_t count = 0; count < adj_list.size(); ++count) - { - for (auto val : adj_list[count]) rev_list[val].push_back(count); - } - return rev_list; + visualize_network(model.get_network()); } - /** - * @brief Find an independent subgraph inside a larger graph. - * @param adj_list adjacency list. - * @param rev_list reversed adjacency list. - * @param remaining_nodes set of nodes that are not currently in a subgraph. - * @param ignore_nodes indexes of nodes to ignore. - * @return connected subgraph of nodes. - * @see make_reverse_list + * @brief Visualize dynamic network using backend. + * + * @param network source network for visualization. + * @param backend visualization backend. + * + * @details Visualize network. + * The network is needed to get the names of static nodes. + * The backend is needed to get all graphs connections from the bus. + * By default, it saves dot/png files of graph to the current directory (the visualization directory is being created). + * For the change save directory, use set_saving_path(directory). */ -std::vector find_connected_set( - const AdjacencyList &adj_list, const AdjacencyList &rev_list, std::unordered_set &remaining_nodes, - const std::unordered_set &ignore_nodes = {}) +void visualize_network(const knp::framework::Network& network, std::shared_ptr& backend) { - auto cleaned_nodes = remaining_nodes; - for (const auto node : ignore_nodes) cleaned_nodes.erase(node); - if (cleaned_nodes.empty()) - { - remaining_nodes = {}; - return {}; - } - - // Select a node. - int node = *cleaned_nodes.begin(); - std::unordered_set processed_nodes; - std::unordered_set nonproc_nodes{node}; - // Go through all its connections and add nodes to the resulting set. - while (!nonproc_nodes.empty()) - { - int curr_node = *nonproc_nodes.begin(); - for (const AdjacencyList &curr_list : {adj_list, rev_list}) - { - // Add all the nodes a selected node is connected to. - const auto &out_nodes = curr_list[curr_node]; - for (auto node_out : out_nodes) - { - int n = static_cast(node_out); - if (n == curr_node) continue; - if (processed_nodes.find(n) != processed_nodes.end()) continue; // Node has already been processed. - if (ignore_nodes.find(n) != ignore_nodes.end()) continue; // Node is explicitly excluded. - nonproc_nodes.insert(n); - } - } - // Move current node from unprocessed to processed. - nonproc_nodes.erase(curr_node); - processed_nodes.insert(curr_node); - } + knp::framework::NetworkGraph graph(network); + knp::framework::ConfigVisualizePathes file_info{"dynamic"}; - for (int node_id : processed_nodes) remaining_nodes.erase(node_id); - - // Return connected nodes as a sorted vector. - std::vector result; - result.resize(processed_nodes.size()); - std::copy(processed_nodes.begin(), processed_nodes.end(), result.begin()); - std::sort(result.begin(), result.end()); - return result; + create_dot_file_for_dynamic_network(file_info.name_dot_file, graph, backend); + create_png_file(file_info.name_dot_file, file_info.name_png_file); } - - /** - * @brief Find all independent components inside a graph. - * @param graph network graph. - */ -std::vector> divide_graph_by_connectivity(const NetworkGraph &graph) -{ - AdjacencyList adj_list = build_adjacency_list(graph); - AdjacencyList rev_list = make_reverse_list(adj_list); - std::unordered_set remaining_nodes; - std::vector> connected_sets; - std::unordered_set ignored_nodes{static_cast(adj_list.size() - 1)}; - for (size_t i = 0; i < adj_list.size(); ++i) remaining_nodes.insert(i); - - remaining_nodes.erase(static_cast(adj_list.size()) - 1); // delete input node - while (!remaining_nodes.empty()) - { - connected_sets.push_back(find_connected_set(adj_list, rev_list, remaining_nodes, ignored_nodes)); - } - - return connected_sets; -} - - -/** - * @brief Print network subset description. - * @param adj_list adjacency list. - * @param rev_list reversed adjacency list. - * @param nodes nodes from a connected subset. - * @see build_adjacency_list, make_reverse_list, and find_connected_set. - */ -void print_connected_subset( - const NetworkGraph &graph, const AdjacencyList &adj_list, const AdjacencyList &rev_list, - const std::vector &nodes) -{ - for (auto node : nodes) - { - std::cout << "Population #" << node << " of size " << graph.nodes_[node].size_ << ": receive from"; - for (auto node_from : rev_list[node]) - if (node_from == adj_list.size() - 1) - std::cout << " #INPUT"; - else - std::cout << " #" << node_from; - std::cout << "; send to"; - for (auto node_to : adj_list[node]) std::cout << " #" << node_to; - std::cout << std::endl; - } -} - - -/** - * @brief Print all connected subsets descriptions. - * @param graph network graph. + * @brief Visualize dynamic network using backend. + * + * @param model source model for visualization. + * @param backend visualization backend. + * + * @details Visualize network. + * The model is needed to get the names of static nodes. + * The backend is needed to get all graphs connections from the bus. + * By default, it saves dot/png files of graph to the current directory (the visualization directory is being created). + * For the change save directory, use set_saving_path(directory). */ -void print_network_description(const NetworkGraph &graph) +void visualize_network(const knp::framework::Model& model, std::shared_ptr& backend) { - AdjacencyList adj_list = build_adjacency_list(graph); - AdjacencyList rev_list = make_reverse_list(adj_list); - auto connected_subsets = divide_graph_by_connectivity(graph); - for (const auto &subset : connected_subsets) - { - print_connected_subset(graph, adj_list, rev_list, subset); - std::cout << std::endl; - } + visualize_network(model.get_network(), backend); } - /** - * @brief Show the process of subgraph adjustment. - * @param graph full network graph. - * @param nodes all nodes that are contained in a subgraph. - * @param screen_size output window size. - * @param margin margin size in pixels. + * @brief Visualize the bus messages. + * + * @param network source network for bus visualization. + * @param backend visualization backend. + * + * @details Visualize the bus messages. The network is needed to get the names of static nodes. + * By default, it saves dot/png files of graph to the current directory (the visualization directory is being created). + * For the change save directory, use set_saving_path(directory). */ -void position_network_test( - const NetworkGraph &graph, const std::vector &nodes, const cv::Size &screen_size, int margin) +void visualize_bus(const knp::framework::Network& network, std::shared_ptr& backend) { - cv::theRNG().state = std::time(nullptr); - AdjacencyList adj_list = build_adjacency_list(graph); - VisualGraph vis_graph(nodes, adj_list); - int key = 0; - - // Create inputs. - std::vector inputs; - inputs.reserve(adj_list.back().size()); - std::transform( - adj_list.back().begin(), adj_list.back().end(), std::back_inserter(inputs), - [](size_t v) { return static_cast(v); }); - // for (auto v : adj_list.back()) inputs.push_back(static_cast(v)); + knp::framework::NetworkGraph graph(network); + knp::framework::ConfigVisualizePathes file_info{"bus"}; - while (key != 27) - { - auto points = vis_graph.scale_graph(screen_size, margin); - cv::Mat img = draw_annotated_subgraph(graph, adj_list, nodes, points, inputs, screen_size); - cv::imshow("Graph", img); - key = cv::waitKey(50) & 255; - vis_graph.iterate(1); - } + create_dot_file_for_bus(file_info.name_dot_file, graph, backend); + create_png_file(file_info.name_dot_file, file_info.name_png_file); } - - /** - * @brief Calculate positions of nodes. - * @param graph full network graph. - * @param nodes all nodes that are contained in a subgraph. - * @param screen_size output window size. - * @param margin margin size in pixels. - * @param num_iterations number of iterations for graph positioning algorithm. - * @return node coordinates. + * @brief Visualize the bus messages. + * + * @param model source model for bus visualization. + * @param backend visualization backend. + * + * @details Visualize the bus messages. The model is needed to get the names of static nodes. + * By default, it saves dot/png files of graph to the current directory (the visualization directory is being created). + * For the change save directory, use set_saving_path(directory). */ -std::vector position_network( - const NetworkGraph &graph, const std::vector &nodes, const cv::Size &screen_size, int margin, - int num_iterations) +void visualize_bus(const knp::framework::Model& model, std::shared_ptr& backend) { - VisualGraph vis_graph(nodes, build_adjacency_list(graph)); - vis_graph.iterate(num_iterations); - auto result = vis_graph.scale_graph(screen_size, margin); - return result; + visualize_bus(model.get_network(), backend); } } // namespace knp::framework diff --git a/knp/base-framework/include/knp/framework/visualizer/visualize_network.h b/knp/base-framework/include/knp/framework/visualizer/visualize_network.h index f0bba766..20ed5f97 100644 --- a/knp/base-framework/include/knp/framework/visualizer/visualize_network.h +++ b/knp/base-framework/include/knp/framework/visualizer/visualize_network.h @@ -20,13 +20,13 @@ */ #pragma once +#include #include +#include #include #include -#include - /** * @brief Framework namespace. @@ -34,13 +34,14 @@ namespace knp::framework { + /** * @brief Network description structure used for visualization. * - * @details The structure stores a flat list of population nodes and projection edges, together with their identifiers, - * names and types. It is constructed from a @ref Network object and then used by the visualizer to build adjacency lists, - * draw sub‑graphs and compute node positions. - * + * @details The structure stores a flat list of population nodes and projection edges, together with their identifiers, + * names and types. It is constructed from a @ref Network object and then used by the visualizer to build adjacency + * lists, draw sub‑graphs and compute node positions. + * * @note You can use this to check network structure. */ struct KNP_DECLSPEC NetworkGraph @@ -49,7 +50,7 @@ struct KNP_DECLSPEC NetworkGraph /** * @brief Description of a population node. * - * @details Each node corresponds to a population in the original network. The fields store the population size, its + * @details Each node corresponds to a population in the original network. The fields store the population size, its * unique identifier, a human‑readable name and the neuron type index (used only for drawing legends). */ struct Node @@ -77,6 +78,22 @@ struct KNP_DECLSPEC NetworkGraph */ // cppcheck-suppress unusedStructMember size_t type_; + + /** + * @brief A flag showing a dynamic or static node. + * + * @details It is needed for the graph extension. + */ + // cppcheck-suppress unusedStructMember + bool is_static = true; + + /** + * @brief A flag showing a visible or invisible node. + * + * @details It is needed for the graph extension. ( for drawing edges without src or dst ) + */ + // cppcheck-suppress unusedStructMember + bool is_invisible = false; }; /** @@ -88,8 +105,9 @@ struct KNP_DECLSPEC NetworkGraph /** * @brief Description of a projection edge. * - * @details An edge connects a source population (@p index_from_) to a target population (@p index_to_). It stores the - * projection size, its UID, a readable name and the synapse type index (used for color‑coding in the visualizer). + * @details An edge connects a source population (@p index_from_) to a target population (@p index_to_). It stores + * the projection size, its UID, a readable name and the synapse type index (used for color‑coding in the + * visualizer). */ struct Edge { @@ -128,6 +146,14 @@ struct KNP_DECLSPEC NetworkGraph */ // cppcheck-suppress unusedStructMember size_t type_; + + /** + * @brief A flag showing a dynamic or static node. + * + * @details It is needed for the graph extension. + */ + // cppcheck-suppress unusedStructMember + bool is_static = true; }; /** @@ -138,85 +164,164 @@ struct KNP_DECLSPEC NetworkGraph /** * @brief Build network graph from a network. - * + * * @param network source network for a graph. - * - * @details Populations are added as nodes and projections as edges. The constructor extracts UIDs, names and sizes from - * the network. + * + * @details Populations are added as nodes and projections as edges. The constructor extracts UIDs, names and sizes + * from the network. */ - explicit NetworkGraph(const knp::framework::Network &network); + explicit NetworkGraph(const knp::framework::Network& network); }; /** - * @brief Print node and edge connections of a network graph. + * @brief Print node and edge connections of a network graph (static). * * @param graph network graph. * - * @details The function writes a textual description of each node (population) and its incoming and outgoing edges to `stdout`. - * It is primarily useful for debugging the connectivity extraction logic. + * @details The function writes a textual description of each node (population) and its incoming and outgoing edges to + * `stdout`. It is primarily useful for debugging the connectivity extraction logic. */ -KNP_DECLSPEC void print_network_description(const NetworkGraph &graph); +KNP_DECLSPEC void print_network_description(const NetworkGraph& graph); /** - * @brief Print whole network information. - * - * @param graph network graph. - * - * @note The output format is not intended for end‑users; it is a raw dump useful for developers. + * @brief Visualize static network. + * + * @param network source network for visualization. + * + * @details Visualize network. + * The model is needed to get the names of static nodes. + * By default, it saves dot/png files of graph to the current directory (the visualization directory is being created). + * For the change save directory, use set_saving_path(directory). */ -KNP_DECLSPEC void print_modified_network_description(const NetworkGraph &graph); +KNP_DECLSPEC void visualize_network(const knp::framework::Network& network); /** - * @brief Divide a network graph into independent sub‑graphs. + * @brief Visualize static network by model. * - * @param graph network graph. + * @param model source model for visualization. * - * @return vector of sub‑graphs, each represented by a list of node indexes. + * @details Visualize network. + * The model is needed to get the names of static nodes. + * By default, it saves dot/png files of graph to the current directory (the visualization directory is being created). + * For the change save directory, use set_saving_path(directory). + */ +KNP_DECLSPEC void visualize_network(const knp::framework::Model& model); + + +/** + * @brief Visualize dynamic network using backend. * - * @details The function builds an adjacency list, creates a reverse list for fast inbound look‑ups, and then repeatedly - * extracts maximal connected components (ignoring the artificial input node). The resulting sets are sorted for - * deterministic ordering. + * @param network source network for visualization. + * @param backend visualization backend. + * + * @details Visualize network. + * The network is needed to get the names of static nodes. + * The backend is needed to get all graphs connections from the bus. + * By default, it saves dot/png files of graph to the current directory (the visualization directory is being created). + * For the change save directory, use set_saving_path(directory). */ -KNP_DECLSPEC std::vector> divide_graph_by_connectivity(const NetworkGraph &graph); +KNP_DECLSPEC void visualize_network( + const knp::framework::Network& network, std::shared_ptr& backend); /** - * @brief Compute positions of nodes in a sub‑graph. + * @brief Visualize dynamic network using backend. * - * @param graph full network graph. - * @param nodes indexes of the nodes that belong to the sub‑graph. - * @param screen_size output window size. - * @param margin border size for the network graph, in pixels. - * @param num_iterations number of iterations for the force‑directed layout algorithm. + * @param model source model for visualization. + * @param backend visualization backend. * - * @return coordinates of the nodes after layout. + * @details Visualize network. + * The model is needed to get the names of static nodes. + * The backend is needed to get all graphs connections from the bus. + * By default, it saves dot/png files of graph to the current directory (the visualization directory is being created). + * For the change save directory, use set_saving_path(directory). + */ +KNP_DECLSPEC void visualize_network(const knp::framework::Model& model, std::shared_ptr& backend); + + +//TODO: +// KNP_DECLSPEC void visualize_bus(std::shared_ptr& backend); + + +/** + * @brief Visualize the bus messages. + * + * @param network source network for bus visualization. + * @param backend visualization backend. + * + * @details Visualize the bus messages. The network is needed to get the names of static nodes. + * By default, it saves dot/png files of graph to the current directory (the visualization directory is being created). + * For the change save directory, use set_saving_path(directory). + */ +KNP_DECLSPEC void visualize_bus(const knp::framework::Network& network, std::shared_ptr& backend); + + +/** + * @brief Visualize the bus messages. + * + * @param model source model for bus visualization. + * @param backend visualization backend. * - * @details The function runs the physics‑based layout for @p num_iterations steps and then rescales the resulting positions - * to fit inside @p screen_size with the requested @p margin. + * @details Visualize the bus messages. The model is needed to get the names of static nodes. + * By default, it saves dot/png files of graph to the current directory (the visualization directory is being created). + * For the change save directory, use set_saving_path(directory). */ -KNP_DECLSPEC std::vector position_network( - const NetworkGraph &graph, const std::vector &nodes, cv::Size screen_size, int margin, int num_iterations); +KNP_DECLSPEC void visualize_bus(const knp::framework::Model& model, std::shared_ptr& backend); /** - * @brief Visualize the iterative positioning of a sub‑graph. - * - * @param graph base network graph. - * @param nodes indexes of the nodes that belong to the sub‑graph. - * @param screen_size output image size. - * @param margin size of borders in pixels (default = 50). - * - * @details The function opens an OpenCV window and repeatedly: - * 1. Scales the current graph to the screen - * 2. Draws the annotated sub‑graph - * 3. Displays the image - * 4. Advances the physics simulation by one iteration. - * - * @note Press **Esc** to exit the visualization. + * @brief Configuration structure for visualization paths. */ -KNP_DECLSPEC void position_network_test( - const NetworkGraph &graph, const std::vector &nodes, const cv::Size &screen_size, int margin = 50); +struct ConfigVisualizePathes +{ + std::string mode = "static"; + std::string name; + + inline static std::string default_path = "visualization_docs"; + std::string dir; + std::string name_dot_file; + std::string name_png_file; + + ConfigVisualizePathes() { init(); } + + explicit ConfigVisualizePathes(const std::string& mode_val) : mode(mode_val) { init(); } + +private: + void init() + { + name = "graph_" + mode; + dir = default_path; + name_dot_file = dir + "/dot_files/" + name + ".dot"; + name_png_file = dir + "/png_files/" + name + ".png"; + + try + { + std::filesystem::create_directories(dir + "/dot_files"); + std::filesystem::create_directories(dir + "/png_files"); + } + catch (const std::filesystem::filesystem_error& ex) + { + std::cerr << "Failed to create directories: " << ex.what() << std::endl; + } + } +}; + + +/** + * @brief Set directory for saving visualization files. + * + * @param dir directory path for saving. + * + * @details Set directory for saving visualization files. + * Directory "visualization" is being created and it contains two subdirectories for dot files and png files. + * If the directory does not exist, it will be created. + * If the first character is /, then the path is absolute. (format: /path/to/absolute/dir) + * if not, relative to the current directory. format: path/to/relative/dir) + */ +KNP_DECLSPEC void set_saving_path(std::string dir); + + } // namespace knp::framework diff --git a/knp/tests/framework/visualize_network_test.cpp b/knp/tests/framework/visualize_network_test.cpp new file mode 100644 index 00000000..6f6bb460 --- /dev/null +++ b/knp/tests/framework/visualize_network_test.cpp @@ -0,0 +1,241 @@ +/** + * @file visualize_network_test.cpp + * @brief Functions for subgraph visualization. + * @kaspersky_support Kirill L. + * @date 05.08.2026 + * @license Apache 2.0 + * @copyright © 2026 AO Kaspersky Lab + * + * 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 +#include +#include +#include +#include +#include + +#include + +#include +#include + + +using BLIFATParams = knp::neuron_traits::neuron_parameters; +using DeltaProjection = knp::core::Projection; +using Synapse = DeltaProjection::Synapse; + + +// Helper function to create a simple network for testing. +knp::framework::Network create_test_network() +{ + knp::framework::Network network; + + // Create two populations. + auto pop1 = knp::framework::population::creators::make_random(5); + auto pop2 = knp::framework::population::creators::make_random(3); + + network.add_population(pop1); + network.add_population(pop2); + + // Connect them. + auto proj_uid = network.connect_populations< + knp::synapse_traits::DeltaSynapse, knp::neuron_traits::BLIFATNeuron, knp::neuron_traits::BLIFATNeuron>( + pop1, pop2); + + return network; +} + + +// Helper function to create a simple model for testing. +knp::framework::Model create_test_model() +{ + knp::framework::Network network = create_test_network(); + return knp::framework::Model(std::move(network)); +} + + +TEST(VisualizeNetworkSuite, NetworkGraphConstruction) +{ + knp::framework::Network network = create_test_network(); + knp::framework::NetworkGraph graph(network); + + // Check that we have correct number of nodes and edges. + ASSERT_EQ(graph.nodes_.size(), 2); + ASSERT_EQ(graph.edges_.size(), 1); + + // Check node properties + ASSERT_EQ(graph.nodes_[0].size_, 5); + ASSERT_EQ(graph.nodes_[1].size_, 3); + + // Check edge properties + ASSERT_EQ(graph.edges_[0].size_, 15); // 5 * 3 = 15 synapses + ASSERT_EQ(graph.edges_[0].index_from_, 0); + ASSERT_EQ(graph.edges_[0].index_to_, 1); +} + + +TEST(VisualizeNetworkSuite, NetworkGraphNodeAndEdgeAccess) +{ + knp::framework::Network network = create_test_network(); + knp::framework::NetworkGraph graph(network); + + // Test accessing nodes + ASSERT_FALSE(graph.nodes_.empty()); + ASSERT_EQ(graph.nodes_.size(), 2); + + // Test accessing edges + ASSERT_FALSE(graph.edges_.empty()); + ASSERT_EQ(graph.edges_.size(), 1); + + // Test node properties + const auto& node0 = graph.nodes_[0]; + const auto& node1 = graph.nodes_[1]; + + ASSERT_GT(node0.size_, 0); + ASSERT_GT(node1.size_, 0); + ASSERT_FALSE(node0.name_.empty()); + ASSERT_FALSE(node1.name_.empty()); + + // Test edge properties + const auto& edge = graph.edges_[0]; + ASSERT_GT(edge.size_, 0); + ASSERT_FALSE(edge.name_.empty()); + ASSERT_EQ(edge.index_from_, 0); + ASSERT_EQ(edge.index_to_, 1); +} + + +TEST(VisualizeNetworkSuite, StaticVisualizationFilesGeneration) +{ + knp::framework::Network network = create_test_network(); + knp::framework::NetworkGraph graph(network); + + // Set up paths for testing + std::string test_dir = "./test_visualization/"; + std::filesystem::remove_all(test_dir); + std::filesystem::create_directories(test_dir); + + // Change the default path for testing + knp::framework::set_saving_path(test_dir); + + // Generate dot file + knp::framework::ConfigVisualizePathes file_info{}; + std::string dot_file = file_info.name_dot_file; + std::string png_file = file_info.name_png_file; + + // Test that we can create dot file. + EXPECT_NO_THROW( + [&]() + { + // Verify that the graph has expected structure. + ASSERT_EQ(graph.nodes_.size(), 2); + ASSERT_EQ(graph.edges_.size(), 1); + + // Verify that the paths are correctly formed. + ASSERT_FALSE(dot_file.empty()); + ASSERT_FALSE(png_file.empty()); + + // Verify that directories exist + std::filesystem::path dot_dir = std::filesystem::path(dot_file).parent_path(); + std::filesystem::path png_dir = std::filesystem::path(png_file).parent_path(); + + ASSERT_TRUE(std::filesystem::exists(dot_dir)); + ASSERT_TRUE(std::filesystem::exists(png_dir)); + ASSERT_TRUE(std::filesystem::is_directory(dot_dir)); + ASSERT_TRUE(std::filesystem::is_directory(png_dir)); + + // Verify that the files don't exist yet (they will be created during visualization). + ASSERT_FALSE(std::filesystem::exists(dot_file)); + ASSERT_FALSE(std::filesystem::exists(png_file)); + + // Test actual visualization function to ensure files are created. + knp::framework::visualize_network(network); + + // Verify that files were created + ASSERT_TRUE(std::filesystem::exists(dot_file)); + ASSERT_TRUE(std::filesystem::exists(png_file)); + + // Verify that files are not empty. + ASSERT_GT(std::filesystem::file_size(dot_file), 0); + ASSERT_GT(std::filesystem::file_size(png_file), 0); + }()) + << "Failed to generate visualization files"; +} + + +TEST(VisualizeNetworkSuite, DynamicVisualizationWithBackend) +{ + knp::framework::Network network = create_test_network(); + knp::framework::Model model(std::move(network)); + + // Create backend using the correct API from backend_loader_test.cpp. + knp::framework::BackendLoader backend_loader; + auto backend = backend_loader.load(knp::testing::get_backend_path()); + + // Test that we can call visualization functions without crashing. + EXPECT_NO_THROW( + [&]() + { + // This should not crash. + knp::framework::visualize_network(model.get_network(), backend); + knp::framework::visualize_bus(model.get_network(), backend); + }()) + << "Dynamic visualization failed with exception"; +} + + +TEST(VisualizeNetworkSuite, ModelVisualization) +{ + knp::framework::Model model = create_test_model(); + + // Test that we can visualize model directly. + EXPECT_NO_THROW( + [&]() + { + knp::framework::visualize_network(model); + // knp::framework::visualize_network(model, nullptr); // Test with null backend. + }()) + << "Model visualization failed with exception"; +} + + +TEST(VisualizeNetworkSuite, NetworkGraphPrintFunctions) +{ + knp::framework::Network network = create_test_network(); + knp::framework::NetworkGraph graph(network); + + // Test that print functions don't crash. + EXPECT_NO_THROW( + [&]() + { + // These functions write to stdout, so we just ensure they don't throw. + knp::framework::print_network_description(graph); + }()) + << "Print functions failed with exception"; +} + + +TEST(VisualizeNetworkSuite, VisualizationPathConfiguration) +{ + // Test setting custom visualization paths. + std::string custom_path = "/tmp/custom_visualization/"; + std::filesystem::create_directories(custom_path); + + knp::framework::set_saving_path(custom_path); + + // Verify default path was changed. + knp::framework::ConfigVisualizePathes config; + ASSERT_EQ(config.dir, custom_path); +}