Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ authors:
repository-code: 'https://github.com/physycom/DynamicalSystemFramework'
url: 'https://physycom.github.io/DynamicalSystemFramework/'
license: CC-BY-NC-SA-4.0
version: 5.3.4
date-released: '2026-04-13'
version: 5.3.5
date-released: '2026-04-15'
153 changes: 30 additions & 123 deletions src/dsf/base/Network.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,30 +21,26 @@
std::unordered_map<Id, std::unique_ptr<node_t>> m_nodes;
std::unordered_map<Id, std::unique_ptr<edge_t>> m_edges;

Id m_cantorHash(Id u, Id v) const;
Id m_cantorHash(std::pair<Id, Id> const& idPair) const;
constexpr inline auto m_cantorHash(Id u, Id v) const {
return ((u + v) * (u + v + 1)) / 2 + v;
}
constexpr inline auto m_cantorHash(std::pair<Id, Id> const& idPair) const {
return m_cantorHash(idPair.first, idPair.second);
}

public:
/// @brief Construct a new empty Network object
Network() = default;

/// @brief Get the nodes as an unordered map
/// @return std::unordered_map<Id, std::unique_ptr<node_t>> The nodes
std::unordered_map<Id, std::unique_ptr<node_t>> const& nodes() const;
inline auto const& nodes() const noexcept { return m_nodes; }
/// @brief Get the edges as an unordered map
/// @return std::unordered_map<Id, std::unique_ptr<edge_t>> The edges
std::unordered_map<Id, std::unique_ptr<edge_t>> const& edges() const;
inline auto const& edges() const noexcept { return m_edges; }
/// @brief Get the number of nodes
/// @return size_t The number of nodes
size_t nNodes() const;
inline auto nNodes() const noexcept { return m_nodes.size(); }
/// @brief Get the number of edges
/// @return size_t The number of edges
size_t nEdges() const;
inline auto nEdges() const noexcept { return m_edges.size(); }

/// @brief Add a node to the network
/// @tparam TNode The type of the node (default is node_t)
/// @tparam TArgs The types of the arguments
/// @param args The arguments to pass to the node's constructor
template <typename TNode = node_t, typename... TArgs>
requires(std::is_base_of_v<node_t, TNode> &&
std::constructible_from<TNode, TArgs...>)
Expand All @@ -54,122 +50,50 @@
requires(std::is_base_of_v<node_t, TNode>)
void addNDefaultNodes(size_t n);

/// @brief Add an edge to the network
/// @tparam TEdge The type of the edge (default is edge_t)
/// @tparam TArgs The types of the arguments
/// @param args The arguments to pass to the edge's constructor
template <typename TEdge = edge_t, typename... TArgs>
requires(std::is_base_of_v<edge_t, TEdge> &&
std::constructible_from<TEdge, TArgs...>)
void addEdge(TArgs&&... args);

/// @brief Get a node by id
/// @param nodeId The node's id
/// @return const node_t& A reference to the node
inline const auto& node(Id nodeId) const { return *m_nodes.at(nodeId); };
/// @brief Get a node by id
/// @param nodeId The node's id
/// @return node_t& A reference to the node
inline auto& node(Id nodeId) { return *m_nodes.at(nodeId); };
/// @brief Get an edge by id
/// @param edgeId The edge's id
/// @return const edge_t& A reference to the edge
inline const auto& edge(Id edgeId) const { return *m_edges.at(edgeId); };
/// @brief Get an edge by id
/// @param edgeId The edge's id
/// @return edge_t& A reference to the edge
inline auto& edge(Id edgeId) { return *m_edges.at(edgeId); }

edge_t& edge(Id source, Id target) const;
/// @brief Get a node by id
/// @tparam TNode The type of the node
/// @param nodeId The node's id
/// @return TNode& A reference to the node

template <typename TNode>
requires(std::is_base_of_v<node_t, TNode>)
TNode& node(Id nodeId);
/// @brief Get an edge by id
/// @tparam TEdge The type of the edge
/// @param edgeId The edge's id
/// @return TEdge& A reference to the edge

template <typename TEdge>
requires(std::is_base_of_v<edge_t, TEdge>)
TEdge& edge(Id edgeId);

/// @brief Compute betweenness centralities for all nodes using Brandes' algorithm
/// @tparam WeightFunc A callable type that takes a const reference to edge_t and returns a double representing the edge weight
/// @param getEdgeWeight A callable that takes a const reference to edge_t and returns a double (must be positive)
/// @details Implements Brandes' algorithm for directed weighted graphs.
/// The computed centrality for each node v is:
/// C_B(v) = sum_{s != v != t} sigma_st(v) / sigma_st
/// where sigma_st is the number of shortest paths from s to t,
/// and sigma_st(v) is the number of those paths passing through v.
/// Results are stored via Node::setBetweennessCentrality.
/// @brief Compute node betweenness centralities using Brandes' algorithm
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The detailed Doxygen docs for the Brandes centrality methods (constraints like “weights must be positive”, and the definition/normalization of the computed centrality) were removed. Since these are public template APIs, restoring the key behavioral/contract details would prevent misuse and reduce ambiguity for callers.

Copilot uses AI. Check for mistakes.
template <typename WeightFunc>
requires(std::is_invocable_r_v<double, WeightFunc, edge_t const&>)
void computeBetweennessCentralities(WeightFunc getEdgeWeight);

/// @brief Compute edge betweenness centralities for all edges using Brandes' algorithm
/// @tparam WeightFunc A callable type that takes a const reference to edge_t and returns a double representing the edge weight
/// @param getEdgeWeight A callable that takes a const reference to edge_t and returns a double (must be positive)
/// @details Implements Brandes' algorithm for directed weighted graphs.
/// The computed centrality for each edge e is:
/// C_B(e) = sum_{s != t} sigma_st(e) / sigma_st
/// where sigma_st is the number of shortest paths from s to t,
/// and sigma_st(e) is the number of those paths using edge e.
/// Results are stored via Edge::setBetweennessCentrality.
/// @brief Compute edge betweenness centralities using Brandes' algorithm
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The detailed Doxygen docs for the Brandes centrality methods (constraints like “weights must be positive”, and the definition/normalization of the computed centrality) were removed. Since these are public template APIs, restoring the key behavioral/contract details would prevent misuse and reduce ambiguity for callers.

Copilot uses AI. Check for mistakes.
template <typename WeightFunc>
requires(std::is_invocable_r_v<double, WeightFunc, edge_t const&>)
void computeEdgeBetweennessCentralities(WeightFunc getEdgeWeight);
};
template <typename node_t, typename edge_t>
requires(std::is_base_of_v<Node, node_t> && std::is_base_of_v<Edge, edge_t>)
Id Network<node_t, edge_t>::m_cantorHash(Id u, Id v) const {
return ((u + v) * (u + v + 1)) / 2 + v;
}
template <typename node_t, typename edge_t>
requires(std::is_base_of_v<Node, node_t> && std::is_base_of_v<Edge, edge_t>)
Id Network<node_t, edge_t>::m_cantorHash(std::pair<Id, Id> const& idPair) const {
return m_cantorHash(idPair.first, idPair.second);
}

template <typename node_t, typename edge_t>
requires(std::is_base_of_v<Node, node_t> && std::is_base_of_v<Edge, edge_t>)
std::unordered_map<Id, std::unique_ptr<node_t>> const& Network<node_t, edge_t>::nodes()
const {
return m_nodes;
}
template <typename node_t, typename edge_t>
requires(std::is_base_of_v<Node, node_t> && std::is_base_of_v<Edge, edge_t>)
std::unordered_map<Id, std::unique_ptr<edge_t>> const& Network<node_t, edge_t>::edges()
const {
return m_edges;
}

template <typename node_t, typename edge_t>
requires(std::is_base_of_v<Node, node_t> && std::is_base_of_v<Edge, edge_t>)
size_t Network<node_t, edge_t>::nNodes() const {
return m_nodes.size();
}
template <typename node_t, typename edge_t>
requires(std::is_base_of_v<Node, node_t> && std::is_base_of_v<Edge, edge_t>)
size_t Network<node_t, edge_t>::nEdges() const {
return m_edges.size();
}

template <typename node_t, typename edge_t>
requires(std::is_base_of_v<Node, node_t> && std::is_base_of_v<Edge, edge_t>)
template <typename TNode, typename... TArgs>
requires(std::is_base_of_v<node_t, TNode> && std::constructible_from<TNode, TArgs...>)
void Network<node_t, edge_t>::addNode(TArgs&&... args) {
// Create unique_ptr directly with perfect forwarding
auto pNode = std::make_unique<TNode>(std::forward<TArgs>(args)...);
if (m_nodes.contains(pNode->id())) {
throw std::invalid_argument(
std::format("Node with id {} already exists in the network.", pNode->id()));
}
m_nodes[pNode->id()] = std::move(pNode);
}

template <typename node_t, typename edge_t>
requires(std::is_base_of_v<Node, node_t> && std::is_base_of_v<Edge, edge_t>)
template <typename TNode>
Expand All @@ -180,6 +104,7 @@
addNode<TNode>(static_cast<Id>(currentSize + i));
}
}

template <typename node_t, typename edge_t>
requires(std::is_base_of_v<Node, node_t> && std::is_base_of_v<Edge, edge_t>)
template <typename TEdge, typename... TArgs>
Expand All @@ -194,16 +119,14 @@
auto const& sourceNodeId = tmpEdge.source();
auto const& targetNodeId = tmpEdge.target();

// Check if source node exists, add if not
if (!m_nodes.contains(sourceNodeId)) {
if (!geometry.empty()) {
addNode(tmpEdge.source(), geometry.front());
} else {
addNode(tmpEdge.source());
}
}

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 12.1 rule Note

MISRA 12.1 rule

// Check if target node exists, add if not
if (!m_nodes.contains(targetNodeId)) {
if (!geometry.empty()) {
addNode(tmpEdge.target(), geometry.back());
Expand All @@ -212,7 +135,6 @@
}
}

// Get fresh references to both nodes after all potential vector reallocations
auto& sourceNode = node(sourceNodeId);
auto& targetNode = node(targetNodeId);
sourceNode.addOutgoingEdge(tmpEdge.id());
Expand Down Expand Up @@ -247,6 +169,7 @@
TNode& Network<node_t, edge_t>::node(Id nodeId) {
return dynamic_cast<TNode&>(node(nodeId));
}

template <typename node_t, typename edge_t>
requires(std::is_base_of_v<Node, node_t> && std::is_base_of_v<Edge, edge_t>)
template <typename TEdge>
Expand All @@ -260,17 +183,15 @@
template <typename WeightFunc>
requires(std::is_invocable_r_v<double, WeightFunc, edge_t const&>)
void Network<node_t, edge_t>::computeBetweennessCentralities(WeightFunc getEdgeWeight) {
// Initialize all node betweenness centralities to 0
for (auto& [nodeId, pNode] : m_nodes) {
pNode->setBetweennessCentrality(0.0);
}

// Brandes' algorithm: run single-source Dijkstra from each node
for (auto const& [sourceId, sourceNode] : m_nodes) {
std::stack<Id> S; // nodes in order of non-increasing distance
std::unordered_map<Id, std::vector<Id>> P; // predecessors on shortest paths
std::unordered_map<Id, double> sigma; // number of shortest paths
std::unordered_map<Id, double> dist; // distance from source
std::stack<Id> S;
std::unordered_map<Id, std::vector<Id>> P;

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 12.3 rule Note

MISRA 12.3 rule
std::unordered_map<Id, double> sigma;

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 12.3 rule Note

MISRA 12.3 rule
std::unordered_map<Id, double> dist;

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 12.3 rule Note

MISRA 12.3 rule

for (auto const& [nId, _] : m_nodes) {
P[nId] = {};
Expand All @@ -280,7 +201,6 @@
sigma[sourceId] = 1.0;
dist[sourceId] = 0.0;

// Min-heap priority queue: (distance, nodeId)
std::priority_queue<std::pair<double, Id>,
std::vector<std::pair<double, Id>>,
std::greater<>>
Expand All @@ -293,18 +213,16 @@
auto [d, v] = pq.top();
pq.pop();

if (visited.contains(v)) {
if (visited.contains(v))

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 15.6 rule Note

MISRA 15.6 rule

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 14.4 rule Note

MISRA 14.4 rule
continue;
}
visited.insert(v);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
S.push(v);

for (auto const& edgeId : m_nodes.at(v)->outgoingEdges()) {
auto const& edgeObj = *m_edges.at(edgeId);
Id w = edgeObj.target();
if (visited.contains(w)) {
if (visited.contains(w))

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 15.6 rule Note

MISRA 15.6 rule

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 14.4 rule Note

MISRA 14.4 rule
continue;
}
double edgeWeight = getEdgeWeight(edgeObj);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
double newDist = dist[v] + edgeWeight;

Expand All @@ -320,11 +238,9 @@
}
}

// Dependency accumulation (backward pass)
std::unordered_map<Id, double> delta;
for (auto const& [nId, _] : m_nodes) {
for (auto const& [nId, _] : m_nodes)

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 15.6 rule Note

MISRA 15.6 rule

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 12.3 rule Note

MISRA 12.3 rule
delta[nId] = 0.0;
}

Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
while (!S.empty()) {
Id w = S.top();
Expand All @@ -346,18 +262,15 @@
requires(std::is_invocable_r_v<double, WeightFunc, edge_t const&>)
void Network<node_t, edge_t>::computeEdgeBetweennessCentralities(
WeightFunc getEdgeWeight) {
// Initialize all edge betweenness centralities to 0
for (auto& [edgeId, pEdge] : m_edges) {
pEdge->setBetweennessCentrality(0.0);
}

// Brandes' algorithm: run single-source Dijkstra from each node
for (auto const& [sourceId, sourceNode] : m_nodes) {
std::stack<Id> S; // nodes in order of non-increasing distance
// predecessors: P[w] = list of (predecessor node id, edge id from pred to w)
std::stack<Id> S;
std::unordered_map<Id, std::vector<std::pair<Id, Id>>> P;
std::unordered_map<Id, double> sigma; // number of shortest paths
std::unordered_map<Id, double> dist; // distance from source
std::unordered_map<Id, double> sigma;

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 12.3 rule Note

MISRA 12.3 rule
std::unordered_map<Id, double> dist;

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 12.3 rule Note

MISRA 12.3 rule

for (auto const& [nId, _] : m_nodes) {
P[nId] = {};
Expand All @@ -367,7 +280,6 @@
sigma[sourceId] = 1.0;
dist[sourceId] = 0.0;

// Min-heap priority queue: (distance, nodeId)
std::priority_queue<std::pair<double, Id>,
std::vector<std::pair<double, Id>>,
std::greater<>>
Expand All @@ -380,18 +292,16 @@
auto [d, v] = pq.top();
pq.pop();

if (visited.contains(v)) {
if (visited.contains(v))

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 14.4 rule Note

MISRA 14.4 rule

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 15.6 rule Note

MISRA 15.6 rule
continue;
}
visited.insert(v);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
S.push(v);

for (auto const& eId : m_nodes.at(v)->outgoingEdges()) {
auto const& edgeObj = *m_edges.at(eId);
Id w = edgeObj.target();
if (visited.contains(w)) {
if (visited.contains(w))

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 15.6 rule Note

MISRA 15.6 rule

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 14.4 rule Note

MISRA 14.4 rule
continue;
}
double edgeWeight = getEdgeWeight(edgeObj);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
double newDist = dist[v] + edgeWeight;

Expand All @@ -407,19 +317,16 @@
}
}

// Dependency accumulation (backward pass)
std::unordered_map<Id, double> delta;
for (auto const& [nId, _] : m_nodes) {
for (auto const& [nId, _] : m_nodes)

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 12.3 rule Note

MISRA 12.3 rule

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 15.6 rule Note

MISRA 15.6 rule
delta[nId] = 0.0;
}

Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
while (!S.empty()) {
Id w = S.top();
S.pop();
for (auto const& [v, eId] : P[w]) {
double c = (sigma[v] / sigma[w]) * (1.0 + delta[w]);
delta[v] += c;
// Accumulate edge betweenness
auto currentBC = m_edges.at(eId)->betweennessCentrality();
m_edges.at(eId)->setBetweennessCentrality(*currentBC + c);
}
Expand Down
17 changes: 13 additions & 4 deletions src/dsf/cartography/cartography.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,20 @@
if "lanes" in data:
lanes_value = data["lanes"]
if isinstance(lanes_value, list):
edge_updates["nlanes"] = max(
min([int(v) for v in lanes_value]), 1
) # Take max if list, ensure at least 1 lane
n = max(min([int(v) for v in lanes_value]), 1)

Check warning

Code scanning / Pylint (reported by Codacy)

Variable name "n" doesn't conform to snake_case naming style Warning

Variable name "n" doesn't conform to snake_case naming style

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Variable name "n" doesn't conform to snake_case naming style Warning

Variable name "n" doesn't conform to snake_case naming style
else:
edge_updates["nlanes"] = max(int(lanes_value), 1)
n = max(int(lanes_value), 1)

Check warning

Code scanning / Pylint (reported by Codacy)

Variable name "n" doesn't conform to snake_case naming style Warning

Variable name "n" doesn't conform to snake_case naming style

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Variable name "n" doesn't conform to snake_case naming style Warning

Variable name "n" doesn't conform to snake_case naming style

# If the road is bidirectional, OSM reports total lanes (both directions).
# Since the DiGraph has one edge per direction, divide by 2.
oneway_val = data.get("oneway", False)
is_oneway = (
oneway_val is True or oneway_val == "yes" or oneway_val == "True"
)
if not is_oneway:
n = max(n // 2, 1) # integer division, ensure at least 1

Check warning

Code scanning / Pylint (reported by Codacy)

Variable name "n" doesn't conform to snake_case naming style Warning

Variable name "n" doesn't conform to snake_case naming style

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Variable name "n" doesn't conform to snake_case naming style Warning

Variable name "n" doesn't conform to snake_case naming style
Comment on lines +135 to +140
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oneway parsing is too narrow and will misclassify common OSM values (e.g., "1", "-1", "true", "YES"), causing incorrect halving of lanes on actually-oneway roads. Consider normalizing string values (trim + lowercase) and treating "yes", "true", "1", and "-1" as oneway (and "no", "false", "0" as not oneway), while keeping boolean handling.

Copilot uses AI. Check for mistakes.

edge_updates["nlanes"] = n
edge_updates["_remove_lanes"] = True
else:
edge_updates["nlanes"] = 1
Expand Down
2 changes: 1 addition & 1 deletion src/dsf/dsf.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

static constexpr uint8_t DSF_VERSION_MAJOR = 5;
static constexpr uint8_t DSF_VERSION_MINOR = 3;
static constexpr uint8_t DSF_VERSION_PATCH = 4;
static constexpr uint8_t DSF_VERSION_PATCH = 5;

static auto const DSF_VERSION =
std::format("{}.{}.{}", DSF_VERSION_MAJOR, DSF_VERSION_MINOR, DSF_VERSION_PATCH);
Expand Down
Loading