Skip to main content

stt_optimize/
recommend.rs

1//! Parameter recommendation engine
2//!
3//! Combines analysis results to generate optimal stt-build parameters.
4
5use crate::analysis::AnalysisResult;
6use serde::{Deserialize, Serialize};
7use std::path::Path;
8
9/// Recommended parameters for stt-build.
10///
11/// `stt-build --auto` folds `min_zoom`, `max_zoom`, and `temporal_bucket_ms`
12/// into its own args and logs `confidence`/`explanations`; all fields also
13/// appear in the standalone `analyze`/`recommend` reports.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Recommendations {
16    /// Minimum zoom level
17    pub min_zoom: u8,
18    /// Maximum zoom level
19    pub max_zoom: u8,
20    /// Suggested temporal bucket size in milliseconds (0 = no bucketing)
21    pub temporal_bucket_ms: u64,
22    /// Human-readable temporal bucket description
23    pub temporal_bucket_human: String,
24    /// Confidence level in recommendations (0-100)
25    pub confidence: u8,
26    /// Explanation of key decisions
27    pub explanations: Vec<String>,
28}
29
30/// Generate recommendations from analysis results
31pub fn generate_recommendations(result: &AnalysisResult) -> Recommendations {
32    let mut explanations = Vec::new();
33
34    // Zoom levels from spatial analysis
35    let min_zoom = result.spatial.recommended_min_zoom;
36    let max_zoom = result.spatial.recommended_max_zoom;
37    explanations.push(format!(
38        "Zoom range {}-{} based on spatial coverage ({})",
39        min_zoom, max_zoom, result.spatial.distribution
40    ));
41
42    // Temporal bucketing
43    let temporal_bucket_ms = result.temporal.recommended_bucket_ms;
44    let temporal_bucket_human = result.temporal.recommended_bucket_human.clone();
45    if temporal_bucket_ms > 0 {
46        explanations.push(format!(
47            "Temporal bucket {} for {} distribution",
48            temporal_bucket_human, result.temporal.distribution
49        ));
50    }
51
52    // Calculate confidence based on data quality
53    let confidence = calculate_confidence(result);
54
55    Recommendations {
56        min_zoom,
57        max_zoom,
58        temporal_bucket_ms,
59        temporal_bucket_human,
60        confidence,
61        explanations,
62    }
63}
64
65/// Calculate confidence score (0-100)
66fn calculate_confidence(result: &AnalysisResult) -> u8 {
67    let mut score = 100u8;
68
69    // Lower confidence with fewer features
70    if result.feature_count < 1000 {
71        score = score.saturating_sub(20);
72    } else if result.feature_count < 10000 {
73        score = score.saturating_sub(10);
74    }
75
76    // Lower confidence with many issues
77    if result.density.issues.len() > 3 {
78        score = score.saturating_sub(15);
79    } else if !result.density.issues.is_empty() {
80        score = score.saturating_sub(5);
81    }
82
83    // Lower confidence with sparse data
84    if matches!(result.spatial.distribution, crate::analysis::spatial::SpatialDistribution::Sparse) {
85        score = score.saturating_sub(10);
86    }
87
88    // Lower confidence with complex geometry
89    if matches!(result.geometry.complexity, crate::analysis::geometry::GeometryComplexity::VeryComplex) {
90        score = score.saturating_sub(10);
91    }
92
93    score
94}
95
96/// Convert recommendations to a build config JSON structure
97pub fn to_build_config(
98    recommendations: &Recommendations,
99    input: &Path,
100    time_field: &str,
101) -> serde_json::Value {
102    serde_json::json!({
103        "input": input.to_string_lossy(),
104        "time_field": time_field,
105        "min_zoom": recommendations.min_zoom,
106        "max_zoom": recommendations.max_zoom,
107        "temporal_bucket_ms": recommendations.temporal_bucket_ms,
108        "confidence": recommendations.confidence,
109        "explanations": recommendations.explanations,
110    })
111}
112
113/// Convert recommendations to an stt-build command line
114pub fn to_command(
115    recommendations: &Recommendations,
116    input: &Path,
117    time_field: &str,
118) -> String {
119    // Suggest the packed dataset DIRECTORY (the input's stem): stt-build's
120    // output is a directory tree, not a single file.
121    let output = input.with_extension("");
122    let output_str = output.file_name()
123        .map(|n| n.to_string_lossy().to_string())
124        .unwrap_or_else(|| "output".to_string());
125
126    let input_str = input.file_name()
127        .map(|n| n.to_string_lossy().to_string())
128        .unwrap_or_else(|| "input.parquet".to_string());
129
130    format!(
131        "stt-build --input {} --output {} \\\n  --time-field {} --min-zoom {} --max-zoom {}",
132        input_str,
133        output_str,
134        time_field,
135        recommendations.min_zoom,
136        recommendations.max_zoom,
137    )
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_to_command() {
146        let rec = Recommendations {
147            min_zoom: 0,
148            max_zoom: 10,
149            temporal_bucket_ms: 3_600_000,
150            temporal_bucket_human: "1 hour".to_string(),
151            confidence: 85,
152            explanations: vec![],
153        };
154
155        let cmd = to_command(&rec, Path::new("data.parquet"), "timestamp");
156        assert!(cmd.contains("--min-zoom 0"));
157        assert!(cmd.contains("--max-zoom 10"));
158    }
159}
160
161