1use std::collections::HashSet;
18use std::sync::atomic::{AtomicUsize, Ordering};
19use std::sync::Arc;
20
21use openjd_expr::value::Float64;
22use openjd_expr::{ExprValue, RangeExpr};
23
24use crate::error::ModelError;
25use crate::job;
26use crate::template::RangeConstraint;
27use crate::types::{TaskParameterSet, TaskParameterType, TaskParameterValue};
28
29fn checked_product_len(children: &[Box<dyn Node>]) -> Result<usize, ModelError> {
33 children.iter().try_fold(1usize, |acc, c| {
34 acc.checked_mul(c.len()).ok_or_else(|| {
35 ModelError::DecodeValidation(
36 "Total parameter space size overflow: the product of parameter dimensions is too large.".into(),
37 )
38 })
39 })
40}
41
42fn tokenize(expr: &str) -> Vec<String> {
44 let mut tokens = Vec::new();
45 let mut current = String::new();
46 for ch in expr.chars() {
47 match ch {
48 '*' | '(' | ')' | ',' => {
49 if !current.is_empty() {
50 tokens.push(std::mem::take(&mut current));
51 }
52 tokens.push(ch.to_string());
53 }
54 c if c.is_whitespace() => {
55 if !current.is_empty() {
56 tokens.push(std::mem::take(&mut current));
57 }
58 }
59 _ => current.push(ch),
60 }
61 }
62 if !current.is_empty() {
63 tokens.push(current);
64 }
65 tokens
66}
67
68fn compress_range_expr(values: &[i64]) -> String {
71 if values.is_empty() {
72 return String::new();
73 }
74 if values.len() == 1 {
75 return values[0].to_string();
76 }
77
78 let mut parts = Vec::new();
81 let mut i = 0;
82 while i < values.len() {
83 if i + 2 < values.len() {
84 let step = values[i + 1] - values[i];
85 if step > 0 && values[i + 2] - values[i + 1] == step {
86 let mut end = i + 2;
88 while end + 1 < values.len() && values[end + 1] - values[end] == step {
89 end += 1;
90 }
91 if step == 1 {
92 parts.push(format!("{}-{}", values[i], values[end]));
93 } else {
94 parts.push(format!("{}-{}:{}", values[i], values[end], step));
95 }
96 i = end + 1;
97 continue;
98 }
99 }
100 parts.push(values[i].to_string());
101 i += 1;
102 }
103 parts.join(",")
104}
105
106fn build_chunk_range_expr(
115 range: &job::TaskParamRange<i64>,
116 constraint: &RangeConstraint,
117 small: usize,
118 leftovers: usize,
119 i: usize,
120) -> RangeExpr {
121 let size = small + if i < leftovers { 1 } else { 0 };
122 let offset = i * small + i.min(leftovers);
123 let build = |vals: &[i64]| -> RangeExpr {
124 let range_str = match constraint {
125 RangeConstraint::Contiguous => {
126 if vals.len() == 1 {
127 vals[0].to_string()
128 } else {
129 format!("{}-{}", vals[0], vals[vals.len() - 1])
130 }
131 }
132 RangeConstraint::Noncontiguous => compress_range_expr(vals),
133 };
134 let expr = range_str
135 .parse::<RangeExpr>()
136 .expect("range string built from valid integers");
137 match constraint {
138 RangeConstraint::Contiguous => expr.with_contiguous(true),
139 RangeConstraint::Noncontiguous => expr,
140 }
141 };
142 match range {
143 job::TaskParamRange::RangeExpr(r) => {
144 let vals: Vec<i64> = (offset..offset + size)
145 .map(|j| r.get(j as i64).expect("chunk element within range bounds"))
146 .collect();
147 build(&vals)
148 }
149 job::TaskParamRange::List(values) => build(&values[offset..offset + size]),
150 }
151}
152
153trait Node: Send + Sync {
157 fn len(&self) -> usize;
158 fn get(&self, index: usize, result: &mut TaskParameterSet);
159 fn validate_containment(&self, params: &TaskParameterSet) -> Result<(), String>;
161 fn iter(&self) -> Box<dyn NodeIterator>;
163}
164
165trait NodeIterator: Send + Sync {
167 fn next(&mut self, result: &mut TaskParameterSet) -> bool;
168 fn reset(&mut self);
169}
170
171struct IndexedNodeIterator {
175 len: usize,
176 index: usize,
177}
178
179impl NodeIterator for IndexedNodeIterator {
180 fn next(&mut self, _result: &mut TaskParameterSet) -> bool {
181 if self.index >= self.len {
182 return false;
183 }
184 self.index += 1;
185 true
186 }
187 fn reset(&mut self) {
188 self.index = 0;
189 }
190}
191
192struct RangeListIterator {
194 name: String,
195 param_type: TaskParameterType,
196 values: Vec<ExprValue>,
197 index: usize,
198}
199
200impl NodeIterator for RangeListIterator {
201 fn next(&mut self, result: &mut TaskParameterSet) -> bool {
202 if self.index >= self.values.len() {
203 return false;
204 }
205 result.insert(
206 self.name.clone(),
207 TaskParameterValue {
208 param_type: self.param_type,
209 value: self.values[self.index].clone(),
210 },
211 );
212 self.index += 1;
213 true
214 }
215 fn reset(&mut self) {
216 self.index = 0;
217 }
218}
219
220struct RangeExprIterator {
222 name: String,
223 range: RangeExpr,
224 index: usize,
225}
226
227impl NodeIterator for RangeExprIterator {
228 fn next(&mut self, result: &mut TaskParameterSet) -> bool {
229 if self.index >= self.range.len() {
230 return false;
231 }
232 result.insert(
233 self.name.clone(),
234 TaskParameterValue {
235 param_type: TaskParameterType::Int,
236 value: ExprValue::Int(
237 self.range
238 .get(self.index as i64)
239 .expect("index checked against range.len()"),
240 ),
241 },
242 );
243 self.index += 1;
244 true
245 }
246 fn reset(&mut self) {
247 self.index = 0;
248 }
249}
250
251struct StaticChunkIterator {
253 name: String,
254 range: job::TaskParamRange<i64>,
255 constraint: RangeConstraint,
256 num_chunks: usize,
257 small: usize,
258 leftovers: usize,
259 index: usize,
260}
261
262impl StaticChunkIterator {
263 fn chunk_range_expr(&self, i: usize) -> RangeExpr {
264 build_chunk_range_expr(&self.range, &self.constraint, self.small, self.leftovers, i)
265 }
266}
267
268impl NodeIterator for StaticChunkIterator {
269 fn next(&mut self, result: &mut TaskParameterSet) -> bool {
270 if self.index >= self.num_chunks {
271 return false;
272 }
273 result.insert(
274 self.name.clone(),
275 TaskParameterValue {
276 param_type: TaskParameterType::ChunkInt,
277 value: ExprValue::RangeExpr(self.chunk_range_expr(self.index)),
278 },
279 );
280 self.index += 1;
281 true
282 }
283 fn reset(&mut self) {
284 self.index = 0;
285 }
286}
287
288struct ContiguousChunkNode {
292 name: String,
293 range: job::TaskParamRange<i64>,
294 default_task_count: usize,
295 num_chunks: usize, total_len: usize,
297}
298
299fn count_contiguous_chunks_for_range(
303 range: &job::TaskParamRange<i64>,
304 default_task_count: usize,
305) -> usize {
306 match range {
307 job::TaskParamRange::List(v) => {
308 if v.is_empty() {
309 return 0;
310 }
311 let mut total = 0usize;
312 let mut interval_start = 0usize;
313 for i in 0..v.len() - 1 {
314 if v[i + 1] != v[i] + 1 {
315 let len = i - interval_start + 1;
316 total += len.div_ceil(default_task_count);
317 interval_start = i + 1;
318 }
319 }
320 total += (v.len() - interval_start).div_ceil(default_task_count);
321 total
322 }
323 job::TaskParamRange::RangeExpr(r) => {
324 count_contiguous_chunks_from_sub_ranges(r, default_task_count)
325 }
326 }
327}
328
329fn count_contiguous_chunks_from_sub_ranges(r: &RangeExpr, default_task_count: usize) -> usize {
332 let sub_ranges = r.ranges();
333 if sub_ranges.is_empty() {
334 return 0;
335 }
336
337 let mut total_chunks = 0usize;
338 let mut interval: Option<(i64, i64)> = None;
340
341 for sr in sub_ranges {
342 if sr.step == 1 {
343 match interval {
345 Some((is, ie)) if sr.start == ie + 1 => {
346 interval = Some((is, sr.end));
348 }
349 Some((is, ie)) => {
350 let len = (ie - is + 1) as usize;
352 total_chunks += len.div_ceil(default_task_count);
353 interval = Some((sr.start, sr.end));
354 }
355 None => {
356 interval = Some((sr.start, sr.end));
357 }
358 }
359 } else {
360 let count = sr.len();
364 for idx in 0..count {
365 let val = sr.get(idx).expect("index within sub-range bounds");
367 match interval {
368 Some((is, ie)) if val == ie + 1 => {
369 interval = Some((is, val));
370 }
371 Some((is, ie)) => {
372 let len = (ie - is + 1) as usize;
373 total_chunks += len.div_ceil(default_task_count);
374 interval = Some((val, val));
375 }
376 None => {
377 interval = Some((val, val));
378 }
379 }
380 }
381 }
382 }
383 if let Some((is, ie)) = interval {
385 let len = (ie - is + 1) as usize;
386 total_chunks += len.div_ceil(default_task_count);
387 }
388 total_chunks
389}
390
391impl ContiguousChunkNode {
392 fn new(name: String, range: job::TaskParamRange<i64>, default_task_count: usize) -> Self {
393 let total_len = match &range {
394 job::TaskParamRange::List(v) => v.len(),
395 job::TaskParamRange::RangeExpr(r) => r.len(),
396 };
397 let dtc = default_task_count.max(1);
398 let num_chunks = count_contiguous_chunks_for_range(&range, dtc);
399 Self {
400 name,
401 range,
402 default_task_count: dtc,
403 num_chunks,
404 total_len,
405 }
406 }
407}
408
409impl Node for ContiguousChunkNode {
410 fn len(&self) -> usize {
411 self.num_chunks
412 }
413 fn get(&self, _index: usize, _result: &mut TaskParameterSet) {
414 }
416 fn validate_containment(&self, params: &TaskParameterSet) -> Result<(), String> {
417 let v = params.get(&self.name).ok_or_else(|| {
418 format!(
419 "Parameter '{}' not found in the provided parameters.",
420 self.name
421 )
422 })?;
423 match &v.value {
424 ExprValue::RangeExpr(r) => {
425 for chunk in ContiguousChunkIterState::new(self) {
427 if chunk == *r {
428 return Ok(());
429 }
430 }
431 Err(format!(
432 "Parameter '{}' value '{}' is not a valid chunk in the parameter space.",
433 self.name, r
434 ))
435 }
436 _ => Err(format!(
437 "Parameter '{}' value '{}' is not in the parameter space range.",
438 self.name,
439 v.value.to_display_string()
440 )),
441 }
442 }
443 fn iter(&self) -> Box<dyn NodeIterator> {
444 Box::new(ContiguousChunkNodeIterator {
445 state: ContiguousChunkIterState::new(self),
446 name: self.name.clone(),
447 })
448 }
449}
450
451struct ContiguousChunkIterState {
455 range: job::TaskParamRange<i64>,
456 default_task_count: usize,
457 total_len: usize,
458 cursor: usize,
459 interval_start_val: i64, interval_chunks_remaining: usize,
462 interval_pos: i64, interval_small: usize,
464 interval_leftovers: usize,
465 interval_chunk_index: usize,
466 interval_chunk_count: usize,
467}
468
469impl ContiguousChunkIterState {
470 fn new(node: &ContiguousChunkNode) -> Self {
471 Self {
472 range: node.range.clone(),
473 default_task_count: node.default_task_count,
474 total_len: node.total_len,
475 cursor: 0,
476 interval_start_val: 0,
477 interval_chunks_remaining: 0,
478 interval_pos: 0,
479 interval_small: 0,
480 interval_leftovers: 0,
481 interval_chunk_index: 0,
482 interval_chunk_count: 0,
483 }
484 }
485
486 fn get_value(&self, i: usize) -> i64 {
487 match &self.range {
488 job::TaskParamRange::List(v) => v[i],
489 job::TaskParamRange::RangeExpr(r) => {
491 r.get(i as i64).expect("index within range bounds")
492 }
493 }
494 }
495
496 fn find_interval_end(&self, start: usize) -> usize {
500 match &self.range {
501 job::TaskParamRange::List(v) => {
502 let mut end = start;
503 while end + 1 < v.len() && v[end + 1] == v[end] + 1 {
504 end += 1;
505 }
506 end
507 }
508 job::TaskParamRange::RangeExpr(r) => {
509 let cumulative = r.cumulative_lengths();
512 let sub_ranges = r.ranges();
513
514 let sr_idx = cumulative.partition_point(|&c| c <= start);
516 let sr_offset = if sr_idx == 0 {
517 0
518 } else {
519 cumulative[sr_idx - 1]
520 };
521
522 let sr = &sub_ranges[sr_idx];
523
524 if sr.step != 1 {
525 return start;
527 }
528
529 let mut end = sr_offset + sr.len() - 1;
531
532 let mut last_val = sr.end;
534 for next_sr in &sub_ranges[sr_idx + 1..] {
535 if next_sr.start == last_val + 1 && next_sr.step == 1 {
536 end += next_sr.len();
537 last_val = next_sr.end;
538 } else if next_sr.start == last_val + 1 && next_sr.step > 1 {
539 end += 1;
541 break;
542 } else {
543 break;
544 }
545 }
546 end
547 }
548 }
549 }
550
551 fn start_next_interval(&mut self) -> bool {
553 if self.cursor >= self.total_len {
554 return false;
555 }
556 let first = self.get_value(self.cursor);
557
558 let end_idx = self.find_interval_end(self.cursor);
560 let last = self.get_value(end_idx);
561 let interval_len = (last - first + 1) as usize;
562 self.cursor = end_idx + 1;
563
564 let chunk_count = interval_len.div_ceil(self.default_task_count);
566 let (small, leftovers) = if chunk_count >= interval_len {
567 (1, 0)
568 } else if chunk_count <= 1 {
569 (interval_len, 0)
570 } else {
571 (interval_len / chunk_count, interval_len % chunk_count)
572 };
573
574 self.interval_start_val = first;
575 self.interval_pos = first;
576 self.interval_chunks_remaining = chunk_count;
577 self.interval_small = small;
578 self.interval_leftovers = leftovers;
579 self.interval_chunk_index = 0;
580 self.interval_chunk_count = chunk_count;
581 true
582 }
583
584 fn next_chunk(&mut self) -> Option<RangeExpr> {
585 while self.interval_chunks_remaining == 0 {
587 if !self.start_next_interval() {
588 return None;
589 }
590 }
591
592 let mut size = self.interval_small;
595 if self.interval_leftovers > 0
596 && (self.interval_chunk_index * self.interval_chunk_count) / self.interval_leftovers
597 != ((self.interval_chunk_index + 1) * self.interval_chunk_count)
598 / self.interval_leftovers
599 {
600 }
603 if self.interval_leftovers > 0 {
608 let idx = self.interval_chunk_index;
609 let cc = self.interval_chunk_count;
610 let lo = self.interval_leftovers;
611 let i_start = (idx * lo).div_ceil(cc);
617 let i_end = ((idx + 1) * lo).div_ceil(cc);
618 if i_start < i_end && i_start < lo {
619 size += 1;
620 }
621 }
622
623 let start = self.interval_pos;
624 let end = start + size as i64 - 1;
625 self.interval_pos = end + 1;
626 self.interval_chunks_remaining -= 1;
627 self.interval_chunk_index += 1;
628
629 let s = format!("{start}-{end}");
630 Some(
631 s.parse::<RangeExpr>()
632 .expect("valid range")
633 .with_contiguous(true),
634 )
635 }
636}
637
638impl Iterator for ContiguousChunkIterState {
639 type Item = RangeExpr;
640 fn next(&mut self) -> Option<RangeExpr> {
641 self.next_chunk()
642 }
643}
644
645struct ContiguousChunkNodeIterator {
647 state: ContiguousChunkIterState,
648 name: String,
649}
650
651impl NodeIterator for ContiguousChunkNodeIterator {
652 fn next(&mut self, result: &mut TaskParameterSet) -> bool {
653 match self.state.next_chunk() {
654 Some(expr) => {
655 result.insert(
656 self.name.clone(),
657 TaskParameterValue {
658 param_type: TaskParameterType::ChunkInt,
659 value: ExprValue::RangeExpr(expr),
660 },
661 );
662 true
663 }
664 None => false,
665 }
666 }
667 fn reset(&mut self) {
668 self.state.cursor = 0;
669 self.state.interval_chunks_remaining = 0;
670 }
671}
672
673struct ZeroDimSpaceNode;
675
676impl Node for ZeroDimSpaceNode {
677 fn len(&self) -> usize {
678 1
679 }
680 fn get(&self, _index: usize, _result: &mut TaskParameterSet) {}
681 fn validate_containment(&self, _params: &TaskParameterSet) -> Result<(), String> {
682 Ok(())
683 }
684 fn iter(&self) -> Box<dyn NodeIterator> {
685 Box::new(IndexedNodeIterator { len: 1, index: 0 })
686 }
687}
688
689struct RangeListNode {
691 name: String,
692 param_type: TaskParameterType,
693 values: Vec<ExprValue>,
694}
695
696impl Node for RangeListNode {
697 fn len(&self) -> usize {
698 self.values.len()
699 }
700 fn get(&self, index: usize, result: &mut TaskParameterSet) {
701 result.insert(
702 self.name.clone(),
703 TaskParameterValue {
704 param_type: self.param_type,
705 value: self.values[index].clone(),
706 },
707 );
708 }
709 fn validate_containment(&self, params: &TaskParameterSet) -> Result<(), String> {
710 let v = params.get(&self.name).ok_or_else(|| {
711 format!(
712 "Parameter '{}' not found in the provided parameters.",
713 self.name
714 )
715 })?;
716 if self.param_type == TaskParameterType::ChunkInt {
717 match &v.value {
719 ExprValue::RangeExpr(r) => {
720 for val in r.iter() {
721 if !self
722 .values
723 .iter()
724 .any(|ev| matches!(ev, ExprValue::Int(i) if *i == val))
725 {
726 return Err(format!(
727 "Parameter '{}' value '{}' is not a subset of the range in the parameter space.",
728 self.name, r
729 ));
730 }
731 }
732 Ok(())
733 }
734 _ => Err(format!(
735 "Parameter '{}' value '{}' is not in the parameter space range.",
736 self.name,
737 v.value.to_display_string()
738 )),
739 }
740 } else if !self.values.iter().any(|ev| expr_value_eq(ev, &v.value)) {
741 Err(format!(
742 "Parameter '{}' value '{}' is not in the parameter space range.",
743 self.name,
744 v.value.to_display_string()
745 ))
746 } else {
747 Ok(())
748 }
749 }
750 fn iter(&self) -> Box<dyn NodeIterator> {
751 Box::new(RangeListIterator {
752 name: self.name.clone(),
753 param_type: self.param_type,
754 values: self.values.clone(),
755 index: 0,
756 })
757 }
758}
759
760struct RangeExprNode {
762 name: String,
763 range: RangeExpr,
764}
765
766impl Node for RangeExprNode {
767 fn len(&self) -> usize {
768 self.range.len()
769 }
770 fn get(&self, index: usize, result: &mut TaskParameterSet) {
771 let val = self
772 .range
773 .get(index as i64)
774 .expect("caller must pass index < self.range.len()");
775 result.insert(
776 self.name.clone(),
777 TaskParameterValue {
778 param_type: TaskParameterType::Int,
779 value: ExprValue::Int(val),
780 },
781 );
782 }
783 fn validate_containment(&self, params: &TaskParameterSet) -> Result<(), String> {
784 let v = params.get(&self.name).ok_or_else(|| {
785 format!(
786 "Parameter '{}' not found in the provided parameters.",
787 self.name
788 )
789 })?;
790 match &v.value {
791 ExprValue::Int(i) => {
792 if self.range.contains(*i) {
793 Ok(())
794 } else {
795 Err(format!(
796 "Parameter '{}' value '{}' is not in the parameter space range.",
797 self.name, i
798 ))
799 }
800 }
801 _ => Err(format!(
802 "Parameter '{}' value '{}' is not in the parameter space range.",
803 self.name,
804 v.value.to_display_string()
805 )),
806 }
807 }
808 fn iter(&self) -> Box<dyn NodeIterator> {
809 Box::new(RangeExprIterator {
810 name: self.name.clone(),
811 range: self.range.clone(),
812 index: 0,
813 })
814 }
815}
816
817struct StaticChunkNode {
819 name: String,
820 range: job::TaskParamRange<i64>,
821 constraint: RangeConstraint,
822 num_chunks: usize,
823 small: usize, leftovers: usize, }
826
827impl StaticChunkNode {
828 fn chunk_range_expr(&self, i: usize) -> RangeExpr {
830 build_chunk_range_expr(&self.range, &self.constraint, self.small, self.leftovers, i)
831 }
832}
833
834impl Node for StaticChunkNode {
835 fn len(&self) -> usize {
836 self.num_chunks
837 }
838 fn get(&self, index: usize, result: &mut TaskParameterSet) {
839 result.insert(
840 self.name.clone(),
841 TaskParameterValue {
842 param_type: TaskParameterType::ChunkInt,
843 value: ExprValue::RangeExpr(self.chunk_range_expr(index)),
844 },
845 );
846 }
847 fn validate_containment(&self, params: &TaskParameterSet) -> Result<(), String> {
848 let v = params.get(&self.name).ok_or_else(|| {
849 format!(
850 "Parameter '{}' not found in the provided parameters.",
851 self.name
852 )
853 })?;
854 match &v.value {
855 ExprValue::RangeExpr(r) => {
856 if (0..self.num_chunks).any(|i| self.chunk_range_expr(i) == *r) {
857 Ok(())
858 } else {
859 Err(format!(
860 "Parameter '{}' value '{}' is not a valid chunk in the parameter space.",
861 self.name, r
862 ))
863 }
864 }
865 _ => Err(format!(
866 "Parameter '{}' value '{}' is not in the parameter space range.",
867 self.name,
868 v.value.to_display_string()
869 )),
870 }
871 }
872 fn iter(&self) -> Box<dyn NodeIterator> {
873 Box::new(StaticChunkIterator {
874 name: self.name.clone(),
875 range: self.range.clone(),
876 constraint: self.constraint.clone(),
877 num_chunks: self.num_chunks,
878 small: self.small,
879 leftovers: self.leftovers,
880 index: 0,
881 })
882 }
883}
884
885struct ProductNode {
887 children: Vec<Box<dyn Node>>,
888 length: usize,
889}
890
891impl Node for ProductNode {
892 fn len(&self) -> usize {
893 self.length
894 }
895 fn get(&self, mut index: usize, result: &mut TaskParameterSet) {
896 for child in self.children.iter().rev() {
897 let child_len = child.len();
898 child.get(index % child_len, result);
899 index /= child_len;
900 }
901 }
902 fn validate_containment(&self, params: &TaskParameterSet) -> Result<(), String> {
903 for child in &self.children {
904 child.validate_containment(params)?;
905 }
906 Ok(())
907 }
908 fn iter(&self) -> Box<dyn NodeIterator> {
909 Box::new(ProductIterator::new(&self.children))
910 }
911}
912
913struct ProductIterator {
917 children: Vec<ChildIterator>,
918 started: bool,
919}
920
921struct ChildIterator {
922 iter: Box<dyn NodeIterator>,
923 current: TaskParameterSet,
924}
925
926impl ProductIterator {
927 fn new(children: &[Box<dyn Node>]) -> Self {
928 let children = children
929 .iter()
930 .map(|child| ChildIterator {
931 iter: child.iter(),
932 current: TaskParameterSet::new(),
933 })
934 .collect();
935 Self {
936 children,
937 started: false,
938 }
939 }
940
941 fn initialize(&mut self) -> bool {
943 for child in &mut self.children {
944 if !child.iter.next(&mut child.current) {
945 return false;
946 }
947 }
948 true
949 }
950}
951
952impl NodeIterator for ProductIterator {
953 fn next(&mut self, result: &mut TaskParameterSet) -> bool {
954 if !self.started {
955 self.started = true;
956 if !self.initialize() {
957 return false;
958 }
959 } else {
960 let mut carry = true;
962 for child in self.children.iter_mut().rev() {
963 if !carry {
964 break;
965 }
966 child.current.clear();
967 if child.iter.next(&mut child.current) {
968 carry = false;
969 } else {
970 child.iter.reset();
972 if !child.iter.next(&mut child.current) {
973 return false;
974 }
975 }
976 }
977 if carry {
978 return false;
979 }
980 }
981 for child in &self.children {
982 result.extend(child.current.iter().map(|(k, v)| (k.clone(), v.clone())));
983 }
984 true
985 }
986 fn reset(&mut self) {
987 self.started = false;
988 for child in &mut self.children {
989 child.iter.reset();
990 child.current.clear();
991 }
992 }
993}
994
995struct AssociationNode {
997 children: Vec<Box<dyn Node>>,
998 length: usize,
999}
1000
1001impl Node for AssociationNode {
1002 fn len(&self) -> usize {
1003 self.length
1004 }
1005 fn get(&self, index: usize, result: &mut TaskParameterSet) {
1006 for child in &self.children {
1007 child.get(index, result);
1008 }
1009 }
1010 fn validate_containment(&self, params: &TaskParameterSet) -> Result<(), String> {
1011 for i in 0..self.length {
1013 let mut candidate = TaskParameterSet::new();
1014 for child in &self.children {
1015 child.get(i, &mut candidate);
1016 }
1017 if params_equal(&candidate, params) {
1018 return Ok(());
1019 }
1020 }
1021 let values: Vec<String> = params
1023 .iter()
1024 .filter(|(k, _)| {
1025 self.children.iter().any(|c| {
1026 let mut ps = TaskParameterSet::new();
1027 c.get(0, &mut ps);
1028 ps.contains_key(*k)
1029 })
1030 })
1031 .map(|(k, v)| format!("{}={}", k, v.value.to_display_string()))
1032 .collect();
1033 Err(format!(
1034 "The values {{{}}}, of an association expression in the combination expression, do not appear in the parameter space.",
1035 values.join(", ")
1036 ))
1037 }
1038 fn iter(&self) -> Box<dyn NodeIterator> {
1039 Box::new(AssociationIterator::new(&self.children))
1040 }
1041}
1042
1043struct AssociationIterator {
1045 children: Vec<ChildIterator>,
1046}
1047
1048impl AssociationIterator {
1049 fn new(children: &[Box<dyn Node>]) -> Self {
1050 let children = children
1051 .iter()
1052 .map(|child| ChildIterator {
1053 iter: child.iter(),
1054 current: TaskParameterSet::new(),
1055 })
1056 .collect();
1057 Self { children }
1058 }
1059}
1060
1061impl NodeIterator for AssociationIterator {
1062 fn next(&mut self, result: &mut TaskParameterSet) -> bool {
1063 for child in &mut self.children {
1064 child.current.clear();
1065 if !child.iter.next(&mut child.current) {
1066 return false;
1067 }
1068 result.extend(child.current.iter().map(|(k, v)| (k.clone(), v.clone())));
1069 }
1070 true
1071 }
1072 fn reset(&mut self) {
1073 for child in &mut self.children {
1074 child.iter.reset();
1075 child.current.clear();
1076 }
1077 }
1078}
1079
1080struct AdaptiveChunkNode {
1082 name: String,
1083 values: Vec<i64>,
1084 default_task_count: Arc<AtomicUsize>,
1085 range_constraint: RangeConstraint,
1086}
1087
1088impl Node for AdaptiveChunkNode {
1089 fn len(&self) -> usize {
1090 let dtc = self.default_task_count.load(Ordering::Relaxed).max(1);
1093 self.values.len().div_ceil(dtc)
1094 }
1095 fn get(&self, _index: usize, _result: &mut TaskParameterSet) {
1096 }
1098 fn validate_containment(&self, params: &TaskParameterSet) -> Result<(), String> {
1099 let v = params.get(&self.name).ok_or_else(|| {
1100 format!(
1101 "Parameter '{}' not found in the provided parameters.",
1102 self.name
1103 )
1104 })?;
1105 match &v.value {
1106 ExprValue::RangeExpr(r) => {
1107 let valid: HashSet<i64> = self.values.iter().copied().collect();
1108 for val in r.iter() {
1109 if !valid.contains(&val) {
1110 return Err(format!(
1111 "Parameter '{}' value '{}' is not a subset of the range in the parameter space.",
1112 self.name, r
1113 ));
1114 }
1115 }
1116 Ok(())
1117 }
1118 _ => Err(format!(
1119 "Parameter '{}' value '{}' is not in the parameter space range.",
1120 self.name,
1121 v.value.to_display_string()
1122 )),
1123 }
1124 }
1125 fn iter(&self) -> Box<dyn NodeIterator> {
1126 Box::new(AdaptiveChunkIterator {
1127 name: self.name.clone(),
1128 values: self.values.clone(),
1129 default_task_count: self.default_task_count.clone(),
1130 range_constraint: self.range_constraint.clone(),
1131 cursor: 0,
1132 })
1133 }
1134}
1135
1136struct AdaptiveChunkIterator {
1138 name: String,
1139 values: Vec<i64>,
1140 default_task_count: Arc<AtomicUsize>,
1141 range_constraint: RangeConstraint,
1142 cursor: usize,
1143}
1144
1145impl AdaptiveChunkIterator {
1146 fn make_chunk(&self, slice: &[i64]) -> RangeExpr {
1147 let range_str = match self.range_constraint {
1148 RangeConstraint::Contiguous => {
1149 if slice.len() == 1 {
1150 slice[0].to_string()
1151 } else {
1152 format!("{}-{}", slice[0], slice[slice.len() - 1])
1153 }
1154 }
1155 RangeConstraint::Noncontiguous => compress_range_expr(slice),
1156 };
1157 let expr = range_str
1158 .parse::<RangeExpr>()
1159 .expect("range string built from valid integers");
1160 match self.range_constraint {
1161 RangeConstraint::Contiguous => expr.with_contiguous(true),
1162 RangeConstraint::Noncontiguous => expr,
1163 }
1164 }
1165}
1166
1167impl NodeIterator for AdaptiveChunkIterator {
1168 fn next(&mut self, result: &mut TaskParameterSet) -> bool {
1169 if self.cursor >= self.values.len() {
1170 return false;
1171 }
1172 let chunk_size = self.default_task_count.load(Ordering::Relaxed).max(1);
1173 let chunk = match self.range_constraint {
1174 RangeConstraint::Contiguous => {
1175 let start = self.cursor;
1176 let mut end = start + 1;
1177 while end < self.values.len()
1178 && end - start < chunk_size
1179 && self.values[end] == self.values[end - 1] + 1
1180 {
1181 end += 1;
1182 }
1183 let slice = &self.values[start..end];
1184 self.cursor = end;
1185 self.make_chunk(slice)
1186 }
1187 RangeConstraint::Noncontiguous => {
1188 let end = (self.cursor + chunk_size).min(self.values.len());
1189 let slice = &self.values[self.cursor..end];
1190 self.cursor = end;
1191 self.make_chunk(slice)
1192 }
1193 };
1194 result.insert(
1195 self.name.clone(),
1196 TaskParameterValue {
1197 param_type: TaskParameterType::ChunkInt,
1198 value: ExprValue::RangeExpr(chunk),
1199 },
1200 );
1201 true
1202 }
1203 fn reset(&mut self) {
1204 self.cursor = 0;
1205 }
1206}
1207
1208pub struct StepParameterSpaceIterator {
1212 root: Box<dyn Node>,
1213 names: HashSet<String>,
1214 current_index: usize,
1215 adaptive: bool,
1216 adaptive_chunk_size: Option<Arc<AtomicUsize>>,
1217 node_iter: Option<Box<dyn NodeIterator>>,
1218 chunks_param_name: Option<String>,
1219 sequential: bool,
1221}
1222
1223impl StepParameterSpaceIterator {
1224 pub fn new(space: &job::StepParameterSpace) -> Result<Self, ModelError> {
1226 Self::new_inner(space, None)
1227 }
1228
1229 pub fn new_with_chunk_override(
1232 space: &job::StepParameterSpace,
1233 override_count: Option<usize>,
1234 ) -> Result<Self, ModelError> {
1235 Self::new_inner(space, override_count)
1236 }
1237
1238 fn new_inner(
1239 space: &job::StepParameterSpace,
1240 chunk_override: Option<usize>,
1241 ) -> Result<Self, ModelError> {
1242 let names: HashSet<String> = space.task_parameter_definitions.keys().cloned().collect();
1243
1244 if space.task_parameter_definitions.is_empty() {
1245 return Ok(Self {
1246 root: Box::new(ZeroDimSpaceNode),
1247 names,
1248 current_index: 0,
1249 adaptive: false,
1250 adaptive_chunk_size: None,
1251 node_iter: None,
1252 chunks_param_name: None,
1253 sequential: false,
1254 });
1255 }
1256
1257 let expr = space.combination.as_deref().unwrap_or("*");
1258
1259 let mut adaptive_info: Option<(String, Arc<AtomicUsize>)> = None;
1261 if chunk_override.is_none() {
1262 for (name, param) in &space.task_parameter_definitions {
1263 if let job::TaskParameter::ChunkInt { chunks, .. } = param {
1264 if chunks.target_runtime_seconds.is_some_and(|t| t > 0) {
1265 let arc = Arc::new(AtomicUsize::new(chunks.default_task_count.max(1)));
1266 adaptive_info = Some((name.clone(), arc));
1267 break;
1268 }
1269 }
1270 }
1271 }
1272
1273 let root = if expr.trim() == "*" {
1274 let mut children: Vec<Box<dyn Node>> = Vec::new();
1276 let mut adaptive_idx = None;
1277 for (i, name) in space.task_parameter_definitions.keys().enumerate() {
1278 if adaptive_info.as_ref().is_some_and(|(n, _)| n == name) {
1279 adaptive_idx = Some(i);
1280 }
1281 children.push(make_leaf_node(name, space, &adaptive_info, chunk_override)?);
1282 }
1283 if let Some(idx) = adaptive_idx {
1285 let child = children.remove(idx);
1286 children.push(child);
1287 }
1288 if children.len() == 1 {
1289 children
1292 .into_iter()
1293 .next()
1294 .expect("non-empty vec with len 1")
1295 } else {
1296 let length = checked_product_len(&children)?;
1297 Box::new(ProductNode { children, length })
1298 }
1299 } else {
1300 let tokens = tokenize(expr);
1301 parse_node_expr(&tokens, space, &adaptive_info, chunk_override)?
1302 };
1303
1304 let adaptive = adaptive_info.is_some();
1305 let chunks_param_name = adaptive_info.as_ref().map(|(n, _)| n.clone());
1306 let adaptive_chunk_size = adaptive_info.map(|(_, rc)| rc);
1307
1308 let needs_sequential = adaptive || has_contiguous_chunks(space);
1311 let node_iter = if needs_sequential {
1312 Some(root.iter())
1313 } else {
1314 None
1315 };
1316
1317 Ok(Self {
1318 root,
1319 names,
1320 current_index: 0,
1321 adaptive,
1322 adaptive_chunk_size,
1323 node_iter,
1324 chunks_param_name,
1325 sequential: needs_sequential,
1326 })
1327 }
1328
1329 pub fn names(&self) -> &HashSet<String> {
1330 &self.names
1331 }
1332
1333 pub fn len(&self) -> usize {
1334 if self.adaptive {
1335 0
1336 } else {
1337 self.root.len()
1338 }
1339 }
1340
1341 pub fn is_empty(&self) -> bool {
1342 if self.adaptive {
1343 false
1344 } else {
1345 self.root.len() == 0
1346 }
1347 }
1348
1349 pub fn get(&self, index: usize) -> Option<TaskParameterSet> {
1352 if self.sequential {
1353 return None;
1354 }
1355 if index >= self.root.len() {
1356 return None;
1357 }
1358 let mut result = TaskParameterSet::new();
1359 self.root.get(index, &mut result);
1360 Some(result)
1361 }
1362
1363 pub fn contains(&self, params: &TaskParameterSet) -> bool {
1365 self.validate_containment(params).is_ok()
1366 }
1367
1368 pub fn validate_containment(&self, params: &TaskParameterSet) -> Result<(), String> {
1371 let mut params_keys: Vec<&str> = params.keys().map(|s| s.as_str()).collect();
1372 let mut space_keys: Vec<&str> = self.names.iter().map(|s| s.as_str()).collect();
1373 params_keys.sort();
1374 space_keys.sort();
1375 if params_keys != space_keys {
1376 return Err(format!(
1377 "Task parameter names {:?} do not match the parameter space names {:?}.",
1378 params_keys, space_keys
1379 ));
1380 }
1381 self.root.validate_containment(params)
1382 }
1383
1384 pub fn chunks_adaptive(&self) -> bool {
1386 self.adaptive
1387 }
1388
1389 pub fn chunks_parameter_name(&self) -> Option<&str> {
1391 self.chunks_param_name.as_deref()
1392 }
1393
1394 pub fn chunks_default_task_count(&self) -> Option<usize> {
1396 self.adaptive_chunk_size
1397 .as_ref()
1398 .map(|a| a.load(Ordering::Relaxed))
1399 }
1400
1401 pub fn set_chunks_default_task_count(&mut self, value: usize) {
1403 if let Some(ref a) = self.adaptive_chunk_size {
1404 a.store(value, Ordering::Relaxed);
1405 }
1407 }
1408
1409 pub fn reset(&mut self) {
1419 self.current_index = 0;
1420 if let Some(iter) = self.node_iter.as_mut() {
1421 iter.reset();
1422 }
1423 }
1424}
1425
1426fn params_equal(a: &TaskParameterSet, b: &TaskParameterSet) -> bool {
1427 if a.len() != b.len() {
1428 return false;
1429 }
1430 a.iter().all(|(k, v)| {
1431 b.get(k)
1432 .is_some_and(|bv| expr_value_eq(&v.value, &bv.value))
1433 })
1434}
1435
1436fn expr_value_eq(a: &ExprValue, b: &ExprValue) -> bool {
1437 match (a, b) {
1438 (ExprValue::Int(x), ExprValue::Int(y)) => x == y,
1439 (ExprValue::Float(x), ExprValue::Float(y)) => x.value() == y.value(),
1440 (ExprValue::String(x), ExprValue::String(y)) => x == y,
1441 (ExprValue::RangeExpr(x), ExprValue::RangeExpr(y)) => x == y,
1442 (ExprValue::Path { value: x, .. }, ExprValue::Path { value: y, .. }) => x == y,
1443 (ExprValue::String(x), ExprValue::Path { value: y, .. }) => x == y,
1444 (ExprValue::Path { value: x, .. }, ExprValue::String(y)) => x == y,
1445 _ => false,
1446 }
1447}
1448
1449impl Iterator for StepParameterSpaceIterator {
1450 type Item = TaskParameterSet;
1451 fn next(&mut self) -> Option<TaskParameterSet> {
1452 if self.sequential {
1453 let iter = self.node_iter.as_mut()?;
1454 let mut result = TaskParameterSet::new();
1455 if iter.next(&mut result) {
1456 Some(result)
1457 } else {
1458 None
1459 }
1460 } else {
1461 let item = self.get(self.current_index)?;
1462 self.current_index += 1;
1463 Some(item)
1464 }
1465 }
1466
1467 fn size_hint(&self) -> (usize, Option<usize>) {
1468 if self.adaptive {
1469 (0, None)
1470 } else {
1471 let remaining = self.root.len().saturating_sub(self.current_index);
1472 (remaining, Some(remaining))
1473 }
1474 }
1475}
1476
1477fn parse_node_expr(
1480 tokens: &[String],
1481 space: &job::StepParameterSpace,
1482 adaptive_info: &Option<(String, Arc<AtomicUsize>)>,
1483 chunk_override: Option<usize>,
1484) -> Result<Box<dyn Node>, ModelError> {
1485 let mut pos = 0;
1486 let result = parse_node_product(tokens, &mut pos, space, adaptive_info, chunk_override)?;
1487 if pos < tokens.len() {
1488 return Err(ModelError::DecodeValidation(format!(
1489 "Unexpected token '{}' in combination expression",
1490 tokens[pos]
1491 )));
1492 }
1493 Ok(result)
1494}
1495
1496fn parse_node_product(
1497 tokens: &[String],
1498 pos: &mut usize,
1499 space: &job::StepParameterSpace,
1500 adaptive_info: &Option<(String, Arc<AtomicUsize>)>,
1501 chunk_override: Option<usize>,
1502) -> Result<Box<dyn Node>, ModelError> {
1503 let mut children = vec![parse_node_element(
1504 tokens,
1505 pos,
1506 space,
1507 adaptive_info,
1508 chunk_override,
1509 )?];
1510 while *pos < tokens.len() && tokens[*pos] == "*" {
1511 *pos += 1;
1512 children.push(parse_node_element(
1513 tokens,
1514 pos,
1515 space,
1516 adaptive_info,
1517 chunk_override,
1518 )?);
1519 }
1520 if children.len() == 1 {
1521 Ok(children
1524 .into_iter()
1525 .next()
1526 .expect("non-empty vec with len 1"))
1527 } else {
1528 let length = checked_product_len(&children)?;
1529 Ok(Box::new(ProductNode { children, length }))
1530 }
1531}
1532
1533fn parse_node_element(
1534 tokens: &[String],
1535 pos: &mut usize,
1536 space: &job::StepParameterSpace,
1537 adaptive_info: &Option<(String, Arc<AtomicUsize>)>,
1538 chunk_override: Option<usize>,
1539) -> Result<Box<dyn Node>, ModelError> {
1540 if *pos >= tokens.len() {
1541 return Err(ModelError::DecodeValidation(
1542 "Unexpected end of combination expression".into(),
1543 ));
1544 }
1545 if tokens[*pos] == "(" {
1546 *pos += 1;
1547 let mut children = vec![parse_node_product(
1548 tokens,
1549 pos,
1550 space,
1551 adaptive_info,
1552 chunk_override,
1553 )?];
1554 while *pos < tokens.len() && tokens[*pos] == "," {
1555 *pos += 1;
1556 children.push(parse_node_product(
1557 tokens,
1558 pos,
1559 space,
1560 adaptive_info,
1561 chunk_override,
1562 )?);
1563 }
1564 if *pos >= tokens.len() || tokens[*pos] != ")" {
1565 return Err(ModelError::DecodeValidation(
1566 "Missing closing parenthesis in combination".into(),
1567 ));
1568 }
1569 *pos += 1;
1570 let length = children[0].len();
1571 for child in children.iter().skip(1) {
1572 if child.len() != length {
1573 return Err(ModelError::DecodeValidation(format!(
1574 "Associative combination: all members must have the same number of values, got {} and {}",
1575 length, child.len()
1576 )));
1577 }
1578 }
1579 if children.len() == 1 {
1580 Err(ModelError::DecodeValidation(
1581 "Association expression must have more than one term.".into(),
1582 ))
1583 } else {
1584 Ok(Box::new(AssociationNode { children, length }))
1585 }
1586 } else {
1587 let name = &tokens[*pos];
1588 *pos += 1;
1589 make_leaf_node(name, space, adaptive_info, chunk_override)
1590 }
1591}
1592
1593fn make_leaf_node(
1595 name: &str,
1596 space: &job::StepParameterSpace,
1597 adaptive_info: &Option<(String, Arc<AtomicUsize>)>,
1598 chunk_override: Option<usize>,
1599) -> Result<Box<dyn Node>, ModelError> {
1600 let param = space.task_parameter_definitions.get(name).ok_or_else(|| {
1601 ModelError::DecodeValidation(format!(
1602 "Unknown parameter '{name}' in combination expression"
1603 ))
1604 })?;
1605
1606 match param {
1607 job::TaskParameter::Int { range, chunks } => {
1608 if let Some(chunk_cfg) = chunks {
1609 return make_chunk_node(name, range, chunk_cfg, adaptive_info, chunk_override);
1610 }
1611 match range {
1612 job::TaskParamRange::List(v) => Ok(Box::new(RangeListNode {
1613 name: name.to_string(),
1614 param_type: TaskParameterType::Int,
1615 values: v.iter().map(|&i| ExprValue::Int(i)).collect(),
1616 })),
1617 job::TaskParamRange::RangeExpr(r) => Ok(Box::new(RangeExprNode {
1618 name: name.to_string(),
1619 range: r.clone(),
1620 })),
1621 }
1622 }
1623 job::TaskParameter::Float { range } => Ok(Box::new(RangeListNode {
1624 name: name.to_string(),
1625 param_type: TaskParameterType::Float,
1626 values: range
1627 .iter()
1628 .map(|&f| {
1629 Float64::new(f).map(ExprValue::Float).map_err(|_| {
1630 ModelError::DecodeValidation(format!(
1631 "Parameter '{name}': float value {f} is not finite"
1632 ))
1633 })
1634 })
1635 .collect::<Result<Vec<_>, _>>()?,
1636 })),
1637 job::TaskParameter::String { range } => Ok(Box::new(RangeListNode {
1638 name: name.to_string(),
1639 param_type: TaskParameterType::String,
1640 values: range.iter().map(|s| ExprValue::String(s.clone())).collect(),
1641 })),
1642 job::TaskParameter::Path { range } => Ok(Box::new(RangeListNode {
1643 name: name.to_string(),
1644 param_type: TaskParameterType::Path,
1645 values: range.iter().map(|s| ExprValue::String(s.clone())).collect(),
1646 })),
1647 job::TaskParameter::ChunkInt { range, chunks } => {
1648 make_chunk_node(name, range, chunks, adaptive_info, chunk_override)
1649 }
1650 }
1651}
1652
1653fn has_contiguous_chunks(space: &job::StepParameterSpace) -> bool {
1655 space.task_parameter_definitions.values().any(|p| {
1656 matches!(
1657 p,
1658 job::TaskParameter::ChunkInt { chunks, .. }
1659 if chunks.range_constraint == RangeConstraint::Contiguous
1660 )
1661 })
1662}
1663
1664fn make_chunk_node(
1668 name: &str,
1669 range: &job::TaskParamRange<i64>,
1670 chunks: &job::ResolvedChunks,
1671 adaptive_info: &Option<(String, Arc<AtomicUsize>)>,
1672 chunk_override: Option<usize>,
1673) -> Result<Box<dyn Node>, ModelError> {
1674 if let Some((adaptive_name, rc)) = adaptive_info {
1676 if adaptive_name == name {
1677 let values: Vec<i64> = match range {
1678 job::TaskParamRange::List(v) => v.clone(),
1679 job::TaskParamRange::RangeExpr(r) => r.iter().collect(),
1680 };
1681 return Ok(Box::new(AdaptiveChunkNode {
1682 name: name.to_string(),
1683 values,
1684 default_task_count: rc.clone(),
1685 range_constraint: chunks.range_constraint.clone(),
1686 }));
1687 }
1688 }
1689
1690 let default_task_count = chunk_override.unwrap_or(chunks.default_task_count).max(1);
1692
1693 let total_len = match range {
1694 job::TaskParamRange::List(v) => v.len(),
1695 job::TaskParamRange::RangeExpr(r) => r.len(),
1696 };
1697 if total_len == 0 {
1698 return Ok(Box::new(RangeListNode {
1699 name: name.to_string(),
1700 param_type: TaskParameterType::ChunkInt,
1701 values: Vec::new(),
1702 }));
1703 }
1704
1705 if chunks.range_constraint == RangeConstraint::Contiguous {
1707 return Ok(Box::new(ContiguousChunkNode::new(
1708 name.to_string(),
1709 range.clone(),
1710 default_task_count,
1711 )));
1712 }
1713
1714 let chunk_count = total_len.div_ceil(default_task_count);
1715 let small = total_len / chunk_count;
1716 let leftovers = total_len % chunk_count;
1717
1718 Ok(Box::new(StaticChunkNode {
1719 name: name.to_string(),
1720 range: range.clone(),
1721 constraint: chunks.range_constraint.clone(),
1722 num_chunks: chunk_count,
1723 small,
1724 leftovers,
1725 }))
1726}
1727
1728#[cfg(test)]
1729mod tests {
1730 use super::*;
1731
1732 #[test]
1733 fn test_compress_range_expr() {
1734 assert_eq!(compress_range_expr(&[1, 2, 3]), "1-3");
1735 assert_eq!(compress_range_expr(&[1, 2, 3, 5, 7, 8, 9]), "1-3,5,7-9");
1736 assert_eq!(compress_range_expr(&[1]), "1");
1737 assert_eq!(compress_range_expr(&[1, 3]), "1,3");
1738 assert_eq!(compress_range_expr(&[]), "");
1739 }
1740
1741 #[test]
1742 fn test_tokenize() {
1743 assert_eq!(tokenize("A * B"), vec!["A", "*", "B"]);
1744 assert_eq!(
1745 tokenize("(A, B) * C"),
1746 vec!["(", "A", ",", "B", ")", "*", "C"]
1747 );
1748 assert_eq!(tokenize("A"), vec!["A"]);
1749 }
1750
1751 fn make_space(
1754 params: Vec<(&str, job::TaskParameter)>,
1755 combination: Option<&str>,
1756 ) -> job::StepParameterSpace {
1757 let mut defs = indexmap::IndexMap::new();
1758 for (name, param) in params {
1759 defs.insert(name.to_string(), param);
1760 }
1761 job::StepParameterSpace {
1762 task_parameter_definitions: defs,
1763 combination: combination.map(|s| s.to_string()),
1764 }
1765 }
1766
1767 fn int_param(values: Vec<i64>) -> job::TaskParameter {
1768 job::TaskParameter::Int {
1769 range: job::TaskParamRange::List(values),
1770 chunks: None,
1771 }
1772 }
1773
1774 fn adaptive_chunk_param(values: Vec<i64>, default_task_count: usize) -> job::TaskParameter {
1775 job::TaskParameter::ChunkInt {
1776 range: job::TaskParamRange::List(values),
1777 chunks: job::ResolvedChunks {
1778 default_task_count,
1779 target_runtime_seconds: Some(60), range_constraint: RangeConstraint::Noncontiguous,
1781 },
1782 }
1783 }
1784
1785 fn range_expr_param(expr: &str) -> job::TaskParameter {
1786 job::TaskParameter::Int {
1787 range: job::TaskParamRange::RangeExpr(expr.parse::<RangeExpr>().unwrap()),
1788 chunks: None,
1789 }
1790 }
1791
1792 fn static_chunk_param(expr: &str, default_task_count: usize) -> job::TaskParameter {
1793 job::TaskParameter::ChunkInt {
1794 range: job::TaskParamRange::RangeExpr(expr.parse::<RangeExpr>().unwrap()),
1795 chunks: job::ResolvedChunks {
1796 default_task_count,
1797 target_runtime_seconds: None,
1798 range_constraint: RangeConstraint::Contiguous,
1799 },
1800 }
1801 }
1802
1803 const HUGE_RANGE: &str = "1-100000000000";
1808
1809 #[test]
1810 fn test_lazy_construction_range_expr() {
1811 let space = make_space(vec![("X", range_expr_param(HUGE_RANGE))], None);
1812 let iter = StepParameterSpaceIterator::new(&space).unwrap();
1813 assert_eq!(iter.len(), 100_000_000_000);
1814 }
1815
1816 #[test]
1817 fn test_lazy_random_access_range_expr() {
1818 let space = make_space(vec![("X", range_expr_param(HUGE_RANGE))], None);
1819 let iter = StepParameterSpaceIterator::new(&space).unwrap();
1820 let first = iter.get(0).unwrap();
1821 assert_eq!(first["X"].value, ExprValue::Int(1));
1822 let last = iter.get(99_999_999_999).unwrap();
1823 assert_eq!(last["X"].value, ExprValue::Int(100_000_000_000));
1824 }
1825
1826 #[test]
1827 fn test_lazy_product_with_huge_range() {
1828 let space = make_space(
1829 vec![
1830 ("A", int_param(vec![1, 2])),
1831 ("X", range_expr_param(HUGE_RANGE)),
1832 ],
1833 None,
1834 );
1835 let iter = StepParameterSpaceIterator::new(&space).unwrap();
1836 assert_eq!(iter.len(), 200_000_000_000);
1837 let mid = iter.get(50_000_000_000).unwrap();
1839 assert!(mid.contains_key("A"));
1840 assert!(mid.contains_key("X"));
1841 }
1842
1843 #[test]
1844 fn test_lazy_iterate_first_few_of_huge_range() {
1845 let space = make_space(vec![("X", range_expr_param(HUGE_RANGE))], None);
1846 let mut iter = StepParameterSpaceIterator::new(&space).unwrap();
1847 let first = iter.next().unwrap();
1848 assert_eq!(first["X"].value, ExprValue::Int(1));
1849 let second = iter.next().unwrap();
1850 assert_eq!(second["X"].value, ExprValue::Int(2));
1851 }
1852
1853 #[test]
1854 fn test_lazy_product_iterate_first_few() {
1855 let space = make_space(
1856 vec![
1857 ("A", int_param(vec![10, 20])),
1858 ("X", range_expr_param(HUGE_RANGE)),
1859 ],
1860 None,
1861 );
1862 let mut iter = StepParameterSpaceIterator::new(&space).unwrap();
1863 let first = iter.next().unwrap();
1865 assert!(first.contains_key("A"));
1866 assert!(first.contains_key("X"));
1867 for _ in 0..10 {
1869 assert!(iter.next().is_some());
1870 }
1871 }
1872
1873 #[test]
1874 fn test_lazy_static_chunk_with_huge_range() {
1875 let space = make_space(vec![("C", static_chunk_param(HUGE_RANGE, 1000))], None);
1877 let iter = StepParameterSpaceIterator::new(&space).unwrap();
1878 assert_eq!(iter.len(), 100_000_000);
1879 let first: Vec<_> = iter.take(3).collect();
1881 assert_eq!(first.len(), 3);
1882 assert!(first[0].contains_key("C"));
1883 }
1884
1885 #[test]
1886 fn test_lazy_iter_of_product_with_huge_range() {
1887 let space = make_space(
1889 vec![
1890 ("A", int_param(vec![1, 2])),
1891 ("X", range_expr_param(HUGE_RANGE)),
1892 ("Chunk", adaptive_chunk_param(vec![10, 20, 30, 40], 2)),
1893 ],
1894 None,
1895 );
1896 let iter = StepParameterSpaceIterator::new(&space).unwrap();
1897 assert!(iter.chunks_adaptive());
1898 let mut count = 0;
1900 for params in iter {
1901 assert!(params.contains_key("A"));
1902 assert!(params.contains_key("X"));
1903 assert!(params.contains_key("Chunk"));
1904 count += 1;
1905 if count >= 5 {
1906 break;
1907 }
1908 }
1909 assert_eq!(count, 5);
1910 }
1911
1912 #[test]
1915 fn test_len_returns_zero_for_adaptive_chunking() {
1916 let space = make_space(
1917 vec![("Chunk", adaptive_chunk_param(vec![1, 2, 3, 4, 5, 6], 2))],
1918 None,
1919 );
1920 let iter = StepParameterSpaceIterator::new(&space).unwrap();
1921 assert!(iter.chunks_adaptive());
1922 assert_eq!(iter.len(), 0);
1923 }
1924
1925 #[test]
1926 fn test_get_returns_none_for_adaptive_chunking() {
1927 let space = make_space(
1928 vec![("Chunk", adaptive_chunk_param(vec![1, 2, 3, 4, 5, 6], 2))],
1929 None,
1930 );
1931 let iter = StepParameterSpaceIterator::new(&space).unwrap();
1932 assert!(iter.chunks_adaptive());
1933 assert!(iter.get(0).is_none());
1934 }
1935
1936 #[test]
1937 fn test_adaptive_chunking_with_multiple_params_iterates() {
1938 let space = make_space(
1939 vec![
1940 ("Frame", int_param(vec![1, 2])),
1941 ("Chunk", adaptive_chunk_param(vec![10, 20, 30, 40], 2)),
1942 ],
1943 None,
1944 );
1945 let iter = StepParameterSpaceIterator::new(&space).unwrap();
1946 assert!(iter.chunks_adaptive());
1947 let mut count = 0;
1948 for params in iter {
1949 assert!(params.contains_key("Frame"));
1950 assert!(params.contains_key("Chunk"));
1951 count += 1;
1952 if count > 100 {
1953 break;
1954 }
1955 }
1956 assert_eq!(count, 4);
1957 }
1958
1959 #[test]
1960 fn test_adaptive_chunking_single_param_iterates() {
1961 let space = make_space(
1962 vec![("Chunk", adaptive_chunk_param(vec![1, 2, 3, 4, 5, 6], 3))],
1963 None,
1964 );
1965 let results: Vec<_> = StepParameterSpaceIterator::new(&space).unwrap().collect();
1966 assert_eq!(results.len(), 2);
1967 }
1968
1969 #[test]
1970 fn test_adaptive_with_association_iterates() {
1971 let space = make_space(
1972 vec![
1973 ("Frame", int_param(vec![1, 2])),
1974 ("Chunk", adaptive_chunk_param(vec![10, 20], 1)),
1975 ],
1976 Some("(Frame, Chunk)"),
1977 );
1978 let results: Vec<_> = StepParameterSpaceIterator::new(&space).unwrap().collect();
1979 assert_eq!(results.len(), 2);
1980 }
1981
1982 fn tpv(param_type: TaskParameterType, value: ExprValue) -> TaskParameterValue {
1985 TaskParameterValue { param_type, value }
1986 }
1987
1988 #[test]
1989 fn test_validate_containment_name_mismatch() {
1990 let space = make_space(vec![("Frame", int_param(vec![1, 2, 3]))], None);
1991 let iter = StepParameterSpaceIterator::new(&space).unwrap();
1992 let mut params = TaskParameterSet::new();
1993 params.insert(
1994 "Wrong".into(),
1995 tpv(TaskParameterType::Int, ExprValue::Int(1)),
1996 );
1997 let err = iter.validate_containment(¶ms).unwrap_err();
1998 assert!(err.contains("do not match"), "got: {err}");
1999 assert!(err.contains("Wrong"), "got: {err}");
2000 assert!(err.contains("Frame"), "got: {err}");
2001 }
2002
2003 #[test]
2004 fn test_validate_containment_value_not_in_range() {
2005 let space = make_space(vec![("Frame", int_param(vec![1, 2, 3]))], None);
2006 let iter = StepParameterSpaceIterator::new(&space).unwrap();
2007 let mut params = TaskParameterSet::new();
2008 params.insert(
2009 "Frame".into(),
2010 tpv(TaskParameterType::Int, ExprValue::Int(99)),
2011 );
2012 let err = iter.validate_containment(¶ms).unwrap_err();
2013 assert!(err.contains("Frame"), "got: {err}");
2014 assert!(err.contains("99"), "got: {err}");
2015 assert!(
2016 err.contains("not in the parameter space range"),
2017 "got: {err}"
2018 );
2019 }
2020
2021 #[test]
2022 fn test_validate_containment_range_expr_value_not_in_range() {
2023 let space = make_space(vec![("X", range_expr_param("1-10"))], None);
2024 let iter = StepParameterSpaceIterator::new(&space).unwrap();
2025 let mut params = TaskParameterSet::new();
2026 params.insert("X".into(), tpv(TaskParameterType::Int, ExprValue::Int(99)));
2027 let err = iter.validate_containment(¶ms).unwrap_err();
2028 assert!(err.contains("X"), "got: {err}");
2029 assert!(err.contains("99"), "got: {err}");
2030 assert!(
2031 err.contains("not in the parameter space range"),
2032 "got: {err}"
2033 );
2034 }
2035
2036 #[test]
2037 fn test_validate_containment_success() {
2038 let space = make_space(vec![("Frame", int_param(vec![1, 2, 3]))], None);
2039 let iter = StepParameterSpaceIterator::new(&space).unwrap();
2040 let mut params = TaskParameterSet::new();
2041 params.insert(
2042 "Frame".into(),
2043 tpv(TaskParameterType::Int, ExprValue::Int(2)),
2044 );
2045 assert!(iter.validate_containment(¶ms).is_ok());
2046 }
2047
2048 #[test]
2049 fn test_validate_containment_association_not_found() {
2050 let space = make_space(
2051 vec![("A", int_param(vec![1, 2])), ("B", int_param(vec![10, 20]))],
2052 Some("(A, B)"),
2053 );
2054 let iter = StepParameterSpaceIterator::new(&space).unwrap();
2055 let mut params = TaskParameterSet::new();
2057 params.insert("A".into(), tpv(TaskParameterType::Int, ExprValue::Int(1)));
2058 params.insert("B".into(), tpv(TaskParameterType::Int, ExprValue::Int(20)));
2059 let err = iter.validate_containment(¶ms).unwrap_err();
2060 assert!(err.contains("association"), "got: {err}");
2061 }
2062
2063 #[test]
2064 fn test_validate_containment_chunk_not_subset() {
2065 let space = make_space(vec![("C", static_chunk_param("1-10", 5))], None);
2066 let iter = StepParameterSpaceIterator::new(&space).unwrap();
2067 let mut params = TaskParameterSet::new();
2069 params.insert(
2070 "C".into(),
2071 tpv(
2072 TaskParameterType::ChunkInt,
2073 ExprValue::RangeExpr("1-99".parse::<RangeExpr>().unwrap()),
2074 ),
2075 );
2076 let err = iter.validate_containment(¶ms).unwrap_err();
2077 assert!(err.contains("C"), "got: {err}");
2078 assert!(err.contains("not"), "got: {err}");
2079 }
2080
2081 #[test]
2084 fn test_contiguous_chunk_stepped_range_iterates_without_panic() {
2085 let space = make_space(vec![("C", static_chunk_param("1-10:2", 2))], None);
2088 let iter = StepParameterSpaceIterator::new(&space).unwrap();
2089 let results: Vec<_> = iter.collect();
2090 assert!(!results.is_empty(), "should produce at least one chunk");
2091 for r in &results {
2092 assert!(r.contains_key("C"));
2093 }
2094 }
2095
2096 #[test]
2097 fn test_range_expr_random_access_does_not_panic() {
2098 let space = make_space(vec![("X", range_expr_param("1-5"))], None);
2100 let iter = StepParameterSpaceIterator::new(&space).unwrap();
2101 for i in 0..5 {
2102 let set = iter.get(i).unwrap();
2103 assert_eq!(set["X"].value, ExprValue::Int(i as i64 + 1));
2104 }
2105 }
2106}