Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add k-shortest-path #435

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -160,5 +160,6 @@ add_subdirectory(refactor_module)
add_subdirectory(merge_module)
add_subdirectory(csv_utils_module)
add_subdirectory(algo_module)
add_subdirectory(shortest_path_module)

add_cugraph_subdirectory(cugraph_module)
5 changes: 5 additions & 0 deletions cpp/shortest_path_module/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
set(shortest_path_module_src
shortest_path_module.cpp
algorithm/k_weighted_shortest_paths.cpp)

add_query_module(shortest_path 1 "${shortest_path_module_src}")
237 changes: 237 additions & 0 deletions cpp/shortest_path_module/algorithm/k_weighted_shortest_paths.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
#include "k_weighted_shortest_paths.hpp"
#include <cstdint>
#include <mgp.hpp>
#include <queue>
#include <string>
#include <unordered_map>
#include "mg_procedure.h"

KWeightedShortestPaths::EdgeWeight KWeightedShortestPaths::GetEdgeWeight(mgp::Graph &graph, mgp::Node &node1,
mgp::Node &node2,
const std::string_view &weight_name) {
for (const auto &relationship : node1.OutRelationships()) {
if (relationship.To() == node2 && relationship.GetProperty(std::string(weight_name)).IsNumeric()) {
return {relationship, relationship.GetProperty(std::string(weight_name)).ValueNumeric()};
}
}
for (const auto &relationship : node1.InRelationships()) {
if (relationship.From() == node2 && relationship.GetProperty(std::string(weight_name)).IsNumeric()) {
return {relationship, relationship.GetProperty(std::string(weight_name)).ValueNumeric()};
}
}
// Dummy relationship with infinite weight, since empty constructor is not supported in C++ API
return {graph.Relationships().begin().operator*(), std::numeric_limits<double>::infinity()};
}

KWeightedShortestPaths::DijkstraResult KWeightedShortestPaths::Dijkstra(
mgp::Graph &graph, mgp::Node &source, mgp::Node &sink, const std::string_view &weight_name,
const std::set<uint64_t> &ignore_nodes, const std::set<std::pair<uint64_t, uint64_t>> &ignore_edges) {
std::unordered_map<uint64_t, double> distances;
std::unordered_map<uint64_t, mgp::Id> previous_nodes;
std::unordered_map<uint64_t, mgp::Id> previous_edges;

std::priority_queue<std::pair<mgp::Node, double>, std::vector<std::pair<mgp::Node, double>>,
KWeightedShortestPaths::CompareWeightDistance>
queue;

for (const auto &node : graph.Nodes()) {
if (node == source) {
distances[node.Id().AsUint()] = 0;
} else {
distances[node.Id().AsUint()] = std::numeric_limits<double>::infinity();
}

queue.push({node, distances[node.Id().AsUint()]});
}

while (!queue.empty()) {
std::pair<mgp::Node, double> element = queue.top();
mgp::Node node = element.first;
queue.pop();

for (const auto &relationship : node.OutRelationships()) {
mgp::Node neighbor = relationship.To();

if (ignore_nodes.find(neighbor.Id().AsUint()) != ignore_nodes.end()) {
continue;
}

if (ignore_edges.find({node.Id().AsUint(), neighbor.Id().AsUint()}) != ignore_edges.end()) {
continue;
}
double weight = relationship.GetProperty(std::string(weight_name)).ValueNumeric();
double alternative_distance = distances[node.Id().AsUint()] + weight;

if (alternative_distance < distances[neighbor.Id().AsUint()]) {
distances[neighbor.Id().AsUint()] = alternative_distance;
previous_nodes[neighbor.Id().AsUint()] = node.Id();
previous_edges[neighbor.Id().AsUint()] = relationship.Id();
queue.push({neighbor, distances[neighbor.Id().AsUint()]});
}
}
}

if (previous_nodes.find(sink.Id().AsUint()) == previous_nodes.end()) {
// Dijkstra's algorithm didn't find a path from the source to the sink
return {{std::numeric_limits<double>::infinity(), {}, {}}};
}
KWeightedShortestPaths::TempPath shortest_path = {0, {}, {}};
for (mgp::Node node = sink; node != source; node = graph.GetNodeById(previous_nodes[node.Id().AsUint()])) {
shortest_path.vertices.push_back(node);
auto relationships = node.InRelationships();
for (const auto &relationship : relationships) {
if (relationship.Id() == previous_edges[node.Id().AsUint()]) {
shortest_path.edges.push_back(relationship);
break;
}
}
}
shortest_path.vertices.push_back(source);
std::reverse(shortest_path.vertices.begin(), shortest_path.vertices.end());
std::reverse(shortest_path.edges.begin(), shortest_path.edges.end());
shortest_path.weight = distances[sink.Id().AsUint()];

return {shortest_path, distances};
}

std::vector<KWeightedShortestPaths::TempPath> KWeightedShortestPaths::YenKSP(mgp::Graph &graph, mgp::Node &source,
mgp::Node &sink, int K,
const std::string_view &weight_name) {
std::vector<KWeightedShortestPaths::TempPath> shortest_paths;
std::priority_queue<KWeightedShortestPaths::TempPath, std::vector<KWeightedShortestPaths::TempPath>,
KWeightedShortestPaths::CompareWeightPath>
candidates;

KWeightedShortestPaths::DijkstraResult result = Dijkstra(graph, source, sink, weight_name);
shortest_paths.push_back(result.path);

for (int k = 1; k < K; ++k) {
KWeightedShortestPaths::TempPath prev_shortest_path = shortest_paths[k - 1];

for (size_t i = 0; i < prev_shortest_path.vertices.size() - 1; ++i) {
mgp::Node spur_node = prev_shortest_path.vertices[i];
KWeightedShortestPaths::TempPath root_path;
root_path.weight = 0;

root_path.vertices.insert(root_path.vertices.end(), prev_shortest_path.vertices.begin(),
prev_shortest_path.vertices.begin() + i + 1);
// Add previous edge weights
for (size_t j = 0; j < root_path.vertices.size() - 1; ++j) {
KWeightedShortestPaths::EdgeWeight edge_weight =
GetEdgeWeight(graph, root_path.vertices[j], root_path.vertices[j + 1], weight_name);

if (root_path.weight == std::numeric_limits<double>::infinity()) {
break;
}
root_path.weight += edge_weight.weight;
root_path.edges.push_back(edge_weight.rel);
}

std::set<uint64_t> ignore_nodes;
std::set<std::pair<uint64_t, uint64_t>> ignore_edges;

for (const KWeightedShortestPaths::TempPath &path : shortest_paths) {
if (path.vertices.size() > i &&
std::equal(path.vertices.begin(), path.vertices.begin() + i + 1, root_path.vertices.begin())) {
ignore_edges.insert({path.vertices[i].Id().AsUint(), path.vertices[i + 1].Id().AsUint()});
}
}

for (size_t j = 0; j < i; ++j) {
ignore_nodes.insert(prev_shortest_path.vertices[j].Id().AsUint());
}

KWeightedShortestPaths::DijkstraResult spur_result =
KWeightedShortestPaths::Dijkstra(graph, spur_node, sink, weight_name, ignore_nodes, ignore_edges);

if (!spur_result.path.vertices.empty()) {
KWeightedShortestPaths::TempPath total_path = root_path;
total_path.weight += spur_result.path.weight;
total_path.vertices.insert(total_path.vertices.end(), spur_result.path.vertices.begin() + 1,
spur_result.path.vertices.end());
total_path.edges.insert(total_path.edges.end(), spur_result.path.edges.begin(), spur_result.path.edges.end());

candidates.push(total_path);
}
}

if (candidates.empty()) {
break;
}

shortest_paths.push_back(candidates.top());
candidates.pop();
}

std::set<KWeightedShortestPaths::TempPath, KWeightedShortestPaths::ComparePaths> shortest_paths_duplicates;
for (const auto &path : shortest_paths) {
shortest_paths_duplicates.insert(path);
}
std::vector<KWeightedShortestPaths::TempPath> shortest_paths_vector(shortest_paths_duplicates.begin(),
shortest_paths_duplicates.end());
std::sort(shortest_paths_vector.begin(), shortest_paths_vector.end(),
[](const KWeightedShortestPaths::TempPath &a, const KWeightedShortestPaths::TempPath &b) {
return a.weight < b.weight;
});

return shortest_paths_vector;
}

void KWeightedShortestPaths::KWeightedShortestPaths(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result,
mgp_memory *memory) {
mgp::MemoryDispatcherGuard guard{memory};
const auto arguments = mgp::List(args);
const auto record_factory = mgp::RecordFactory(result);
mgp::List paths{};
try {
auto start_node = arguments[0];
auto end_node = arguments[1];
auto number_of_weighted_shortest_paths = arguments[2];
auto weight_name = arguments[3];

if (!start_node.IsNode()) {
throw mgp::ValueException("Start node needs to be a node!");
}
if (!end_node.IsNode()) {
throw mgp::ValueException("End node needs to be a node!");
}
if (!number_of_weighted_shortest_paths.IsInt() or number_of_weighted_shortest_paths.ValueInt() < 1) {
throw mgp::ValueException("Number of weighted shortest paths needs to be an integer and bigger then 0!");
}
if (!weight_name.IsString()) {
throw mgp::ValueException("Weight name needs to be a string!");
}
auto source = start_node.ValueNode();
auto sync = end_node.ValueNode();
auto k = number_of_weighted_shortest_paths.ValueInt();
auto weight = weight_name.ValueString();

mgp::Graph graph{memgraph_graph};

auto k_shortest_paths = KWeightedShortestPaths::YenKSP(graph, source, sync, k, weight);

for (auto path : k_shortest_paths) {
mgp::Map path_map{};
path_map.Insert("weight", mgp::Value(path.weight));
mgp::List path_nodes;
mgp::List path_edges;
mgp::Path path_object{path.vertices.front()};
for (auto node : path.vertices) {
path_nodes.AppendExtend(mgp::Value(node));
}
for (auto edge : path.edges) {
path_edges.AppendExtend(mgp::Value(edge));
path_object.Expand(edge);
}
path_map.Insert(KWeightedShortestPaths::kResultPathNodes, mgp::Value(path_nodes));
path_map.Insert(KWeightedShortestPaths::kResultPathEdges, mgp::Value(path_edges));
path_map.Insert(KWeightedShortestPaths::kResultPathObject, mgp::Value(path_object));
auto record = record_factory.NewRecord();
record.Insert(KWeightedShortestPaths::kResultPaths, path_map);
}

} catch (const std::exception &e) {
record_factory.SetErrorMessage(e.what());
return;
}
}
75 changes: 75 additions & 0 deletions cpp/shortest_path_module/algorithm/k_weighted_shortest_paths.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#pragma once

#include <sys/types.h>
#include <mgp.hpp>
#include <string_view>

namespace KWeightedShortestPaths {

/* KWeightedShortestPath constants */
constexpr const char *kProcedure = "k_weighted_shortest_paths";
constexpr const char *kArgumentStartNode = "start_node";
constexpr const char *kArgumentEndNode = "end_node";
constexpr const char *kArgumentNumberOfWeightedShortestPaths = "number_of_weighted_shortest_paths";
constexpr const char *kArgumentWeightName = "weight_property_name";
constexpr const char *kResultPaths = "paths";
constexpr const char *kResultPathNodes = "path_nodes";
constexpr const char *kResultPathEdges = "path_edges";
constexpr const char *kResultPathObject = "path_object";
constexpr const int64_t kDefaultNumberOfWeightedShortestPaths = 5;
constexpr const char *kDefaultWeightName = "weight";

struct TempPath {
double weight;
std::vector<mgp::Node> vertices;
std::vector<mgp::Relationship> edges;
};

struct EdgeWeight {
mgp::Relationship rel;
double weight;
};

struct ComparePaths {
bool operator()(const TempPath &path1, const TempPath &path2) const {
if (path1.vertices.size() != path2.vertices.size()) {
return path1.vertices.size() < path2.vertices.size();
}

for (size_t i = 0; i < path1.vertices.size(); ++i) {
if (path1.vertices[i].Id().AsUint() != path2.vertices[i].Id().AsUint()) {
return path1.vertices[i].Id().AsUint() < path2.vertices[i].Id().AsUint();
}
}
// Compare weights with a tolerance
double tolerance = 1e-9;
return std::abs(path1.weight - path2.weight) > tolerance;
}
};

struct CompareWeightDistance {
bool operator()(std::pair<mgp::Node, double> const &n1, std::pair<mgp::Node, double> const &n2) {
return n1.second > n2.second;
}
};
struct CompareWeightPath {
bool operator()(TempPath const &p1, TempPath const &p2) { return p1.weight > p2.weight; }
};

struct DijkstraResult {
TempPath path;
std::unordered_map<uint64_t, double> distances;
};

EdgeWeight GetEdgeWeight(mgp::Graph &graph, mgp::Node &node1, mgp::Node &node2, const std::string_view &weight_name);

DijkstraResult Dijkstra(mgp::Graph &graph, mgp::Node &source, mgp::Node &sink, const std::string_view &weight_name,
const std::set<uint64_t> &ignore_nodes = {},
const std::set<std::pair<uint64_t, uint64_t>> &ignore_edges = {});

void KWeightedShortestPaths(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory);

std::vector<TempPath> YenKSP(mgp::Graph &graph, mgp::Node &source, mgp::Node &sink, int K,
const std::string_view &weight_name);

} // namespace KWeightedShortestPaths
25 changes: 25 additions & 0 deletions cpp/shortest_path_module/shortest_path_module.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#include <mg_utils.hpp>
#include "algorithm/k_weighted_shortest_paths.hpp"
#include "mgp.hpp"

extern "C" int mgp_init_module(struct mgp_module *module, struct mgp_memory *memory) {
try {
mgp::MemoryDispatcherGuard guard{memory};

AddProcedure(KWeightedShortestPaths::KWeightedShortestPaths, KWeightedShortestPaths::kProcedure,
mgp::ProcedureType::Read,
{mgp::Parameter(KWeightedShortestPaths::kArgumentStartNode, mgp::Type::Node),
mgp::Parameter(KWeightedShortestPaths::kArgumentEndNode, mgp::Type::Node),
mgp::Parameter(KWeightedShortestPaths::kArgumentNumberOfWeightedShortestPaths, mgp::Type::Int,
mgp::Value(KWeightedShortestPaths::kDefaultNumberOfWeightedShortestPaths)),
mgp::Parameter(KWeightedShortestPaths::kArgumentWeightName, mgp::Type::String,
mgp::Value(KWeightedShortestPaths::kDefaultWeightName))},

{mgp::Return(KWeightedShortestPaths::kResultPaths, mgp::Type::Map)}, module, memory);
} catch (const std::exception &e) {
return 1;
}
return 0;
}

extern "C" int mgp_shutdown_module() { return 0; }
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE (:Node {id:1});
CREATE (:Node {id:2});
CREATE (:Node {id:3});
CREATE (:Node {id:4});
CREATE (:Node {id:5});
MATCH (n1:Node {id:1}), (n2:Node{id:2}) CREATE (n1)-[:REL {weight:1, rel:"1 and 2"}]->(n2);
MATCH (n1:Node {id:1}), (n2:Node{id:3}) CREATE (n1)-[:REL {weight:4, rel:"1 and 3"}]->(n2);
MATCH (n1:Node {id:1}), (n2:Node{id:4}) CREATE (n1)-[:REL {weight:4, rel:"1 and 4"}]->(n2);
MATCH (n1:Node {id:2}), (n2:Node{id:3}) CREATE (n1)-[:REL {weight:1, rel:"2 and 3"}]->(n2);
MATCH (n1:Node {id:3}), (n2:Node{id:4}) CREATE (n1)-[:REL {weight:2, rel:"3 and 4"}]->(n2);
MATCH (n1:Node {id:3}), (n2:Node{id:5}) CREATE (n1)-[:REL {weight:2, rel:"3 and 5"}]->(n2);
MATCH (n1:Node {id:4}), (n2:Node{id:5}) CREATE (n1)-[:REL {weight:2, rel:"4 and 5"}]->(n2);
MATCH (n1:Node {id:1}), (n2:Node{id:5}) CREATE (n1)-[:REL {weight:3, rel:"1 and 5"}]->(n2);
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
query: >
MATCH (startNode:Node{id:1})
WITH startNode
MATCH (endNode:Node{id:5})
WITH startNode, endNode
CALL shortest_path.k_weighted_shortest_paths(startNode, endNode, 3, "weight") YIELD paths
WITH paths.path_edges AS edges
UNWIND edges AS edge
RETURN edge.rel AS rel



output:
- rel: "1 and 5" # 1. path
- rel: "1 and 2"
- rel: "2 and 3"
- rel: "3 and 5" # 2. path
- rel: "1 and 3"
- rel: "3 and 5" # 3. path

Loading
Loading