diff --git a/README.md b/README.md index 4cae730..f3aee3e 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ 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 | +| `allocation_alphas` | Allocation exponents to test: budgets follow size^α (`1` = size-proportional, `0` = equal). Exports for α ≠ 1 get an `_a<α>` suffix | ## Tests diff --git a/engine/configs/geolife.json b/engine/configs/geolife.json index 6cfbf36..8e29bbc 100644 --- a/engine/configs/geolife.json +++ b/engine/configs/geolife.json @@ -4,6 +4,7 @@ "export_dir": "data/output/geolife", "seed": 42, "budget_shares": [0.0025, 0.005, 0.01, 0.02], + "allocation_alphas": [1.0, 0.25, 0.0], "split": { "type": "Random", "query_percentage": 0.15 diff --git a/engine/include/backend/engine/allocation/budget_allocator.hpp b/engine/include/backend/engine/allocation/budget_allocator.hpp index c0a11c0..04d745d 100644 --- a/engine/include/backend/engine/allocation/budget_allocator.hpp +++ b/engine/include/backend/engine/allocation/budget_allocator.hpp @@ -8,5 +8,6 @@ enum class AllocatorType { SizeProportional }; std::vector create_trajectory_budgets(const core::TrajectoryDb &trajectories, AllocatorType allocator, - std::size_t total_budget); -} // namespace allocation + std::size_t total_budget, double alpha = 1.0); + +} // namespace allocation \ No newline at end of file diff --git a/engine/include/backend/engine/allocation/size_proportional.hpp b/engine/include/backend/engine/allocation/size_proportional.hpp index 61b584e..d02b39a 100644 --- a/engine/include/backend/engine/allocation/size_proportional.hpp +++ b/engine/include/backend/engine/allocation/size_proportional.hpp @@ -7,9 +7,10 @@ namespace allocation { -// Returns vector of budgets for each trip, which is proportional to the number of points in the -// trip. Keeps at least start and end points +// Returns one budget per trajectory, proportional to size^alpha. alpha = 1 is size-proportional, +// alpha = 0 gives every trajectory the same budget. Keeps at least start and end points and never +// more points than a trajectory has. std::vector proportional_budget(const core::TrajectoryDb &trajectories, - std::size_t total_budget); + std::size_t total_budget, double alpha = 1.0); } // namespace allocation diff --git a/engine/include/backend/engine/pipeline/config.hpp b/engine/include/backend/engine/pipeline/config.hpp index dff0a74..965903f 100644 --- a/engine/include/backend/engine/pipeline/config.hpp +++ b/engine/include/backend/engine/pipeline/config.hpp @@ -20,6 +20,7 @@ struct Config { std::filesystem::path export_dir = "output/queries"; std::uint64_t seed = 42; split::SplitConfig split; + std::vector allocation_alphas{1.0}; std::vector budget_shares{0.0025, 0.005, 0.01, 0.02}; std::vector algorithms{selection::AlgorithmType::RDP, selection::AlgorithmType::SED}; diff --git a/engine/src/allocation/budget_allocator.cpp b/engine/src/allocation/budget_allocator.cpp index 07a0075..3ee5c5c 100644 --- a/engine/src/allocation/budget_allocator.cpp +++ b/engine/src/allocation/budget_allocator.cpp @@ -10,11 +10,11 @@ namespace allocation { std::vector create_trajectory_budgets(const core::TrajectoryDb &trajectories, AllocatorType allocator, - std::size_t total_budget) { + std::size_t total_budget, double alpha) { switch (allocator) { case AllocatorType::SizeProportional: - return allocation::proportional_budget(trajectories, total_budget); + return allocation::proportional_budget(trajectories, total_budget, alpha); } throw std::invalid_argument("create_trajectory_budgets: unknown allocator"); } diff --git a/engine/src/allocation/size_proportional.cpp b/engine/src/allocation/size_proportional.cpp index 426ee27..ae3fa2f 100644 --- a/engine/src/allocation/size_proportional.cpp +++ b/engine/src/allocation/size_proportional.cpp @@ -1,6 +1,7 @@ #include "backend/engine/allocation/size_proportional.hpp" #include "backend/engine/allocation/constants.hpp" #include +#include #include #include #include @@ -14,25 +15,27 @@ namespace { struct TrajectoryRatio { std::size_t trajectory_index; std::size_t budget; - std::size_t size; + double weight; // a/b < c/d <=> a*d < c*b - // no need for float on size_t + // Exact while the products stay below 2^53, so alpha = 1 gives the old integer results. bool operator<(const TrajectoryRatio &other) const { - return budget * other.size < other.budget * size; + return static_cast(budget) * other.weight < + static_cast(other.budget) * weight; } bool operator>(const TrajectoryRatio &other) const { return other < *this; } }; void distribute_leftovers(const std::vector &sizes, std::size_t total_budget, - std::size_t &allocated, std::vector &budgets) { + std::size_t &allocated, std::vector &budgets, + const std::vector &weights) { std::priority_queue, std::greater> min_ratio; for (std::size_t i = 0; i < sizes.size(); ++i) { if (budgets[i] < sizes[i]) { - min_ratio.push({i, budgets[i], sizes[i]}); + min_ratio.push({i, budgets[i], weights[i]}); } } @@ -42,20 +45,21 @@ void distribute_leftovers(const std::vector &sizes, std::size_t tot budgets[top_element.trajectory_index]++; top_element.budget++; allocated++; - if (top_element.budget < top_element.size) { + if (top_element.budget < sizes[top_element.trajectory_index]) { min_ratio.push(top_element); } } } void take_back_from_overshoot(const std::vector &sizes, std::size_t total_budget, - std::size_t &allocated, std::vector &budgets) { + std::size_t &allocated, std::vector &budgets, + const std::vector &weights) { std::priority_queue, std::less> max_ratio; for (std::size_t i = 0; i < sizes.size(); ++i) { if (budgets[i] > kMinPointsPerTrajectory) { - max_ratio.push({i, budgets[i], sizes[i]}); + max_ratio.push({i, budgets[i], weights[i]}); } } @@ -74,7 +78,9 @@ void take_back_from_overshoot(const std::vector &sizes, std::size_t } // namespace std::vector proportional_budget(const core::TrajectoryDb &trajectories, - std::size_t total_budget) { + std::size_t total_budget, double alpha) { + if (!std::isfinite(alpha)) + throw std::invalid_argument("proportional_budget: alpha must be finite"); const std::size_t num_trajectories = trajectories.size(); const std::size_t num_points = trajectories.num_points(); @@ -98,20 +104,31 @@ std::vector proportional_budget(const core::TrajectoryDb &trajector return sizes; } + std::vector weights(num_trajectories); + double weight_sum = 0.0; + for (std::size_t i = 0; i < num_trajectories; ++i) { + weights[i] = std::pow(static_cast(sizes[i]), alpha); + weight_sum += weights[i]; + } + if (!std::isfinite(weight_sum) || weight_sum <= 0.0) + throw std::invalid_argument("proportional_budget: alpha out of range for these sizes"); + std::vector budgets(num_trajectories); std::size_t allocated = 0; for (std::size_t i = 0; i < num_trajectories; ++i) { - const std::size_t share = sizes[i] * total_budget / num_points; + // Multiply before dividing: with alpha 1: every value is an exact integer, so the floor + // equals the old sizes[i] * total_budget / num_points + const auto share = + static_cast(static_cast(total_budget) * weights[i] / weight_sum); budgets[i] = std::clamp(share, kMinPointsPerTrajectory, sizes[i]); allocated += budgets[i]; } if (allocated > total_budget) { - take_back_from_overshoot(sizes, total_budget, allocated, budgets); + take_back_from_overshoot(sizes, total_budget, allocated, budgets, weights); } else if (allocated < total_budget) { - distribute_leftovers(sizes, total_budget, allocated, budgets); + distribute_leftovers(sizes, total_budget, allocated, budgets, weights); } return budgets; } - } // namespace allocation diff --git a/engine/src/pipeline/config.cpp b/engine/src/pipeline/config.cpp index 3b66c0a..bc48c14 100644 --- a/engine/src/pipeline/config.cpp +++ b/engine/src/pipeline/config.cpp @@ -95,7 +95,8 @@ void parse_join_config(Config &config, const json &join) { Config parse_config(const json &data) { validate_keys(data, {"data_path", "export_results", "export_dir", "seed", "budget_shares", - "algorithms", "split", "knn", "range", "where_at", "where_at_scoring", "join"}, + "allocation_alphas", "algorithms", "split", "knn", "range", "where_at", + "where_at_scoring", "join"}, "root"); Config config; @@ -113,6 +114,9 @@ Config parse_config(const json &data) { if (!std::isfinite(share) || share < 0.0 || share > 1.0) throw std::invalid_argument("config: budget_shares must be finite and between 0 and 1"); } + config.allocation_alphas = data.value("allocation_alphas", config.allocation_alphas); + if (config.allocation_alphas.empty()) + throw std::invalid_argument("config: allocation_alphas must not be empty"); config.seed = data.value("seed", config.seed); if (data.contains("algorithms")) { config.algorithms.clear(); diff --git a/engine/src/pipeline/pipeline.cpp b/engine/src/pipeline/pipeline.cpp index 9e0bfda..b566f9d 100644 --- a/engine/src/pipeline/pipeline.cpp +++ b/engine/src/pipeline/pipeline.cpp @@ -122,26 +122,34 @@ void run(const Config &config) { print_scores(floor); for (const double share : config.budget_shares) { - const auto budgets = allocation::create_trajectory_budgets( - database, allocation::AllocatorType::SizeProportional, total_budget(database, share)); - - for (const auto algorithm : config.algorithms) { - const auto simplification = selection::simplify_database(database, algorithm, budgets); - const auto compressed = core::materialize(database, simplification); - std::cout << share * 100 << "% " << magic_enum::enum_name(algorithm) << ": "; - print_size("compressed", compressed); - - const evaluator::Evaluator evaluator(database, compressed, workloads, - config.where_at_scoring); - const auto evaluation = evaluator.evaluate(); - print_scores(evaluation.scores); - print_normalized(evaluator::normalize_scores(evaluation.scores, floor)); - - if (config.export_results) { - const std::string name = - std::format("{}_{:g}pct", magic_enum::enum_name(algorithm), share * 100); - io::export_trajectories(config.export_dir, name, compressed); - io::export_results(config.export_dir, name, evaluation); + for (const double alpha : config.allocation_alphas) { + const auto budgets = allocation::create_trajectory_budgets( + database, allocation::AllocatorType::SizeProportional, + total_budget(database, share), alpha); + + for (const auto algorithm : config.algorithms) { + const auto simplification = + selection::simplify_database(database, algorithm, budgets); + const auto compressed = core::materialize(database, simplification); + std::cout << share * 100 << "% " << magic_enum::enum_name(algorithm) + << " alpha=" << alpha << ": "; + print_size("compressed", compressed); + + const evaluator::Evaluator evaluator(database, compressed, workloads, + config.where_at_scoring); + const auto evaluation = evaluator.evaluate(); + print_scores(evaluation.scores); + print_normalized(evaluator::normalize_scores(evaluation.scores, floor)); + + if (config.export_results) { + std::string name = + std::format("{}_{:g}pct", magic_enum::enum_name(algorithm), share * 100); + // alpha = 1 keeps the old name, which the Travis frontend expects + if (alpha != 1.0) + name += std::format("_a{:g}", alpha); + io::export_trajectories(config.export_dir, name, compressed); + io::export_results(config.export_dir, name, evaluation); + } } } } diff --git a/engine/tests/config_test.cpp b/engine/tests/config_test.cpp index b5ad4d3..b6cc8cb 100644 --- a/engine/tests/config_test.cpp +++ b/engine/tests/config_test.cpp @@ -27,6 +27,7 @@ TEST(Config, MissingKeysKeepDefaults) { EXPECT_DOUBLE_EQ(config.split.query_percentage, defaults.split.query_percentage); EXPECT_EQ(config.seed, 42u); EXPECT_EQ(config.budget_shares, defaults.budget_shares); + EXPECT_EQ(config.allocation_alphas, defaults.allocation_alphas); EXPECT_EQ(config.algorithms, defaults.algorithms); EXPECT_EQ(config.knn.k, defaults.knn.k); EXPECT_EQ(config.range.query_count, defaults.range.query_count); @@ -169,3 +170,15 @@ TEST(Config, SelfJoinModeIsExplicitAndDefaultsToHeldOut) { })")), std::invalid_argument); } + +TEST(Config, ReadsAllocationAlphas) { + const auto config = pipeline::load_config(write_config( + "alphas.json", R"({"data_path": "d", "allocation_alphas": [1.0, 0.25, 0.0]})")); + EXPECT_EQ(config.allocation_alphas, (std::vector{1.0, 0.25, 0.0})); +} + +TEST(Config, RejectsEmptyAllocationAlphas) { + EXPECT_THROW(pipeline::load_config(write_config( + "no_alphas.json", R"({"data_path": "d", "allocation_alphas": []})")), + std::invalid_argument); +} diff --git a/engine/tests/size_proportional_test.cpp b/engine/tests/size_proportional_test.cpp index 5cb95bb..6acdd15 100644 --- a/engine/tests/size_proportional_test.cpp +++ b/engine/tests/size_proportional_test.cpp @@ -1,5 +1,6 @@ #include +#include #include #include #include @@ -91,24 +92,48 @@ TEST(ProportionalBudget, ThrowsWhenTrajectoryHasTooFewPoints) { TEST(ProportionalBudget, RandomInputsSatisfyInvariants) { std::mt19937 rng(42); - for (int iteration = 0; iteration < 2000; ++iteration) { - const std::size_t num_trajectories = 1 + rng() % 20; - std::vector sizes(num_trajectories); - std::size_t num_points = 0; - for (std::size_t &size : sizes) { - size = allocation::kMinPointsPerTrajectory + rng() % 500; - num_points += size; + for (const double alpha : {-0.5, 0.0, 0.25, 0.5, 1.0}) { + for (int iteration = 0; iteration < 2000; ++iteration) { + const std::size_t num_trajectories = 1 + rng() % 20; + std::vector sizes(num_trajectories); + std::size_t num_points = 0; + for (std::size_t &size : sizes) { + size = allocation::kMinPointsPerTrajectory + rng() % 500; + num_points += size; + } + const std::size_t min_budget = allocation::kMinPointsPerTrajectory * num_trajectories; + const std::size_t budget = min_budget + rng() % (num_points - min_budget + 1); + + const auto budgets = allocation::proportional_budget(MakeDb(sizes), budget, alpha); + + ASSERT_EQ(budgets.size(), num_trajectories); + EXPECT_EQ(Sum(budgets), budget); + for (std::size_t i = 0; i < num_trajectories; ++i) { + EXPECT_GE(budgets[i], allocation::kMinPointsPerTrajectory); + EXPECT_LE(budgets[i], sizes[i]); + } } - const std::size_t min_budget = allocation::kMinPointsPerTrajectory * num_trajectories; - const std::size_t budget = min_budget + rng() % (num_points - min_budget + 1); + } +} - const auto budgets = allocation::proportional_budget(MakeDb(sizes), budget); +TEST(ProportionalBudget, AlphaZeroGivesEqualBudgets) { + const std::vector expected = {5, 5, 5}; + EXPECT_EQ(allocation::proportional_budget(MakeDb({10, 20, 30}), 15, 0.0), expected); +} - ASSERT_EQ(budgets.size(), num_trajectories); - EXPECT_EQ(Sum(budgets), budget); - for (std::size_t i = 0; i < num_trajectories; ++i) { - EXPECT_GE(budgets[i], allocation::kMinPointsPerTrajectory); - EXPECT_LE(budgets[i], sizes[i]); - } - } +TEST(ProportionalBudget, AlphaHalfFollowsSquareRootOfSize) { + // Weights are sqrt(16) = 4 and sqrt(64) = 8 + const std::vector expected = {4, 8}; + EXPECT_EQ(allocation::proportional_budget(MakeDb({16, 64}), 12, 0.5), expected); +} + +TEST(ProportionalBudget, NegativeAlphaCapsShortTrajectoryAndMovesTheRest) { + // The short trajectory's share is 19 but it only has 10 points, the rest goes to the long one + const std::vector expected = {10, 10}; + EXPECT_EQ(allocation::proportional_budget(MakeDb({10, 1000}), 20, -1.0), expected); +} + +TEST(ProportionalBudget, ThrowsOnNonFiniteAlpha) { + EXPECT_THROW(allocation::proportional_budget(MakeDb({10, 10}), 5, std::nan("")), + std::invalid_argument); }