Skip to main content

struct_compression_analyzer/brute_force/
brute_force_custom.rs

1use super::{
2    find_optimal_coefficients_for_metrics_parallel, BruteForceComparisonMetrics, BruteForceConfig,
3    OptimizationResult,
4};
5use crate::results::analysis_results::AnalysisResults;
6
7/// Result of a brute force optimization on a custom comparison.
8#[derive(Debug, Clone)]
9pub struct CustomComparisonOptimizationResult {
10    /// Optimal parameters for the baseline group
11    pub baseline: OptimizationResult,
12    /// Optimal parameters for each comparison group
13    pub comparisons: Box<[OptimizationResult]>,
14}
15
16/// Finds the optimal values for `lz_match_multiplier` and `entropy_multiplier` for all custom
17/// comparison results within a given [`AnalysisResults`] item.
18///
19/// # Arguments
20///
21/// * `individual_results` - Mutable reference to the [`AnalysisResults`] containing the data.
22///   This is where we pull the data from, and where we will update the results.
23/// * `config` - Configuration for the optimization process (optional, uses default if [`None`])
24pub fn find_optimal_custom_result_coefficients(
25    individual_results: &mut [AnalysisResults],
26    config: Option<&BruteForceConfig>,
27) -> Vec<(String, CustomComparisonOptimizationResult)> {
28    // Use a reference to the default config if None is provided
29    let default_config = BruteForceConfig::default();
30    let config = config.unwrap_or(&default_config);
31
32    let mut results: Vec<(String, CustomComparisonOptimizationResult)> = Vec::new();
33
34    for (comparison_idx, comparison) in individual_results[0].custom_comparisons.iter().enumerate()
35    {
36        results.push((
37            comparison.name.clone(),
38            find_optimal_custom_result_coefficients_for_comparison(
39                comparison_idx,
40                config,
41                individual_results,
42            ),
43        ));
44    }
45
46    results
47}
48
49/// This function finds the optimal coefficients for the baseline and each comparison group in a custom comparison
50#[allow(clippy::needless_range_loop)]
51fn find_optimal_custom_result_coefficients_for_comparison(
52    comparison_idx: usize,
53    config: &BruteForceConfig,
54    original_results: &[AnalysisResults], // guaranteed non-empty
55) -> CustomComparisonOptimizationResult {
56    // Get the first result to determine how many comparison groups exist
57    let first_result = &original_results[0].custom_comparisons[comparison_idx];
58    let num_comparisons = first_result.group_metrics.len();
59
60    // Extract baseline metrics and find optimal coefficients
61    let baseline_metrics = extract_baseline_metrics(comparison_idx, original_results);
62    let baseline_best = find_optimal_coefficients_for_metrics_parallel(&baseline_metrics, config);
63
64    // Initialize comparison group optimization results
65    let mut comparison_bests = Vec::with_capacity(num_comparisons);
66
67    // Process each comparison group separately
68    for group_idx in 0..num_comparisons {
69        let group_metrics =
70            extract_comparison_group_metrics(comparison_idx, group_idx, original_results);
71
72        // Find optimal coefficients for this comparison group
73        let group_best = find_optimal_coefficients_for_metrics_parallel(&group_metrics, config);
74        comparison_bests.push(group_best);
75    }
76
77    CustomComparisonOptimizationResult {
78        baseline: baseline_best,
79        comparisons: comparison_bests.into_boxed_slice(),
80    }
81}
82
83/// Extracts all the baseline metrics from each [`AnalysisResults`], at a given comparison index.
84/// Returns a boxed slice of all metrics.
85fn extract_baseline_metrics(
86    comparison_idx: usize,
87    original_results: &[AnalysisResults], // guaranteed non-empty
88) -> Box<[BruteForceComparisonMetrics]> {
89    original_results
90        .iter()
91        .map(|result| {
92            result.custom_comparisons[comparison_idx]
93                .baseline_metrics
94                .into()
95        })
96        .collect()
97}
98
99/// Extracts all the metrics for a specific comparison group from each [`AnalysisResults`], at a given comparison index.
100/// Returns a boxed slice of all metrics.
101///
102/// # Arguments
103///
104/// * `comparison_idx` - The index of the custom comparison in the custom_comparisons array
105/// * `group_idx` - The index of the comparison group in the group_metrics array
106/// * `original_results` - The original results to extract metrics from
107fn extract_comparison_group_metrics(
108    comparison_idx: usize,
109    group_idx: usize,
110    original_results: &[AnalysisResults], // guaranteed non-empty
111) -> Box<[BruteForceComparisonMetrics]> {
112    original_results
113        .iter()
114        .map(|result| result.custom_comparisons[comparison_idx].group_metrics[group_idx].into())
115        .collect()
116}
117
118/// Print optimization results in a user-friendly format.
119///
120/// # Arguments
121///
122/// * `writer` - The writer to print results to
123/// * `results` - Vector of (comparison name, CustomComparisonOptimizationResult) tuples
124pub fn print_optimization_results<W: std::io::Write>(
125    writer: &mut W,
126    results: &[(String, CustomComparisonOptimizationResult)],
127) -> std::io::Result<()> {
128    writeln!(
129        writer,
130        "\n=== Custom Comparison Parameter Optimization Results ==="
131    )?;
132    writeln!(
133        writer,
134        "Comparison Name | Group | LZ Multiplier | Entropy Multiplier |"
135    )?;
136    writeln!(
137        writer,
138        "----------------|-------|---------------|--------------------|"
139    )?;
140
141    for (name, result) in results {
142        writeln!(
143            writer,
144            "{:<16}|{:<7}|{:<15.3}|{:<20.3}|",
145            name, "BASE", result.baseline.lz_match_multiplier, result.baseline.entropy_multiplier
146        )?;
147
148        for (i, comparison) in result.comparisons.iter().enumerate() {
149            writeln!(
150                writer,
151                "{:<16}|{:<7}|{:<15.3}|{:<20.3}|",
152                "", i, comparison.lz_match_multiplier, comparison.entropy_multiplier
153            )?;
154        }
155    }
156
157    Ok(())
158}
159
160#[cfg(test)]
161mod tests {
162    use ahash::AHashMap;
163
164    use super::*;
165    use crate::{
166        brute_force::calculate_error_for_bruteforce_metrics,
167        comparison::{
168            compare_groups::GroupComparisonResult, GroupComparisonMetrics, GroupDifference,
169        },
170        schema::Metadata,
171    };
172
173    // Helper function to create a mock GroupComparisonResult for testing
174    #[allow(clippy::too_many_arguments)]
175    fn create_mock_group_comparison_result(
176        name: &str,
177        baseline_lz_matches: u64,
178        baseline_entropy: f64,
179        baseline_zstd_size: u64,
180        baseline_original_size: u64,
181        comparison_group_count: usize,
182        comparison_lz_matches: u64,
183        comparison_entropy: f64,
184        comparison_zstd_size: u64,
185        comparison_original_size: u64,
186    ) -> GroupComparisonResult {
187        let baseline_metrics = GroupComparisonMetrics {
188            lz_matches: baseline_lz_matches,
189            entropy: baseline_entropy,
190            estimated_size: 0, // Not used in testing
191            zstd_size: baseline_zstd_size,
192            original_size: baseline_original_size,
193        };
194
195        let mut group_names = Vec::with_capacity(comparison_group_count);
196        let mut group_metrics = Vec::with_capacity(comparison_group_count);
197        let mut differences = Vec::with_capacity(comparison_group_count);
198
199        for i in 0..comparison_group_count {
200            group_names.push(format!("group_{}", i));
201
202            let metrics = GroupComparisonMetrics {
203                lz_matches: comparison_lz_matches,
204                entropy: comparison_entropy,
205                estimated_size: 0, // Not used in testing
206                zstd_size: comparison_zstd_size,
207                original_size: comparison_original_size,
208            };
209
210            group_metrics.push(metrics);
211            differences.push(GroupDifference::from_metrics(&baseline_metrics, &metrics));
212        }
213
214        GroupComparisonResult {
215            name: name.to_string(),
216            description: "Test comparison".to_string(),
217            baseline_metrics,
218            group_names,
219            group_metrics,
220            differences,
221        }
222    }
223
224    // Helper function to create a mock AnalysisResults with a custom comparison
225    #[allow(clippy::too_many_arguments)]
226    fn create_mock_analysis_results_with_custom(
227        comparison_name: &str,
228        baseline_lz_matches: u64,
229        baseline_entropy: f64,
230        baseline_zstd_size: u64,
231        baseline_original_size: u64,
232        comparison_0_group_count: usize,
233        comparison_0_lz_matches: u64,
234        comparison_0_entropy: f64,
235        comparison_0_zstd_size: u64,
236        comparison_0_original_size: u64,
237    ) -> AnalysisResults {
238        let custom_comparison = create_mock_group_comparison_result(
239            comparison_name,
240            baseline_lz_matches,
241            baseline_entropy,
242            baseline_zstd_size,
243            baseline_original_size,
244            comparison_0_group_count,
245            comparison_0_lz_matches,
246            comparison_0_entropy,
247            comparison_0_zstd_size,
248            comparison_0_original_size,
249        );
250
251        AnalysisResults {
252            schema_metadata: Metadata {
253                name: "Test Schema".to_string(),
254                description: "Test Schema Description".to_string(),
255            },
256            file_entropy: 0.0,
257            file_lz_matches: 0,
258            zstd_file_size: 0,
259            original_size: 0,
260            per_field: AHashMap::new(),
261            split_comparisons: Vec::new(),
262            custom_comparisons: vec![custom_comparison],
263        }
264    }
265
266    #[test]
267    fn can_find_optimal_custom_result_coefficients() {
268        // Create a mock analysis results with a custom comparison
269        let analysis_results1 = create_mock_analysis_results_with_custom(
270            "test_comparison",
271            100, // baseline
272            1.0,
273            110,
274            1000,
275            2, // comparison_group_count
276            210,
277            1.6,
278            230,
279            1000,
280        );
281
282        let config = BruteForceConfig::default();
283
284        // Find optimal coefficients
285        let mut original_results = vec![analysis_results1];
286        let optimal_results =
287            find_optimal_custom_result_coefficients(&mut original_results, Some(&config));
288
289        // Verify results
290        assert_eq!(optimal_results.len(), 1);
291        assert_eq!(optimal_results[0].0, "test_comparison");
292
293        // Verify there are results for baseline and 2 comparison groups
294        // Check baseline values are within config range
295        assert!(optimal_results[0].1.baseline.lz_match_multiplier >= config.min_lz_multiplier);
296        assert!(optimal_results[0].1.baseline.lz_match_multiplier <= config.max_lz_multiplier);
297        assert!(optimal_results[0].1.baseline.entropy_multiplier >= config.min_entropy_multiplier);
298        assert!(optimal_results[0].1.baseline.entropy_multiplier <= config.max_entropy_multiplier);
299
300        // Check we have the right number of comparison groups
301        assert_eq!(optimal_results[0].1.comparisons.len(), 2);
302
303        // Check first comparison group values are within config range
304        let comparisons = &optimal_results[0].1.comparisons;
305        assert!(comparisons[0].lz_match_multiplier >= config.min_lz_multiplier);
306        assert!(comparisons[0].lz_match_multiplier <= config.max_lz_multiplier);
307        assert!(comparisons[0].entropy_multiplier >= config.min_entropy_multiplier);
308        assert!(comparisons[0].entropy_multiplier <= config.max_entropy_multiplier);
309
310        // Check second comparison group values are within config range
311        assert!(comparisons[1].lz_match_multiplier >= config.min_lz_multiplier);
312        assert!(comparisons[1].lz_match_multiplier <= config.max_lz_multiplier);
313        assert!(comparisons[1].entropy_multiplier >= config.min_entropy_multiplier);
314        assert!(comparisons[1].entropy_multiplier <= config.max_entropy_multiplier);
315
316        // Calculate baseline error using the optimal parameters
317        let baseline_metrics = extract_baseline_metrics(0, &original_results);
318        let baseline_error = calculate_error_for_bruteforce_metrics(
319            &baseline_metrics,
320            optimal_results[0].1.baseline.lz_match_multiplier,
321            optimal_results[0].1.baseline.entropy_multiplier,
322        );
323
324        // Assert the error is below a reasonable threshold
325        assert!(
326            baseline_error < 5.0,
327            "Baseline error {} should be less than 5.0",
328            baseline_error
329        );
330
331        // Check errors for each comparison group
332        for i in 0..2 {
333            let group_metrics = extract_comparison_group_metrics(0, i, &original_results);
334            let group_error = calculate_error_for_bruteforce_metrics(
335                &group_metrics,
336                optimal_results[0].1.comparisons[i].lz_match_multiplier,
337                optimal_results[0].1.comparisons[i].entropy_multiplier,
338            );
339
340            assert!(
341                group_error < 5.0,
342                "Comparison group {} error {} should be less than 5.0",
343                i,
344                group_error
345            );
346        }
347    }
348
349    #[test]
350    fn handles_empty_custom_results() {
351        let analysis_results = AnalysisResults::default();
352
353        // Find optimal coefficients
354        let mut original_results = vec![analysis_results];
355        let optimal_results = find_optimal_custom_result_coefficients(&mut original_results, None);
356
357        // Verify results are empty
358        assert!(optimal_results.is_empty());
359    }
360}