1use super::AssembledPlan;
4use super::CuMissionPlan;
5use super::CuPlanner;
6use super::LaneOccurrence;
7use super::LanePlan;
8use super::LaneWorker;
9use super::PlanningInput;
10use super::Serial;
11use super::StepOrder;
12use super::assemble_from_order;
13use super::assemble_runtime_plan_with_planner;
14use super::build_plan_graph;
15use super::mission_graphs;
16use super::schedule::PlanShape;
17use super::step_key;
18use crate::config::ComponentConfig;
19use crate::config::CuConfig;
20use crate::config::CuGraph;
21use crate::config::NodeId;
22use crate::config::PlannerConfig;
23use crate::config::PlannerKind;
24use crate::config::RuntimeConfig;
25use crate::config::Value;
26use crate::curuntime::CuExecutionUnit;
27use crate::curuntime::CuStepPhase;
28use alloc::collections::BTreeMap;
29use alloc::format;
30use alloc::string::String;
31use alloc::vec::Vec;
32use cu29_traits::CuError;
33use cu29_traits::CuResult;
34use serde::Deserialize;
35use serde::Serialize;
36
37#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(deny_unknown_fields)]
44pub struct CuPlan {
45 #[serde(default, skip_serializing_if = "Vec::is_empty")]
47 pub concurrent_resources: Vec<String>,
48 pub missions: BTreeMap<String, CuMissionPlan>,
50}
51
52impl CuPlan {
53 pub fn from_config(config: &CuConfig) -> CuResult<Self> {
55 super::resolve_schedule(config)
56 }
57
58 pub fn from_config_cyclic(config: &CuConfig, copperlists_per_cycle: u32) -> CuResult<Self> {
60 Self::from_config_cyclic_with_resources(config, copperlists_per_cycle, &[])
61 }
62
63 pub(crate) fn from_config_cyclic_with_resources(
64 config: &CuConfig,
65 copperlists_per_cycle: u32,
66 concurrent_resources: &[String],
67 ) -> CuResult<Self> {
68 if copperlists_per_cycle == 0 {
69 return Err(CuError::from("copperlists_per_cycle must be positive"));
70 }
71 let mut missions = BTreeMap::new();
72 for (mission, graph) in mission_graphs(config) {
73 let assembled = assemble_runtime_plan_with_planner(config, graph, &Serial)?;
74 missions.insert(
75 mission.clone(),
76 PlanShape::new(&assembled, config, graph, &mission, concurrent_resources)?
77 .cyclic_plan(copperlists_per_cycle)?,
78 );
79 }
80 Ok(Self {
81 concurrent_resources: concurrent_resources.to_vec(),
82 missions,
83 })
84 }
85
86 pub fn validate(&self, config: &CuConfig) -> CuResult<()> {
88 self.validate_orders(config).map(|_| ())
89 }
90
91 fn validate_orders(&self, config: &CuConfig) -> CuResult<BTreeMap<String, Vec<usize>>> {
92 let missions = mission_graphs(config);
93 if !self.missions.keys().eq(missions.iter().map(|(id, _)| id)) {
94 return Err(CuError::from(
95 "Execution plan must contain exactly the configured missions",
96 ));
97 }
98 let mut orders = BTreeMap::new();
99 for (mission, graph) in missions {
100 let canonical = assemble_runtime_plan_with_planner(config, graph, &Serial)?;
101 let shape = PlanShape::new(
102 &canonical,
103 config,
104 graph,
105 &mission,
106 &self.concurrent_resources,
107 )?;
108 let order = self.missions[&mission]
109 .validate(config, &shape)
110 .map_err(|error| CuError::from(format!("Plan for mission '{mission}': {error}")))?;
111 orders.insert(mission, order);
112 }
113 Ok(orders)
114 }
115
116 pub fn provide_capacity(&self, config: &mut CuConfig) {
118 let needed = self
119 .missions
120 .values()
121 .map(|mission| mission.max_in_flight as usize)
122 .max()
123 .unwrap_or(0);
124 let current = config
125 .logging
126 .as_ref()
127 .and_then(|logging| logging.copperlist_count)
128 .unwrap_or(super::DEFAULT_COPPERLIST_COUNT);
129 if needed > current {
130 config
131 .logging
132 .get_or_insert_with(Default::default)
133 .copperlist_count = Some(needed);
134 }
135 }
136
137 pub fn serialize_ron(&self) -> CuResult<String> {
139 ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::default())
140 .map_err(|error| CuError::new_with_cause("Could not serialize execution plan", error))
141 }
142
143 pub fn deserialize_ron(text: &str) -> CuResult<Self> {
145 ron::from_str(text)
146 .map_err(|error| CuError::new_with_cause("Could not parse execution plan", error))
147 }
148
149 #[cfg(feature = "std")]
151 pub fn read(path: &std::path::Path) -> CuResult<Self> {
152 let text = std::fs::read_to_string(path)
153 .map_err(|error| CuError::new_with_cause("Could not read execution plan", error))?;
154 Self::deserialize_ron(&text)
155 }
156
157 #[cfg(feature = "std")]
159 pub fn write(&self, path: &std::path::Path) -> CuResult<()> {
160 std::fs::write(path, self.serialize_ron()?)
161 .map_err(|error| CuError::new_with_cause("Could not write execution plan", error))
162 }
163}
164
165#[derive(Clone, Debug)]
167pub struct ExplicitSchedule {
168 plan: CuPlan,
169}
170
171impl CuPlanner for ExplicitSchedule {
172 fn plan(&self, _input: &PlanningInput<'_>) -> CuResult<CuPlan> {
173 Ok(self.plan.clone())
174 }
175}
176
177impl ExplicitSchedule {
178 pub fn new(plan: CuPlan) -> Self {
180 Self { plan }
181 }
182
183 pub fn apply(&self, config: &mut CuConfig) -> CuResult<()> {
185 let mut prepared = config.clone();
186 self.plan.provide_capacity(&mut prepared);
187 self.plan.validate(&prepared)?;
188 let value = cu29_value::to_value(&self.plan)
189 .map_err(|error| CuError::new_with_cause("Could not encode execution plan", error))?;
190 let value = Value::deserialize(value)
191 .map_err(|error| CuError::new_with_cause("Could not embed execution plan", error))?;
192 let mut params = ComponentConfig::default();
193 params.set("plan", value);
194 prepared
195 .runtime
196 .get_or_insert_with(RuntimeConfig::default)
197 .planner = Some(PlannerConfig {
198 kind: PlannerKind::ExplicitSchedule,
199 config: Some(params),
200 });
201 *config = prepared;
202 Ok(())
203 }
204
205 pub(super) fn assemble(
206 &self,
207 config: &CuConfig,
208 graph: &CuGraph,
209 mission: &str,
210 ) -> CuResult<AssembledPlan> {
211 let orders = self.plan.validate_orders(config)?;
212 self.assemble_steps(config, graph, mission, &orders[mission])
213 .map_err(|error| {
214 CuError::from(format!(
215 "ExplicitSchedule plan for mission '{mission}': {error}"
216 ))
217 })
218 }
219
220 fn assemble_steps(
221 &self,
222 config: &CuConfig,
223 graph: &CuGraph,
224 mission: &str,
225 order: &[usize],
226 ) -> CuResult<AssembledPlan> {
227 let plan = self
228 .plan
229 .missions
230 .get(mission)
231 .ok_or_else(|| CuError::from("Missing mission"))?;
232 let requested = if plan.is_serial() {
233 plan.serial_keys()?
234 } else {
235 plan.layout_keys(order)
236 };
237 let canonical = assemble_runtime_plan_with_planner(config, graph, &Serial)?;
238 let keys = execution_keys(&canonical, mission)?;
239 let by_key: BTreeMap<_, _> = keys.iter().enumerate().map(|(i, key)| (key, i)).collect();
240 let mut node_order = Vec::with_capacity(canonical.entities.len());
241 for key in &requested {
242 let index = *by_key
243 .get(key)
244 .ok_or_else(|| CuError::from(format!("Unknown process step '{key}'")))?;
245 let CuExecutionUnit::Step(step) = &canonical.execution.steps[index] else {
246 return Err(CuError::from("Nested loops cannot be materialized"));
247 };
248 if step.phase != CuStepPhase::AnytimeRefine {
249 node_order.push(step.node_id);
250 }
251 }
252 let mut assembled =
253 assemble_from_order(build_plan_graph(config, graph)?, StepOrder(node_order))?;
254 let remapped_keys = execution_keys(&assembled, mission)?;
255 let mut units: BTreeMap<_, _> = remapped_keys
256 .into_iter()
257 .zip(assembled.execution.steps)
258 .collect();
259 assembled.execution.steps = requested
260 .iter()
261 .map(|key| {
262 units.remove(key).ok_or_else(|| {
263 CuError::from(format!("Could not materialize process step '{key}'"))
264 })
265 })
266 .collect::<CuResult<Vec<_>>>()?;
267 assembled.background = plan.background.clone();
268 if !plan.is_serial() {
269 let step_of: BTreeMap<_, _> = requested
270 .iter()
271 .enumerate()
272 .map(|(index, key)| (key.as_str(), index))
273 .collect();
274 assembled.lanes = Some(LanePlan {
275 copperlists_per_cycle: plan.copperlists_per_cycle,
276 max_in_flight: plan.max_in_flight,
277 occurrences: plan
278 .steps
279 .iter()
280 .map(|step| LaneOccurrence {
281 step: step_of[step.key.as_str()],
282 copperlist: step.copperlist,
283 })
284 .collect(),
285 workers: plan
286 .workers
287 .iter()
288 .map(|worker| LaneWorker {
289 id: worker.id.clone(),
290 placement: worker.placement.clone(),
291 occurrences: worker.steps.iter().map(|&index| index as usize).collect(),
292 })
293 .collect(),
294 dispatcher: plan.dispatcher.clone(),
295 dependencies: plan.dependencies.clone(),
296 });
297 }
298 Ok(assembled)
299 }
300}
301
302pub(super) fn execution_keys(plan: &AssembledPlan, mission: &str) -> CuResult<Vec<String>> {
303 let mut refines: BTreeMap<NodeId, u32> = BTreeMap::new();
304 plan.execution
305 .steps
306 .iter()
307 .map(|unit| {
308 let CuExecutionUnit::Step(step) = unit else {
309 return Err(CuError::from(
310 "Nested loops are not supported by portable plans",
311 ));
312 };
313 let ordinal = if step.phase == CuStepPhase::AnytimeRefine {
314 let next = refines.entry(step.node_id).or_default();
315 *next += 1;
316 Some(*next)
317 } else {
318 None
319 };
320 Ok(step_key(
321 mission,
322 &plan.entities[step.node_id as usize],
323 step.phase,
324 ordinal,
325 ))
326 })
327 .collect()
328}