Spaces:
Build error
Build error
| use pyo3::prelude::*; | |
| use rand::Rng; | |
| use std::collections::HashMap; | |
| /// Implements the classic Ski Rental problem, a foundational online algorithm. | |
| /// | |
| /// This algorithm models a scenario where a person must decide each day whether to rent skis | |
| /// for a fixed daily cost or to buy them outright for a larger fixed cost. The goal is to | |
| /// minimize the total cost over a skiing season of unknown length. This learning-augmented | |
| /// version incorporates a prediction for the total number of ski days to make a more | |
| /// informed decision. | |
| /// | |
| /// The algorithm achieves a competitive ratio of 2 in the worst case and approaches 1 | |
| /// (optimal) with perfect predictions. | |
| pub struct SkiRental { | |
| buy_cost: f64, | |
| } | |
| impl SkiRental { | |
| /// Creates a new `SkiRental` instance. | |
| /// | |
| /// # Arguments | |
| /// | |
| /// * `buy_cost` - The total cost of buying skis. This is the threshold against which | |
| /// the cumulative rental cost is compared. | |
| pub fn new(buy_cost: f64) -> Self { | |
| SkiRental { buy_cost } | |
| } | |
| /// Makes the daily decision to either rent or buy skis. | |
| /// | |
| /// The decision is based on a threshold that balances the certainty of the traditional | |
| /// deterministic algorithm with the insight from the machine learning prediction. | |
| /// | |
| /// # Arguments | |
| /// | |
| /// * `day` - The current day of the skiing season (1-indexed). | |
| /// * `prediction` - The predicted total number of ski days. | |
| /// * `trust` - A confidence score in the prediction, typically in the range [0.0, 1.0]. | |
| /// A value of 0 ignores the prediction, while 1 relies on it completely. | |
| /// | |
| /// # Returns | |
| /// | |
| /// * `bool` - Returns `true` if the decision is to buy, and `false` to continue renting. | |
| pub fn decide(&self, day: u32, prediction: f64, trust: f64) -> bool { | |
| let threshold = (1.0 - trust) * self.buy_cost + trust * prediction.min(self.buy_cost); | |
| day as f64 >= threshold | |
| } | |
| } | |
| /// Implements a randomized learning-augmented Ski Rental algorithm. | |
| /// | |
| /// This version introduces randomization into the decision-making process to achieve a | |
| /// better competitive ratio in the worst-case scenario compared to its deterministic | |
| /// counterpart. It is approximately 1.58-competitive. The algorithm uses the prediction | |
| /// to influence a probabilistic decision. | |
| pub struct RandomizedSkiRental { | |
| buy_cost: f64, | |
| } | |
| impl RandomizedSkiRental { | |
| /// Creates a new `RandomizedSkiRental` instance. | |
| /// | |
| /// # Arguments | |
| /// | |
| /// * `buy_cost` - The total cost of buying skis. | |
| pub fn new(buy_cost: f64) -> Self { | |
| RandomizedSkiRental { buy_cost } | |
| } | |
| /// Makes a probabilistic daily decision to either rent or buy skis. | |
| /// | |
| /// # Arguments | |
| /// | |
| /// * `day` - The current day of the skiing season (1-indexed). | |
| /// * `prediction` - The predicted total number of ski days. | |
| /// * `trust` - A confidence score in the prediction, typically in [0.0, 1.0]. | |
| /// | |
| /// # Returns | |
| /// | |
| /// * `bool` - Returns `true` if the decision is to buy, and `false` to rent. | |
| pub fn decide(&self, day: u32, prediction: f64, trust: f64) -> bool { | |
| let rent_cost = day as f64; | |
| // Clamp the prediction, as any prediction > buy_cost implies the same strategy. | |
| let effective_prediction = prediction.min(self.buy_cost); | |
| // The threshold is a blend of the deterministic buy cost and the clamped prediction. | |
| let threshold = (1.0 - trust) * self.buy_cost + trust * effective_prediction; | |
| // The probability scales with the rent_cost and the threshold, normalized by buy_cost^2 * e. | |
| // This ensures that low predictions (and thus low thresholds) lead to a low | |
| // probability of buying, making the algorithm robust to underestimation. | |
| let prob = (rent_cost * threshold / (self.buy_cost * self.buy_cost * std::f64::consts::E)).max(0.0).min(1.0); | |
| let mut rng = rand::thread_rng(); | |
| rng.gen_bool(prob) | |
| } | |
| } | |
| /// Implements a learning-augmented caching algorithm. | |
| /// | |
| /// This algorithm manages a cache of a fixed size. When an item is accessed, it must | |
| /// decide whether to keep or evict items in the cache. This implementation uses predictions | |
| /// about the next time an item will be accessed to make smarter eviction choices than | |
| /// traditional algorithms like LRU (Least Recently Used). | |
| pub struct Caching { | |
| cache_size: usize, | |
| predictions: HashMap<u32, u32>, | |
| } | |
| impl Caching { | |
| /// Creates a new `Caching` instance. | |
| /// | |
| /// # Arguments | |
| /// | |
| /// * `cache_size` - The maximum number of items the cache can hold. | |
| /// * `predictions` - A map where keys are item IDs and values are the predicted | |
| /// time of the next access. Lower values indicate sooner access. | |
| pub fn new(cache_size: usize, predictions: HashMap<u32, u32>) -> Self { | |
| Caching { | |
| cache_size, | |
| predictions, | |
| } | |
| } | |
| /// Processes an item access and updates the cache state. | |
| /// | |
| /// If the item is already in the cache, it's a "hit". If not, it's a "miss". | |
| /// If the cache is full on a miss, another item is evicted based on the predictions | |
| /// (item with the latest predicted next access time is evicted). | |
| /// | |
| /// # Arguments | |
| /// | |
| /// * `item` - The unique identifier of the item being accessed. | |
| /// * `cache` - A `Vec<u32>` representing the current items in the cache. | |
| /// | |
| /// # Returns | |
| /// | |
| /// A tuple `(bool, Vec<u32>)`: | |
| /// * `_ .0` (bool): `true` for a cache hit, `false` for a miss. | |
| /// * `_ .1` (Vec<u32>): The new state of the cache after the access. | |
| pub fn decide(&self, item: u32, cache: Vec<u32>) -> (bool, Vec<u32>) { | |
| let mut new_cache = cache.clone(); | |
| if new_cache.contains(&item) { | |
| return (true, new_cache); | |
| } | |
| if new_cache.len() < self.cache_size { | |
| new_cache.push(item); | |
| return (true, new_cache); | |
| } | |
| let mut evict_item_index = 0; | |
| let mut max_prediction = 0; | |
| for i in 0..new_cache.len() { | |
| let prediction = self.predictions.get(&new_cache[i]).unwrap_or(&u32::MAX); | |
| if *prediction > max_prediction { | |
| max_prediction = *prediction; | |
| evict_item_index = i; | |
| } | |
| } | |
| new_cache.remove(evict_item_index); | |
| new_cache.push(item); | |
| (false, new_cache) | |
| } | |
| } | |
| /// Implements a learning-augmented Oneway Trading algorithm. | |
| /// | |
| /// This algorithm addresses the problem of converting an initial amount of one asset | |
| /// into another by choosing the best time to execute the trade. The goal is to maximize | |
| /// the amount of the target asset obtained. This version uses a price prediction to | |
| /// decide when to trade. | |
| pub struct OnewayTrading { | |
| buy_price: f64, | |
| } | |
| impl OnewayTrading { | |
| /// Creates a new `OnewayTrading` instance. | |
| /// | |
| /// # Arguments | |
| /// | |
| /// * `buy_price` - The initial price of the asset, used as a reference for the | |
| /// deterministic part of the algorithm. | |
| pub fn new(buy_price: f64) -> Self { | |
| OnewayTrading { buy_price } | |
| } | |
| /// Decides whether to execute the trade or to wait. | |
| /// | |
| /// The decision is based on a threshold that blends the initial price with the | |
| /// predicted future price, weighted by the trust parameter. | |
| /// | |
| /// # Arguments | |
| /// | |
| /// * `current_price` - The current market price of the asset. | |
| /// * `prediction` - The predicted future price of the asset. | |
| /// * `trust` - A confidence score in the prediction, typically in [0.0, 1.0]. | |
| /// | |
| /// # Returns | |
| /// | |
| /// * `bool` - Returns `true` to execute the trade, `false` to wait. | |
| pub fn decide(&self, current_price: f64, prediction: f64, trust: f64) -> bool { | |
| let threshold = (1.0 - trust) * self.buy_price + trust * prediction; | |
| current_price >= threshold | |
| } | |
| } | |
| /// Implements a learning-augmented scheduling algorithm (makespan minimization). | |
| /// | |
| /// This algorithm assigns a set of jobs to a fixed number of machines with the goal | |
| /// of minimizing the makespan, which is the total time until the last job completes. | |
| /// It uses predictions of job lengths to sort them, aiming to schedule shorter jobs | |
| /// first (a variant of the Shortest Processing Time heuristic). | |
| pub struct Scheduling { | |
| num_machines: usize, | |
| } | |
| impl Scheduling { | |
| /// Creates a new `Scheduling` instance. | |
| /// | |
| /// # Arguments | |
| /// | |
| /// * `num_machines` - The number of identical machines available for processing jobs. | |
| pub fn new(num_machines: usize) -> Self { | |
| Scheduling { num_machines } | |
| } | |
| /// Assigns a list of jobs to the available machines. | |
| /// | |
| /// This method sorts jobs based on their predicted lengths and then assigns each | |
| /// job to the machine that will become free earliest. | |
| /// | |
| /// # Arguments | |
| /// | |
| /// * `job_lengths` - A `Vec<u32>` containing the true, actual lengths of the jobs. | |
| /// * `predictions` - A `Vec<u32>` containing the predicted lengths of the jobs. | |
| /// The order of predictions must correspond to the order of `job_lengths`. | |
| /// | |
| /// # Returns | |
| /// | |
| /// * `Vec<usize>` - A vector where the element at index `i` is the machine ID | |
| /// (0 to `num_machines - 1`) assigned to job `i`. | |
| pub fn decide(&self, job_lengths: Vec<u32>, predictions: Vec<u32>) -> Vec<usize> { | |
| let jobs: Vec<(usize, u32)> = job_lengths.iter().map(|&x| x).enumerate().collect(); | |
| let mut sorted_jobs: Vec<(usize, u32)> = predictions.iter().map(|&x| x).enumerate().collect(); | |
| sorted_jobs.sort_by_key(|k| k.1); | |
| let mut assignments = vec![0; jobs.len()]; | |
| let mut machine_loads = vec![0; self.num_machines]; | |
| for (job_index, _) in sorted_jobs { | |
| let mut best_machine = 0; | |
| let mut min_load = u32::MAX; | |
| for j in 0..self.num_machines { | |
| if machine_loads[j] < min_load { | |
| min_load = machine_loads[j]; | |
| best_machine = j; | |
| } | |
| } | |
| assignments[job_index] = best_machine; | |
| machine_loads[best_machine] += jobs[job_index].1; | |
| } | |
| assignments | |
| } | |
| } | |
| /// Implements a learning-augmented search algorithm. | |
| /// | |
| /// This algorithm finds the maximum value in a list. It uses a prediction for the | |
| /// index of the maximum value as a starting point for its search, which can improve | |
| /// performance in certain online or resource-constrained scenarios. | |
| pub struct Search { | |
| max_value: u32, | |
| } | |
| impl Search { | |
| /// Creates a new `Search` instance. | |
| /// | |
| /// # Arguments | |
| /// | |
| /// * `max_value` - The theoretical maximum possible value in the search space. | |
| /// (Note: This is not currently used in the `decide` method but | |
| /// is part of the class structure for future extensions). | |
| pub fn new(max_value: u32) -> Self { | |
| Search { max_value } | |
| } | |
| /// Finds the index of the maximum value in a list. | |
| /// | |
| /// It starts its search from the predicted index and wraps around the list, | |
| /// which can be advantageous if the search can be terminated early. | |
| /// | |
| /// # Arguments | |
| /// | |
| /// * `values` - The `Vec<u32>` of values to search through. | |
| /// * `prediction` - The predicted index of the maximum value. | |
| /// | |
| /// # Returns | |
| /// | |
| /// * `usize` - The index of the first occurrence of the maximum value found. | |
| pub fn decide(&self, values: Vec<u32>, prediction: u32) -> usize { | |
| let mut best_index = 0; | |
| let mut max_value = 0; | |
| let mut start_index = 0; | |
| if prediction < values.len() as u32 { | |
| start_index = prediction as usize; | |
| } | |
| for i in 0..values.len() { | |
| let index = (start_index + i) % values.len(); | |
| if values[index] > max_value { | |
| max_value = values[index]; | |
| best_index = index; | |
| } | |
| } | |
| best_index | |
| } | |
| } | |
| /// Defines the Python module for the Learning-Augmented Algorithms core library. | |
| /// | |
| /// This function exposes the Rust implementations of the LAA classes (`SkiRental`, | |
| /// `Caching`, `OnewayTrading`, `Scheduling`, `Search`) to Python, allowing them | |
| /// to be imported and used seamlessly. | |
| fn laa_core(m: &Bound<'_, PyModule>) -> PyResult<()> { | |
| m.add_class::<SkiRental>()?; | |
| m.add_class::<RandomizedSkiRental>()?; | |
| m.add_class::<Caching>()?; | |
| m.add_class::<OnewayTrading>()?; | |
| m.add_class::<Scheduling>()?; | |
| m.add_class::<Search>()?; | |
| Ok(()) | |
| } | |
| mod tests { | |
| use super::*; | |
| fn test_ski_rental_no_trust() { | |
| let ski_rental = SkiRental::new(100.0); | |
| assert_eq!(ski_rental.decide(99, 10.0, 0.0), false); | |
| assert_eq!(ski_rental.decide(100, 10.0, 0.0), true); | |
| } | |
| fn test_ski_rental_full_trust_good_prediction() { | |
| let ski_rental = SkiRental::new(100.0); | |
| assert_eq!(ski_rental.decide(24, 25.0, 1.0), false); | |
| assert_eq!(ski_rental.decide(25, 25.0, 1.0), true); | |
| } | |
| fn test_ski_rental_full_trust_bad_prediction() { | |
| let ski_rental = SkiRental::new(100.0); | |
| // Prediction is 120, but it's clamped at buy_cost (100), so threshold is 100. | |
| assert_eq!(ski_rental.decide(99, 120.0, 1.0), false); | |
| assert_eq!(ski_rental.decide(100, 120.0, 1.0), true); | |
| } | |
| fn test_caching_hit() { | |
| let caching = Caching::new(3, HashMap::new()); | |
| let cache = vec![1, 2, 3]; | |
| let (hit, new_cache) = caching.decide(2, cache.clone()); | |
| assert_eq!(hit, true); | |
| assert_eq!(new_cache, cache); | |
| } | |
| fn test_caching_miss_no_eviction() { | |
| let caching = Caching::new(3, HashMap::new()); | |
| let cache = vec![1, 2]; | |
| let (hit, new_cache) = caching.decide(3, cache.clone()); | |
| assert_eq!(hit, true); | |
| assert_eq!(new_cache, vec![1, 2, 3]); | |
| } | |
| fn test_caching_miss_with_eviction() { | |
| let mut predictions = HashMap::new(); | |
| predictions.insert(1, 10); | |
| predictions.insert(2, 5); | |
| predictions.insert(3, 15); | |
| let caching = Caching::new(3, predictions); | |
| let cache = vec![1, 2, 3]; | |
| let (hit, new_cache) = caching.decide(4, cache.clone()); | |
| assert_eq!(hit, false); | |
| assert_eq!(new_cache, vec![1, 2, 4]); | |
| } | |
| fn test_oneway_trading_no_trust() { | |
| let trading = OnewayTrading::new(100.0); | |
| assert_eq!(trading.decide(99.0, 120.0, 0.0), false); | |
| assert_eq!(trading.decide(100.0, 120.0, 0.0), true); | |
| } | |
| fn test_oneway_trading_full_trust() { | |
| let trading = OnewayTrading::new(100.0); | |
| assert_eq!(trading.decide(119.0, 120.0, 1.0), false); | |
| assert_eq!(trading.decide(120.0, 120.0, 1.0), true); | |
| } | |
| fn test_scheduling() { | |
| let scheduling = Scheduling::new(2); | |
| let job_lengths = vec![10, 5, 12]; | |
| let predictions = vec![5, 10, 12]; | |
| let assignments = scheduling.decide(job_lengths, predictions); | |
| assert_eq!(assignments, vec![0, 1, 1]); | |
| } | |
| fn test_search() { | |
| let search = Search::new(100); | |
| let values = vec![10, 5, 12, 50, 99]; | |
| let prediction = 4; | |
| let best_index = search.decide(values, prediction); | |
| assert_eq!(best_index, 4); | |
| } | |
| fn test_randomized_ski_rental_statistical() { | |
| let buy_cost = 100.0; | |
| let num_simulations = 10000; | |
| let mut total_ratio = 0.0; | |
| let mut rng = rand::thread_rng(); | |
| for _ in 0..num_simulations { | |
| let ski_days = rng.gen_range(1..150); | |
| // Add more realistic noise to the prediction | |
| let noise = rng.gen_range(0.7..1.3); | |
| let prediction = (ski_days as f64 * noise).max(1.0); | |
| let trust = 0.8; | |
| let rental = RandomizedSkiRental::new(buy_cost); | |
| let mut alg_cost = ski_days as f64; // Default cost is renting all days | |
| for day in 1..=ski_days { | |
| if rental.decide(day, prediction, trust) { | |
| alg_cost = (day - 1) as f64 + buy_cost; | |
| break; // Decision is final | |
| } | |
| } | |
| let optimal_cost = (ski_days as f64).min(buy_cost); | |
| total_ratio += alg_cost / optimal_cost; | |
| } | |
| let avg_ratio = total_ratio / num_simulations as f64; | |
| assert!(avg_ratio < 1.7, "Average ratio was {}", avg_ratio); | |
| } | |
| } | |