Skip to main content

mecha10_sim/sim/
selector.rs

1//! Environment selection and scoring algorithm
2
3use anyhow::Result;
4use std::collections::HashSet;
5use std::path::Path;
6
7use super::catalog::{Environment, EnvironmentCatalog};
8use super::profile::RobotProfile;
9
10#[derive(Debug, Clone)]
11pub struct EnvironmentMatch {
12    pub environment: Environment,
13    pub score: i32,
14    pub reasons: Vec<String>,
15}
16
17pub struct EnvironmentSelector {
18    catalog: EnvironmentCatalog,
19}
20
21impl EnvironmentSelector {
22    /// Create a new selector, loading the catalog from `<robot_tasks_dir>/catalog.json` under a
23    /// freshly-fetched `mecha10-assets` extraction (`configs/simulation/environments/robot-tasks`).
24    pub fn new(robot_tasks_dir: &Path) -> Result<Self> {
25        let catalog = EnvironmentCatalog::load_default(robot_tasks_dir)?;
26        Ok(Self { catalog })
27    }
28
29    /// Create selector with custom catalog
30    pub fn with_catalog(catalog: EnvironmentCatalog) -> Self {
31        Self { catalog }
32    }
33
34    /// Select best matching environments for robot profile
35    pub fn select_environments(&self, profile: &RobotProfile, max_envs: usize) -> Result<Vec<EnvironmentMatch>> {
36        let mut matches: Vec<EnvironmentMatch> = self
37            .catalog
38            .environments
39            .iter()
40            .map(|env| self.score_environment(env, profile))
41            .filter(|m| m.score > 0)
42            .collect();
43
44        matches.sort_by_key(|m| -m.score);
45
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        for task_node in &profile.task_nodes {
55            if env.task_nodes.contains(&task_node.name) {
56                score += 20;
57                reasons.push(format!("Task node match: {}", task_node.name));
58            }
59        }
60
61        let node_tags: HashSet<String> = profile.task_nodes.iter().flat_map(|n| n.tags.iter().cloned()).collect();
62
63        for tag in &node_tags {
64            if env.tags.contains(tag) {
65                score += 10;
66                reasons.push(format!("Tag match: {}", tag));
67            }
68        }
69
70        let has_required_sensors = env.is_compatible_with_sensors(&profile.sensors);
71
72        if has_required_sensors {
73            score += 5;
74            reasons.push("All required sensors present".to_string());
75        } else {
76            score -= 50;
77            let missing: Vec<_> = env
78                .required_sensors
79                .iter()
80                .filter(|s| !profile.sensors.contains(s))
81                .collect();
82            reasons.push(format!(
83                "Missing required sensors: {}",
84                missing.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", ")
85            ));
86        }
87
88        if env.is_compatible_with_platform(&profile.platform) {
89            score += 3;
90            reasons.push(format!("Platform match: {}", profile.platform));
91        }
92
93        if env.difficulty == profile.difficulty_preference {
94            score += 2;
95            reasons.push(format!("Difficulty match: {}", env.difficulty));
96        }
97
98        for capability in &profile.capabilities {
99            if env.tags.contains(capability) {
100                score += 1;
101                reasons.push(format!("Capability match: {}", capability));
102            }
103        }
104
105        EnvironmentMatch {
106            environment: env.clone(),
107            score,
108            reasons,
109        }
110    }
111
112    /// Get catalog reference
113    pub fn catalog(&self) -> &EnvironmentCatalog {
114        &self.catalog
115    }
116}