Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cpp/memilio/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ add_library(memilio
mobility/graph_simulation.cpp
mobility/graph.h
mobility/graph.cpp
mobility/graph_builder.h
mobility/graph_builder.cpp
timer/auto_timer.h
timer/basic_timer.cpp
timer/basic_timer.h
Expand Down
40 changes: 27 additions & 13 deletions cpp/memilio/mobility/graph.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,17 @@
#ifndef GRAPH_H
#define GRAPH_H

#include <functional>
#include "memilio/utils/stl_util.h"
#include "memilio/epidemiology/age_group.h"
#include "memilio/utils/date.h"
#include "memilio/utils/uncertain_value.h"
#include "memilio/utils/parameter_distributions.h"
#include "memilio/epidemiology/damping.h"
#include "memilio/geography/regions.h"
#include <algorithm>
#include <functional>
#include <iostream>
#include <ranges>

#include "boost/filesystem.hpp"

Expand Down Expand Up @@ -152,30 +154,42 @@ class Graph
using NodeProperty = NodePropertyT;
using EdgeProperty = EdgePropertyT;

Graph() = default;
Graph(std::vector<Node<NodePropertyT>>&& nodes, std::vector<Edge<EdgePropertyT>>&& edges)
: m_nodes(std::move(nodes))
, m_edges(std::move(edges))
{
}

/**
* @brief add a node to the graph. property of the node is constructed from arguments.
* @brief add a node to the graph. The property of the node is constructed from arguments.
*
* @param id id for the node
* @tparam args additional arguments for node construction
*/
template <class... Args>
Node<NodePropertyT>& add_node(int id, Args&&... args)
void add_node(int id, Args&&... args)
{
m_nodes.emplace_back(id, std::forward<Args>(args)...);
return m_nodes.back();
}

/**
* @brief add an edge to the graph. property of the edge is constructed from arguments.
* @brief add an edge to the graph. The property of the edge is constructed from arguments.
* @param start_node_idx id of start node
* @param end_node_idx id of end node
* @tparam args additional arguments for edge construction
*
* If an edge with the same start and end node indices already exists, it is replaced by the newly constructed edge.
*/
template <class... Args>
Edge<EdgePropertyT>& add_edge(size_t start_node_idx, size_t end_node_idx, Args&&... args)
void add_edge(size_t start_node_idx, size_t end_node_idx, Args&&... args)
{
assert(m_nodes.size() > start_node_idx && m_nodes.size() > end_node_idx);
return *insert_sorted_replace(m_edges,
Edge<EdgePropertyT>(start_node_idx, end_node_idx, std::forward<Args>(args)...),
[](auto&& e1, auto&& e2) {
return e1.start_node_idx == e2.start_node_idx
? e1.end_node_idx < e2.end_node_idx
: e1.start_node_idx < e2.start_node_idx;
});
insert_sorted_replace(m_edges, Edge<EdgePropertyT>(start_node_idx, end_node_idx, std::forward<Args>(args)...),
[](auto&& e1, auto&& e2) {
return e1.start_node_idx == e2.start_node_idx ? e1.end_node_idx < e2.end_node_idx
: e1.start_node_idx < e2.start_node_idx;
});
}

/**
Expand Down
25 changes: 25 additions & 0 deletions cpp/memilio/mobility/graph_builder.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2020-2025 MEmilio
*
* Authors: Kilian Volmer
*
* Contact: Martin J. Kuehn <[email protected]>
*
* 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 "memilio/mobility/graph_builder.h"

namespace mio
{

}
159 changes: 159 additions & 0 deletions cpp/memilio/mobility/graph_builder.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* Copyright (C) 2020-2025 MEmilio
*
* Authors: Kilian Volmer
*
* Contact: Martin J. Kuehn <[email protected]>
*
* 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.
*/
#ifndef GRAPH_BUILDER_H
#define GRAPH_BUILDER_H

#include "memilio/mobility/graph.h"
#include "memilio/utils/logging.h"
#include "memilio/utils/stl_util.h"
#include <algorithm>
#include <functional>
#include <iostream>
#include <ranges>

namespace mio
{

/**
* @brief A builder class for constructing graphs.
*
* This class provides a interface for adding nodes and edges to a graph. It allows for efficient construction of large
* graphs by reserving space for nodes and edges in advance. The build method finalizes the graph by sorting edges and
* optionally removing duplicates.
* The advantage over the :ref add_edge function of the Graph class is that edges are only sorted once during the build
* process, improving performance when adding many edges.
*
* @tparam NodePropertyT Type of the node property.
* @tparam EdgePropertyT Type of the edge property.
*/
template <class NodePropertyT, class EdgePropertyT>
class GraphBuilder
{
public:
using NodeProperty = NodePropertyT;
using EdgeProperty = EdgePropertyT;

GraphBuilder() = default;
GraphBuilder(const size_t num_nodes, const size_t num_edges)
{
m_nodes.reserve(num_nodes);
m_edges.reserve(num_edges);
}

/**
* @brief Add a node to the GraphBuilder.
*
* The property of the node is constructed from arguments.
* @param id Id for the node.
* @tparam args Additional arguments for node construction.
*/
template <class... Args>
void add_node(int id, Args&&... args)
{
m_nodes.emplace_back(id, std::forward<Args>(args)...);
}

/**
* @brief Add an edge to the GraphBuilder.
*
* @param start_node_idx Id of start node
* @param end_node_idx Id of end node
* @tparam args Additional arguments for edge construction
*/
template <class... Args>
void add_edge(size_t start_node_idx, size_t end_node_idx, Args&&... args)
{
assert(m_nodes.size() > start_node_idx && m_nodes.size() > end_node_idx);
m_edges.emplace_back(start_node_idx, end_node_idx, std::forward<Args>(args)...);
}

/**
* @brief Build the graph from the added nodes and edges.
*
* Sorts the edges and optionally removes duplicate edges (same start and end node indices).
* Without duplicate removal, multiple edges between the same nodes are allowed and the order of insertion is stable.
* @param make_unique If true, duplicate edges are removed. The last added edge is kept!
* @return Graph<NodePropertyT, EdgePropertyT> The constructed graph.
*/
Graph<NodeProperty, EdgeProperty> build(bool make_unique = false) &&
{
sort_edges();
if (make_unique) {
remove_duplicate_edges();
}
Graph<NodeProperty, EdgeProperty> graph(std::move(m_nodes), std::move(m_edges));
return graph;
}

private:
/**
* @brief Sort the edge vector of a graph.
*
* Sorts the edges first by start node index, then by end node index. We use stable_sort to keep the order of insertion
* for edges with the same start and end node indices.
*/
void sort_edges()
{
std::stable_sort(m_edges.begin(), m_edges.end(), [](auto&& e1, auto&& e2) {
return e1.start_node_idx == e2.start_node_idx ? e1.end_node_idx < e2.end_node_idx
: e1.start_node_idx < e2.start_node_idx;
});
}

/**
* @brief Remove duplicate edges from a sorted edge vector.
*
* Copies all the unique edges to a new vector and replaces the original edge vector with it. Unique means that
* the start and end node indices are unique. Other edge properties are not checked and may get lost. Only the last
* edge in the vector is kept.
*/
void remove_duplicate_edges()
{
std::vector<Edge<EdgePropertyT>> unique_edges;
unique_edges.reserve(m_edges.size());
bool duplicate_edges = false;
auto curr_elem = m_edges.begin();

while (curr_elem != m_edges.end()) {
auto next_elem = std::next(curr_elem);
if (next_elem != m_edges.end() && curr_elem->start_node_idx == next_elem->start_node_idx &&
curr_elem->end_node_idx == next_elem->end_node_idx) {
duplicate_edges = true;
}
else if (next_elem != m_edges.end()) {
std::copy(curr_elem, next_elem, std::back_inserter(unique_edges));
}
curr_elem = next_elem;
}
std::copy(std::prev(m_edges.end()), m_edges.end(), std::back_inserter(unique_edges));
m_edges = std::move(unique_edges);
if (duplicate_edges) {
mio::log_warning("Removed duplicate edge(s)");
}
}

private:
std::vector<Node<NodePropertyT>> m_nodes;
std::vector<Edge<EdgePropertyT>> m_edges;
};

} // namespace mio

#endif // GRAPH_BUILDER_H
10 changes: 5 additions & 5 deletions cpp/memilio/utils/stl_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,26 +62,26 @@ inline std::ostream& set_ostream_format(std::ostream& out, size_t width, size_t
* @param item item to insert
* @param pred binary comparator, pred(item, a) returns true if item should go before element a,
* pred(a, item) returns true if element a should go before item
* @return iterator to inserted or replaced item in vec
*/
template <typename T, typename Pred>
typename std::vector<T>::iterator insert_sorted_replace(std::vector<T>& vec, T const& item, Pred pred)
void insert_sorted_replace(std::vector<T>& vec, T const& item, Pred pred)
{
auto bounds = std::equal_range(begin(vec), end(vec), item, pred);
auto lb = bounds.first;
auto ub = bounds.second;
assert(ub - lb <= 1); //input vector contains at most one item that is equal to the new item
if (ub - lb == 1) {
*lb = item;
return lb;
return;
}
else {
return vec.insert(lb, item);
vec.insert(lb, item);
return;
}
}

template <typename T>
typename std::vector<T>::iterator insert_sorted_replace(std::vector<T>& vec, T const& item)
void insert_sorted_replace(std::vector<T>& vec, T const& item)
{
return insert_sorted_replace(vec, item, std::less<T>());
}
Expand Down
40 changes: 39 additions & 1 deletion cpp/tests/test_graph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* limitations under the License.
*/
#include "memilio/mobility/graph.h"
#include "memilio/mobility/graph_builder.h"
#include "memilio/epidemiology/age_group.h"
#include "memilio/utils/compiler_diagnostics.h"
#include "memilio/utils/date.h"
Expand Down Expand Up @@ -325,7 +326,7 @@ TEST(TestGraph, set_edges_saving_edges)
EXPECT_EQ(indices_edge1, indices_save_edges);
}

TEST(TestGraph, ot_edges)
TEST(TestGraph, out_edges)
{
mio::Graph<int, int> g;
g.add_node(0);
Expand All @@ -344,6 +345,43 @@ TEST(TestGraph, ot_edges)
EXPECT_THAT(g.out_edges(1), testing::ElementsAreArray(v1));
}

TEST(TestGraphBuilder, Build)
{
mio::GraphBuilder<int, int> builder(3, 3);
builder.add_node(0, 100);
builder.add_node(1, 100);
builder.add_node(2, 100);
builder.add_edge(0, 1, 100);
builder.add_edge(2, 1, 100);
builder.add_edge(1, 2, 100);

auto g = std::move(builder).build();

EXPECT_EQ(g.nodes().size(), 3);
EXPECT_EQ(g.edges().size(), 3);
}

TEST(TestGraphBuilder, Build_unique)
{
mio::GraphBuilder<int, int> builder;
builder.add_node(0, 100);
builder.add_node(1, 100);
builder.add_node(2, 100);
builder.add_edge(1, 2, 100);
builder.add_edge(0, 1, 100);
builder.add_edge(2, 1, 100);
builder.add_edge(1, 2, 200);
builder.add_edge(2, 1, 300);

auto g = std::move(builder).build(true);

EXPECT_EQ(g.nodes().size(), 3);
EXPECT_EQ(g.edges().size(), 3);
// The last added edges are kept:
EXPECT_EQ(g.edges()[1].property, 200);
EXPECT_EQ(g.edges()[2].property, 300);
}

namespace
{

Expand Down
16 changes: 0 additions & 16 deletions cpp/tests/test_stl_util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,22 +109,6 @@ TEST(TestInsertSortedReplace, normal)
EXPECT_THAT(v, testing::ElementsAre(1, 2, 5, 6, 7));
}

TEST(TestInsertSortedReplace, returnsValidIterator)
{
std::vector<int> v;
int x;

//There is no GTEST_NO_DEATH macro so we just let the test crash.
//If this test crashes, the function does not return a valid iterator.
//Dereferencing an invalid iterator is undefined behavior so the test
//may behave unexpectedly (pass, fail, or something else) if the iterator is invalid.
x = *mio::insert_sorted_replace(v, 5);
x = *mio::insert_sorted_replace(v, 1);
x = *mio::insert_sorted_replace(v, 4);
x = *mio::insert_sorted_replace(v, 7);
ASSERT_EQ(x, 7);
}

TEST(TestInsertSortedReplace, reverse)
{
std::vector<int> v = {5};
Expand Down
Loading