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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions engine/configs/geolife.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions engine/include/backend/engine/allocation/budget_allocator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ enum class AllocatorType { SizeProportional };

std::vector<std::size_t> 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
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::size_t> proportional_budget(const core::TrajectoryDb &trajectories,
std::size_t total_budget);
std::size_t total_budget, double alpha = 1.0);

} // namespace allocation
1 change: 1 addition & 0 deletions engine/include/backend/engine/pipeline/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ struct Config {
std::filesystem::path export_dir = "output/queries";
std::uint64_t seed = 42;
split::SplitConfig split;
std::vector<double> allocation_alphas{1.0};
std::vector<double> budget_shares{0.0025, 0.005, 0.01, 0.02};
std::vector<selection::AlgorithmType> algorithms{selection::AlgorithmType::RDP,
selection::AlgorithmType::SED};
Expand Down
4 changes: 2 additions & 2 deletions engine/src/allocation/budget_allocator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ namespace allocation {

std::vector<std::size_t> 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");
}
Expand Down
43 changes: 30 additions & 13 deletions engine/src/allocation/size_proportional.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "backend/engine/allocation/size_proportional.hpp"
#include "backend/engine/allocation/constants.hpp"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <format>
#include <functional>
Expand All @@ -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<double>(budget) * other.weight <
static_cast<double>(other.budget) * weight;
}
bool operator>(const TrajectoryRatio &other) const { return other < *this; }
};

void distribute_leftovers(const std::vector<std::size_t> &sizes, std::size_t total_budget,
std::size_t &allocated, std::vector<std::size_t> &budgets) {
std::size_t &allocated, std::vector<std::size_t> &budgets,
const std::vector<double> &weights) {
std::priority_queue<TrajectoryRatio, std::vector<TrajectoryRatio>,
std::greater<TrajectoryRatio>>
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]});
}
}

Expand All @@ -42,20 +45,21 @@ void distribute_leftovers(const std::vector<std::size_t> &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<std::size_t> &sizes, std::size_t total_budget,
std::size_t &allocated, std::vector<std::size_t> &budgets) {
std::size_t &allocated, std::vector<std::size_t> &budgets,
const std::vector<double> &weights) {

std::priority_queue<TrajectoryRatio, std::vector<TrajectoryRatio>, std::less<TrajectoryRatio>>
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]});
}
}

Expand All @@ -74,7 +78,9 @@ void take_back_from_overshoot(const std::vector<std::size_t> &sizes, std::size_t
} // namespace

std::vector<std::size_t> 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();

Expand All @@ -98,20 +104,31 @@ std::vector<std::size_t> proportional_budget(const core::TrajectoryDb &trajector
return sizes;
}

std::vector<double> weights(num_trajectories);
double weight_sum = 0.0;
for (std::size_t i = 0; i < num_trajectories; ++i) {
weights[i] = std::pow(static_cast<double>(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<std::size_t> 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<std::size_t>(static_cast<double>(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
6 changes: 5 additions & 1 deletion engine/src/pipeline/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();
Expand Down
48 changes: 28 additions & 20 deletions engine/src/pipeline/pipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions engine/tests/config_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<double>{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);
}
59 changes: 42 additions & 17 deletions engine/tests/size_proportional_test.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <gtest/gtest.h>

#include <cmath>
#include <cstddef>
#include <cstdint>
#include <numeric>
Expand Down Expand Up @@ -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<std::size_t> 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<std::size_t> 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<std::size_t> 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<std::size_t> 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<std::size_t> 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);
}
Loading