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