1use super::pref::{FalliblePreference, Preference, PreferenceError, StandardConfig};
2
3#[derive(Clone, Debug)]
5pub struct IndiffConfig {
6 pub n_points: usize,
8 pub tol: f64,
10}
11
12impl Default for IndiffConfig {
13 fn default() -> Self {
14 let standard = StandardConfig::get();
15 Self {
16 n_points: standard.indifference.indiff_n_points,
17 tol: standard.indifference.indiff_tol,
18 }
19 }
20}
21
22pub fn trace_2d<F: Fn(&[f64]) -> f64>(
39 pref: &Preference<F>,
40 target_utility: f64,
41 good_i: usize,
42 good_j: usize,
43 config: IndiffConfig,
44) -> Result<Vec<(f64, f64)>, String> {
45 let lb = pref.min_bounds();
46 let ub = pref.max_bounds();
47 let dims = lb.len();
48
49 if good_i >= dims {
50 return Err(format!(
51 "good_i ({}) is out of bounds for a {}-good preference",
52 good_i, dims
53 ));
54 }
55 if good_j >= dims {
56 return Err(format!(
57 "good_j ({}) is out of bounds for a {}-good preference",
58 good_j, dims
59 ));
60 }
61 if good_i == good_j {
62 return Err("good_i and good_j must be different goods".into());
63 }
64 if config.n_points < 2 {
65 return Err("n_points must be at least 2".into());
66 }
67
68 let base: Vec<f64> = (0..dims).map(|d| (lb[d] + ub[d]) / 2.0).collect();
71
72 let i_min = lb[good_i];
73 let i_max = ub[good_i];
74 let step = (i_max - i_min) / (config.n_points - 1) as f64;
75
76 let mut points = Vec::with_capacity(config.n_points);
77
78 for k in 0..config.n_points {
79 let xi = i_min + k as f64 * step;
80 let state = BisectState {
81 base: &base,
82 good_i,
83 xi,
84 good_j,
85 target: target_utility,
86 };
87
88 if let Some(xj) = bisect(pref, &state, lb[good_j], ub[good_j], config.tol) {
89 points.push((xi, xj));
90 }
91 }
92
93 if points.is_empty() {
94 return Err(format!(
95 "No points found on the indifference curve for target utility {}. \
96 Check that the target utility is reachable within the bounds.",
97 target_utility
98 ));
99 }
100
101 Ok(points)
102}
103
104pub fn trace_2d_fallible<F, E>(
110 pref: &FalliblePreference<F, E>,
111 target_utility: f64,
112 good_i: usize,
113 good_j: usize,
114 config: IndiffConfig,
115) -> Result<Vec<(f64, f64)>, PreferenceError<E>>
116where
117 F: Fn(&[f64]) -> Result<f64, E>,
118{
119 let lb = pref.min_bounds();
120 let ub = pref.max_bounds();
121 let dims = lb.len();
122
123 if good_i >= dims {
124 return Err(PreferenceError::Config(format!(
125 "good_i ({}) is out of bounds for a {}-good preference",
126 good_i, dims
127 )));
128 }
129 if good_j >= dims {
130 return Err(PreferenceError::Config(format!(
131 "good_j ({}) is out of bounds for a {}-good preference",
132 good_j, dims
133 )));
134 }
135 if good_i == good_j {
136 return Err(PreferenceError::Config(
137 "good_i and good_j must be different goods".into(),
138 ));
139 }
140 if config.n_points < 2 {
141 return Err(PreferenceError::Config(
142 "n_points must be at least 2".into(),
143 ));
144 }
145
146 let base: Vec<f64> = (0..dims).map(|d| (lb[d] + ub[d]) / 2.0).collect();
147 let i_min = lb[good_i];
148 let i_max = ub[good_i];
149 let step = (i_max - i_min) / (config.n_points - 1) as f64;
150 let mut points = Vec::with_capacity(config.n_points);
151
152 for k in 0..config.n_points {
153 let xi = i_min + k as f64 * step;
154 let state = BisectState {
155 base: &base,
156 good_i,
157 xi,
158 good_j,
159 target: target_utility,
160 };
161 if let Some(xj) = bisect_fallible(pref, &state, lb[good_j], ub[good_j], config.tol)? {
162 points.push((xi, xj));
163 }
164 }
165
166 if points.is_empty() {
167 return Err(PreferenceError::Config(format!(
168 "No points found on the indifference curve for target utility {}. \
169 Check that the target utility is reachable within the bounds.",
170 target_utility
171 )));
172 }
173
174 Ok(points)
175}
176
177struct BisectState<'a> {
178 base: &'a [f64],
179 good_i: usize,
180 xi: f64,
181 good_j: usize,
182 target: f64,
183}
184
185fn bisect<F: Fn(&[f64]) -> f64>(
188 pref: &Preference<F>,
189 state: &BisectState<'_>,
190 j_lo: f64,
191 j_hi: f64,
192 tol: f64,
193) -> Option<f64> {
194 let eval = |xj: f64| -> f64 {
195 let mut bundle = state.base.to_vec();
196 bundle[state.good_i] = state.xi;
197 bundle[state.good_j] = xj;
198 pref.get_utility(&bundle) - state.target
199 };
200
201 let mut lo = j_lo;
202 let mut hi = j_hi;
203 let mut f_lo = eval(lo);
204 let f_hi = eval(hi);
205
206 if f_lo * f_hi > 0.0 {
208 return None;
209 }
210
211 for _ in 0..200 {
212 let mid = (lo + hi) / 2.0;
213 let f_mid = eval(mid);
214
215 if f_mid.abs() < tol || (hi - lo) / 2.0 < tol {
216 return Some(mid);
217 }
218
219 if f_lo * f_mid < 0.0 {
220 hi = mid;
221 } else {
222 lo = mid;
223 f_lo = f_mid;
224 }
225 }
226
227 Some((lo + hi) / 2.0)
228}
229
230fn bisect_fallible<F, E>(
231 pref: &FalliblePreference<F, E>,
232 state: &BisectState<'_>,
233 j_lo: f64,
234 j_hi: f64,
235 tol: f64,
236) -> Result<Option<f64>, PreferenceError<E>>
237where
238 F: Fn(&[f64]) -> Result<f64, E>,
239{
240 let eval = |xj: f64| -> Result<f64, PreferenceError<E>> {
241 let mut bundle = state.base.to_vec();
242 bundle[state.good_i] = state.xi;
243 bundle[state.good_j] = xj;
244 Ok(pref.get_utility(&bundle)? - state.target)
245 };
246
247 let mut lo = j_lo;
248 let mut hi = j_hi;
249 let mut f_lo = eval(lo)?;
250 let f_hi = eval(hi)?;
251
252 if f_lo * f_hi > 0.0 {
253 return Ok(None);
254 }
255
256 for _ in 0..200 {
257 let mid = (lo + hi) / 2.0;
258 let f_mid = eval(mid)?;
259
260 if f_mid.abs() < tol || (hi - lo) / 2.0 < tol {
261 return Ok(Some(mid));
262 }
263
264 if f_lo * f_mid < 0.0 {
265 hi = mid;
266 } else {
267 lo = mid;
268 f_lo = f_mid;
269 }
270 }
271
272 Ok(Some((lo + hi) / 2.0))
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use crate::pref::Preference;
279
280 fn cobb_douglas(bundle: &[f64]) -> f64 {
282 bundle.iter().product::<f64>().sqrt()
283 }
284
285 fn linear(bundle: &[f64]) -> f64 {
287 bundle.iter().sum()
288 }
289
290 #[test]
291 fn test_trace_2d_cobb_douglas_points_lie_on_curve() {
292 let pref = Preference::new(cobb_douglas, vec![0.1, 0.1], vec![10.0, 10.0]).unwrap();
294 let target = 2.0;
295 let config = IndiffConfig::default();
296
297 let points = trace_2d(&pref, target, 0, 1, config).unwrap();
298
299 assert!(!points.is_empty(), "Expected at least one point");
300 for (xi, xj) in &points {
301 let u = cobb_douglas(&[*xi, *xj]);
302 assert!(
303 (u - target).abs() < 1e-6,
304 "Point ({}, {}) has utility {} ~= {}",
305 xi,
306 xj,
307 u,
308 target
309 );
310 }
311 }
312
313 #[test]
314 fn test_trace_2d_linear_returns_expected_n_points() {
315 let pref = Preference::new(linear, vec![0.0, 0.0], vec![10.0, 10.0]).unwrap();
317 let config = IndiffConfig {
318 n_points: 50,
319 tol: 1e-10,
320 };
321
322 let points = trace_2d(&pref, 8.0, 0, 1, config).unwrap();
323
324 assert!(
325 points.len() >= 40,
326 "Expected close to 50 points, got {}",
327 points.len()
328 );
329 }
330
331 #[test]
332 fn test_trace_2d_same_good_raises_err() {
333 let pref = Preference::new(cobb_douglas, vec![0.1, 0.1], vec![10.0, 10.0]).unwrap();
334
335 let result = trace_2d(&pref, 2.0, 0, 0, IndiffConfig::default());
336
337 assert!(result.is_err());
338 assert!(result.unwrap_err().contains("must be different"));
339 }
340
341 #[test]
342 fn test_trace_2d_out_of_bounds_good_raises_err() {
343 let pref = Preference::new(cobb_douglas, vec![0.1, 0.1], vec![10.0, 10.0]).unwrap();
344
345 let result = trace_2d(&pref, 2.0, 0, 5, IndiffConfig::default());
346
347 assert!(result.is_err());
348 assert!(result.unwrap_err().contains("out of bounds"));
349 }
350
351 #[test]
352 fn test_trace_2d_unreachable_utility_raises_err() {
353 let pref = Preference::new(cobb_douglas, vec![0.1, 0.1], vec![10.0, 10.0]).unwrap();
355
356 let result = trace_2d(&pref, 999.0, 0, 1, IndiffConfig::default());
357
358 assert!(result.is_err());
359 assert!(result.unwrap_err().contains("No points found"));
360 }
361
362 #[test]
363 fn test_trace_2d_n_points_less_than_2_raises_err() {
364 let pref = Preference::new(cobb_douglas, vec![0.1, 0.1], vec![10.0, 10.0]).unwrap();
365 let config = IndiffConfig {
366 n_points: 1,
367 tol: 1e-10,
368 };
369
370 let result = trace_2d(&pref, 2.0, 0, 1, config);
371
372 assert!(result.is_err());
373 assert!(result.unwrap_err().contains("n_points must be at least 2"));
374 }
375}