| 1 | /** \file |
| 2 | * \brief Test helpers for layout algorithms |
| 3 | * |
| 4 | * \author Tilo Wiedera |
| 5 | * |
| 6 | * \par License: |
| 7 | * This file is part of the Open Graph Drawing Framework (OGDF). |
| 8 | * |
| 9 | * \par |
| 10 | * Copyright (C)<br> |
| 11 | * See README.md in the OGDF root directory for details. |
| 12 | * |
| 13 | * \par |
| 14 | * This program is free software; you can redistribute it and/or |
| 15 | * modify it under the terms of the GNU General Public License |
| 16 | * Version 2 or 3 as published by the Free Software Foundation; |
| 17 | * see the file LICENSE.txt included in the packaging of this file |
| 18 | * for details. |
| 19 | * |
| 20 | * \par |
| 21 | * This program is distributed in the hope that it will be useful, |
| 22 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 23 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 24 | * GNU General Public License for more details. |
| 25 | * |
| 26 | * \par |
| 27 | * You should have received a copy of the GNU General Public |
| 28 | * License along with this program; if not, see |
| 29 | * http://www.gnu.org/copyleft/gpl.html |
| 30 | */ |
| 31 | |
| 32 | #pragma once |
| 33 | |
| 34 | #include <iomanip> |
| 35 | #include <random> |
| 36 | #include <regex> |
| 37 | |
| 38 | #include <ogdf/basic/DisjointSets.h> |
| 39 | #include <ogdf/basic/GraphAttributes.h> |
| 40 | #include <ogdf/basic/PriorityQueue.h> |
| 41 | #include <ogdf/module/LayoutModule.h> |
| 42 | #include <ogdf/basic/LayoutStatistics.h> |
| 43 | |
| 44 | #include <ogdf/planarity/PlanarSubgraphCactus.h> |
| 45 | #include <ogdf/planarity/MaximalPlanarSubgraphSimple.h> |
| 46 | |
| 47 | #include <graphs.h> |
| 48 | |
| 49 | //#define OGDF_LAYOUT_HELPERS_PRINT_DRAWINGS |
| 50 | |
| 51 | #define TEST_LAYOUT(TYPE, ...) describeLayout<TYPE>(#TYPE, 0, {__VA_ARGS__}) |
| 52 | |
| 53 | #ifdef OGDF_LAYOUT_HELPERS_PRINT_DRAWINGS |
| 54 | namespace layout_helpers { |
| 55 | int drawingCounter = 0; |
| 56 | } |
| 57 | #endif |
| 58 | |
| 59 | inline void getRandomLayout(GraphAttributes &GA) |
| 60 | { |
| 61 | const Graph &G = GA.constGraph(); |
| 62 | double max_x = 2.0 * sqrt(G.numberOfNodes()); |
| 63 | double max_y = max_x; |
| 64 | |
| 65 | std::minstd_rand rng(randomSeed()); |
| 66 | std::uniform_real_distribution<> rand_x(0.0,max_x); |
| 67 | std::uniform_real_distribution<> rand_y(0.0,max_y); |
| 68 | |
| 69 | for(node v : G.nodes) { |
| 70 | GA.x(v) = rand_x(rng); |
| 71 | GA.y(v) = rand_y(rng); |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | //! Calls the layout algorithm \p L on \p G. |
| 76 | /** |
| 77 | * Executes the layout algorithm, prints statistics and performs several assertions. |
| 78 | * |
| 79 | * @param name Name of the instance. Used only for debug output. |
| 80 | * @param G Input graph. |
| 81 | * @param L Algorithm to execute. |
| 82 | * @param extraAttributes GraphAttribute flags that this algorithm requires (besides graphics and style). |
| 83 | * @param algoPlanarizes Whether the algorithm planarizes non-planar graphs internally (i.e., produces drawings with a reasonable crossing number). |
| 84 | * @param algoRequiresPlanar Whether the algorithm requires planar graphs (not necessarily embeddings) as input. |
| 85 | * @param instanceIsPlanar Whether \p L is a planar graph. |
| 86 | * |
| 87 | **/ |
| 88 | inline int64_t callLayout(const string& name, const Graph &G, LayoutModule &L, long , bool algoPlanarizes, bool algoRequiresPlanar, bool instanceIsPlanar) |
| 89 | { |
| 90 | GraphAttributes GA(G, extraAttributes | GraphAttributes::nodeGraphics | GraphAttributes::nodeStyle | GraphAttributes::edgeGraphics | GraphAttributes::edgeStyle); |
| 91 | getRandomLayout(GA); |
| 92 | |
| 93 | int64_t result; |
| 94 | int64_t time; |
| 95 | System::usedRealTime(time); |
| 96 | L.call(GA); |
| 97 | result = System::usedRealTime(time); |
| 98 | |
| 99 | #ifdef OGDF_LAYOUT_HELPERS_PRINT_DRAWINGS |
| 100 | double sumWidths = 0; |
| 101 | double sumHeights = 0; |
| 102 | |
| 103 | GA.addAttributes(GraphAttributes::nodeLabel | GraphAttributes::edgeArrow); |
| 104 | |
| 105 | for (node v : G.nodes) { |
| 106 | sumWidths += GA.width(v); |
| 107 | sumHeights += GA.height(v); |
| 108 | |
| 109 | GA.fillColor(v) = Color::Name::Red; |
| 110 | GA.strokeColor(v) = Color::Name::Black; |
| 111 | GA.label(v) = to_string(v->index()); |
| 112 | } |
| 113 | |
| 114 | for(edge e : G.edges) { |
| 115 | GA.strokeWidth(e) = 1; |
| 116 | GA.strokeColor(e) = Color::Name::Blue; |
| 117 | GA.arrowType(e) = EdgeArrow::Last; |
| 118 | } |
| 119 | |
| 120 | GA.scale(sumWidths / GA.boundingBox().width(), sumHeights / GA.boundingBox().height(), false); |
| 121 | GA.scale(1.5, false); |
| 122 | |
| 123 | std::regex reg("\\W+" ); |
| 124 | string filename = name; |
| 125 | std::transform(filename.begin(), filename.end(), filename.begin(), ::tolower); |
| 126 | std::ofstream of("drawing-" + std::regex_replace(filename, reg, "_" ) |
| 127 | + "-n=" + to_string(G.numberOfNodes()) + "-m=" + to_string(G.numberOfEdges()) |
| 128 | + "-" + to_string(layout_helpers::drawingCounter) + ".svg" ); |
| 129 | GraphIO::drawSVG(GA, of); |
| 130 | layout_helpers::drawingCounter++; |
| 131 | #endif |
| 132 | |
| 133 | string indent = " " ; |
| 134 | std::cout << std::endl; |
| 135 | double resolution = (Math::minValue(LayoutStatistics::angles(GA))*100) / (2*Math::pi); |
| 136 | double avgEdgeLength = Math::mean(LayoutStatistics::edgeLengths(GA)); |
| 137 | double avgBends = Math::mean(LayoutStatistics::numberOfBends(GA)); |
| 138 | double avgNodeCrossings = Math::mean(LayoutStatistics::numberOfNodeCrossings(GA)); |
| 139 | double avgNodeOverlaps = Math::mean(LayoutStatistics::numberOfNodeOverlaps(GA)); |
| 140 | std::cout << indent << "angular resolution: " << std::setw(18) << std::setprecision(2) << std::fixed << resolution << " %" << std::endl; |
| 141 | std::cout << indent << "average edge length: " << std::setw(17) << avgEdgeLength << std::endl; |
| 142 | std::cout << indent << "average bends per edge: " << std::setw(14) << avgBends << std::endl; |
| 143 | std::cout << indent << "average node crossings per edge: " << std::setw(5) << avgNodeCrossings << std::endl; |
| 144 | std::cout << indent << "average node overlaps per node: " << std::setw(6) << avgNodeOverlaps << std::endl; |
| 145 | |
| 146 | // Assert that we do not have any needless bendpoints |
| 147 | for(edge e : G.edges) { |
| 148 | auto toPoint = [&](node v) { return DPoint(GA.x(v), GA.y(v)); }; |
| 149 | DPolyline bends = GA.bends(e); |
| 150 | |
| 151 | if(!bends.empty()) { |
| 152 | AssertThat(bends.front(), !Equals(toPoint(e->source()))); |
| 153 | AssertThat(bends.back(), !Equals(toPoint(e->target()))); |
| 154 | } |
| 155 | |
| 156 | int size = bends.size(); |
| 157 | bends.normalize(); |
| 158 | AssertThat(bends.size(), Equals(size)); |
| 159 | } |
| 160 | |
| 161 | // Assume that any layout algorithm that requires planar graphs or planarize produces planar drawings |
| 162 | if(algoPlanarizes || algoRequiresPlanar) { |
| 163 | int crossingNumber = Math::sum(LayoutStatistics::numberOfCrossings(GA)) / 2; |
| 164 | |
| 165 | std::cout << indent << "crossing number: " << std::setw(9) << crossingNumber << std::endl; |
| 166 | |
| 167 | if(instanceIsPlanar) { |
| 168 | AssertThat(crossingNumber, Equals(0)); |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | return result; |
| 173 | } |
| 174 | |
| 175 | /** |
| 176 | * Runs several tests for a given layout module. |
| 177 | * The layout algorithm is executed for different graphs. |
| 178 | * There are no assertions yet. |
| 179 | * |
| 180 | * \param name |
| 181 | * the name to be used for describing this module |
| 182 | * \param L |
| 183 | * the module to be tested |
| 184 | * \param extraAttributes |
| 185 | * init attributes for GraphAttributes |
| 186 | * \param req |
| 187 | * the requirements for graphs to be drawn, see GraphPropertyFlags for details |
| 188 | * \param sizes |
| 189 | * determines the approximate number of nodes (and instances) of randomly generated graphs |
| 190 | * \param planarizes |
| 191 | * whether the layout computes a planarization (i.e., we can expect few crossings for non-planar graphs) |
| 192 | * \param skipMe |
| 193 | * set this to true to skip the entire test |
| 194 | */ |
| 195 | inline void describeLayout( |
| 196 | const std::string name, |
| 197 | LayoutModule &L, |
| 198 | long = 0, |
| 199 | std::set<GraphProperty> req = {}, |
| 200 | bool planarizes = false, |
| 201 | const GraphSizes& sizes = GraphSizes(), |
| 202 | bool skipMe = false) |
| 203 | { |
| 204 | describe(name, [&] { |
| 205 | forEachGraphItWorks(req, [&](const Graph& G, const std::string& graphName, const std::set<GraphProperty>& props) { |
| 206 | callLayout(graphName, G, L, extraAttributes, planarizes, |
| 207 | doesInclude({GraphProperty::planar}, req), |
| 208 | doesInclude({GraphProperty::planar}, props)); |
| 209 | }, sizes); |
| 210 | }, skipMe); |
| 211 | } |
| 212 | |
| 213 | template<typename T> |
| 214 | inline void describeLayout( |
| 215 | const string &name, |
| 216 | int = 0, |
| 217 | std::set<GraphProperty> req = {}, |
| 218 | bool planarizes = false, |
| 219 | const GraphSizes& sizes = GraphSizes(), |
| 220 | bool skipMe = false) { |
| 221 | T layout; |
| 222 | describeLayout(name, layout, extraAttr, req, planarizes, sizes, skipMe); |
| 223 | } |
| 224 | |