-
Martin Christoph Hierholzer authoredMartin Christoph Hierholzer authored
VariableNetworkModuleGraphDumpingVisitor.cc 10.72 KiB
// SPDX-FileCopyrightText: Deutsches Elektronen-Synchrotron DESY, MSK, ChimeraTK Project <chimeratk-support@desy.de>
// SPDX-License-Identifier: LGPL-3.0-or-later
#include "VariableNetworkModuleGraphDumpingVisitor.h"
#include "Application.h"
#include "DeviceModule.h"
#include "Module.h"
#include "VariableNetwork.h"
#include "VisitorHelper.h"
#include <boost/algorithm/string.hpp>
#include <algorithm>
#include <sstream>
#include <typeinfo>
namespace ChimeraTK {
/********************************************************************************************************************/
VariableNetworkModuleGraphDumpingVisitor::VariableNetworkModuleGraphDumpingVisitor(std::ostream& stream)
: Visitor<Application, Module, VariableNetwork>(), VariableNetworkNodeDumpingVisitor(stream, "\\n") {}
/********************************************************************************************************************/
void VariableNetworkModuleGraphDumpingVisitor::dispatch(const Application& t) {
stream() << "digraph application {\n"
<< " label=\"<" << boost::core::demangle(typeid(t).name()) << ">" << t.getName() << "\";\n"
<< " tooltip=\"\";\n"
<< " layout=\"twopi\";\n"
<< " fontname=\"Sans\";\n"
<< " fontsize=\"36\";\n"
<< " overlap=\"scalexy\";\n"
<< " node [style=\"filled,rounded\", shape=box, fontsize=\"10\"];\n"
<< " edge [fontsize=\"10\"];\n"
<< " root=\"TheCenter\";\n"
<< " splines=true;\n"
<< " \n";
// create nodes for all ApplicationModules
for(const auto& module : t.getSubmoduleListRecursive()) {
if(module->getModuleType() != EntityOwner::ModuleType::ApplicationModule) continue;
module->accept(*this);
}
/*
// create nodes for all DeviceModules
for(const auto& pair : t.deviceModuleMap) {
pair.second->accept(*this);
} */
// collect edges (one edge per pair of connected modules)
for(const auto& network : t.networkList) {
network.accept(*this);
}
// Correct directions of the edges: to get useful graphs, graphviz must be able to rank the modules properly
// The Devices should have the first rank. (The ControlSystem is not represented by a node in the graph...)
// Arrows are pointing to the last rank
// In the twopi mode, the first rank is in the middle
// First, create a symmetric connection map containing all connections (twice, because symmetric)
std::map<std::string, std::unordered_set<std::string>> connectionMap;
for(const auto& edge : _edgeMap) {
auto arrowPos = edge.first.find("->");
std::string first = edge.first.substr(0, arrowPos);
std::string second = edge.first.substr(arrowPos + 2);
connectionMap[first].insert(second);
connectionMap[second].insert(first);
}
// Now compute for each edge the longest distance to any device for both ends of the edge
std::list<std::pair<std::string, std::string>> swapList;
std::unordered_set<std::string> constraintMap;
for(const auto& edge : _edgeMap) {
std::list<std::string> longestPath;
// Define functor to recurse the connection map
std::function<void(const std::list<std::string>& path, size_t&)> recurse;
recurse = [&](const std::list<std::string>& path, size_t& distance) {
auto nextList = connectionMap[path.back()];
for(const auto& next : nextList) {
// stop recursion if device is found
if(next.substr(0, 7) == "Device_") {
if(path.size() > distance) {
// update distance
distance = path.size();
// update longestPath
longestPath = path;
longestPath.push_back(next);
}
return;
}
// stop recursion if loop is found
if(std::find(path.begin(), path.end(), next) != path.end()) {
return;
}
// continue recursion
std::list<std::string> nextPath = path;
nextPath.push_back(next);
recurse(nextPath, distance);
}
// if highest level: update constraintMap from longestPath
if(path.size() == 1) {
std::string prevNode;
for(const auto& node : longestPath) {
if(!prevNode.empty()) {
constraintMap.insert(prevNode + "->" + node);
constraintMap.insert(node + "->" + prevNode);
}
prevNode = node;
}
}
};
// split edge name to get the two module names
auto arrowPos = edge.first.find("->");
std::string first = edge.first.substr(0, arrowPos);
std::string second = edge.first.substr(arrowPos + 2);
size_t firstDistance = 0;
size_t secondDistance = 0;
// fast path: if one of the modules is a Device, there is no need to compute the distances
if(first.substr(0, 7) == "Device_") {
// first is the device, do not swap
secondDistance = 1;
constraintMap.insert(first + "->" + second);
constraintMap.insert(second + "->" + first);
}
else if(second.substr(0, 7) == "Device_") {
// second is the device, swap
firstDistance = 1;
constraintMap.insert(first + "->" + second);
constraintMap.insert(second + "->" + first);
}
else {
// compute the distances
recurse({first}, firstDistance);
recurse({second}, secondDistance);
}
// the end with the smaller (longest) distance must come first
if(secondDistance < firstDistance) {
// swap the two ends later (we cannot alter the map in the loop)
swapList.emplace_back(std::make_pair(first, second));
}
}
// perform the pre-recorded swaps
for(auto& swapPair : swapList) {
_edgeMap[swapPair.second + "->" + swapPair.first] = _edgeMap.at(swapPair.first + "->" + swapPair.second);
_edgeMap[swapPair.second + "->" + swapPair.first].second = true;
auto it = _edgeMap.find(swapPair.first + "->" + swapPair.second);
assert(it != _edgeMap.end());
_edgeMap.erase(it);
}
// create edges from device to invisble center
stream() << "TheCenter[label=\"TheCenter\",style=\"invis\"]\n";
for(const auto& device : _deviceList) {
stream() << "TheCenter->" << device << " [style=\"invis\"]\n";
}
// create collected edges
for(const auto& edge : _edgeMap) {
stream() << edge.first;
stream() << "[tooltip=\"" << edge.second.first << "\"";
if(!constraintMap.count(edge.first)) {
stream() << ",constraint=false";
}
if(edge.second.second) {
stream() << ",dir=back";
}
stream() << "]\n";
}
stream() << "}\n";
}
/********************************************************************************************************************/
void VariableNetworkModuleGraphDumpingVisitor::dispatch(const Module& t) {
if(t.getModuleType() == EntityOwner::ModuleType::ApplicationModule) {
// use pointer to module as unique ID
stream() << "Module_" + std::to_string(size_t(&t)) << "[\n";
stream() << " fillcolor=\"#0099ff\";\n";
stream() << " tooltip=\"" << boost::core::demangle(typeid(t).name()) << "\";\n";
}
else if(t.getModuleType() == EntityOwner::ModuleType::Device) {
/*
const auto& tc = dynamic_cast<const DeviceManager&>(t);
stream() << "Device_" + tc.getDeviceAliasOrURI() << "[\n";
stream() << " fillcolor=\"#00ff00\";\n";
stream() << " tooltip=\"\";\n";
_deviceList.push_back("Device_" + tc.getDeviceAliasOrURI());
*/
}
else {
return; // ignore
}
// use qualified name as label, but strip leading application name for ApplicationModules (DeviceModules don't have
// this)
auto name = t.getQualifiedName();
auto secondSlash = name.find_first_of('/', 1);
if(secondSlash != std::string::npos) name = name.substr(secondSlash);
name = name.substr(1); // strip leading slash (for both ApplicationModules and DeviceModules)
// replace slashes with new line (<br/>)
boost::replace_all(name, "<", ""); // remove "<" and ">" as they would confuse the HTML parser (cf. DeviceModule)
boost::replace_all(name, ">", "");
boost::replace_all(name, "/", "/<br/>");
// write label
stream() << " label=<" << name << ">\n";
stream() << "]\n";
}
/********************************************************************************************************************/
void VariableNetworkModuleGraphDumpingVisitor::dispatch(const VariableNetwork& t) {
std::string feedingId;
const auto& feeder = t.getFeedingNode();
if(feeder.getType() == NodeType::Application) {
auto& owner = dynamic_cast<Module&>(*feeder.getOwningModule());
try {
feedingId = "Module_" + std::to_string(size_t(owner.findApplicationModule()));
}
catch(ChimeraTK::logic_error& e) {
// ignore if findApplicationModule() throws (may happen for the application variables coming from DeviceModules)
// TODO: handle these cases properly!
return;
}
}
else if(feeder.getType() == NodeType::Device) {
feedingId = "Device_" + feeder.getDeviceAlias();
}
else {
// ignore ControlSystem and constants etc.
return;
}
for(auto& consumer : t.getConsumingNodes()) {
std::string consumingId;
if(consumer.getType() == NodeType::Application) {
auto& owner = dynamic_cast<Module&>(*consumer.getOwningModule());
try {
consumingId = "Module_" + std::to_string(size_t(owner.findApplicationModule()));
}
catch(ChimeraTK::logic_error& e) {
// ignore if findApplicationModule() throws (may happen for the application variables coming from DeviceModules)
// TODO: handle these cases properly!
return;
}
}
else if(consumer.getType() == NodeType::Device) {
consumingId = "Device_" + consumer.getDeviceAlias();
}
else {
// ignore ControlSystem and constants etc.
continue;
}
// put/extend entry in edgeMap
if(!_edgeMap[feedingId + "->" + consumingId].first.empty()) {
_edgeMap[feedingId + "->" + consumingId].first += ",";
}
else {
_edgeMap[feedingId + "->" + consumingId].second = false;
}
// use variable name without path for better readability
auto name = feeder.getName();
size_t idx = name.find_last_of('/');
if(idx != std::string::npos) {
name = name.substr(idx + 1);
}
_edgeMap[feedingId + "->" + consumingId].first += name;
}
}
/********************************************************************************************************************/
} // namespace ChimeraTK