Skip to main content

mecha10_sim/sim/
selector.rs

1//! Environment selection and scoring algorithm
2
3use anyhow::Result;
4use std::collections::HashSet;
5
6use super::catalog::{Environment, EnvironmentCatalog};
7use super::profile::RobotProfile;
8
9#[derive(Debug, Clone)]
10pub struct EnvironmentMatch {
11    pub environment: Environment,
12    pub score: i32,
13    pub reasons: Vec<String>,
14}
15
16pub struct EnvironmentSelector {
17    catalog: EnvironmentCatalog,
18}
19
20impl EnvironmentSelector {
21    /// Create new selector with default catalog
22    pub fn new() -> Result<Self> {
23        let catalog = EnvironmentCatalog::load_default()?;
24        Ok(Self { catalog })
25    }
26
27    /// Create selector with custom catalog
28    pub fn with_catalog(catalog: EnvironmentCatalog) -> Self {
29        Self { catalog }
30    }
31
32    /// Select best matching environments for robot profile
33    pub fn select_environments(&self, profile: &RobotProfile, max_envs: usize) -> Result<Vec<EnvironmentMatch>> {
34        let mut matches: Vec<EnvironmentMatch> = self
35            .catalog
36            .environments
37            .iter()
38            .map(|env| self.score_environment(env, profile))
39            .filter(|m| m.score > 0) // Only positive matches
40            .collect();
41
42        // Sort by score (descending)
43        matches.sort_by_key(|m| -m.score);
44
45        // Return top N
46        Ok(matches.into_iter().take(max_envs).collect())
47    }
48
49    /// Score a single environment against robot profile
50    fn score_environment(&self, env: &Environment, profile: &RobotProfile) -> EnvironmentMatch {
51        let mut score = 0;
52        let mut reasons = Vec::new();
53
54        // 1. Task node matching (highest priority: +20 per match)
55        for task_node in &profile.task_nodes {
56            if env.task_nodes.contains(&task_node.name) {
57                score += 20;
58                reasons.push(format!("Task node match: {}", task_node.name));
59            }
60        }
61
62        // 2. Tag matching from task nodes (+10 per tag match)
63        let node_tags: HashSet<String> = profile.task_nodes.iter().flat_map(|n| n.tags.iter().cloned()).collect();
64
65        for tag in &node_tags {
66            if env.tags.contains(tag) {
67                score += 10;
68                reasons.push(format!("Tag match: {}", tag));
69            }
70        }
71
72        // 3. Sensor requirements (+5 if all present, -50 if missing any)
73        let has_required_sensors = env.is_compatible_with_sensors(&profile.sensors);
74
75        if has_required_sensors {
76            score += 5;
77            reasons.push("All required sensors present".to_string());
78        } else {
79            score -= 50; // Hard penalty for missing sensors
80            let missing: Vec<_> = env
81                .required_sensors
82                .iter()
83                .filter(|s| !profile.sensors.contains(s))
84                .collect();
85            reasons.push(format!(
86                "Missing required sensors: {}",
87                missing.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", ")
88            ));
89        }
90
91        // 4. Platform compatibility (+3)
92        if env.is_compatible_with_platform(&profile.platform) {
93            score += 3;
94            reasons.push(format!("Platform match: {}", profile.platform));
95        }
96
97        // 5. Difficulty preference (+2)
98        if env.difficulty == profile.difficulty_preference {
99            score += 2;
100            reasons.push(format!("Difficulty match: {}", env.difficulty));
101        }
102
103        // 6. Capability matching (+1 per capability)
104        for capability in &profile.capabilities {
105            if env.tags.contains(capability) {
106                score += 1;
107                reasons.push(format!("Capability match: {}", capability));
108            }
109        }
110
111        EnvironmentMatch {
112            environment: env.clone(),
113            score,
114            reasons,
115        }
116    }
117
118    /// Get catalog reference
119    pub fn catalog(&self) -> &EnvironmentCatalog {
120        &self.catalog
121    }
122}