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    let output = input.with_extension("stt");
120    let output_str = output.file_name()
121        .map(|n| n.to_string_lossy().to_string())
122        .unwrap_or_else(|| "output.stt".to_string());
123
124    let input_str = input.file_name()
125        .map(|n| n.to_string_lossy().to_string())
126        .unwrap_or_else(|| "input.parquet".to_string());
127
128    format!(
129        "stt-build --input {} --output {} \\\n  --time-field {} --min-zoom {} --max-zoom {}",
130        input_str,
131        output_str,
132        time_field,
133        recommendations.min_zoom,
134        recommendations.max_zoom,
135    )
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_to_command() {
144        let rec = Recommendations {
145            min_zoom: 0,
146            max_zoom: 10,
147            temporal_bucket_ms: 3_600_000,
148            temporal_bucket_human: "1 hour".to_string(),
149            confidence: 85,
150            explanations: vec![],
151        };
152
153        let cmd = to_command(&rec, Path::new("data.parquet"), "timestamp");
154        assert!(cmd.contains("--min-zoom 0"));
155        assert!(cmd.contains("--max-zoom 10"));
156    }
157}
158
159