From 49aa557c149e585752515973e85c3966d569a864 Mon Sep 17 00:00:00 2001 From: Quentin Quadrat Date: Tue, 25 Jun 2024 02:08:24 +0200 Subject: [PATCH] Implement spring forces on net when importing format where positions are not defined #25 --- include/TimedPetriNetEditor/PetriNet.hpp | 2 +- src/Editor/DearImGui/Editor.cpp | 25 +++- src/Editor/DearImGui/Editor.hpp | 14 +- src/Editor/DearImGui/KeyBindings.hpp | 1 + src/Net/Imports/ImportTimedEventGraph.cpp | 2 +- src/Net/Imports/Imports.cpp | 8 +- src/Net/Imports/Imports.hpp | 2 + src/Net/PetriNet.cpp | 6 +- src/Utils/ForceDirected.cpp | 150 +++++++++++++++++++ src/Utils/ForceDirected.hpp | 166 ++++++++++++++++++++++ src/julia/Julia.cpp | 3 +- 11 files changed, 366 insertions(+), 13 deletions(-) create mode 100644 src/Utils/ForceDirected.cpp create mode 100644 src/Utils/ForceDirected.hpp diff --git a/include/TimedPetriNetEditor/PetriNet.hpp b/include/TimedPetriNetEditor/PetriNet.hpp index eebef14..766c878 100644 --- a/include/TimedPetriNetEditor/PetriNet.hpp +++ b/include/TimedPetriNetEditor/PetriNet.hpp @@ -821,7 +821,7 @@ bool convertTo(Net& net, TypeOfNet const type, std::string& error, std::vector @@ -94,6 +95,7 @@ class Editor: public PetriNetEditor, public Application void clearNet(); void undo(); void redo(); + void springify(); private: // Error logs @@ -195,9 +197,12 @@ class Editor: public PetriNetEditor, public Application // ******************************************************************** struct Canvas { - ImVec2 corners[2]; - ImVec2 size; - ImVec2 origin; + ImVec2 corners[2] = {{0.0f, 0.0f}, {0.0f, 0.0f}}; + ImVec2 size{800.0f, 600.0f}; // FIXME: size is undefined while not + // rendered once but when importing nets + // we need to know th windows size which + // is 0x0 + ImVec2 origin{0.0f, 0.0f}; ImVec2 scrolling{0.0f, 0.0f}; ImDrawList* draw_list; @@ -272,6 +277,9 @@ class Editor: public PetriNetEditor, public Application //! \brief Single Petri net the editor can edit. //! \fixme Manage several nets (like done with GEMMA). Net m_net; + //! Apply spring positive/negative forces on arcs/nodes to unfold imported + //! Petri nets that do not have positions on their nodes. + ForceDirected m_spring; //! \brief History of modifications of the net. History m_history; //! \brief Instance allowing to do timed simulation. diff --git a/src/Editor/DearImGui/KeyBindings.hpp b/src/Editor/DearImGui/KeyBindings.hpp index e1eac00..4cf08ca 100644 --- a/src/Editor/DearImGui/KeyBindings.hpp +++ b/src/Editor/DearImGui/KeyBindings.hpp @@ -25,6 +25,7 @@ // FIXME: The backend raylib does not support other than US keyboard meaning that // other keyboard mapping are fucked up. # define KEY_QUIT_APPLICATION ImGuiKey_Escape +# define KEY_SPRINGIFY_NET ImGuiKey_A # define KEY_RUN_SIMULATION ImGuiKey_Space # define KEY_RUN_SIMULATION_ALT ImGuiKey_R # define KEY_ROTATE_CW ImGuiKey_PageUp diff --git a/src/Net/Imports/ImportTimedEventGraph.cpp b/src/Net/Imports/ImportTimedEventGraph.cpp index c6c3716..3db3bea 100644 --- a/src/Net/Imports/ImportTimedEventGraph.cpp +++ b/src/Net/Imports/ImportTimedEventGraph.cpp @@ -74,7 +74,7 @@ std::string importFromTimedEventGraph(Net& net, std::string const& filename) size_t x = margin, y = margin; for (size_t id = 0u; id < transitions; ++id) { - net.addTransition(id, Transition::to_str(id), float(x), float(y), 0); + net.addTransition(id, Transition::to_str(id), randomInt(0,800), randomInt(0,600), 0); x += dx; if (x > w - margin) { x = margin; y += dy; } } diff --git a/src/Net/Imports/Imports.cpp b/src/Net/Imports/Imports.cpp index 6d5ae5d..d58164b 100644 --- a/src/Net/Imports/Imports.cpp +++ b/src/Net/Imports/Imports.cpp @@ -26,10 +26,10 @@ namespace tpne { std::vector const& importers() { static const std::vector s_importers = { - { "JSON", ".json", importFromJSON }, - { "Petri Net Markup Language", ".pnml", importFromPNML }, - { "Flowshop", ".flowshop", importFlowshop }, - { "Timed Event Graph", ".teg", importFromTimedEventGraph } + { "JSON", ".json", importFromJSON, false }, + { "Petri Net Markup Language", ".pnml", importFromPNML, false }, + { "Flowshop", ".flowshop", importFlowshop, false }, + { "Timed Event Graph", ".teg", importFromTimedEventGraph, true } }; return s_importers; diff --git a/src/Net/Imports/Imports.hpp b/src/Net/Imports/Imports.hpp index e10ac79..163484e 100644 --- a/src/Net/Imports/Imports.hpp +++ b/src/Net/Imports/Imports.hpp @@ -55,6 +55,8 @@ struct Importer std::string extensions; //! \brief the pointer function for importing (i.e. importFromJSON) ImportFunc importFct; + //! \brief Shall apply spring force on the net ? + bool springify; }; //! \brief Container of file formats we can import a Petri net from. diff --git a/src/Net/PetriNet.cpp b/src/Net/PetriNet.cpp index 9ee71c3..8b4ba21 100644 --- a/src/Net/PetriNet.cpp +++ b/src/Net/PetriNet.cpp @@ -755,7 +755,7 @@ std::string saveToFile(Net const& net, std::string const& filepath) } //------------------------------------------------------------------------------ -std::string loadFromFile(Net& net, std::string const& filepath) +std::string loadFromFile(Net& net, std::string const& filepath, bool& springify) { // Search the importer Importer const* importer = getImporter(extension(filepath)); @@ -771,6 +771,10 @@ std::string loadFromFile(Net& net, std::string const& filepath) { net.reset(net.type()); } + else + { + springify = importer->springify; + } // Get a name to the net if (net.name == "") diff --git a/src/Utils/ForceDirected.cpp b/src/Utils/ForceDirected.cpp new file mode 100644 index 0000000..c373c34 --- /dev/null +++ b/src/Utils/ForceDirected.cpp @@ -0,0 +1,150 @@ +/* ***************************************************************************** +** MIT License +** +** Copyright (c) 2022 Quentin Quadrat +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in all +** copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +** SOFTWARE. +******************************************************************************** +*/ + +#include "Utils/ForceDirected.hpp" + +namespace tpne { + +//------------------------------------------------------------------------------ +void ForceDirected::reset(float width, float height, Net& net) +{ + std::cout << "ForceDirected::reset " << width << " x " << height << "\n"; + m_net = &net; + m_width = width; + m_height = height; + + N = net.transitions().size() + net.places().size(); + K = sqrtf(m_width * m_height / 2.0f / float(N)); + m_temperature = m_width + m_height; + m_vertices.clear(); + m_vertices.reserve(N); + + // Copy Petri nodes to Graph vertices + for (auto& it: net.transitions()) + { + m_vertices.emplace_back(it); + } + for (auto& it: net.places()) + { + m_vertices.emplace_back(it); + } + + // Lookup table: Node ID to index on the vector + std::map lookup; + for (size_t n = 0u; n < N; ++n) + { + lookup[m_vertices[n].node->key] = n; + } + + // Add edges "source node" -> "destination node". + // Since, we need undirected graph so add edges "destination node" -> "source node" + for (size_t n = 0u; n < N; ++n) + { + Vertex& v = m_vertices[n]; + v.neighbors.reserve(2u * (v.node->arcsIn.size() + v.node->arcsOut.size())); + + for (auto& it: v.node->arcsIn) + { + v.neighbors.emplace_back(m_vertices[lookup[it->from.key]].node); + v.neighbors.emplace_back(m_vertices[lookup[it->to.key]].node); + } + + for (auto& it: v.node->arcsOut) + { + v.neighbors.emplace_back(m_vertices[lookup[it->from.key]].node); + v.neighbors.emplace_back(m_vertices[lookup[it->to.key]].node); + } + } +} + +//------------------------------------------------------------------------------ +void ForceDirected::update() +{ + if (m_net == nullptr) + return ; + + if (m_temperature < 0.1f) + return ; + + step(); +} + +//------------------------------------------------------------------------------ +void ForceDirected::step() +{ + if (m_net == nullptr) + return ; + + for (auto& v: m_vertices) + { + const ImVec2 v_position(v.node->x, v.node->y); + + // Repulsive forces: nodes -- nodes + for (auto& u: m_vertices) + { + if (u.node->key == v.node->key) + continue ; + + const ImVec2 u_position(u.node->x, u.node->y); + const ImVec2 direction(v_position - u_position); + const float dist = distance(direction); + const float rf = repulsive_force(dist); + v.displacement += direction * rf / dist; + } + + // Attractive forces: edges + for (auto& u: v.neighbors) + { + if (u->key == v.node->key) + continue ; + + const ImVec2 u_position(u->x, u->y); + const ImVec2 direction(v_position - u_position); + const float dist = distance(direction); + const float af = attractive_force(dist); + v.displacement -= direction * af / dist; + } + } + + // Update position and constrain position to the window bounds + for (auto& v: m_vertices) + { + constexpr ImVec2 LAYOUT_BORDER(50.0f, 50.0f); + const float dist = distance(v.displacement); + ImVec2 v_position(v.node->x, v.node->y); + v_position += (dist > m_temperature) + ? v.displacement * m_temperature / dist + : v.displacement; + v.node->x = std::min(m_width - LAYOUT_BORDER.x, + std::max(LAYOUT_BORDER.x, v_position.x)); + v.node->y = std::min(m_height - LAYOUT_BORDER.y, + std::max(LAYOUT_BORDER.y, v_position.y)); + v.displacement = { 0.0f, 0.0f }; + } + + cooling(); +} + +} // namespace tpne \ No newline at end of file diff --git a/src/Utils/ForceDirected.hpp b/src/Utils/ForceDirected.hpp new file mode 100644 index 0000000..149d9db --- /dev/null +++ b/src/Utils/ForceDirected.hpp @@ -0,0 +1,166 @@ +/* ***************************************************************************** +** MIT License +** +** Copyright (c) 2022 Quentin Quadrat +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in all +** copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +** SOFTWARE. +******************************************************************************** +*/ + +#ifndef FORCEDIRECTEDGRAPH_HPP +# define FORCEDIRECTEDGRAPH_HPP + +# include "TimedPetriNetEditor/PetriNet.hpp" +# include "Editor/DearImGui/DearUtils.hpp" +# include +# include +# include +# include + +namespace tpne { + +// ***************************************************************************** +//! \brief Force-directed graph drawing algorithms are a class of algorithms for +//! drawing graphs in an aesthetically-pleasing way. Their purpose is to +//! position the nodes of a graph in two-dimensional or three-dimensional space +//! so that all the edges are of more or less equal length and there are as few +//! crossing edges as possible, by assigning forces among the set of edges and +//! the set of nodes, based on their relative positions, and then using these +//! forces either to simulate the motion of the edges and nodes or to minimize +//! their energy. +//! +//! Use the spring/repulsion model of Fruchterman and Reingold (1991) with: +//! - Attractive force: af(d) = d^2 / k +//! - Repulsive force: rf(d) = -k^2 / d +//! where d is distance between two vertices and the optimal distance between +//! vertices k is defined as C * sqrt(area / num_vertices) where C is a +//! parameter we can adjust. +//! +//! For more information see this video https://youtu.be/WWm-g2nLHds +//! This code source is largely inspired by: +//! https://github.com/qdHe/Parallelized-Force-directed-Graph-Drawing +// ***************************************************************************** +class ForceDirected +{ +public: + + // ************************************************************************* + //! \brief Vertex is a 2D representation of a graph node. + // ************************************************************************* + struct Vertex + { + explicit Vertex(Transition& tr) : node(&tr) {} + explicit Vertex(Place& p) : node(&p) {} + + //! \brief Place or transition. Need to access to position. + Node* node = nullptr; + //! \brief Displacement due to attractive and reuplsive forces. + ImVec2 displacement = { 0.0f, 0.0f }; + //! \brief List of neighboring nodes. + std::vector neighbors; + }; + + using Vertices = std::vector; + +public: + + //---------------------------------------------------------------------- + //! \brief Restore initial states. + //---------------------------------------------------------------------- + void reset(float width, float height, Net& net); + void reset() { m_net = nullptr; } + + //---------------------------------------------------------------------- + //! \brief Compute one step of forces if temperature is still hot else + //! do nothing. + //---------------------------------------------------------------------- + void update(); + + //---------------------------------------------------------------------- + //! \brief Const getter of vertices. + //---------------------------------------------------------------------- + inline Vertices const& vertices() const + { + return m_vertices; + } + +private: + + //---------------------------------------------------------------------- + //! \brief Do a single step for computing forces. + //---------------------------------------------------------------------- + void step(); + + //---------------------------------------------------------------------- + //! \brief Euclidian norm. + //! \param[in] p world coordinate position. + //---------------------------------------------------------------------- + inline float distance(ImVec2 const& p) const + { + return std::max(0.001f, sqrtf(p.x * p.x + p.y * p.y)); + } + + //---------------------------------------------------------------------- + //! \brief Compute repulsive force. + //! \param[in] distance. + //---------------------------------------------------------------------- + inline float repulsive_force(float const distance) const + { + return K * K / distance / float(N) / 2.0f; + } + + //---------------------------------------------------------------------- + //! \brief Compute attractive force. + //! \param[in] distance. + //---------------------------------------------------------------------- + inline float attractive_force(float const distance) const + { + return distance * distance / K / float(N); + } + + //---------------------------------------------------------------------- + //! \brief Reduce effect of forces. + //---------------------------------------------------------------------- + inline float cooling() + { + m_temperature *= 0.98f; + return m_temperature; + } + +private: + + //! \brief The directional graph to display. + Net* m_net = nullptr; + //! \brief Collection of nodes to display. + Vertices m_vertices; + //! \brief Dimension of the screen. + float m_width; + //! \brief Dimension of the screen. + float m_height; + //! \brief Reduce effect of forces. + float m_temperature; + //! \brief Force coeficient: sqrt(area / num_vertices) + float K; + //! \brief Number of vertices. + size_t N; +}; + +} // namespace tpne + +#endif \ No newline at end of file diff --git a/src/julia/Julia.cpp b/src/julia/Julia.cpp index cb24ddb..9cf107c 100644 --- a/src/julia/Julia.cpp +++ b/src/julia/Julia.cpp @@ -367,7 +367,8 @@ bool petri_load(int64_t const pn, const char* filepath) { CHECK_VALID_PETRI_HANDLE(pn, false); - std::string err = tpne::loadFromFile(*g_petri_nets[size_t(pn)], filepath); + bool springify = false; + std::string err = tpne::loadFromFile(*g_petri_nets[size_t(pn)], filepath, springify); if (err.empty()) return true; std::cerr << "Failed loading net from " << filepath << "Reason is '"