1use std::collections::HashMap;
4use std::fmt;
5use std::iter::FusedIterator;
6use std::path::{Path, PathBuf};
7use std::slice;
8use std::sync::Arc;
9
10use super::super::error::ConfigurationError;
11use super::super::parameter_path::ParameterPath;
12use super::super::parameter_tree::{ParameterLeaf, paths_conflict};
13use super::super::source::{
14 StrictValue, parse_strict_json, read_source, require_object, validate_name,
15};
16use super::super::sweep::{ParsedScope, SweepPlan, parse_scope};
17use super::resolved_configuration::{ConfigurationIter, ResolvedConfiguration};
18
19const PARAMETERS_FILE: &str = "parameters.json";
20const CONFIGURATION_DIRECTORY: &str = "config";
21
22#[derive(Clone)]
27pub struct StudyConfiguration {
28 inner: Arc<StudyConfigurationInner>,
29}
30
31#[derive(Clone)]
33pub struct PhaseConfiguration {
34 pub(super) inner: Arc<PhaseConfigurationInner>,
35}
36
37impl StudyConfiguration {
38 pub fn load(study_root: impl Into<PathBuf>) -> Result<Self, ConfigurationError> {
40 let study_root = study_root.into();
41 let configuration_directory = Arc::new(study_root.join(CONFIGURATION_DIRECTORY));
42 let source_path = configuration_directory.join(PARAMETERS_FILE);
43 let source = read_source(&source_path)?;
44 let document = parse_strict_json(&source_path, &source)?;
45 let mut root = require_object(
46 &source_path,
47 document,
48 "parameters.json root must be an object",
49 )?;
50 let global = take_required(&source_path, &mut root, "global")?;
51 let phase_groups = take_required(&source_path, &mut root, "phase_group")?;
52 reject_remaining(&source_path, &root, "parameters.json")?;
53
54 let global = Arc::new(ScopeConfiguration::new(parse_scope(
55 &source_path,
56 require_object(&source_path, global, "`global` must be an object")?,
57 "global scope",
58 )?)?);
59 let groups = require_object(
60 &source_path,
61 phase_groups,
62 "`phase_group` must be an object",
63 )?;
64 if groups.is_empty() {
65 return super::super::source::invalid(
66 &source_path,
67 "`phase_group` must contain at least one group",
68 );
69 }
70
71 let mut phases = HashMap::with_capacity(groups.len());
72 for (group_key, group) in groups {
73 validate_name(&source_path, &group_key, "phase-group key")?;
74 let mut group = require_object(
75 &source_path,
76 group,
77 format!("phase group `{group_key}` must be an object"),
78 )?;
79 let shared = take_required(&source_path, &mut group, "shared")?;
80 let phase = take_required(&source_path, &mut group, "phase")?;
81 reject_remaining(&source_path, &group, &format!("phase group `{group_key}`"))?;
82 let shared = Arc::new(ScopeConfiguration::new(parse_scope(
83 &source_path,
84 require_object(
85 &source_path,
86 shared,
87 format!("phase group `{group_key}` field `shared` must be an object"),
88 )?,
89 &format!("phase group `{group_key}` shared scope"),
90 )?)?);
91 let phase = require_object(
92 &source_path,
93 phase,
94 format!("phase group `{group_key}` field `phase` must be an object"),
95 )?;
96 if phase.is_empty() {
97 return super::super::source::invalid(
98 &source_path,
99 format!("phase group `{group_key}` must contain at least one phase"),
100 );
101 }
102 let mut group_phases = HashMap::with_capacity(phase.len());
103 for (phase_key, values) in phase {
104 validate_name(&source_path, &phase_key, "phase key")?;
105 let local = Arc::new(ScopeConfiguration::new(parse_scope(
106 &source_path,
107 require_object(
108 &source_path,
109 values,
110 format!("phase `{group_key}/{phase_key}` must be an object"),
111 )?,
112 &format!("phase `{group_key}/{phase_key}`"),
113 )?)?);
114 let space = compose_space(
115 Arc::clone(&configuration_directory),
116 &source_path,
117 &group_key,
118 &phase_key,
119 [Arc::clone(&global), Arc::clone(&shared), local],
120 )?;
121 group_phases.insert(phase_key.into_boxed_str(), Arc::new(space));
122 }
123 phases.insert(group_key.into_boxed_str(), group_phases);
124 }
125 Ok(Self {
126 inner: Arc::new(StudyConfigurationInner {
127 study_root,
128 configuration_directory,
129 source_path,
130 source: source.into_boxed_slice(),
131 phases,
132 }),
133 })
134 }
135
136 pub fn study_root(&self) -> &Path {
138 &self.inner.study_root
139 }
140
141 pub fn configuration_directory(&self) -> &Path {
143 self.inner.configuration_directory.as_path()
144 }
145
146 pub fn source_path(&self) -> &Path {
148 &self.inner.source_path
149 }
150
151 pub fn source_json(&self) -> &[u8] {
153 &self.inner.source
154 }
155
156 pub fn phase(
158 &self,
159 phase_group: &str,
160 phase: &str,
161 ) -> Result<PhaseConfiguration, ConfigurationError> {
162 let inner = self
163 .inner
164 .phases
165 .get(phase_group)
166 .and_then(|phases| phases.get(phase))
167 .map(Arc::clone)
168 .ok_or_else(|| ConfigurationError::UnknownPhaseConfiguration {
169 phase_group: phase_group.to_owned(),
170 phase: phase.to_owned(),
171 })?;
172 Ok(PhaseConfiguration { inner })
173 }
174}
175
176impl PhaseConfiguration {
177 pub fn configuration_directory(&self) -> &Path {
179 self.inner.configuration_directory.as_path()
180 }
181
182 pub fn phase_group(&self) -> &str {
184 &self.inner.phase_group
185 }
186
187 pub fn phase(&self) -> &str {
189 &self.inner.phase
190 }
191
192 pub fn combination_count(&self) -> u64 {
194 self.inner.combination_count
195 }
196
197 pub fn fixed_value_count(&self) -> usize {
199 self.inner.fixed_leaves().count()
200 }
201
202 pub fn swept_value_count(&self) -> usize {
204 self.inner
205 .scopes
206 .iter()
207 .map(|scope| scope.sweep.selectable_paths().len())
208 .sum()
209 }
210
211 pub fn contains(&self, key: &str) -> bool {
213 let Some(path) = ParameterPath::parse(key) else {
214 return false;
215 };
216 self.inner
217 .fixed_leaves()
218 .any(|leaf| leaf.path == path || path.is_ancestor_of(&leaf.path))
219 || self
220 .inner
221 .selectable_paths()
222 .any(|candidate| candidate == &path || path.is_ancestor_of(candidate))
223 }
224
225 pub fn fixed_keys(&self) -> impl ExactSizeIterator<Item = &str> {
227 self.inner.fixed_leaves().map(|leaf| leaf.path.identifier())
228 }
229
230 pub fn sweep_keys(&self) -> impl ExactSizeIterator<Item = &str> {
232 self.inner.selectable_paths().map(ParameterPath::identifier)
233 }
234
235 pub fn combination(&self, ordinal: u64) -> Result<ResolvedConfiguration, ConfigurationError> {
237 if ordinal >= self.combination_count() {
238 return Err(ConfigurationError::CombinationOrdinalOutOfBounds {
239 ordinal,
240 combination_count: self.combination_count(),
241 });
242 }
243 Ok(ResolvedConfiguration::new(Arc::clone(&self.inner), ordinal))
244 }
245
246 pub fn combinations(&self) -> ConfigurationIter {
248 ConfigurationIter::new(Arc::clone(&self.inner), self.combination_count())
249 }
250}
251
252impl fmt::Debug for StudyConfiguration {
253 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
254 formatter
255 .debug_struct("StudyConfiguration")
256 .field("study_root", &self.study_root())
257 .field(
258 "phases",
259 &self.inner.phases.values().map(HashMap::len).sum::<usize>(),
260 )
261 .finish_non_exhaustive()
262 }
263}
264
265impl fmt::Debug for PhaseConfiguration {
266 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
267 formatter
268 .debug_struct("PhaseConfiguration")
269 .field("phase_group", &self.phase_group())
270 .field("phase", &self.phase())
271 .field("combinations", &self.combination_count())
272 .finish_non_exhaustive()
273 }
274}
275
276struct StudyConfigurationInner {
277 study_root: PathBuf,
278 configuration_directory: Arc<PathBuf>,
279 source_path: PathBuf,
280 source: Box<[u8]>,
281 phases: HashMap<Box<str>, HashMap<Box<str>, Arc<PhaseConfigurationInner>>>,
282}
283
284pub(crate) struct PhaseConfigurationInner {
285 pub(super) configuration_directory: Arc<PathBuf>,
286 pub(super) phase_group: Box<str>,
287 pub(super) phase: Box<str>,
288 scopes: [Arc<ScopeConfiguration>; 3],
289 pub(super) combination_count: u64,
290}
291
292fn compose_space(
293 configuration_directory: Arc<PathBuf>,
294 source_path: &Path,
295 phase_group: &str,
296 phase: &str,
297 scopes: [Arc<ScopeConfiguration>; 3],
298) -> Result<PhaseConfigurationInner, ConfigurationError> {
299 validate_scopes_disjoint(source_path, &scopes)?;
300 let combination_count = scopes.iter().try_fold(1_u64, |count, scope| {
301 count.checked_mul(scope.combination_count()).ok_or_else(|| {
302 ConfigurationError::CombinationCountOverflow {
303 axis: format!("phase `{phase_group}/{phase}`"),
304 }
305 })
306 })?;
307 Ok(PhaseConfigurationInner {
308 configuration_directory,
309 phase_group: phase_group.to_owned().into_boxed_str(),
310 phase: phase.to_owned().into_boxed_str(),
311 scopes,
312 combination_count,
313 })
314}
315
316struct ScopeConfiguration {
317 fixed: Vec<ParameterLeaf>,
318 fixed_by_path: HashMap<ParameterPath, usize>,
319 sweep: SweepPlan,
320}
321
322impl ScopeConfiguration {
323 fn new(scope: ParsedScope) -> Result<Self, ConfigurationError> {
324 let fixed_by_path = scope
325 .fixed
326 .iter()
327 .enumerate()
328 .map(|(index, leaf)| (leaf.path.clone(), index))
329 .collect();
330 Ok(Self {
331 fixed: scope.fixed,
332 fixed_by_path,
333 sweep: SweepPlan::new(scope.dimensions)?,
334 })
335 }
336
337 const fn combination_count(&self) -> u64 {
338 self.sweep.combination_count()
339 }
340}
341
342impl PhaseConfigurationInner {
343 pub(super) fn fixed_leaves(&self) -> ScopeSliceIter<'_, ParameterLeaf> {
344 ScopeSliceIter::new([
345 &self.scopes[0].fixed,
346 &self.scopes[1].fixed,
347 &self.scopes[2].fixed,
348 ])
349 }
350
351 pub(super) fn selected_leaves(&self, ordinal: u64) -> impl Iterator<Item = &ParameterLeaf> {
352 self.scopes[0]
353 .sweep
354 .selected_leaves(self.scope_ordinal(ordinal, 0))
355 .chain(
356 self.scopes[1]
357 .sweep
358 .selected_leaves(self.scope_ordinal(ordinal, 1)),
359 )
360 .chain(
361 self.scopes[2]
362 .sweep
363 .selected_leaves(self.scope_ordinal(ordinal, 2)),
364 )
365 }
366
367 pub(super) fn selectable_paths(&self) -> ScopeSliceIter<'_, ParameterPath> {
368 ScopeSliceIter::new([
369 self.scopes[0].sweep.selectable_paths(),
370 self.scopes[1].sweep.selectable_paths(),
371 self.scopes[2].sweep.selectable_paths(),
372 ])
373 }
374
375 pub(super) fn fixed_leaf(&self, path: &ParameterPath) -> Option<&ParameterLeaf> {
376 self.scopes.iter().find_map(|scope| {
377 scope
378 .fixed_by_path
379 .get(path)
380 .map(|&position| &scope.fixed[position])
381 })
382 }
383
384 pub(super) fn scope_ordinal(&self, ordinal: u64, scope: usize) -> u64 {
385 let following = self.scopes[(scope + 1)..]
386 .iter()
387 .map(|scope| scope.combination_count())
388 .product::<u64>();
389 (ordinal / following) % self.scopes[scope].combination_count()
390 }
391}
392
393fn validate_scopes_disjoint(
394 source_path: &Path,
395 scopes: &[Arc<ScopeConfiguration>; 3],
396) -> Result<(), ConfigurationError> {
397 let mut paths: Vec<&ParameterPath> = Vec::new();
398 for path in scopes.iter().flat_map(|scope| {
399 scope
400 .fixed
401 .iter()
402 .map(|leaf| &leaf.path)
403 .chain(scope.sweep.selectable_paths())
404 }) {
405 if let Some(previous) = paths.iter().find(|previous| paths_conflict(previous, path)) {
406 return super::super::source::invalid(
407 source_path,
408 format!("parameter paths `{previous}` and `{path}` overlap"),
409 );
410 }
411 paths.push(path);
412 }
413 Ok(())
414}
415
416pub(super) struct ScopeSliceIter<'a, T> {
417 scopes: [slice::Iter<'a, T>; 3],
418 current: usize,
419 remaining: usize,
420}
421
422impl<'a, T> ScopeSliceIter<'a, T> {
423 fn new(scopes: [&'a [T]; 3]) -> Self {
424 Self {
425 remaining: scopes.iter().map(|scope| scope.len()).sum(),
426 scopes: scopes.map(<[T]>::iter),
427 current: 0,
428 }
429 }
430}
431
432impl<'a, T> Iterator for ScopeSliceIter<'a, T> {
433 type Item = &'a T;
434
435 fn next(&mut self) -> Option<Self::Item> {
436 while self.current < self.scopes.len() {
437 if let Some(value) = self.scopes[self.current].next() {
438 self.remaining -= 1;
439 return Some(value);
440 }
441 self.current += 1;
442 }
443 None
444 }
445
446 fn size_hint(&self) -> (usize, Option<usize>) {
447 (self.remaining, Some(self.remaining))
448 }
449}
450
451impl<T> ExactSizeIterator for ScopeSliceIter<'_, T> {}
452impl<T> FusedIterator for ScopeSliceIter<'_, T> {}
453
454fn take_required(
455 path: &Path,
456 entries: &mut Vec<(String, StrictValue)>,
457 name: &str,
458) -> Result<StrictValue, ConfigurationError> {
459 let Some(position) = entries.iter().position(|(key, _)| key == name) else {
460 return super::super::source::invalid(path, format!("required field `{name}` is missing"));
461 };
462 Ok(entries.remove(position).1)
463}
464
465fn reject_remaining(
466 path: &Path,
467 entries: &[(String, StrictValue)],
468 context: &str,
469) -> Result<(), ConfigurationError> {
470 if let Some((name, _)) = entries.first() {
471 return super::super::source::invalid(
472 path,
473 format!("{context} contains unknown field `{name}`"),
474 );
475 }
476 Ok(())
477}