1use std::fmt;
4use std::ops::Range;
5use std::str::FromStr;
6
7use crate::action::{ActionDescriptor, Schedule, Scheduled};
8use crate::explore::design::{Block, DesignError, DesignKind, MAX_CONFIGS, generate_within};
9use crate::explore::design_csv::{DesignTableError, read_table};
10use crate::explore::factor::{FactorError, FactorSlot, FactorTarget};
11use crate::explore::fingerprint::{plan_hash, results_fingerprint, run_key, schema_hash};
12use crate::explore::measure::{MeasureError, MeasurePlan};
13use crate::explore::outcome::PlannedRun;
14use crate::explore::replay::Replay;
15use crate::explore::seed::design_seed;
16use crate::explore::spec::{ActionSpec, BlockSpec, MeasureSettings, RunSettings, SeedSettings, SweepSpec};
17use crate::explore::stop::StopError;
18use crate::explore::value::{ValueError, resolve_params};
19use crate::params::{ParamDescriptor, ParamValue};
20use crate::view::StatDescriptor;
21
22pub const MAX_RUNS: u64 = 1 << 24;
27
28#[derive(Debug, Clone, Copy)]
30pub struct ModelSchema<'a> {
31 pub id: &'a str,
33 pub params: &'a [ParamDescriptor],
35 pub stats: &'a [StatDescriptor],
37 pub actions: &'a [ActionDescriptor],
39}
40
41#[derive(Debug, Clone, PartialEq)]
43pub struct Config {
44 pub block: usize,
46 pub params: Vec<ParamValue>,
48 pub action_ticks: Vec<u64>,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct PlannedBlock {
55 pub design: DesignKind,
57 pub configs: Range<u64>,
59 pub design_seed: Option<u64>,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum PlanWarning {
66 ActionAfterEnd {
68 name: String,
70 latest_tick: u64,
72 last_tick: u64,
74 config_count: u64,
76 },
77}
78
79impl fmt::Display for PlanWarning {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 match self {
82 Self::ActionAfterEnd {
83 name,
84 latest_tick,
85 last_tick,
86 config_count,
87 } => {
88 let configs = if *config_count == 1 { "config" } else { "configs" };
89 write!(
90 f,
91 "action '{name}' is due after the last tick {last_tick} in {config_count} {configs} (latest tick \
92 {latest_tick}) and will not run there"
93 )
94 }
95 }
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub struct Shard {
104 index: u64,
105 count: u64,
106}
107
108impl Shard {
109 pub const WHOLE: Self = Self { index: 0, count: 1 };
111
112 pub fn new(index: u64, count: u64) -> Result<Self, ShardError> {
118 if count == 0 {
119 return Err(ShardError::ZeroCount);
120 }
121 if index >= count {
122 return Err(ShardError::IndexPastCount { index, count });
123 }
124 Ok(Self { index, count })
125 }
126
127 pub fn index(self) -> u64 {
129 self.index
130 }
131
132 pub fn count(self) -> u64 {
134 self.count
135 }
136
137 pub fn contains(self, run_id: u64) -> bool {
139 run_id % self.count == self.index
140 }
141
142 pub fn run_count(self, plan_runs: u64) -> u64 {
144 if self.index < plan_runs {
145 (plan_runs - 1 - self.index) / self.count + 1
146 } else {
147 0
148 }
149 }
150}
151
152impl Default for Shard {
153 fn default() -> Self {
154 Self::WHOLE
155 }
156}
157
158impl fmt::Display for Shard {
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 write!(f, "{}/{}", self.index, self.count)
161 }
162}
163
164impl FromStr for Shard {
165 type Err = ShardError;
166
167 fn from_str(raw: &str) -> Result<Self, Self::Err> {
169 let (index, count) = raw
170 .split_once('/')
171 .and_then(|(index, count)| Some((index.trim().parse().ok()?, count.trim().parse().ok()?)))
172 .ok_or_else(|| ShardError::BadText { raw: raw.to_owned() })?;
173 Self::new(index, count)
174 }
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
179pub enum ShardError {
180 BadText {
182 raw: String,
184 },
185 ZeroCount,
187 IndexPastCount {
189 index: u64,
191 count: u64,
193 },
194}
195
196impl fmt::Display for ShardError {
197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198 match self {
199 Self::BadText { raw } => write!(f, "invalid shard '{raw}', expected INDEX/COUNT"),
200 Self::ZeroCount => write!(f, "shard count must be at least 1"),
201 Self::IndexPastCount { index, count } => {
202 write!(f, "shard index {index} must be less than shard count {count}")
203 }
204 }
205 }
206}
207
208impl std::error::Error for ShardError {}
209
210#[derive(Debug, Clone, PartialEq)]
215pub struct Plan {
216 model: String,
217 configs: Vec<Config>,
218 blocks: Vec<PlannedBlock>,
219 run_settings: RunSettings,
220 measure: MeasureSettings,
221 seeds: SeedSettings,
222 actions: Vec<ActionSpec>,
223 action_indices: Vec<usize>,
225 warnings: Vec<PlanWarning>,
226 run_count: u64,
227 schema_hash: u64,
228 results_fingerprint: u64,
229 plan_hash: u64,
230}
231
232impl Plan {
233 pub fn model(&self) -> &str {
235 &self.model
236 }
237
238 pub fn configs(&self) -> &[Config] {
240 &self.configs
241 }
242
243 pub fn config(&self, config_id: u64) -> Option<&Config> {
245 self.configs.get(usize::try_from(config_id).ok()?)
246 }
247
248 pub fn blocks(&self) -> &[PlannedBlock] {
250 &self.blocks
251 }
252
253 pub fn run_settings(&self) -> &RunSettings {
255 &self.run_settings
256 }
257
258 pub fn measure_settings(&self) -> &MeasureSettings {
260 &self.measure
261 }
262
263 pub fn seed_settings(&self) -> SeedSettings {
265 self.seeds
266 }
267
268 pub fn actions(&self) -> &[ActionSpec] {
270 &self.actions
271 }
272
273 pub fn warnings(&self) -> &[PlanWarning] {
275 &self.warnings
276 }
277
278 pub fn replicates(&self) -> u64 {
280 self.run_settings.replicates
281 }
282
283 pub fn run_count(&self) -> u64 {
285 self.run_count
286 }
287
288 pub fn run(&self, run_id: u64) -> Option<PlannedRun> {
290 (run_id < self.run_count).then(|| self.planned_run(run_id))
291 }
292
293 pub fn runs(&self) -> impl Iterator<Item = PlannedRun> + '_ {
295 (0..self.run_count).map(|run_id| self.planned_run(run_id))
296 }
297
298 pub fn runs_in_shard(&self, shard: Shard) -> impl Iterator<Item = PlannedRun> + '_ {
300 (0..shard.run_count(self.run_count)).map(move |position| self.planned_run(shard.index + position * shard.count))
301 }
302
303 fn planned_run(&self, run_id: u64) -> PlannedRun {
304 let config_id = run_id / self.run_settings.replicates;
305 let rep = run_id % self.run_settings.replicates;
306 PlannedRun {
307 run_id,
308 config_id,
309 rep,
310 seed: self.seeds.scheme.seed(self.seeds.root, config_id, rep),
311 }
312 }
313
314 pub fn schedule(&self, config: &Config) -> Schedule {
320 assert_eq!(
321 config.action_ticks.len(),
322 self.actions.len(),
323 "a config holds one tick per action"
324 );
325 let mut entries: Vec<Scheduled> = self
326 .actions
327 .iter()
328 .zip(&self.action_indices)
329 .zip(&config.action_ticks)
330 .map(|((action, &index), &tick)| Scheduled {
331 index,
332 id: action.id.clone(),
333 tick,
334 })
335 .collect();
336 entries.sort_by_key(|entry| entry.tick);
337 Schedule::from_entries(entries)
338 }
339
340 pub fn replay(&self, run_id: u64) -> Option<Replay> {
342 let run = self.run(run_id)?;
343 let config = self.config(run.config_id)?;
344 Some(Replay {
345 model: self.model.clone(),
346 params: config.params.clone(),
347 seed: run.seed,
348 schedule: self.schedule(config),
349 ticks: self.run_settings.warmup + self.run_settings.steps,
350 label: format!("Sweep run {run_id}: config {}, replicate {}", run.config_id, run.rep),
351 })
352 }
353
354 pub fn run_key(&self, run: &PlannedRun) -> u64 {
360 let config = self
361 .config(run.config_id)
362 .expect("a planned run's config is in its plan");
363 run_key(self.results_fingerprint, &config.params, &config.action_ticks, run.seed)
364 }
365
366 pub fn schema_hash(&self) -> u64 {
368 self.schema_hash
369 }
370
371 pub fn results_fingerprint(&self) -> u64 {
373 self.results_fingerprint
374 }
375
376 pub fn plan_hash(&self) -> u64 {
380 self.plan_hash
381 }
382}
383
384impl SweepSpec {
385 pub fn plan(&self, schema: &ModelSchema<'_>) -> Result<Plan, PlanError> {
395 if self.model != schema.id {
396 return Err(PlanError::WrongModel {
397 spec_model: self.model.clone(),
398 schema_model: schema.id.to_owned(),
399 });
400 }
401 if self.run.replicates == 0 {
402 return Err(PlanError::NoReplicates);
403 }
404 MeasurePlan::check(&self.run, &self.measure).map_err(PlanError::Measure)?;
405 for reducer in &self.measure.reducers {
406 reducer
407 .check_label(schema.stats)
408 .map_err(|error| PlanError::Measure(MeasureError::Reducer(error)))?;
409 }
410 if let Some(stop) = &self.run.stop {
411 stop.check_label(schema.stats).map_err(PlanError::Stop)?;
412 }
413 let action_indices = self.resolve_actions(schema)?;
414 let fixed = resolve_params(schema.params, &self.fixed).map_err(|error| match error {
415 ValueError::UnknownParam { id, known } => PlanError::UnknownFixed { id, known },
416 error => PlanError::Fixed(error),
417 })?;
418 let fixed_ticks: Vec<u64> = self.actions.iter().map(|action| action.tick).collect();
419
420 let lone_block = BlockSpec::default();
421 let block_specs = if self.blocks.is_empty() {
422 std::slice::from_ref(&lone_block)
423 } else {
424 self.blocks.as_slice()
425 };
426 let mut configs = Vec::new();
427 let mut blocks = Vec::with_capacity(block_specs.len());
428 for (index, block_spec) in block_specs.iter().enumerate() {
429 let block = self.resolve_block(index, block_spec, schema)?;
430 let base = Config {
431 block: index,
432 params: fixed.clone(),
433 action_ticks: fixed_ticks.clone(),
434 };
435 let start = configs.len() as u64;
436 let limit = MAX_CONFIGS - configs.len();
437 configs.extend(
438 generate_within(&block, &base, limit).map_err(|source| PlanError::Design { block: index, source })?,
439 );
440 blocks.push(PlannedBlock {
441 design: block.design.clone(),
442 configs: start..configs.len() as u64,
443 design_seed: block.design.is_sampled().then_some(block.design_seed),
444 });
445 }
446
447 let run_count = (configs.len() as u64)
448 .checked_mul(self.run.replicates)
449 .filter(|&count| count <= MAX_RUNS)
450 .ok_or(PlanError::TooManyRuns)?;
451 let schema_hash = schema_hash(schema);
452 let results_fingerprint = results_fingerprint(schema_hash, &self.run, &self.measure, &self.actions);
453 let plan_hash = plan_hash(results_fingerprint, &self.seeds, &self.actions, &blocks, &configs);
454 let warnings = late_actions(&self.actions, &configs, self.run.warmup + self.run.steps);
455 Ok(Plan {
456 model: self.model.clone(),
457 configs,
458 blocks,
459 run_settings: self.run.clone(),
460 measure: self.measure.clone(),
461 seeds: self.seeds,
462 actions: self.actions.clone(),
463 action_indices,
464 warnings,
465 run_count,
466 schema_hash,
467 results_fingerprint,
468 plan_hash,
469 })
470 }
471
472 fn resolve_actions(&self, schema: &ModelSchema<'_>) -> Result<Vec<usize>, PlanError> {
474 let mut indices = Vec::with_capacity(self.actions.len());
475 for (position, action) in self.actions.iter().enumerate() {
476 if self.actions[..position]
477 .iter()
478 .any(|earlier| earlier.name == action.name)
479 {
480 return Err(PlanError::DuplicateActionName {
481 name: action.name.clone(),
482 });
483 }
484 let index = schema
485 .actions
486 .iter()
487 .position(|declared| declared.id == action.id)
488 .ok_or_else(|| PlanError::UnknownAction {
489 id: action.id.clone(),
490 known: schema.actions.iter().map(|declared| declared.id).collect(),
491 })?;
492 indices.push(index);
493 }
494 Ok(indices)
495 }
496
497 fn resolve_block(
499 &self,
500 index: usize,
501 block_spec: &BlockSpec,
502 schema: &ModelSchema<'_>,
503 ) -> Result<Block, PlanError> {
504 let factors = if let DesignKind::Table { text } = &block_spec.design {
505 if !block_spec.factors.is_empty() {
506 return Err(PlanError::TableWithFactors { block: index });
507 }
508 read_table(text, schema.params, &self.actions)
509 .map_err(|source| PlanError::Table { block: index, source })?
510 } else {
511 let mut factors = Vec::with_capacity(block_spec.factors.len());
512 for (position, factor) in block_spec.factors.iter().enumerate() {
513 if block_spec.factors[..position]
514 .iter()
515 .any(|earlier| earlier.target == factor.target)
516 {
517 return Err(PlanError::VariedTwice {
518 target: factor.target.clone(),
519 block: index,
520 });
521 }
522 factors.push(
523 factor
524 .resolve(schema.params, &self.actions, &block_spec.design)
525 .map_err(|source| PlanError::Factor { block: index, source })?,
526 );
527 }
528 factors
529 };
530 for factor in &factors {
531 if let FactorSlot::Param(param) = factor.slot {
532 let id = schema.params[param].id;
533 if self.fixed.iter().any(|(fixed_id, _)| fixed_id == id) {
534 return Err(PlanError::FixedAndVaried {
535 id: id.to_owned(),
536 block: index,
537 });
538 }
539 }
540 }
541 Ok(Block {
542 design: block_spec.design.clone(),
543 factors,
544 design_seed: block_spec
545 .design_seed
546 .unwrap_or_else(|| design_seed(self.seeds.root, index)),
547 })
548 }
549}
550
551fn late_actions(actions: &[ActionSpec], configs: &[Config], last_tick: u64) -> Vec<PlanWarning> {
553 let mut warnings = Vec::new();
554 for (position, action) in actions.iter().enumerate() {
555 let late = configs
556 .iter()
557 .map(|config| config.action_ticks[position])
558 .filter(|&tick| tick > last_tick);
559 let (count, latest) = late.fold((0, 0), |(count, latest), tick| (count + 1, latest.max(tick)));
560 if count > 0 {
561 warnings.push(PlanWarning::ActionAfterEnd {
562 name: action.name.clone(),
563 latest_tick: latest,
564 last_tick,
565 config_count: count,
566 });
567 }
568 }
569 warnings
570}
571
572#[derive(Debug, Clone, PartialEq)]
574pub enum PlanError {
575 WrongModel {
577 spec_model: String,
579 schema_model: String,
581 },
582 NoReplicates,
584 Measure(MeasureError),
586 Stop(StopError),
588 UnknownAction {
590 id: String,
592 known: Vec<&'static str>,
594 },
595 DuplicateActionName {
597 name: String,
599 },
600 Fixed(ValueError),
602 UnknownFixed {
604 id: String,
606 known: Vec<&'static str>,
608 },
609 FixedAndVaried {
611 id: String,
613 block: usize,
615 },
616 VariedTwice {
618 target: FactorTarget,
620 block: usize,
622 },
623 Factor {
625 block: usize,
627 source: FactorError,
629 },
630 TableWithFactors {
632 block: usize,
634 },
635 Table {
637 block: usize,
639 source: DesignTableError,
641 },
642 Design {
644 block: usize,
646 source: DesignError,
648 },
649 TooManyRuns,
651}
652
653impl fmt::Display for PlanError {
654 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
655 match self {
656 Self::WrongModel {
657 spec_model,
658 schema_model,
659 } => {
660 write!(f, "spec is for model '{spec_model}', expected '{schema_model}'")
661 }
662 Self::NoReplicates => write!(f, "replicates must be at least 1"),
663 Self::Measure(_) => write!(f, "measurement settings"),
664 Self::Stop(_) => write!(f, "stop condition"),
665 Self::UnknownAction { id, known } if known.is_empty() => {
666 write!(f, "unknown action '{id}' (model declares no actions)")
667 }
668 Self::UnknownAction { id, known } => {
669 write!(f, "unknown action '{id}', expected one of {}", known.join(", "))
670 }
671 Self::DuplicateActionName { name } => {
672 write!(f, "two actions are named '{name}', and a name must be unique")
673 }
674 Self::Fixed(_) => write!(f, "fixed values"),
675 Self::UnknownFixed { id, known } => {
676 write!(f, "unknown parameter '{id}', expected one of {}", known.join(", "))
677 }
678 Self::FixedAndVaried { id, block } => {
679 write!(f, "parameter '{id}' is both fixed and varied in block {block}")
680 }
681 Self::VariedTwice { target, block } => write!(f, "block {block} varies {target} twice"),
682 Self::TableWithFactors { block } => {
683 write!(f, "block {block} reads its configs from a table, and takes no factors")
684 }
685 Self::Factor { block, .. } | Self::Table { block, .. } | Self::Design { block, .. } => {
686 write!(f, "block {block}")
687 }
688 Self::TooManyRuns => write!(f, "plan has more than {MAX_RUNS} runs"),
689 }
690 }
691}
692
693impl std::error::Error for PlanError {
694 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
695 match self {
696 Self::Measure(error) => Some(error),
697 Self::Stop(error) => Some(error),
698 Self::Fixed(error) => Some(error),
699 Self::Factor { source, .. } => Some(source),
700 Self::Table { source, .. } => Some(source),
701 Self::Design { source, .. } => Some(source),
702 Self::WrongModel { .. }
703 | Self::NoReplicates
704 | Self::UnknownAction { .. }
705 | Self::DuplicateActionName { .. }
706 | Self::UnknownFixed { .. }
707 | Self::FixedAndVaried { .. }
708 | Self::VariedTwice { .. }
709 | Self::TableWithFactors { .. }
710 | Self::TooManyRuns => None,
711 }
712 }
713}
714
715#[cfg(test)]
716mod tests {
717 use std::collections::BTreeSet;
718
719 use super::{MAX_RUNS, ModelSchema, PlanError, PlanWarning, Shard, ShardError};
720 use crate::action::{ActionDescriptor, Scheduled};
721 use crate::explore::design::{DesignError, DesignKind};
722 use crate::explore::design_csv::DesignTableError;
723 use crate::explore::factor::{FactorError, FactorSpec, FactorTarget, LevelSpec};
724 use crate::explore::measure::MeasureError;
725 use crate::explore::reducer::{ReducerError, ReducerKind, ReducerSpec};
726 use crate::explore::seed::{SeedScheme, design_seed, run_seed};
727 use crate::explore::spec::{ActionSpec, BlockSpec, SweepSpec};
728 use crate::explore::stop::{StopError, StopSpec};
729 use crate::helpers::{choice_param, f32_param, u32_param};
730 use crate::params::{ParamDescriptor, ParamValue};
731 use crate::view::StatDescriptor;
732
733 const COLOR: [u8; 4] = [0, 0, 0, 255];
734 const STATS: &[StatDescriptor] = &[
735 StatDescriptor::new("Infected", COLOR),
736 StatDescriptor::new("Recovered", COLOR),
737 ];
738 const ACTIONS: &[ActionDescriptor] = &[
739 ActionDescriptor::new("seed_outbreak", "Seed outbreak"),
740 ActionDescriptor::new("vaccinate", "Vaccinate"),
741 ];
742 const NEIGHBORHOODS: &[&str] = &["moore", "von_neumann"];
743
744 fn params() -> Vec<ParamDescriptor> {
745 vec![
746 f32_param("infection_rate", "Infection rate", 0.2, 0.0, 1.0, Some(0.01)),
747 f32_param("recovery_rate", "Recovery rate", 0.05, 0.0, 1.0, None),
748 u32_param("grid_width", "Grid width", 64, 1, 4096),
749 choice_param("neighborhood", "Neighborhood", NEIGHBORHOODS, 0),
750 ]
751 }
752
753 fn values(raw: &[&str]) -> LevelSpec {
754 LevelSpec::Values(raw.iter().map(|&text| text.to_owned()).collect())
755 }
756
757 fn block(design: DesignKind, factors: Vec<FactorSpec>) -> BlockSpec {
758 BlockSpec {
759 design,
760 factors,
761 design_seed: None,
762 }
763 }
764
765 fn plan(spec: &SweepSpec) -> Result<super::Plan, PlanError> {
766 let params = params();
767 let schema = ModelSchema {
768 id: "sir",
769 params: ¶ms,
770 stats: STATS,
771 actions: ACTIONS,
772 };
773 spec.plan(&schema)
774 }
775
776 fn spec_with_actions() -> SweepSpec {
778 let mut spec = SweepSpec::new("sir");
779 spec.run.steps = 100;
780 spec.run.replicates = 2;
781 spec.actions = vec![
782 ActionSpec::new("seed_outbreak", 50),
783 ActionSpec {
784 name: "second_wave".to_owned(),
785 ..ActionSpec::new("seed_outbreak", 50)
786 },
787 ];
788 spec
789 }
790
791 #[test]
792 fn blocks_concatenate_and_number_configs_in_order() {
793 let mut spec = SweepSpec::new("sir");
794 spec.blocks = vec![
795 block(
796 DesignKind::Factorial,
797 vec![FactorSpec::param("grid_width", values(&["16", "32"]))],
798 ),
799 block(
800 DesignKind::Zip,
801 vec![
802 FactorSpec::param("infection_rate", values(&["0.1", "0.2", "0.3"])),
803 FactorSpec::param("neighborhood", values(&["von_neumann", "moore", "1"])),
804 ],
805 ),
806 ];
807 let plan = plan(&spec).expect("a valid spec");
808 let blocks: Vec<usize> = plan.configs().iter().map(|config| config.block).collect();
809 assert_eq!(blocks, [0, 0, 1, 1, 1]);
810 assert_eq!(plan.blocks()[0].configs, 0..2);
811 assert_eq!(plan.blocks()[1].configs, 2..5);
812 assert_eq!(plan.blocks()[1].design, DesignKind::Zip);
813 let second = plan.config(3).expect("config 3 exists");
814 assert_eq!(
815 second.params,
816 [
817 ParamValue::F32(0.2),
818 ParamValue::F32(0.05),
819 ParamValue::U32(64),
820 ParamValue::Choice(0)
821 ],
822 "a block leaves the parameters it does not vary at their fixed values"
823 );
824 }
825
826 #[test]
827 fn blocks_past_the_limit_together_are_refused() {
828 let mut spec = SweepSpec::new("sir");
830 spec.actions = vec![ActionSpec::new("seed_outbreak", 0)];
831 spec.blocks = vec![
832 block(
833 DesignKind::Factorial,
834 vec![FactorSpec::param("neighborhood", LevelSpec::All)],
835 ),
836 block(
837 DesignKind::Factorial,
838 vec![
839 FactorSpec::param(
840 "grid_width",
841 LevelSpec::Range {
842 min: 1.0,
843 max: 4096.0,
844 step: None,
845 },
846 ),
847 FactorSpec::action(
848 "seed_outbreak",
849 LevelSpec::Range {
850 min: 0.0,
851 max: 4095.0,
852 step: None,
853 },
854 ),
855 ],
856 ),
857 ];
858 assert!(matches!(
859 plan(&spec),
860 Err(PlanError::Design {
861 block: 1,
862 source: DesignError::TooManyConfigs
863 })
864 ));
865 }
866
867 #[test]
868 fn a_plan_past_the_run_limit_is_refused() {
869 let mut spec = SweepSpec::new("sir");
870 spec.run.replicates = MAX_RUNS;
871 assert_eq!(plan(&spec).expect("a plan at the limit").run_count(), MAX_RUNS);
872 spec.run.replicates = MAX_RUNS + 1;
873 assert!(matches!(plan(&spec), Err(PlanError::TooManyRuns)));
874 }
875
876 #[test]
877 fn runs_are_numbered_config_by_config() {
878 let mut spec = SweepSpec::new("sir");
879 spec.run.replicates = 3;
880 spec.seeds.root = 42;
881 spec.blocks = vec![block(
882 DesignKind::Factorial,
883 vec![FactorSpec::param("grid_width", values(&["16", "32"]))],
884 )];
885 let common = plan(&spec).expect("a valid spec");
886 assert_eq!(common.run_count(), 6);
887 let runs: Vec<(u64, u64, u64)> = common.runs().map(|run| (run.run_id, run.config_id, run.rep)).collect();
888 assert_eq!(runs, [(0, 0, 0), (1, 0, 1), (2, 0, 2), (3, 1, 0), (4, 1, 1), (5, 1, 2)]);
889 let fifth = common.run(4).expect("run 4 exists");
890 let second = common.run(1).expect("run 1 exists");
891 assert_eq!(fifth.seed, run_seed(42, 1), "common random numbers by default");
892 assert_eq!(fifth.seed, second.seed);
893 assert_ne!(common.run_key(&fifth), common.run_key(&second), "the configs differ");
894 assert_eq!(common.run(6), None);
895
896 spec.seeds.scheme = SeedScheme::Independent;
897 let independent = plan(&spec).expect("a valid spec");
898 assert_ne!(
899 independent.run(4).map(|run| run.seed),
900 independent.run(1).map(|run| run.seed)
901 );
902 }
903
904 #[test]
905 fn a_spec_with_no_blocks_runs_its_fixed_values() {
906 let mut spec = SweepSpec::new("sir");
907 spec.fixed = vec![("grid_width".to_owned(), "128".to_owned())];
908 let plan = plan(&spec).expect("a valid spec");
909 assert_eq!(plan.configs().len(), 1);
910 assert_eq!(plan.configs()[0].params[2], ParamValue::U32(128));
911 assert_eq!(plan.blocks().len(), 1);
912 }
913
914 #[test]
915 fn a_parameter_varied_twice_in_a_block_is_refused() {
916 let mut spec = SweepSpec::new("sir");
917 spec.blocks = vec![block(
918 DesignKind::Factorial,
919 vec![
920 FactorSpec::param("grid_width", values(&["16"])),
921 FactorSpec::param("infection_rate", values(&["0.1"])),
922 FactorSpec::param("grid_width", values(&["32"])),
923 ],
924 )];
925 assert_eq!(
926 plan(&spec),
927 Err(PlanError::VariedTwice {
928 target: FactorTarget::Param("grid_width".to_owned()),
929 block: 0
930 })
931 );
932 assert_eq!(
933 plan(&spec).map_err(|error| error.to_string()),
934 Err("block 0 varies parameter 'grid_width' twice".to_owned())
935 );
936 spec.blocks.insert(0, block(DesignKind::Factorial, Vec::new()));
937 assert!(
938 matches!(plan(&spec), Err(PlanError::VariedTwice { block: 1, .. })),
939 "the error names the block"
940 );
941
942 let mut apart = SweepSpec::new("sir");
943 apart.blocks = vec![
944 block(
945 DesignKind::Factorial,
946 vec![FactorSpec::param("grid_width", values(&["16"]))],
947 ),
948 block(
949 DesignKind::Factorial,
950 vec![FactorSpec::param("grid_width", values(&["32"]))],
951 ),
952 ];
953 assert!(plan(&apart).is_ok(), "two blocks can vary one parameter");
954 }
955
956 #[test]
957 fn a_parameter_both_fixed_and_varied_is_refused() {
958 let mut spec = SweepSpec::new("sir");
959 spec.fixed = vec![("grid_width".to_owned(), "128".to_owned())];
960 spec.blocks = vec![block(
961 DesignKind::Factorial,
962 vec![FactorSpec::param("grid_width", values(&["16", "32"]))],
963 )];
964 assert_eq!(
965 plan(&spec),
966 Err(PlanError::FixedAndVaried {
967 id: "grid_width".to_owned(),
968 block: 0
969 })
970 );
971 }
972
973 #[test]
974 fn a_spec_the_model_refuses_is_refused() {
975 let mut other_model = SweepSpec::new("boids");
976 assert!(matches!(plan(&other_model), Err(PlanError::WrongModel { .. })));
977 other_model.model = "sir".to_owned();
978
979 let mut spec = other_model.clone();
980 spec.run.replicates = 0;
981 assert_eq!(plan(&spec), Err(PlanError::NoReplicates));
982
983 let mut spec = other_model.clone();
984 spec.measure.stats_every = 0;
985 assert_eq!(plan(&spec), Err(PlanError::Measure(MeasureError::ZeroStatsEvery)));
986
987 let mut spec = other_model.clone();
988 spec.measure.reducers = vec![ReducerSpec {
989 column: "Susceptible".to_owned(),
990 kind: ReducerKind::Max,
991 }];
992 assert!(matches!(
993 plan(&spec),
994 Err(PlanError::Measure(MeasureError::Reducer(
995 ReducerError::UnknownColumn { .. }
996 )))
997 ));
998
999 let mut spec = other_model.clone();
1000 spec.fixed = vec![("grid_width".to_owned(), "0".to_owned())];
1001 assert!(matches!(plan(&spec), Err(PlanError::Fixed(_))));
1002
1003 let mut spec = other_model.clone();
1004 spec.fixed = vec![("nope".to_owned(), "1".to_owned())];
1005 let error = plan(&spec).expect_err("the model has no parameter 'nope'");
1006 assert_eq!(
1007 error.to_string(),
1008 "unknown parameter 'nope', expected one of infection_rate, recovery_rate, grid_width, neighborhood"
1009 );
1010
1011 let mut spec = other_model.clone();
1012 spec.blocks = vec![block(
1013 DesignKind::Factorial,
1014 vec![FactorSpec::param(
1015 "infection_rate",
1016 LevelSpec::Range {
1017 min: 0.1,
1018 max: 0.5,
1019 step: None,
1020 },
1021 )],
1022 )];
1023 assert!(
1024 matches!(
1025 plan(&spec),
1026 Err(PlanError::Factor {
1027 block: 0,
1028 source: FactorError::MissingStep { .. }
1029 })
1030 ),
1031 "a factorial over an F32 range needs a step"
1032 );
1033
1034 let mut spec = other_model;
1035 spec.blocks = vec![block(
1036 DesignKind::Zip,
1037 vec![
1038 FactorSpec::param("grid_width", values(&["16", "32"])),
1039 FactorSpec::param("infection_rate", values(&["0.1"])),
1040 ],
1041 )];
1042 assert!(matches!(
1043 plan(&spec),
1044 Err(PlanError::Design {
1045 block: 0,
1046 source: DesignError::UnequalLengths { .. }
1047 })
1048 ));
1049 }
1050
1051 #[test]
1052 fn an_action_tick_factor_moves_the_action() {
1053 let mut spec = spec_with_actions();
1054 spec.blocks = vec![block(
1055 DesignKind::Factorial,
1056 vec![
1057 FactorSpec::param("grid_width", values(&["16", "32"])),
1058 FactorSpec::action("second_wave", values(&["10", "90"])),
1059 ],
1060 )];
1061 let plan = plan(&spec).expect("a valid spec");
1062 let ticks: Vec<Vec<u64>> = plan
1063 .configs()
1064 .iter()
1065 .map(|config| config.action_ticks.clone())
1066 .collect();
1067 assert_eq!(
1068 ticks,
1069 [[50, 10], [50, 90], [50, 10], [50, 90]],
1070 "the other action keeps its tick"
1071 );
1072 let run = plan.run(1).expect("run 1 exists");
1073 let moved = plan.run(3).expect("run 3 exists");
1074 assert_eq!(run.seed, moved.seed);
1075 assert_ne!(plan.run_key(&run), plan.run_key(&moved), "the tick is part of the key");
1076 assert!(plan.warnings().is_empty());
1077 }
1078
1079 #[test]
1080 fn a_replay_holds_its_runs_config_seed_and_schedule() {
1081 let mut spec = spec_with_actions();
1082 spec.run.warmup = 5;
1083 spec.seeds.root = 9;
1084 spec.blocks = vec![block(
1085 DesignKind::Factorial,
1086 vec![
1087 FactorSpec::param("grid_width", values(&["16", "32"])),
1088 FactorSpec::action("second_wave", values(&["10"])),
1089 ],
1090 )];
1091 let plan = plan(&spec).expect("a valid spec");
1092 assert_eq!(plan.model(), "sir");
1093 let run = plan.run(3).expect("run 3 exists");
1094 let config = plan.config(run.config_id).expect("the config exists");
1095 let replay = plan.replay(3).expect("run 3 exists");
1096 assert_eq!(replay.model, "sir");
1097 assert_eq!(replay.params, config.params);
1098 assert_eq!(replay.params[2], ParamValue::U32(32));
1099 assert_eq!(replay.seed, run.seed);
1100 assert_eq!(replay.schedule, plan.schedule(config));
1101 let ticks: Vec<u64> = replay.schedule.entries().iter().map(|entry| entry.tick).collect();
1102 assert_eq!(ticks, [10, 50]);
1103 assert_eq!(replay.ticks, 105, "the warm-up is part of the run");
1104 assert_eq!(replay.label, "Sweep run 3: config 1, replicate 1");
1105 assert_eq!(plan.replay(plan.run_count()), None);
1106 }
1107
1108 #[test]
1109 fn a_schedule_orders_by_tick_then_spec_order() {
1110 let mut spec = spec_with_actions();
1111 spec.actions.push(ActionSpec {
1112 name: "vaccination".to_owned(),
1113 ..ActionSpec::new("vaccinate", 20)
1114 });
1115 spec.blocks = vec![block(
1116 DesignKind::Zip,
1117 vec![FactorSpec::action("second_wave", values(&["50", "0"]))],
1118 )];
1119 let plan = plan(&spec).expect("a valid spec");
1120 let order = |config_id: u64| {
1121 let config = plan.config(config_id).expect("the config exists");
1122 plan.schedule(config)
1123 .entries()
1124 .iter()
1125 .map(|Scheduled { index, tick, .. }| (*index, *tick))
1126 .collect::<Vec<_>>()
1127 };
1128 assert_eq!(order(0), [(1, 20), (0, 50), (0, 50)], "two at one tick keep spec order");
1129 assert_eq!(order(1), [(0, 0), (1, 20), (0, 50)]);
1130 assert_eq!(plan.schedule(&plan.configs()[1]).entries()[0].id, "seed_outbreak");
1131 }
1132
1133 #[test]
1134 fn an_action_the_spec_cannot_fire_is_refused() {
1135 let mut spec = spec_with_actions();
1136 spec.actions[1].name = "seed_outbreak".to_owned();
1137 assert_eq!(
1138 plan(&spec),
1139 Err(PlanError::DuplicateActionName {
1140 name: "seed_outbreak".to_owned()
1141 })
1142 );
1143
1144 let mut spec = spec_with_actions();
1145 spec.actions.push(ActionSpec::new("reset", 3));
1146 let error = plan(&spec).expect_err("no action 'reset'");
1147 assert_eq!(
1148 error.to_string(),
1149 "unknown action 'reset', expected one of seed_outbreak, vaccinate"
1150 );
1151
1152 let mut spec = spec_with_actions();
1153 spec.blocks = vec![block(
1154 DesignKind::Factorial,
1155 vec![FactorSpec::action("third_wave", values(&["10"]))],
1156 )];
1157 assert!(matches!(
1158 plan(&spec),
1159 Err(PlanError::Factor {
1160 source: FactorError::UnknownAction { .. },
1161 ..
1162 })
1163 ));
1164
1165 let mut spec = spec_with_actions();
1166 spec.blocks = vec![block(
1167 DesignKind::Factorial,
1168 vec![
1169 FactorSpec::action("second_wave", values(&["10"])),
1170 FactorSpec::action("second_wave", values(&["20"])),
1171 ],
1172 )];
1173 assert!(matches!(
1174 plan(&spec),
1175 Err(PlanError::VariedTwice {
1176 target: FactorTarget::Action(_),
1177 ..
1178 })
1179 ));
1180 }
1181
1182 #[test]
1183 fn an_action_past_the_end_is_a_warning() {
1184 let mut spec = spec_with_actions();
1185 spec.blocks = vec![block(
1186 DesignKind::Factorial,
1187 vec![FactorSpec::action("second_wave", values(&["100", "101", "150"]))],
1188 )];
1189 let plan = plan(&spec).expect("a late action still plans");
1190 assert_eq!(
1191 plan.warnings(),
1192 [PlanWarning::ActionAfterEnd {
1193 name: "second_wave".to_owned(),
1194 latest_tick: 150,
1195 last_tick: 100,
1196 config_count: 2,
1197 }],
1198 "tick 100 is the last tick, and fires after the last step"
1199 );
1200 let lone = PlanWarning::ActionAfterEnd {
1201 name: "second_wave".to_owned(),
1202 latest_tick: 150,
1203 last_tick: 100,
1204 config_count: 1,
1205 };
1206 assert_eq!(
1207 lone.to_string(),
1208 "action 'second_wave' is due after the last tick 100 in 1 config (latest tick 150) and will not run there"
1209 );
1210 }
1211
1212 #[test]
1213 fn a_stop_over_an_unknown_label_is_refused() {
1214 let mut spec = SweepSpec::new("sir");
1215 spec.run.stop = Some(StopSpec::parse("Infected <= 0", 5).expect("a well-formed condition"));
1216 assert!(plan(&spec).is_ok());
1217 spec.run.stop = Some(StopSpec::parse("Susceptible <= 0", 5).expect("a well-formed condition"));
1218 assert!(matches!(
1219 plan(&spec),
1220 Err(PlanError::Stop(StopError::UnknownColumn { .. }))
1221 ));
1222 }
1223
1224 #[test]
1226 fn a_threshold_that_is_not_finite_is_refused_at_plan_time() {
1227 let mut spec = SweepSpec::new("sir");
1228 let mut stop = StopSpec::parse("Infected <= 0", 0).expect("a well-formed condition");
1229 stop.comparison.threshold = f64::INFINITY;
1230 spec.run.stop = Some(stop);
1231 let error = plan(&spec).expect_err("an infinite threshold");
1232 assert!(
1233 matches!(
1234 error,
1235 PlanError::Measure(MeasureError::Stop(StopError::NonFiniteThreshold { .. }))
1236 ),
1237 "{error:?}"
1238 );
1239
1240 spec.run.stop = None;
1241 let mut first = "Infected:first>=10"
1242 .parse::<ReducerSpec>()
1243 .expect("a well-formed reducer");
1244 if let ReducerKind::FirstCrossing(comparison) = &mut first.kind {
1245 comparison.threshold = f64::NEG_INFINITY;
1246 }
1247 spec.measure.reducers = vec![first];
1248 let error = plan(&spec).expect_err("an infinite threshold");
1249 assert!(
1250 matches!(
1251 error,
1252 PlanError::Measure(MeasureError::Reducer(ReducerError::Comparison { .. }))
1253 ),
1254 "{error:?}"
1255 );
1256 }
1257
1258 #[test]
1259 fn a_sampled_block_records_its_design_seed() {
1260 let mut spec = spec_with_actions();
1261 spec.seeds.root = 42;
1262 let sampled = |design_seed| BlockSpec {
1263 design_seed,
1264 ..block(
1265 DesignKind::LatinHypercube { samples: 6 },
1266 vec![
1267 FactorSpec::param(
1268 "infection_rate",
1269 LevelSpec::Range {
1270 min: 0.1,
1271 max: 0.9,
1272 step: None,
1273 },
1274 ),
1275 FactorSpec::action(
1276 "second_wave",
1277 LevelSpec::Range {
1278 min: 0.0,
1279 max: 100.0,
1280 step: None,
1281 },
1282 ),
1283 ],
1284 )
1285 };
1286 spec.blocks = vec![block(DesignKind::Zip, Vec::new()), sampled(None), sampled(Some(7))];
1287 let plan = plan(&spec).expect("a valid spec");
1288 let seeds: Vec<Option<u64>> = plan.blocks().iter().map(|block| block.design_seed).collect();
1289 assert_eq!(seeds, [None, Some(design_seed(42, 1)), Some(7)]);
1290 assert_eq!(plan.configs().len(), 13);
1291 assert_eq!(plan.blocks()[2].configs, 7..13);
1292 }
1293
1294 #[test]
1295 fn a_table_block_reads_its_configs() {
1296 let mut spec = spec_with_actions();
1297 spec.fixed = vec![("recovery_rate".to_owned(), "0.1".to_owned())];
1298 let table = "infection_rate,action.second_wave\n0.25,5\n0.5,6\n";
1299 spec.blocks = vec![block(DesignKind::Table { text: table.to_owned() }, Vec::new())];
1300 let table_plan = plan(&spec).expect("a valid spec");
1301 assert_eq!(table_plan.configs().len(), 2);
1302 assert_eq!(table_plan.configs()[1].params[0], ParamValue::F32(0.5));
1303 assert_eq!(
1304 table_plan.configs()[1].params[1],
1305 ParamValue::F32(0.1),
1306 "fixed values fill the rest"
1307 );
1308 assert_eq!(table_plan.configs()[1].action_ticks, [50, 6]);
1309 assert_eq!(table_plan.blocks()[0].design_seed, None);
1310
1311 let mut listed = spec.clone();
1312 listed.blocks[0].factors = vec![FactorSpec::param("grid_width", values(&["16"]))];
1313 assert_eq!(plan(&listed), Err(PlanError::TableWithFactors { block: 0 }));
1314
1315 let mut fixed = spec.clone();
1316 fixed.fixed = vec![("infection_rate".to_owned(), "0.1".to_owned())];
1317 assert!(matches!(plan(&fixed), Err(PlanError::FixedAndVaried { .. })));
1318
1319 let mut unknown = spec;
1320 unknown.blocks[0].design = DesignKind::Table {
1321 text: "speed\n1\n".to_owned(),
1322 };
1323 assert!(matches!(
1324 plan(&unknown),
1325 Err(PlanError::Table {
1326 block: 0,
1327 source: DesignTableError::UnknownColumn { .. }
1328 })
1329 ));
1330 }
1331
1332 #[test]
1333 fn shards_partition_the_runs() {
1334 let mut spec = SweepSpec::new("sir");
1335 spec.run.replicates = 3;
1336 spec.blocks = vec![block(
1337 DesignKind::Factorial,
1338 vec![FactorSpec::param("grid_width", values(&["16", "32", "48", "64", "80"]))],
1339 )];
1340 let plan = plan(&spec).expect("a valid spec");
1341 let every: Vec<u64> = plan.runs().map(|run| run.run_id).collect();
1342 assert_eq!(
1343 plan.runs_in_shard(Shard::WHOLE).collect::<Vec<_>>(),
1344 plan.runs().collect::<Vec<_>>()
1345 );
1346 for count in 1..=17 {
1347 let mut seen = BTreeSet::new();
1348 for index in 0..count {
1349 let shard = Shard::new(index, count).expect("a valid shard");
1350 let runs: Vec<u64> = plan.runs_in_shard(shard).map(|run| run.run_id).collect();
1351 assert_eq!(runs.len() as u64, shard.run_count(plan.run_count()), "shard {shard}");
1352 assert!(
1353 runs.iter()
1354 .all(|&run_id| run_id % count == index && shard.contains(run_id))
1355 );
1356 assert!(runs.windows(2).all(|pair| pair[0] < pair[1]), "in run order");
1357 for run_id in runs {
1358 assert!(seen.insert(run_id), "run {run_id} is in one shard only");
1359 }
1360 }
1361 assert_eq!(
1362 seen.into_iter().collect::<Vec<_>>(),
1363 every,
1364 "{count} shards cover every run"
1365 );
1366 }
1367 let second_run = plan.runs_in_shard(Shard::new(1, 4).expect("a valid shard")).nth(1);
1368 assert_eq!(second_run, plan.run(5), "shard runs carry their plan's seeds");
1369 }
1370
1371 #[test]
1372 fn a_shard_reads_as_index_over_count() {
1373 assert_eq!("2/5".parse::<Shard>(), Shard::new(2, 5));
1374 assert_eq!("0/1".parse::<Shard>(), Ok(Shard::WHOLE));
1375 assert_eq!(Shard::new(3, 8).map(|shard| shard.to_string()), Ok("3/8".to_owned()));
1376 assert_eq!("1/0".parse::<Shard>(), Err(ShardError::ZeroCount));
1377 assert_eq!(
1378 "4/4".parse::<Shard>(),
1379 Err(ShardError::IndexPastCount { index: 4, count: 4 })
1380 );
1381 for bad in ["", "1", "1/", "a/2", "-1/2", "1/2/3"] {
1382 assert_eq!(
1383 bad.parse::<Shard>(),
1384 Err(ShardError::BadText { raw: bad.to_owned() }),
1385 "{bad}"
1386 );
1387 }
1388 assert_eq!(Shard::WHOLE.run_count(0), 0);
1389 assert_eq!(Shard::new(5, 6).map(|shard| shard.run_count(5)), Ok(0));
1390 }
1391}