1use oximo_expr::Expr;
2
3use crate::set::{FromIndexKey, Set};
4
5#[diagnostic::on_unimplemented(
13 message = "`{Self}` is not a valid index domain over key type `{K}`",
14 label = "the domain's keys are not `{K}`",
15 note = "the loop/closure binding type must match the domain's keys",
16 note = "for a `Set<T>` write `for x in set` (the key type is inferred) or annotate `for x: T in set`. Integer ranges yield `usize`/`i64`/`i32`. A slice/`Vec`/array yields its element type"
17)]
18pub trait SumDomain<K> {
19 fn keys(&self) -> impl Iterator<Item = K> + '_;
20}
21
22impl<K: FromIndexKey> SumDomain<K> for Set<K> {
27 fn keys(&self) -> impl Iterator<Item = K> + '_ {
28 self.iter().map(|k| K::from_index_key(&k))
29 }
30}
31
32impl<K: Copy> SumDomain<K> for [K] {
33 fn keys(&self) -> impl Iterator<Item = K> + '_ {
34 self.iter().copied()
35 }
36}
37
38impl<K: Copy> SumDomain<K> for Vec<K> {
39 fn keys(&self) -> impl Iterator<Item = K> + '_ {
40 self.iter().copied()
41 }
42}
43
44impl<K: Copy, const N: usize> SumDomain<K> for [K; N] {
45 fn keys(&self) -> impl Iterator<Item = K> + '_ {
46 self.iter().copied()
47 }
48}
49
50impl<K, D: SumDomain<K> + ?Sized> SumDomain<K> for &D {
53 fn keys(&self) -> impl Iterator<Item = K> + '_ {
54 (**self).keys()
55 }
56}
57
58impl SumDomain<usize> for std::ops::Range<usize> {
62 fn keys(&self) -> impl Iterator<Item = usize> + '_ {
63 self.clone()
64 }
65}
66
67impl SumDomain<i64> for std::ops::Range<i64> {
68 fn keys(&self) -> impl Iterator<Item = i64> + '_ {
69 self.clone()
70 }
71}
72
73impl SumDomain<i32> for std::ops::Range<i32> {
74 fn keys(&self) -> impl Iterator<Item = i32> + '_ {
75 self.clone()
76 }
77}
78
79#[doc(hidden)]
92pub fn __sum_over<'a, K, D, F>(domain: &D, f: F) -> Expr<'a>
93where
94 D: SumDomain<K> + ?Sized,
95 F: FnMut(K) -> Expr<'a>,
96{
97 let terms: Vec<Expr<'a>> = domain.keys().map(f).collect();
98 assert!(!terms.is_empty(), "sum_over on empty domain");
99 terms.into_iter().sum()
100}
101
102#[cfg(test)]
103mod tests {
104 use oximo_expr::extract_linear;
105
106 use super::*;
107 use crate::model::Model;
108
109 #[test]
110 fn sum_over_scalar_set() {
111 let m = Model::new("scalar");
112 let items = Set::range(0..4);
113 let x = m.__indexed_var("x", &items).lb(0.0).build();
114
115 let total = __sum_over(&items, |i: usize| x[i]);
116 let arena = m.arena();
117 let terms = extract_linear(&arena, total.id).expect("linear");
118 assert_eq!(terms.coeffs.len(), 4);
119 assert!(terms.coeffs.iter().all(|(_, c)| (c - 1.0).abs() < f64::EPSILON));
120 }
121
122 #[test]
123 fn sum_over_tuple_set() {
124 let m = Model::new("tuple");
125 let plants = Set::strings(["seattle", "san-diego"]);
126 let markets = Set::strings(["nyc", "chicago", "topeka"]);
127 let routes = &plants * &markets;
128 let x = m.__indexed_var("x", &routes).lb(0.0).build();
129
130 let total = __sum_over(&routes, |(p, q): (String, String)| x[(p, q)]);
131 let arena = m.arena();
132 let terms = extract_linear(&arena, total.id).expect("linear");
133 assert_eq!(terms.coeffs.len(), 6);
134 }
135
136 #[test]
137 fn nested_sum_over_double_sum() {
138 let m = Model::new("nested");
139 let plants = Set::strings(["a", "b"]);
140 let markets = Set::strings(["x", "y", "z"]);
141 let routes = &plants * &markets;
142 let x = m.__indexed_var("x", &routes).lb(0.0).build();
143
144 let total = __sum_over(&plants, |p: String| __sum_over(&markets, |q: String| x[(&p, q)]));
145 let arena = m.arena();
146 let terms = extract_linear(&arena, total.id).expect("linear");
147 assert_eq!(terms.coeffs.len(), 6);
148 }
149
150 #[test]
151 fn sum_over_passes_typed_usize_key() {
152 let m = Model::new("usizekey");
153 let items = Set::range(0..3);
154 let x = m.__indexed_var("x", &items).lb(0.0).build();
155
156 let total = __sum_over(&items, |i: usize| x[i]);
157 let arena = m.arena();
158 let terms = extract_linear(&arena, total.id).expect("linear");
159 assert_eq!(terms.coeffs.len(), 3);
160 }
161
162 #[test]
163 fn sum_over_slice_of_usize() {
164 let m = Model::new("slice");
165 let items = Set::range(0..5);
166 let x = m.__indexed_var("x", &items).lb(0.0).build();
167
168 let picked: &[usize] = &[0, 2, 4];
169 let total = __sum_over(picked, |i: usize| x[i]);
170 let arena = m.arena();
171 let terms = extract_linear(&arena, total.id).expect("linear");
172 assert_eq!(terms.coeffs.len(), 3);
173 }
174
175 #[test]
176 fn sum_over_vec_of_usize() {
177 let m = Model::new("vec");
178 let items = Set::range(0..5);
179 let x = m.__indexed_var("x", &items).lb(0.0).build();
180
181 let picked: Vec<usize> = vec![1, 3];
182 let total = __sum_over(&picked, |i: usize| x[i]);
183 let arena = m.arena();
184 let terms = extract_linear(&arena, total.id).expect("linear");
185 assert_eq!(terms.coeffs.len(), 2);
186 }
187
188 #[test]
189 fn sum_over_array_of_usize() {
190 let m = Model::new("array");
191 let items = Set::range(0..5);
192 let x = m.__indexed_var("x", &items).lb(0.0).build();
193
194 let picked: [usize; 4] = [0, 1, 2, 3];
195 let total = __sum_over(&picked, |i: usize| x[i]);
196 let arena = m.arena();
197 let terms = extract_linear(&arena, total.id).expect("linear");
198 assert_eq!(terms.coeffs.len(), 4);
199 }
200
201 #[test]
202 #[should_panic(expected = "sum_over on empty domain")]
203 fn sum_over_empty_set_panics() {
204 let m = Model::new("empty");
205 let empty = Set::range(0..0);
206 let _x = m.__indexed_var("x", &Set::range(0..1)).lb(0.0).build();
207 let _ = __sum_over(&empty, |_: usize| panic!("closure should not run"));
208 }
209
210 #[test]
211 #[should_panic(expected = "sum_over on empty domain")]
212 fn sum_over_empty_slice_panics() {
213 let m = Model::new("empty_slice");
214 let _x = m.__indexed_var("x", &Set::range(0..1)).lb(0.0).build();
215 let empty: &[usize] = &[];
216 let _ = __sum_over(empty, |_: usize| panic!("closure should not run"));
217 }
218}