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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ and it then falls back to its default. [engine/configs/geolife.json](engine/conf
| `range` | Box size (m), time window (s) and sampling limits for range queries |
| `where_at` | Probes per trajectory and max time gap (s) between points to probe |
| `where_at_scoring` | Hit radius and tolerance range (m) for scoring where-at queries |
| `frechet` | Fréchet variant for kNN, join and clustering: `Discrete` (kept vertices only, the default) or `Continuous` (polyline distance, unaffected by adding a vertex on an edge) |
| `clustering` | Route clustering metric: `enabled`, neighbour threshold `eps_m` (m) and `min_size` per cluster. Reuses the self join when `join.mode` is `Self` with the same eps |
| `allocation_alphas` | Allocation exponents to test: budgets follow size^α (`1` = size-proportional, `0` = equal). Exports for α ≠ 1 get an `_a<α>` suffix |

## Tests
Expand Down
5 changes: 5 additions & 0 deletions engine/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@ add_library(engine
src/evaluator/knn.cpp
src/query/knn.cpp
src/query/frechet_helper.cpp
src/query/continuous_frechet.cpp
src/evaluator/where_at.cpp
src/query/range_query.cpp
src/evaluator/range.cpp
src/query/join.cpp
src/evaluator/join.cpp
src/evaluator/combined.cpp
src/query/route_clustering.cpp
src/evaluator/route_clustering.cpp
)

target_include_directories(engine PUBLIC include)
Expand Down Expand Up @@ -69,6 +72,8 @@ add_executable(engine_tests
tests/config_test.cpp
tests/join_test.cpp
tests/combined_test.cpp
tests/route_clustering_test.cpp
tests/continuous_frechet_test.cpp
)
target_link_libraries(engine_tests PRIVATE engine GTest::gtest_main Arrow::arrow_shared Parquet::parquet_shared)
gtest_discover_tests(engine_tests)
10 changes: 8 additions & 2 deletions engine/configs/geolife.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
},
"join": {
"mode": "Self",
"eps_m": 500.0
}
"eps_m": 150.0
},
"clustering": {
"enabled": true,
"eps_m": 150.0,
"min_size": 5
},
"frechet": "Continuous"
}
5 changes: 4 additions & 1 deletion engine/include/backend/engine/evaluator/combined.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
namespace evaluator {

// 0 = the floor (endpoints only), 1 = perfect, without clipping. Combined averages
// one primary score per type: recall@k, range F1, where-at within-radius hit rate, join F1.
// one primary score per type: recall@k, range F1, where-at within-radius hit rate, join F1
// and route clustering.
// Missing primaries are skipped, never replaced with a diagnostic score.
// The radius is configured by hit_radius_m (100 m for the research protocol).
// Average-hit remains a diagnostic and does not contribute to the combined score.
Expand All @@ -21,11 +22,13 @@ struct NormalizedScores {
double where_at_within_radius = missing;
double where_at_average_hit = missing;
double join_f1 = missing;
double clustering_score = missing;

double knn = missing;
double range = missing;
double where_at = missing;
double join = missing;
double clustering = missing;

double combined = missing;
};
Expand Down
5 changes: 5 additions & 0 deletions engine/include/backend/engine/evaluator/evaluator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
#include "backend/engine/core/trajectory_db.hpp"
#include "backend/engine/evaluator/join.hpp"
#include "backend/engine/evaluator/knn.hpp"
#include "backend/engine/evaluator/route_clustering.hpp"
#include "backend/engine/evaluator/where_at.hpp"
#include "backend/engine/query/join.hpp"
#include "backend/engine/query/knn.hpp"
#include "backend/engine/query/range_query.hpp"
#include "backend/engine/query/route_clustering.hpp"

#include <limits>
#include <span>
Expand All @@ -19,6 +21,7 @@ struct Workloads {
std::span<const query::WhereAtQuery> where_at = {};
std::span<const query::RangeQuery> range = {};
const query::JoinWorkload *join = nullptr; // a pointer, as it also carries eps and the queries
const query::RouteClusteringWorkload *clustering = nullptr;
};

// Kendall's tau is in [-1, 1] and the others in [0, 1], or NaN if it could not be computed
Expand All @@ -28,6 +31,7 @@ struct Scoretable {
WhereAtScores where_at;
JoinScores join;
double range_f1 = std::numeric_limits<double>::quiet_NaN();
RouteClusteringScores clustering;
};

struct EvaluationResult {
Expand All @@ -49,6 +53,7 @@ class Evaluator {
double get_range_f1() const;
WhereAtScores get_where_at_scores() const;
JoinScores get_join_scores() const;
RouteClusteringScores get_clustering_scores() const;

private:
const core::TrajectoryDb &m_simplified;
Expand Down
34 changes: 34 additions & 0 deletions engine/include/backend/engine/evaluator/route_clustering.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#pragma once

#include "backend/engine/core/trajectory_db.hpp"
#include "backend/engine/query/route_clustering.hpp"

#include <cstddef>
#include <limits>

namespace evaluator {

// Reruns the route clustering on the compressed database and compares it with the reference.
// score is the symmetric macro best-match F1: each cluster's best overlap F1 with any cluster on
// the other side, averaged per side, then over both sides. NaN without reference clusters, 0 if
// nothing is predicted.
struct RouteClusteringScores {
static constexpr double missing = std::numeric_limits<double>::quiet_NaN();

double score = missing;
std::size_t reference_clusters = 0;
std::size_t predicted_clusters = 0;
std::size_t reference_covered = 0;
std::size_t predicted_covered = 0;
};

query::RouteClusters compressed_route_clusters(const core::TrajectoryDb &compressed,
const query::RouteClusteringWorkload &workload);

RouteClusteringScores compare_route_clusters(const query::RouteClusters &reference,
const query::RouteClusters &predicted);

RouteClusteringScores evaluate_route_clustering(const core::TrajectoryDb &compressed,
const query::RouteClusteringWorkload &workload);

} // namespace evaluator
2 changes: 2 additions & 0 deletions engine/include/backend/engine/pipeline/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "backend/engine/query/join.hpp"
#include "backend/engine/query/knn.hpp"
#include "backend/engine/query/range_query.hpp"
#include "backend/engine/query/route_clustering.hpp"
#include "backend/engine/query/where_at_query.hpp"
#include "backend/engine/selection/selector.hpp"
#include "backend/engine/split/split.hpp"
Expand All @@ -29,6 +30,7 @@ struct Config {
query::WhereAtConfig where_at;
evaluator::WhereAtScoringConfig where_at_scoring;
query::JoinConfig join;
query::RouteClusteringConfig clustering;
};

// Reads a JSON config. Only data_path is required, a missing key keeps its default.
Expand Down
15 changes: 15 additions & 0 deletions engine/include/backend/engine/query/frechet_helper.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

namespace query {

// Discrete compares kept vertices only. Continuous (Alt and Godau) compares the polylines, so
// adding a vertex on an edge never changes it. Both operands of a comparison use the same variant.
enum class FrechetVariant { Discrete, Continuous };

// Squared instead of std::hypot, which is slow. Fréchet only takes max and min of distances,
// Square still has order
inline double squared_point_dist(double a_x, double a_y, double b_x, double b_y) {
Expand All @@ -31,4 +35,15 @@ bool frechet_within(const core::TrajectoryView &a, const core::TrajectoryView &b

double endpoint_lower_bound(const core::TrajectoryView &a, const core::TrajectoryView &b);

// Whether the continuous Fréchet distance is at most max_dist, up to a relative 1e-12
bool continuous_frechet_within(const core::TrajectoryView &a, const core::TrajectoryView &b,
double max_dist);

// Continuous Fréchet distance within 1 mm, searched in [lower, upper]. Infinity above upper.
double continuous_frechet_distance(const core::TrajectoryView &a, const core::TrajectoryView &b,
double lower, double upper);

bool frechet_within(const core::TrajectoryView &a, const core::TrajectoryView &b, double max_dist,
FrechetVariant variant);

} // namespace query
9 changes: 7 additions & 2 deletions engine/include/backend/engine/query/join.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include "backend/engine/core/trajectory_db.hpp"
#include "backend/engine/query/frechet_helper.hpp"

#include <cstddef>
#include <cstdint>
Expand All @@ -14,6 +15,7 @@ struct JoinConfig {
std::size_t query_count = 10; // 0 = use every trajectory in the query pool
double eps_m = 500.0; // join threshold in metres, must be positive
JoinMode mode = JoinMode::HeldOut; // Self ignores query_count and the query pool
FrechetVariant frechet = FrechetVariant::Discrete;
};

struct JoinQuery {
Expand All @@ -28,6 +30,7 @@ struct JoinWorkload {
std::vector<JoinQuery> entries;
double eps_m = 0.0;
JoinMode mode = JoinMode::HeldOut;
FrechetVariant frechet = FrechetVariant::Discrete;
// Self entries contain only indices j > i; both operands are evaluated compressed.
};

Expand All @@ -39,9 +42,11 @@ JoinWorkload generate_join_queries(const core::TrajectoryDb &original,

// All unordered database pairs, endpoint-pruned, with reference truth on originals.
// No held-out queries or sampling. Nonempty trajectories must retain their endpoints/IDs/order.
JoinWorkload generate_self_join(const core::TrajectoryDb &original, double eps_m = 500.0);
JoinWorkload generate_self_join(const core::TrajectoryDb &original, double eps_m = 500.0,
FrechetVariant variant = FrechetVariant::Discrete);

std::vector<std::size_t> join(const core::TrajectoryDb &trajectories,
const core::TrajectoryView &query, double eps);
const core::TrajectoryView &query, double eps,
FrechetVariant variant = FrechetVariant::Discrete);

} // namespace query
11 changes: 8 additions & 3 deletions engine/include/backend/engine/query/knn.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include "backend/engine/core/trajectory_db.hpp"
#include "backend/engine/query/frechet_helper.hpp"

#include <cstddef>
#include <cstdint>
Expand All @@ -17,13 +18,15 @@ struct KnnConfig {
std::size_t k = 5;
std::size_t query_count = 10; // 0 = use every trajectory in the query pool
double p = 0.5; // top-k tau penalty for a pair one list holds neither of, in [0, 1]
FrechetVariant frechet = FrechetVariant::Discrete;
};

struct KnnWorkload {
core::TrajectoryDb queries;
std::vector<KnnQuery> entries;
std::size_t k;
double p = 0.5;
FrechetVariant frechet = FrechetVariant::Discrete;
};

// Samples query_count trajectories from query_pool (the held-out split) and stores their k
Expand All @@ -32,9 +35,11 @@ KnnWorkload generate_knn_queries(const core::TrajectoryDb &original,
const core::TrajectoryDb &query_pool, const KnnConfig &config = {},
std::uint64_t seed = 42);

// Indices into trajectories of the k trajectories closest to query by discrete Fréchet
// distance, closest first. Equal distances are ordered by database index.
// Indices into trajectories of the k trajectories closest to query by Fréchet distance, closest
// first. Equal distances are ordered by database index; continuous distances within 1 mm count
// as equal.
std::vector<std::size_t> knn(const core::TrajectoryDb &trajectories,
const core::TrajectoryView &query, std::size_t k);
const core::TrajectoryView &query, std::size_t k,
FrechetVariant variant = FrechetVariant::Discrete);

} // namespace query
60 changes: 60 additions & 0 deletions engine/include/backend/engine/query/route_clustering.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#pragma once

#include "backend/engine/core/trajectory_db.hpp"
#include "backend/engine/query/join.hpp"

#include <cstddef>
#include <cstdint>
#include <span>
#include <utility>
#include <vector>

namespace query {

struct RouteClusteringConfig {
bool enabled = false;
double eps_m = 500.0; // neighbour threshold, same test as the self join
std::size_t min_size = 5; // a cluster needs at least this many trajectories, medoid included
FrechetVariant frechet = FrechetVariant::Discrete;
};

using TrajectoryPair = std::pair<std::size_t, std::size_t>;

// Greedy medoid grouping. Repeatedly takes the unassigned trajectory with the most unassigned
// neighbours and groups it with them if that gives at least min_size trajectories. Every member
// neighbours its medoid, so clusters cannot chain. Ties go to the earliest start time, then the
// smallest start x, then the smallest trajectory ID, which makes the result order independent.
struct RouteClusters {
std::vector<std::ptrdiff_t> label; // cluster per trajectory, -1 = unclustered
std::vector<std::vector<std::size_t>> members; // members[c][0] is the medoid
};

RouteClusters cluster_routes(const core::TrajectoryDb &trajectories,
std::span<const TrajectoryPair> neighbours, std::size_t min_size);

// ID and endpoints of an original trajectory. The candidate pairs were pruned by endpoint
// distance, so a database is only comparable if every one of these matches.
struct TrajectoryEndpoints {
std::int32_t trajectory_id = 0;
double first_x = 0.0, first_y = 0.0, last_x = 0.0, last_y = 0.0;
bool operator==(const TrajectoryEndpoints &) const = default;
};

TrajectoryEndpoints endpoints_of(const core::TrajectoryView &trajectory);

struct RouteClusteringWorkload {
std::vector<TrajectoryPair> candidates; // endpoint-pruned pairs, i < j
RouteClusters reference;
std::vector<TrajectoryEndpoints> originals;
double eps_m = 0.0;
std::size_t min_size = 0;
FrechetVariant frechet = FrechetVariant::Discrete;
};

// Clusters the original database. A self join workload over the same trajectories, eps and
// Fréchet variant is reused instead of recomputing the Fréchet tests on the originals.
RouteClusteringWorkload generate_route_clustering(const core::TrajectoryDb &original,
const RouteClusteringConfig &config = {},
const JoinWorkload *self_join = nullptr);

} // namespace query
5 changes: 4 additions & 1 deletion engine/src/evaluator/combined.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,16 @@ NormalizedScores normalize_scores(const Scoretable &scores, const Scoretable &fl
result.where_at_average_hit =
normalize(scores.where_at.average_hit, floor.where_at.average_hit);
result.join_f1 = normalize(scores.join.f1, floor.join.f1);
result.clustering_score = normalize(scores.clustering.score, floor.clustering.score);

result.knn = result.knn_recall_at_k;
result.range = result.range_f1;
result.where_at = result.where_at_within_radius;
result.join = result.join_f1;
result.clustering = result.clustering_score;

result.combined = mean_ignoring_nan({result.knn, result.range, result.where_at, result.join});
result.combined = mean_ignoring_nan(
{result.knn, result.range, result.where_at, result.join, result.clustering});
return result;
}

Expand Down
8 changes: 7 additions & 1 deletion engine/src/evaluator/evaluator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ EvaluationResult Evaluator::evaluate() const {
.range_recall = range.recall,
.where_at = get_where_at_scores(),
.join = get_join_scores(),
.range_f1 = range.f1},
.range_f1 = range.f1,
.clustering = get_clustering_scores()},
.knn = std::move(knn.answers),
.range = std::move(range.answers)};
}
Expand All @@ -52,4 +53,9 @@ JoinScores Evaluator::get_join_scores() const {
return m_workloads.join ? evaluate_join(m_simplified, *m_workloads.join) : JoinScores{};
}

RouteClusteringScores Evaluator::get_clustering_scores() const {
return m_workloads.clustering ? evaluate_route_clustering(m_simplified, *m_workloads.clustering)
: RouteClusteringScores{};
}

} // namespace evaluator
3 changes: 2 additions & 1 deletion engine/src/evaluator/join.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ JoinScores evaluate_join(const core::TrajectoryDb &compressed,
// Simplification keeps endpoints, so the original candidates cover every possible match
Counts counts;
for (const auto j : entry.candidates) {
if (!query::frechet_within(query, compressed.trajectory(j), workload.eps_m))
if (!query::frechet_within(query, compressed.trajectory(j), workload.eps_m,
workload.frechet))
continue;
++counts.found;
if (std::ranges::binary_search(entry.reference, j))
Expand Down
2 changes: 1 addition & 1 deletion engine/src/evaluator/knn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ KnnEvaluation evaluate_knn(const core::TrajectoryDb &compressed,
core::parallel_for(workload.entries.size(), [&](std::size_t i) {
const auto &query = workload.entries[i];
const auto trajectory = workload.queries.trajectory(query.query_index);
const auto simplified = query::knn(compressed, trajectory, workload.k);
const auto simplified = query::knn(compressed, trajectory, workload.k, workload.frechet);
auto &answer = result.answers[i];
answer.query_trajectory_id = trajectory.trajectory_id;
answer.neighbors.reserve(simplified.size());
Expand Down
Loading
Loading