scientific_workflow/configuration/parameters/
space.rs1use std::collections::HashMap;
4use std::fmt;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use super::super::error::ConfigurationError;
9use super::super::parameter_path::ParameterPath;
10use super::super::parameter_tree::{ParameterLeaf, flatten_root, paths_conflict, reconstruct};
11use super::super::source::{parse_strict_json, read_source, require_object, validate_name};
12use super::super::sweep::{SweepPlan, parse_sweep};
13use super::task::{TaskParameters, TaskParametersIter};
14
15const FIXED_FILE: &str = "fixed.json";
16const SWEEP_FILE: &str = "sweep.json";
17
18#[derive(Clone)]
19pub struct ParameterSpace {
20 pub(super) inner: Arc<ParameterSpaceInner>,
21}
22
23impl ParameterSpace {
24 pub fn load(configuration_directory: impl Into<PathBuf>) -> Result<Self, ConfigurationError> {
25 let configuration_directory = configuration_directory.into();
26 let fixed_path = configuration_directory.join(FIXED_FILE);
27 let sweep_path = configuration_directory.join(SWEEP_FILE);
28 let fixed_source = read_source(&fixed_path)?;
29 let sweep_source = read_source(&sweep_path)?;
30 let fixed_document = parse_strict_json(&fixed_path, &fixed_source)?;
31 let sweep_document = parse_strict_json(&sweep_path, &sweep_source)?;
32 let fixed_entries = require_object(
33 &fixed_path,
34 fixed_document,
35 "fixed.json root must be an object",
36 )?;
37 for (name, _) in &fixed_entries {
38 validate_name(&fixed_path, name, "fixed parameter")?;
39 }
40 let fixed = flatten_root(fixed_entries);
41 let fixed_document = reconstruct(fixed.iter());
42 let sweep = parse_sweep(&sweep_path, sweep_document)?;
43 validate_disjoint(&fixed_path, &sweep_path, &fixed, &sweep)?;
44
45 let fixed_by_path = fixed
46 .iter()
47 .enumerate()
48 .map(|(index, leaf)| (leaf.path.clone(), index))
49 .collect();
50 let task_count = sweep.task_count();
51 Ok(Self {
52 inner: Arc::new(ParameterSpaceInner {
53 configuration_directory,
54 fixed_source: fixed_source.into_boxed_slice(),
55 sweep_source: sweep_source.into_boxed_slice(),
56 fixed,
57 fixed_document,
58 fixed_by_path,
59 sweep,
60 task_count,
61 }),
62 })
63 }
64
65 pub fn configuration_directory(&self) -> &Path {
66 &self.inner.configuration_directory
67 }
68
69 pub fn fixed_source_json(&self) -> &[u8] {
70 &self.inner.fixed_source
71 }
72
73 pub fn sweep_source_json(&self) -> &[u8] {
74 &self.inner.sweep_source
75 }
76
77 pub fn fixed_parameter_count(&self) -> usize {
79 self.inner.fixed.len()
80 }
81
82 pub fn sweep_parameter_count(&self) -> usize {
84 self.inner.sweep.selectable_paths().len()
85 }
86
87 pub fn parameter_count(&self) -> usize {
89 self.task(0).map_or(0, |task| task.len())
90 }
91
92 pub fn task_count(&self) -> u64 {
93 self.inner.task_count
94 }
95
96 pub fn contains_parameter(&self, key: &str) -> bool {
97 let Some(path) = ParameterPath::parse(key) else {
98 return false;
99 };
100 self.inner
101 .fixed
102 .iter()
103 .any(|leaf| leaf.path == path || path.is_ancestor_of(&leaf.path))
104 || self
105 .inner
106 .sweep
107 .all_leaf_paths()
108 .any(|candidate| candidate == &path || path.is_ancestor_of(candidate))
109 }
110
111 pub fn fixed_keys(&self) -> impl ExactSizeIterator<Item = &str> {
112 self.inner.fixed.iter().map(|leaf| leaf.path.identifier())
113 }
114
115 pub fn sweep_keys(&self) -> impl ExactSizeIterator<Item = &str> {
116 self.inner
117 .sweep
118 .selectable_paths()
119 .iter()
120 .map(ParameterPath::identifier)
121 }
122
123 pub fn task(&self, ordinal: u64) -> Result<TaskParameters, ConfigurationError> {
124 if ordinal >= self.task_count() {
125 return Err(ConfigurationError::TaskOrdinalOutOfBounds {
126 ordinal,
127 task_count: self.task_count(),
128 });
129 }
130 Ok(TaskParameters::new(Arc::clone(&self.inner), ordinal))
131 }
132
133 pub fn tasks(&self) -> TaskParametersIter {
134 TaskParametersIter::new(Arc::clone(&self.inner), self.task_count())
135 }
136}
137
138impl fmt::Debug for ParameterSpace {
139 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
140 formatter
141 .debug_struct("ParameterSpace")
142 .field("configuration_directory", &self.configuration_directory())
143 .field("fixed_parameters", &self.fixed_parameter_count())
144 .field("sweep_parameters", &self.sweep_parameter_count())
145 .field("task_count", &self.task_count())
146 .finish_non_exhaustive()
147 }
148}
149
150pub(crate) struct ParameterSpaceInner {
151 pub(super) configuration_directory: PathBuf,
152 pub(super) fixed_source: Box<[u8]>,
153 pub(super) sweep_source: Box<[u8]>,
154 pub(super) fixed: Vec<ParameterLeaf>,
155 pub(super) fixed_document: serde_json::Value,
156 pub(super) fixed_by_path: HashMap<ParameterPath, usize>,
157 pub(super) sweep: SweepPlan,
158 pub(super) task_count: u64,
159}
160
161fn validate_disjoint(
162 fixed_path: &Path,
163 sweep_path: &Path,
164 fixed: &[ParameterLeaf],
165 sweep: &SweepPlan,
166) -> Result<(), ConfigurationError> {
167 for fixed_leaf in fixed {
168 if let Some(sweep_path_value) = sweep
169 .all_leaf_paths()
170 .find(|candidate| paths_conflict(&fixed_leaf.path, candidate))
171 {
172 return Err(ConfigurationError::FixedSweepKeyConflict {
173 key: if fixed_leaf.path == *sweep_path_value {
174 fixed_leaf.path.identifier().to_owned()
175 } else {
176 format!("{} <> {}", fixed_leaf.path, sweep_path_value)
177 },
178 fixed_path: fixed_path.to_path_buf(),
179 sweep_path: sweep_path.to_path_buf(),
180 });
181 }
182 }
183 Ok(())
184}