apsis-rs/crates/apsis-rs/examples/integrate_sim.rs

173 lines
5.4 KiB
Rust

//! Cross-layer integration check: running a scenario as a simulation.
//!
//! This example walks the simulation layer end to end and asserts that each
//! step produces sensible input for the next. It is not a numerical
//! cross-check; the per-layer tests do that. Its purpose is to fail when a
//! change in one crate breaks the assumptions of another.
//!
//! The chain is:
//!
//! 1. Load a scenario from text, which describes the resources, the tasks,
//! and the precedence between them.
//! 2. Prepare it with `apsis-sim`, which plans the tasks and builds a
//! timeline.
//! 3. Start a session and run it, recording state at each activity.
//! 4. Read the run report back, which holds the engine outcome and the
//! trace.
//!
//! Usage:
//!
//! ```text
//! cargo run -p apsis-rs --features sim --example integrate_sim
//! ```
use apsis_sim::{RecordKind, Scenario, Simulation, StopReason};
/// The scenario the example runs.
///
/// The station is available in two passes. An observation uses the payload,
/// and the downlink that follows uses the transmitter, so the two are joined
/// by a precedence constraint. A third task of low priority may or may not
/// fit, which is what makes the plan interesting to report.
const SCENARIO: &str = "\
# A short downlink scenario.
scenario leo downlink
epoch 0
time_limit 4000
resource 0 capacity 1
window 0 0 4000
task 0 duration 120
task_window 0 200 1500
task_priority 0 10
require 0 0 1
task 1 duration 90
task_window 1 0 2000
task_priority 1 9
require 1 0 1
task 2 duration 60
task_window 2 2500 3000
task_priority 2 1
require 2 0 1
precedence 0 1
";
fn main() {
// Step 1: load the scenario. A failure here is a problem with the text,
// so the example reports it rather than unwrapping.
let scenario = match Scenario::parse(SCENARIO) {
Ok(scenario) => scenario,
Err(error) => {
eprintln!("the scenario does not load: {error}");
return;
}
};
let summary = scenario.summary();
println!(
"scenario {}: {} resource(s), {} task(s), {} precedence edge(s)",
scenario.name(),
summary.resources,
summary.tasks,
summary.precedence_edges
);
// Step 2: prepare the simulation. This plans the tasks and builds the
// timeline that the plan implies.
let simulation = match Simulation::prepare(scenario) {
Ok(simulation) => simulation,
Err(error) => {
eprintln!("the simulation does not prepare: {error}");
return;
}
};
let evaluation = simulation.evaluate();
println!(
"plan: {} of {} placed, makespan {:.0} s",
evaluation.placed_count,
evaluation.offered_count,
evaluation.makespan()
);
println!(
"plan: {} start and end event(s), full plan: {}",
simulation.activity_count(),
simulation.has_full_plan()
);
if !simulation.deferred().is_empty() {
println!("plan: deferred {:?}", simulation.deferred());
}
assert!(simulation.honours_precedence());
// Step 3: run the timeline. The handler records what it sees, which is
// what a real model would do with the physics it has.
let mut session = simulation.start();
let mut starts = 0_u32;
let mut ends = 0_u32;
let report = session.run(|_engine, recorder, event| {
if event.payload.is_start() {
starts += 1;
recorder.note(event.time, "started", event.payload.task().index() as f64);
} else {
ends += 1;
recorder.note(event.time, "finished", event.payload.task().index() as f64);
}
});
// Step 4: read the report back.
println!(
"run: {} activity(s) executed, {} start(s), {} end(s)",
report.executed(),
starts,
ends
);
println!(
"run: stopped by {} ({})",
report.outcome.reason, report.outcome
);
assert_eq!(report.outcome.reason, StopReason::QueueEmpty);
assert_eq!(report.executed(), u64::from(starts + ends));
assert_eq!(
report.trace.of_kind(RecordKind::State).len(),
starts as usize + ends as usize
);
// Two tasks are joined by precedence, so their ends and starts must line
// up in the timeline the handler saw.
let trace = &report.trace;
let collection_end = trace
.with_label("finished")
.iter()
.filter(|record| record.value == 0.0)
.map(|record| record.time)
.next();
let downlink_start = trace
.with_label("started")
.iter()
.filter(|record| record.value == 1.0)
.map(|record| record.time)
.next();
if let (Some(collection_end), Some(downlink_start)) = (collection_end, downlink_start) {
println!(
"sequence: collection ends at {collection_end} s, downlink starts at {downlink_start} s"
);
assert!(downlink_start >= collection_end);
}
// A run of the same prepared simulation must reproduce the trace.
let mut again = simulation.start();
let repeated = again.run(|_engine, recorder, event| {
if event.payload.is_start() {
recorder.note(event.time, "started", event.payload.task().index() as f64);
} else {
recorder.note(event.time, "finished", event.payload.task().index() as f64);
}
});
assert_eq!(repeated.executed(), report.executed());
assert_eq!(repeated.trace.records(), report.trace.records());
println!("integration check passed");
}