mecha10_sim/sim/
selector.rs1use 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 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 pub fn with_catalog(catalog: EnvironmentCatalog) -> Self {
31 Self { catalog }
32 }
33
34 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 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 pub fn catalog(&self) -> &EnvironmentCatalog {
114 &self.catalog
115 }
116}