Skip to main content

scientific_workflow/configuration/parameters/
space.rs

1//! Shared immutable parameter-space storage and loading.
2
3use 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    /// Number of terminal values contributed by `fixed.json`.
78    pub fn fixed_parameter_count(&self) -> usize {
79        self.inner.fixed.len()
80    }
81
82    /// Number of independently selectable axes or explicit-case fields.
83    pub fn sweep_parameter_count(&self) -> usize {
84        self.inner.sweep.selectable_paths().len()
85    }
86
87    /// Number of terminal values in the first resolved task.
88    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}