From eeec85ae5bb7b57646cb878f4c4e59ac5fb66ccf Mon Sep 17 00:00:00 2001 From: Fatigue24 Date: Sun, 3 Nov 2024 02:29:14 +0200 Subject: [PATCH 1/7] Improving the baseline algorithm for QKP --- tig-challenges/src/knapsack.rs | 126 +++++++++++++++++++++++++++++---- 1 file changed, 111 insertions(+), 15 deletions(-) diff --git a/tig-challenges/src/knapsack.rs b/tig-challenges/src/knapsack.rs index e65e238..a354c7e 100644 --- a/tig-challenges/src/knapsack.rs +++ b/tig-challenges/src/knapsack.rs @@ -102,31 +102,127 @@ impl crate::ChallengeTrait for Challenge { // Precompute the ratio between the total value (value + sum of interactive values) and // weight for each item. Pair the ratio with the item's weight and index - let mut value_weight_ratios: Vec<(usize, f32, u32)> = (0..difficulty.num_items) + let mut item_values: Vec<(usize, f32)> = (0..difficulty.num_items) .map(|i| { let total_value = values[i] as i32 + interaction_values[i].iter().sum::(); - let weight = weights[i]; - let ratio = total_value as f32 / weight as f32; - (i, ratio, weight) + let ratio = total_value as f32 / weights[i] as f32; + (i, ratio) }) .collect(); - // Sort the list of tuples by value-to-weight ratio in descending order - value_weight_ratios.sort_unstable_by(|&(_, ratio_a, _), &(_, ratio_b, _)| { - ratio_b.partial_cmp(&ratio_a).unwrap() - }); + // Sort the list of ratios in descending order + item_values.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); + // Step 1: Initial solution obtained by greedily selecting items based on value-weight ratio + let mut selected_items = Vec::with_capacity(difficulty.num_items); + let mut unselected_items = Vec::with_capacity(difficulty.num_items); let mut total_weight = 0; - let mut selected_indices = Vec::new(); - for &(i, _, weight) in &value_weight_ratios { - if total_weight + weight <= max_weight { - selected_indices.push(i); - total_weight += weight; + let mut total_value = 0; + + for &(item, _) in &item_values { + if total_weight + weights[item] <= max_weight { + total_weight += weights[item]; + total_value += values[item] as i32; + + for &prev_item in &selected_items { + total_value += interaction_values[item][prev_item]; + } + selected_items.push(item); + } else { + unselected_items.push(item); } } - selected_indices.sort_unstable(); + + // Step 2: Improvement of solution with Local Search and Tabu-List + // Precompute sum of interaction values with each selected item for all items(if i==j, then interaction_values[i][j] == 0) + let mut interaction_sum_list = vec![0; difficulty.num_items]; + for x in 0..difficulty.num_items { + interaction_sum_list[x] = values[x] as i32; + for &item in &selected_items { + interaction_sum_list[x] += interaction_values[x][item]; + } + } + + // Optimized local search with tabu list + let max_iterations = 100; + let mut tabu_list = vec![0; difficulty.num_items]; - let mut min_value = calculate_total_value(&selected_indices, &values, &interaction_values); + for _ in 0..max_iterations { + let mut best_improvement = 0; + let mut best_swap = None; + + for i in 0..unselected_items.len() { + let new_item = unselected_items[i]; + if tabu_list[new_item] > 0 { + continue; + } + + let new_item_values_sum = interaction_sum_list[new_item]; + // Greedy assumption that remove_item_values_sum + interaction_values[new_item][remove_item] is higher or equal to zero + // It's not only a huge optimization, but it also produces better solutions most of the time + if new_item_values_sum < best_improvement { + continue; + } + + // Compute minimal weight of remove_item required to put new_item + let min_weight = weights[new_item] as i32 - (max_weight as i32 - total_weight as i32); + for j in 0..selected_items.len() { + let remove_item = selected_items[j]; + if tabu_list[remove_item] > 0 { + continue; + } + + // Don't check the weight if there is enough remaining capacity + if min_weight < 0 + { + // Skip a remove_item if the remaining capacity after removal is insufficient to push a new_item + let removed_item_weight = weights[remove_item] as i32; + if removed_item_weight < min_weight { + continue; + } + } + + let remove_item_values_sum = interaction_sum_list[remove_item]; + let value_diff = new_item_values_sum - remove_item_values_sum - interaction_values[new_item][remove_item]; + + if value_diff > best_improvement { + best_improvement = value_diff; + best_swap = Some((i, j)); + } + } + } + + if let Some((unselected_index, selected_index)) = best_swap { + let new_item = unselected_items[unselected_index]; + let remove_item = selected_items[selected_index]; + + selected_items.swap_remove(selected_index); + unselected_items.swap_remove(unselected_index); + selected_items.push(new_item); + unselected_items.push(remove_item); + + total_value += best_improvement; + total_weight = total_weight + weights[new_item] - weights[remove_item]; + + // Update sum of interaction values after swapping items + for x in 0..difficulty.num_items { + interaction_sum_list[x] += interaction_values[x][new_item] - interaction_values[x][remove_item]; + } + + // Update tabu list + tabu_list[new_item] = 3; + tabu_list[remove_item] = 3; + } else { + break; // No improvement found, terminate local search + } + + // Decrease tabu counters + for t in tabu_list.iter_mut() { + *t = if *t > 0 { *t - 1 } else { 0 }; + } + } + + let mut min_value = calculate_total_value(&selected_items, &values, &interaction_values); min_value = (min_value as f32 * (1.0 + difficulty.better_than_baseline as f32 / 1000.0)) .round() as u32; From cd12b6f72d3c01af67cf01be666393fc6c764f82 Mon Sep 17 00:00:00 2001 From: Fatigue24 Date: Sun, 3 Nov 2024 02:58:48 +0200 Subject: [PATCH 2/7] fixed condition for skipping weight check --- tig-challenges/src/knapsack.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tig-challenges/src/knapsack.rs b/tig-challenges/src/knapsack.rs index a354c7e..d4e833a 100644 --- a/tig-challenges/src/knapsack.rs +++ b/tig-challenges/src/knapsack.rs @@ -173,7 +173,7 @@ impl crate::ChallengeTrait for Challenge { } // Don't check the weight if there is enough remaining capacity - if min_weight < 0 + if min_weight >= 0 { // Skip a remove_item if the remaining capacity after removal is insufficient to push a new_item let removed_item_weight = weights[remove_item] as i32; From fea23bf2219c50d6e1f705f028809e46e38b0599 Mon Sep 17 00:00:00 2001 From: Fatigue24 Date: Wed, 6 Nov 2024 18:23:50 +0200 Subject: [PATCH 3/7] Align knapsack instance generation with academic benchmarks --- tig-challenges/src/knapsack.rs | 84 +++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 26 deletions(-) diff --git a/tig-challenges/src/knapsack.rs b/tig-challenges/src/knapsack.rs index d4e833a..06a5af3 100644 --- a/tig-challenges/src/knapsack.rs +++ b/tig-challenges/src/knapsack.rs @@ -77,29 +77,48 @@ impl crate::ChallengeTrait for Challenge { fn generate_instance(seed: [u8; 32], difficulty: &Difficulty) -> Result { let mut rng = SmallRng::from_seed(StdRng::from_seed(seed).gen()); - + + // Set constant density for value generation + let density = 0.25; + // Generate weights w_i in the range [1, 50] let weights: Vec = (0..difficulty.num_items) .map(|_| rng.gen_range(1..=50)) .collect(); - // Generate values v_i in the range [50, 100] + + // Generate values v_i in the range [1, 100] with density probability, 0 otherwise let values: Vec = (0..difficulty.num_items) - .map(|_| rng.gen_range(50..=100)) + .map(|_| { + if rng.gen_bool(density) { + rng.gen_range(1..=100) + } else { + 0 + } + }) .collect(); - - // Generate interactive values V_ij in the range [1, 50], with V_ij == V_ji and V_ij where i==j is 0. - let mut interaction_values: Vec> = - vec![vec![0; difficulty.num_items]; difficulty.num_items]; + + // Generate interaction values V_ij with the following properties: + // - V_ij == V_ji (symmetric matrix) + // - V_ii == 0 (diagonal is zero) + // - Values are in range [1, 100] with density probability, 0 otherwise + let mut interaction_values: Vec> = vec![vec![0; difficulty.num_items]; difficulty.num_items]; + for i in 0..difficulty.num_items { for j in (i + 1)..difficulty.num_items { - let value = rng.gen_range(-50..=50); + let value = if rng.gen_bool(density) { + rng.gen_range(1..=100) + } else { + 0 + }; + + // Set both V_ij and V_ji due to symmetry interaction_values[i][j] = value; interaction_values[j][i] = value; } } - + let max_weight: u32 = weights.iter().sum::() / 2; - + // Precompute the ratio between the total value (value + sum of interactive values) and // weight for each item. Pair the ratio with the item's weight and index let mut item_values: Vec<(usize, f32)> = (0..difficulty.num_items) @@ -109,15 +128,16 @@ impl crate::ChallengeTrait for Challenge { (i, ratio) }) .collect(); - + // Sort the list of ratios in descending order item_values.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); - + // Step 1: Initial solution obtained by greedily selecting items based on value-weight ratio let mut selected_items = Vec::with_capacity(difficulty.num_items); let mut unselected_items = Vec::with_capacity(difficulty.num_items); let mut total_weight = 0; let mut total_value = 0; + let mut is_selected = vec![false; difficulty.num_items]; for &(item, _) in &item_values { if total_weight + weights[item] <= max_weight { @@ -128,13 +148,14 @@ impl crate::ChallengeTrait for Challenge { total_value += interaction_values[item][prev_item]; } selected_items.push(item); + is_selected[item] = true; } else { unselected_items.push(item); } } // Step 2: Improvement of solution with Local Search and Tabu-List - // Precompute sum of interaction values with each selected item for all items(if i==j, then interaction_values[i][j] == 0) + // Precompute sum of interaction values with each selected item for all items let mut interaction_sum_list = vec![0; difficulty.num_items]; for x in 0..difficulty.num_items { interaction_sum_list[x] = values[x] as i32; @@ -143,10 +164,17 @@ impl crate::ChallengeTrait for Challenge { } } + let mut min_selected_item_values = i32::MAX; + for x in 0..difficulty.num_items { + if is_selected[x] { + min_selected_item_values = min_selected_item_values.min(interaction_sum_list[x]); + } + } + // Optimized local search with tabu list let max_iterations = 100; let mut tabu_list = vec![0; difficulty.num_items]; - + for _ in 0..max_iterations { let mut best_improvement = 0; let mut best_swap = None; @@ -158,30 +186,27 @@ impl crate::ChallengeTrait for Challenge { } let new_item_values_sum = interaction_sum_list[new_item]; - // Greedy assumption that remove_item_values_sum + interaction_values[new_item][remove_item] is higher or equal to zero - // It's not only a huge optimization, but it also produces better solutions most of the time - if new_item_values_sum < best_improvement { + if new_item_values_sum < best_improvement + min_selected_item_values { continue; } - + // Compute minimal weight of remove_item required to put new_item - let min_weight = weights[new_item] as i32 - (max_weight as i32 - total_weight as i32); + let min_weight = weights[new_item] as i32 - (max_weight as i32 - total_weight as i32); for j in 0..selected_items.len() { let remove_item = selected_items[j]; if tabu_list[remove_item] > 0 { continue; } - + // Don't check the weight if there is enough remaining capacity - if min_weight >= 0 - { + if min_weight > 0 { // Skip a remove_item if the remaining capacity after removal is insufficient to push a new_item let removed_item_weight = weights[remove_item] as i32; if removed_item_weight < min_weight { continue; } } - + let remove_item_values_sum = interaction_sum_list[remove_item]; let value_diff = new_item_values_sum - remove_item_values_sum - interaction_values[new_item][remove_item]; @@ -201,12 +226,19 @@ impl crate::ChallengeTrait for Challenge { selected_items.push(new_item); unselected_items.push(remove_item); + is_selected[new_item] = true; + is_selected[remove_item] = false; + total_value += best_improvement; total_weight = total_weight + weights[new_item] - weights[remove_item]; // Update sum of interaction values after swapping items + min_selected_item_values = i32::MAX; for x in 0..difficulty.num_items { interaction_sum_list[x] += interaction_values[x][new_item] - interaction_values[x][remove_item]; + if is_selected[x] { + min_selected_item_values = min_selected_item_values.min(interaction_sum_list[x]); + } } // Update tabu list @@ -215,17 +247,17 @@ impl crate::ChallengeTrait for Challenge { } else { break; // No improvement found, terminate local search } - + // Decrease tabu counters for t in tabu_list.iter_mut() { *t = if *t > 0 { *t - 1 } else { 0 }; } } - + let mut min_value = calculate_total_value(&selected_items, &values, &interaction_values); min_value = (min_value as f32 * (1.0 + difficulty.better_than_baseline as f32 / 1000.0)) .round() as u32; - + Ok(Challenge { seed, difficulty: difficulty.clone(), From 30a0f59f30f0637dd8b40e1c29c5e03cf83db0eb Mon Sep 17 00:00:00 2001 From: FiveMovesAhead Date: Thu, 9 Jan 2025 15:46:37 +0800 Subject: [PATCH 4/7] Update description for knapsack. --- docs/challenges/knapsack.md | 48 ++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/docs/challenges/knapsack.md b/docs/challenges/knapsack.md index cecb097..be7e71d 100644 --- a/docs/challenges/knapsack.md +++ b/docs/challenges/knapsack.md @@ -1,50 +1,54 @@ # Knapsack Problem -The quadratic knapsack problem is one of the most popular variants of the single knapsack problem with applications in many optimization problems. The aim is to maximise the value of individual items placed in the knapsack while satisfying a weight constraint. However, pairs of items also have interaction values which may be negative or positive that are added to the total value within the knapsack. +The quadratic knapsack problem is one of the most popular variants of the single knapsack problem, with applications in many optimization contexts. The aim is to maximize the value of individual items placed in the knapsack while satisfying a weight constraint. However, pairs of items also have positive interaction values, contributing to the total value within the knapsack. -# Example + +## Challenge Overview For our challenge, we use a version of the quadratic knapsack problem with configurable difficulty, where the following two parameters can be adjusted in order to vary the difficulty of the challenge: - Parameter 1: $num\textunderscore{ }items$ is the number of items from which you need to select a subset to put in the knapsack. -- Parameter 2: $better\textunderscore{ }than\textunderscore{ }baseline \geq 1$ is the factor by which a solution must be better than the baseline value [link TIG challenges for explanation of baseline value]. - +- Parameter 2: $better\textunderscore{ }than\textunderscore{ }baseline \geq 1$ is the factor by which a solution must be better than the baseline value. The larger the $num\textunderscore{ }items$, the more number of possible $S_{knapsack}$, making the challenge more difficult. Also, the higher $better\textunderscore{ }than\textunderscore{ }baseline$, the less likely a given $S_{knapsack}$ will be a solution, making the challenge more difficult. -The weight $w_i$ of each of the $num\textunderscore{ }items$ is an integer, chosen independently, uniformly at random, and such that each of the item weights $1 <= w_i <= 50$, for $i=1,2,...,num\textunderscore{ }items$. The individual values of the items $v_i$ are selected by random from the range $50 <= v_i <= 100$, and the interaction values of pairs of items $V_{ij}$ are selected by random from the range $-50 <= V_{ij} <= 50$. +The weight $w_i$ of each of the $num\textunderscore{ }items$ is an integer, chosen independently, uniformly at random, and such that each of the item weights $1 <= w_i <= 50$, for $i=1,2,...,num\textunderscore{ }items$. The values of the items are nonzero with a density of 25%, meaning they have a 25% probability of being nonzero. The nonzero individual values of the item, $v_i$, and the nonzero interaction values of pairs of items, $V_{ij}$, are selected at random from the range $[1,100]$. -The total value of a knapsack is determined by summing up the individual values of items in the knapsack, as well as the interaction values of every pair of items $(i,j)$ where $i > j$ in the knapsack: +The total value of a knapsack is determined by summing up the individual values of items in the knapsack, as well as the interaction values of every pair of items \((i,j)\), where \( i > j \), in the knapsack: -$$V_{knapsack} = \sum_{i \in knapsack}{v_i} + \sum_{(i,j)\in knapsack}{V_{ij}}$$ +$$ +V_{knapsack} = \sum_{i \in knapsack}{v_i} + \sum_{(i,j)\in knapsack}{V_{ij}} +$$ We impose a weight constraint $W(S_{knapsack}) <= 0.5 \cdot W(S_{all})$, where the knapsack can hold at most half the total weight of all items. -Consider an example of a challenge instance with `num_items=4` and `better_than_baseline = 1.10`. Let the baseline value be 150: +# Example + +Consider an example of a challenge instance with `num_items=4` and `better_than_baseline = 1.50`. Let the baseline value be 46: ``` -weights = [26, 20, 39, 13] -individual_values = [63, 87, 52, 97] -interaction_values = [ 0, 23, -18, -37 - 23, 0, 42, -28 - -18, 42, 0, 32 - -37, -28, 32, 0] -max_weight = 60 -min_value = baseline*better_than_baseline = 165 +weights = [39, 29, 15, 43] +individual_values = [0, 14, 0, 75] +interaction_values = [ 0, 0, 0, 0 + 0, 0, 32, 0 + 0, 32, 0, 0 + 0, 0, 0, 0] +max_weight = 63 +min_value = baseline*better_than_baseline = 69 ``` -The objective is to find a set of items where the total weight is at most 60 but has a total value of at least 165. +The objective is to find a set of items where the total weight is at most 63 but has a total value of at least 69. Now consider the following selection: ``` -selected_items = [0, 1, 3] +selected_items = [2, 3] ``` -When evaluating this selection, we can confirm that the total weight is less than 60, and the total value is more than 165, thereby this selection of items is a solution: +When evaluating this selection, we can confirm that the total weight is less than 63, and the total value is more than 69, thereby this selection of items is a solution: -* Total weight = 26 + 20 + 13 = 59 -* Total value = 63 + 52 + 97 + 23 - 37 - 28 = 170 +* Total weight = 15 + 43 = 58 +* Total value = 0 + 75 + 0 = 75 # Our Challenge -In TIG, the baseline value is determined by a greedy algorithm that simply iterates through items sorted by potential value to weight ratio, adding them if knapsack is still below the weight constraint. +In TIG, the baseline value is determined by a two-stage approach. First, items are selected based on their value-to-weight ratio, including interaction values, until the capacity is reached. Then, a tabu-based local search refines the solution by swapping items to improve value while avoiding reversals, with early termination for unpromising swaps. From f7910611429b5e9f250838ad39ae354642ed99bc Mon Sep 17 00:00:00 2001 From: FiveMovesAhead Date: Sat, 11 Jan 2025 01:44:27 +0800 Subject: [PATCH 5/7] Increase default max fuel to 10 billion. --- scripts/test_algorithm.sh | 8 +++++++- tig-worker/src/main.rs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/test_algorithm.sh b/scripts/test_algorithm.sh index 14b04b2..fb6ebf2 100644 --- a/scripts/test_algorithm.sh +++ b/scripts/test_algorithm.sh @@ -76,6 +76,12 @@ if ! is_positive_integer "$num_workers"; then echo "Error: Number of workers must be a positive integer." exit 1 fi +read -p "Enter max fuel (default is 10000000000): " max_fuel +max_fuel=${max_fuel:-1} +if ! is_positive_integer "$max_fuel"; then + echo "Error: Max fuel must be a positive integer." + exit 1 +fi read -p "Enable debug mode? (leave blank to disable) " enable_debug if [[ -n $enable_debug ]]; then debug_mode=true @@ -117,7 +123,7 @@ while [ $remaining_nonces -gt 0 ]; do start_time=$(date +%s%3N) stdout=$(mktemp) stderr=$(mktemp) - ./target/release/tig-worker compute_batch "$SETTINGS" "random_string" $current_nonce $nonces_to_compute $power_of_2_nonces $REPO_DIR/tig-algorithms/wasm/$CHALLENGE/$ALGORITHM.wasm --workers $nonces_to_compute >"$stdout" 2>"$stderr" + ./target/release/tig-worker compute_batch "$SETTINGS" "random_string" $current_nonce $nonces_to_compute $power_of_2_nonces $REPO_DIR/tig-algorithms/wasm/$CHALLENGE/$ALGORITHM.wasm --workers $nonces_to_compute --fuel $max_fuel >"$stdout" 2>"$stderr" exit_code=$? output_stdout=$(cat "$stdout") output_stderr=$(cat "$stderr") diff --git a/tig-worker/src/main.rs b/tig-worker/src/main.rs index 260ad47..e2bea8b 100644 --- a/tig-worker/src/main.rs +++ b/tig-worker/src/main.rs @@ -28,7 +28,7 @@ fn cli() -> Command { .arg(arg!( "Path to a wasm file").value_parser(clap::value_parser!(PathBuf))) .arg( arg!(--fuel [FUEL] "Optional maximum fuel parameter for WASM VM") - .default_value("2000000000") + .default_value("10000000000") .value_parser(clap::value_parser!(u64)), ) .arg( From cb4d8779883716dc21b9118819f47ec4ffd4d097 Mon Sep 17 00:00:00 2001 From: FiveMovesAhead Date: Thu, 23 Jan 2025 20:14:16 +0800 Subject: [PATCH 6/7] Address deposit edge case. --- tig-protocol/src/contracts/players.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tig-protocol/src/contracts/players.rs b/tig-protocol/src/contracts/players.rs index 7991a8c..407bd28 100644 --- a/tig-protocol/src/contracts/players.rs +++ b/tig-protocol/src/contracts/players.rs @@ -273,7 +273,7 @@ pub(crate) async fn update(cache: &mut AddBlockCache) { for deposit in active_deposit_details.values() { let total_time = PreciseNumber::from(deposit.end_timestamp - deposit.start_timestamp); for i in 0..lock_period_cap { - if round_timestamps[i + 1] <= deposit.start_timestamp { + if i + 1 < lock_period_cap && round_timestamps[i + 1] <= deposit.start_timestamp { continue; } if round_timestamps[i] >= deposit.end_timestamp { From 424d096a858fd5ee55ae9805385921b3026d0c70 Mon Sep 17 00:00:00 2001 From: FiveMovesAhead Date: Sun, 26 Jan 2025 10:20:21 +0800 Subject: [PATCH 7/7] Only update adoption for active breakthroughs. --- tig-protocol/src/contracts/algorithms.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tig-protocol/src/contracts/algorithms.rs b/tig-protocol/src/contracts/algorithms.rs index 3cb824f..4a48514 100644 --- a/tig-protocol/src/contracts/algorithms.rs +++ b/tig-protocol/src/contracts/algorithms.rs @@ -216,10 +216,9 @@ pub(crate) async fn update(cache: &mut AddBlockCache) { if let Some(breakthrough_id) = &active_algorithms_details[algorithm_id].breakthrough_id { - active_breakthroughs_block_data - .get_mut(breakthrough_id) - .unwrap() - .adoption += adoption; + if let Some(block_data) = active_breakthroughs_block_data.get_mut(breakthrough_id) { + block_data.adoption += adoption; + } } } }