#![cfg_attr(feature = "lambda", allow(dead_code))]
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct WineFeatures {
pub fixed_acidity: f32,
pub volatile_acidity: f32,
pub citric_acid: f32,
pub residual_sugar: f32,
pub chlorides: f32,
pub free_sulfur_dioxide: f32,
pub total_sulfur_dioxide: f32,
pub density: f32,
pub ph: f32,
pub sulphates: f32,
pub alcohol: f32,
}
impl WineFeatures {
pub fn to_vec(&self) -> Vec<f32> {
vec![
self.fixed_acidity,
self.volatile_acidity,
self.citric_acid,
self.residual_sugar,
self.chlorides,
self.free_sulfur_dioxide,
self.total_sulfur_dioxide,
self.density,
self.ph,
self.sulphates,
self.alcohol,
]
}
pub fn normalize(&self) -> Vec<f32> {
vec![
(self.fixed_acidity - 4.0) / 12.0, (self.volatile_acidity - 0.1) / 1.5, self.citric_acid, (self.residual_sugar - 0.9) / 14.1, (self.chlorides - 0.01) / 0.59, (self.free_sulfur_dioxide - 1.0) / 71.0, (self.total_sulfur_dioxide - 6.0) / 283.0, (self.density - 0.99) / 0.05, (self.ph - 2.7) / 1.3, (self.sulphates - 0.3) / 1.7, (self.alcohol - 8.0) / 7.0, ]
}
pub fn example_bordeaux() -> Self {
Self {
fixed_acidity: 7.4,
volatile_acidity: 0.28,
citric_acid: 0.45,
residual_sugar: 2.1,
chlorides: 0.076,
free_sulfur_dioxide: 15.0,
total_sulfur_dioxide: 46.0,
density: 0.9958,
ph: 3.35,
sulphates: 0.68,
alcohol: 12.8,
}
}
pub fn example_table_wine() -> Self {
Self {
fixed_acidity: 8.5,
volatile_acidity: 0.72,
citric_acid: 0.12,
residual_sugar: 3.8,
chlorides: 0.092,
free_sulfur_dioxide: 8.0,
total_sulfur_dioxide: 28.0,
density: 0.9972,
ph: 3.42,
sulphates: 0.48,
alcohol: 10.2,
}
}
pub fn example_napa_cabernet() -> Self {
Self {
fixed_acidity: 6.8,
volatile_acidity: 0.22,
citric_acid: 0.52,
residual_sugar: 1.8,
chlorides: 0.058,
free_sulfur_dioxide: 18.0,
total_sulfur_dioxide: 42.0,
density: 0.9948,
ph: 3.28,
sulphates: 0.72,
alcohol: 13.5,
}
}
}
#[derive(Debug, Clone)]
pub struct WinePrediction {
pub quality: f32,
pub category: WineCategory,
pub confidence: f32,
pub latency_ms: f32,
pub feature_importance: HashMap<String, f32>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WineCategory {
Poor, Average, Good, Excellent, }
impl WineCategory {
pub fn from_score(score: f32) -> Self {
match score {
s if s < 5.0 => WineCategory::Poor,
s if s < 7.0 => WineCategory::Average,
s if s < 8.0 => WineCategory::Good,
_ => WineCategory::Excellent,
}
}
pub fn as_str(&self) -> &'static str {
match self {
WineCategory::Poor => "Poor",
WineCategory::Average => "Average",
WineCategory::Good => "Good",
WineCategory::Excellent => "Excellent",
}
}
}
pub struct WinePredictor {
weights: Vec<f32>,
bias: f32,
inference_count: std::sync::atomic::AtomicU64,
}
impl WinePredictor {
pub fn new() -> Self {
Self {
weights: vec![
-0.05, -0.85, 0.45, 0.02, -0.15, 0.08, -0.12, -0.30, -0.10, 0.55, 0.95, ],
bias: 5.5, inference_count: std::sync::atomic::AtomicU64::new(0),
}
}
pub fn predict(&self, features: &WineFeatures) -> WinePrediction {
let start = std::time::Instant::now();
let normalized = features.normalize();
let mut score: f32 = self.bias;
for (w, x) in self.weights.iter().zip(normalized.iter()) {
score += w * x;
}
let quality = score.clamp(0.0, 10.0);
let confidence = self.calculate_confidence(&normalized);
let _count = self
.inference_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let latency_ms = start.elapsed().as_secs_f32() * 1000.0;
let feature_names = [
"fixed_acidity",
"volatile_acidity",
"citric_acid",
"residual_sugar",
"chlorides",
"free_sulfur_dioxide",
"total_sulfur_dioxide",
"density",
"ph",
"sulphates",
"alcohol",
];
let mut feature_importance = HashMap::new();
for (name, (w, x)) in feature_names
.iter()
.zip(self.weights.iter().zip(normalized.iter()))
{
feature_importance.insert(name.to_string(), (w * x).abs());
}
WinePrediction {
quality,
category: WineCategory::from_score(quality),
confidence,
latency_ms,
feature_importance,
}
}
fn calculate_confidence(&self, normalized: &[f32]) -> f32 {
let in_range_count = normalized
.iter()
.filter(|&&x| (0.0..=1.0).contains(&x))
.count();
(in_range_count as f32) / (normalized.len() as f32)
}
pub fn is_cold_start(&self) -> bool {
self.inference_count
.load(std::sync::atomic::Ordering::Relaxed)
== 1
}
pub fn inference_count(&self) -> u64 {
self.inference_count
.load(std::sync::atomic::Ordering::Relaxed)
}
}
impl Default for WinePredictor {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Default)]
pub struct WineMetrics {
pub requests_total: u64,
pub predictions_by_category: HashMap<String, u64>,
pub total_latency_ms: f64,
pub cold_starts: u64,
pub drift_alerts: u64,
}
impl WineMetrics {
pub fn new() -> Self {
Self::default()
}
pub fn record(&mut self, prediction: &WinePrediction, cold_start: bool) {
self.requests_total += 1;
self.total_latency_ms += prediction.latency_ms as f64;
let category = prediction.category.as_str().to_string();
*self.predictions_by_category.entry(category).or_insert(0) += 1;
if cold_start {
self.cold_starts += 1;
}
}
pub fn avg_latency_ms(&self) -> f64 {
if self.requests_total == 0 {
0.0
} else {
self.total_latency_ms / self.requests_total as f64
}
}
pub fn to_prometheus(&self) -> String {
let mut output = String::new();
output.push_str(&format!(
"# HELP wine_predictions_total Total wine quality predictions\n\
# TYPE wine_predictions_total counter\n\
wine_predictions_total {}\n\n",
self.requests_total
));
output.push_str(
"# HELP wine_predictions_by_category Predictions by quality category\n\
# TYPE wine_predictions_by_category counter\n",
);
for (category, count) in &self.predictions_by_category {
output.push_str(&format!(
"wine_predictions_by_category{{category=\"{}\"}} {}\n",
category, count
));
}
output.push_str(&format!(
"\n# HELP wine_latency_avg_ms Average prediction latency\n\
# TYPE wine_latency_avg_ms gauge\n\
wine_latency_avg_ms {:.4}\n\n",
self.avg_latency_ms()
));
output.push_str(&format!(
"# HELP wine_cold_starts_total Cold start count\n\
# TYPE wine_cold_starts_total counter\n\
wine_cold_starts_total {}\n",
self.cold_starts
));
output
}
}
pub struct WineDriftDetector {
reference_means: Vec<f32>,
reference_stds: Vec<f32>,
threshold: f32,
}
impl WineDriftDetector {
pub fn new() -> Self {
Self {
reference_means: vec![
8.32, 0.53, 0.27, 2.54, 0.087, 15.87, 46.47, 0.9967, 3.31, 0.66, 10.42, ],
reference_stds: vec![
1.74, 0.18, 0.19, 1.41, 0.047, 10.46, 32.89, 0.0019, 0.15, 0.17, 1.07, ],
threshold: 3.0, }
}
pub fn check_drift(&self, features: &WineFeatures) -> DriftResult {
let values = features.to_vec();
let mut z_scores = Vec::new();
let mut drifted_features = Vec::new();
let feature_names = [
"fixed_acidity",
"volatile_acidity",
"citric_acid",
"residual_sugar",
"chlorides",
"free_sulfur_dioxide",
"total_sulfur_dioxide",
"density",
"ph",
"sulphates",
"alcohol",
];
for (i, &value) in values.iter().enumerate() {
let z = (value - self.reference_means[i]) / self.reference_stds[i];
z_scores.push(z);
if z.abs() > self.threshold {
drifted_features.push(feature_names[i].to_string());
}
}
let max_z = z_scores.iter().map(|z| z.abs()).fold(0.0f32, f32::max);
DriftResult {
is_drifted: !drifted_features.is_empty(),
max_z_score: max_z,
drifted_features,
}
}
}
impl Default for WineDriftDetector {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct DriftResult {
pub is_drifted: bool,
pub max_z_score: f32,
pub drifted_features: Vec<String>,
}
fn main() {
println!("=== Wine Quality Prediction - Lambda Example ===\n");
println!("Inspired by: https://github.com/paiml/wine-ratings\n");
let predictor = WinePredictor::new();
let drift_detector = WineDriftDetector::new();
let mut metrics = WineMetrics::new();
let wines = vec![
("Bordeaux Red (Premium)", WineFeatures::example_bordeaux()),
("Table Wine (Budget)", WineFeatures::example_table_wine()),
(
"Napa Cabernet (Premium)",
WineFeatures::example_napa_cabernet(),
),
];
println!("Wine Quality Predictions:");
println!("{:-<70}", "");
for (name, features) in &wines {
let prediction = predictor.predict(features);
let cold_start = predictor.is_cold_start();
metrics.record(&prediction, cold_start);
let drift = drift_detector.check_drift(features);
println!("\n{}", name);
println!(
" Quality Score: {:.2}/10 ({})",
prediction.quality,
prediction.category.as_str()
);
println!(" Confidence: {:.0}%", prediction.confidence * 100.0);
println!(" Latency: {:.3}ms", prediction.latency_ms);
if cold_start {
println!(" [COLD START]");
}
if drift.is_drifted {
println!(" [DRIFT WARNING] Features: {:?}", drift.drifted_features);
}
let mut importance: Vec<_> = prediction.feature_importance.iter().collect();
importance.sort_by(|a, b| {
b.1.partial_cmp(a.1)
.expect("importance values should be comparable floats")
});
println!(
" Top factors: {}, {}, {}",
importance[0].0, importance[1].0, importance[2].0
);
}
println!("\n{:-<70}", "");
println!("\nBatch Prediction (100 random wines):");
let start = std::time::Instant::now();
for i in 0..100 {
let features = WineFeatures {
fixed_acidity: 6.0 + (i as f32 * 0.1) % 4.0,
volatile_acidity: 0.2 + (i as f32 * 0.01) % 0.6,
citric_acid: 0.1 + (i as f32 * 0.01) % 0.5,
residual_sugar: 1.5 + (i as f32 * 0.05) % 5.0,
chlorides: 0.05 + (i as f32 * 0.001) % 0.1,
free_sulfur_dioxide: 10.0 + (i as f32 * 0.5) % 30.0,
total_sulfur_dioxide: 30.0 + (i as f32) % 100.0,
density: 0.995 + (i as f32 * 0.0001) % 0.01,
ph: 3.1 + (i as f32 * 0.01) % 0.5,
sulphates: 0.4 + (i as f32 * 0.01) % 0.5,
alcohol: 9.0 + (i as f32 * 0.05) % 4.0,
};
let prediction = predictor.predict(&features);
metrics.record(&prediction, false);
}
let batch_time = start.elapsed();
println!(" Processed: 100 predictions");
println!(" Total time: {:.2}ms", batch_time.as_secs_f32() * 1000.0);
println!(
" Throughput: {:.0} predictions/sec",
100.0 / batch_time.as_secs_f32()
);
println!("\n{:-<70}", "");
println!("\nService Metrics:");
println!(" Total requests: {}", metrics.requests_total);
println!(" Cold starts: {}", metrics.cold_starts);
println!(" Avg latency: {:.4}ms", metrics.avg_latency_ms());
println!("\n By category:");
for (category, count) in &metrics.predictions_by_category {
let pct = (*count as f64 / metrics.requests_total as f64) * 100.0;
println!(" {}: {} ({:.1}%)", category, count, pct);
}
println!("\n{:-<70}", "");
println!("\nPrometheus Metrics Export:");
println!("{}", metrics.to_prometheus());
println!("{:-<70}", "");
println!("\nAWS Lambda Deployment:");
println!(" Runtime: provided.al2 (custom runtime)");
println!(" Architecture: arm64 (Graviton2)");
println!(" Memory: 128MB (sufficient for this model)");
println!(" Expected cold start: <10ms");
println!(" Expected warm latency: <1ms");
println!("\n Build command:");
println!(" cargo build --release --target aarch64-unknown-linux-gnu --features lambda");
println!("\n=== Wine Prediction Complete ===");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wine_features_to_vec() {
let wine = WineFeatures::example_bordeaux();
let vec = wine.to_vec();
assert_eq!(vec.len(), 11);
assert!((vec[0] - 7.4).abs() < 0.001); }
#[test]
fn test_wine_features_normalize() {
let wine = WineFeatures::example_bordeaux();
let normalized = wine.normalize();
assert_eq!(normalized.len(), 11);
for &val in &normalized {
assert!(
val >= -0.5 && val <= 1.5,
"Value {} out of expected range",
val
);
}
}
#[test]
fn test_predictor_output_range() {
let predictor = WinePredictor::new();
let wine = WineFeatures::example_bordeaux();
let prediction = predictor.predict(&wine);
assert!(prediction.quality >= 0.0 && prediction.quality <= 10.0);
assert!(prediction.confidence >= 0.0 && prediction.confidence <= 1.0);
assert!(prediction.latency_ms >= 0.0);
}
#[test]
fn test_predictor_premium_vs_budget() {
let predictor = WinePredictor::new();
let premium = predictor.predict(&WineFeatures::example_napa_cabernet());
let budget = predictor.predict(&WineFeatures::example_table_wine());
assert!(
premium.quality > budget.quality,
"Premium ({:.2}) should score higher than budget ({:.2})",
premium.quality,
budget.quality
);
}
#[test]
fn test_wine_category_classification() {
assert_eq!(WineCategory::from_score(3.5), WineCategory::Poor);
assert_eq!(WineCategory::from_score(5.5), WineCategory::Average);
assert_eq!(WineCategory::from_score(7.2), WineCategory::Good);
assert_eq!(WineCategory::from_score(8.5), WineCategory::Excellent);
}
#[test]
fn test_cold_start_detection() {
let predictor = WinePredictor::new();
let wine = WineFeatures::example_bordeaux();
let _ = predictor.predict(&wine);
assert!(predictor.is_cold_start());
let _ = predictor.predict(&wine);
assert!(!predictor.is_cold_start());
}
#[test]
fn test_metrics_recording() {
let predictor = WinePredictor::new();
let mut metrics = WineMetrics::new();
for _ in 0..10 {
let prediction = predictor.predict(&WineFeatures::example_bordeaux());
metrics.record(&prediction, false);
}
assert_eq!(metrics.requests_total, 10);
assert!(metrics.avg_latency_ms() > 0.0);
}
#[test]
fn test_prometheus_export() {
let mut metrics = WineMetrics::new();
metrics.requests_total = 100;
metrics.cold_starts = 1;
metrics.total_latency_ms = 50.0;
metrics
.predictions_by_category
.insert("Good".to_string(), 60);
metrics
.predictions_by_category
.insert("Average".to_string(), 40);
let prometheus = metrics.to_prometheus();
assert!(prometheus.contains("wine_predictions_total 100"));
assert!(prometheus.contains("wine_cold_starts_total 1"));
assert!(prometheus.contains("wine_latency_avg_ms"));
}
#[test]
fn test_drift_detection_normal() {
let detector = WineDriftDetector::new();
let wine = WineFeatures::example_bordeaux();
let result = detector.check_drift(&wine);
assert!(!result.is_drifted || result.drifted_features.len() <= 1);
}
#[test]
fn test_drift_detection_extreme() {
let detector = WineDriftDetector::new();
let extreme = WineFeatures {
fixed_acidity: 20.0, volatile_acidity: 2.0, citric_acid: 0.3,
residual_sugar: 2.0,
chlorides: 0.08,
free_sulfur_dioxide: 15.0,
total_sulfur_dioxide: 45.0,
density: 0.997,
ph: 3.3,
sulphates: 0.6,
alcohol: 10.0,
};
let result = detector.check_drift(&extreme);
assert!(result.is_drifted);
assert!(result
.drifted_features
.contains(&"fixed_acidity".to_string()));
}
#[test]
fn test_feature_importance() {
let predictor = WinePredictor::new();
let prediction = predictor.predict(&WineFeatures::example_bordeaux());
assert_eq!(prediction.feature_importance.len(), 11);
assert!(prediction.feature_importance.contains_key("alcohol"));
assert!(prediction
.feature_importance
.contains_key("volatile_acidity"));
}
}