1use super::pref::{FalliblePreference, Preference, PreferenceError, StandardConfig};
2
3pub struct BudgetConstraint {
5 pub prices: Vec<f64>,
7 pub income: f64,
9}
10
11#[derive(Clone, Debug)]
13pub struct OptimConfig {
14 pub mu_init: f64,
16 pub mu_decay: f64,
18 pub outer_iters: usize,
20 pub inner_iters: usize,
22 pub step_size: f64,
24 pub tol: f64,
26}
27
28impl Default for OptimConfig {
29 fn default() -> Self {
30 let standard = StandardConfig::get();
31 Self {
32 mu_init: standard.optimisation.optim_mu_init,
33 mu_decay: standard.optimisation.optim_mu_decay,
34 outer_iters: standard.optimisation.optim_outer_iters,
35 inner_iters: standard.optimisation.optim_inner_iters,
36 step_size: standard.optimisation.optim_step_size,
37 tol: standard.optimisation.optim_tol,
38 }
39 }
40}
41
42pub fn optimal_bundle<F: Fn(&[f64]) -> f64>(
59 pref: &Preference<F>,
60 constraint: &BudgetConstraint,
61 config: OptimConfig,
62) -> Result<Vec<f64>, String> {
63 let lb = pref.min_bounds();
64 let ub = pref.max_bounds();
65 let dims = lb.len();
66 let prices = &constraint.prices;
67 let income = constraint.income;
68
69 if prices.len() != dims {
70 return Err("prices length must match the number of goods in the Preference".into());
71 }
72 if income <= 0.0 {
73 return Err("income must be positive".into());
74 }
75 if prices.iter().any(|&p| p <= 0.0) {
76 return Err("all prices must be positive".into());
77 }
78
79 let mut x: Vec<f64> = (0..dims)
82 .map(|i| {
83 let equal_share = income / (dims as f64 * prices[i]);
84 lb[i] + 0.5 * (equal_share.min(ub[i]) - lb[i])
85 })
86 .collect();
87
88 if !is_feasible(&x, lb, ub, prices, income) {
89 return Err("Could not find a feasible interior starting point".into());
90 }
91
92 let mut mu = config.mu_init;
93
94 for _ in 0..config.outer_iters {
95 for _ in 0..config.inner_iters {
96 let grad = barrier_gradient(pref, &x, lb, ub, prices, income, mu);
97
98 let grad_norm: f64 = grad.iter().map(|g| g * g).sum::<f64>().sqrt();
100 if grad_norm < config.tol {
101 return Ok(x);
102 }
103
104 let b_current = barrier_value(pref, &x, lb, ub, prices, income, mu);
107 let mut step = config.step_size;
108 let x_new = loop {
109 let candidate: Vec<f64> =
110 x.iter().zip(&grad).map(|(xi, gi)| xi + step * gi).collect();
111
112 if is_feasible(&candidate, lb, ub, prices, income) {
113 let b_new = barrier_value(pref, &candidate, lb, ub, prices, income, mu);
114 if b_new > b_current {
115 break candidate;
116 }
117 }
118
119 step *= 0.5;
120
121 if step < 1e-15 {
123 break x.clone();
124 }
125 };
126
127 x = x_new;
128 }
129
130 mu *= config.mu_decay;
131 }
132
133 Ok(x)
134}
135
136pub fn optimal_bundle_fallible<F, E>(
142 pref: &FalliblePreference<F, E>,
143 constraint: &BudgetConstraint,
144 config: OptimConfig,
145) -> Result<Vec<f64>, PreferenceError<E>>
146where
147 F: Fn(&[f64]) -> Result<f64, E>,
148{
149 let lb = pref.min_bounds();
150 let ub = pref.max_bounds();
151 let dims = lb.len();
152 let prices = &constraint.prices;
153 let income = constraint.income;
154
155 if prices.len() != dims {
156 return Err(PreferenceError::Config(
157 "prices length must match the number of goods in the Preference".into(),
158 ));
159 }
160 if income <= 0.0 {
161 return Err(PreferenceError::Config("income must be positive".into()));
162 }
163 if prices.iter().any(|&p| p <= 0.0) {
164 return Err(PreferenceError::Config(
165 "all prices must be positive".into(),
166 ));
167 }
168
169 let mut x: Vec<f64> = (0..dims)
170 .map(|i| {
171 let equal_share = income / (dims as f64 * prices[i]);
172 lb[i] + 0.5 * (equal_share.min(ub[i]) - lb[i])
173 })
174 .collect();
175
176 if !is_feasible(&x, lb, ub, prices, income) {
177 return Err(PreferenceError::Config(
178 "Could not find a feasible interior starting point".into(),
179 ));
180 }
181
182 let mut mu = config.mu_init;
183
184 for _ in 0..config.outer_iters {
185 for _ in 0..config.inner_iters {
186 let grad = barrier_gradient_fallible(pref, &x, lb, ub, prices, income, mu)?;
187 let grad_norm: f64 = grad.iter().map(|g| g * g).sum::<f64>().sqrt();
188 if grad_norm < config.tol {
189 return Ok(x);
190 }
191
192 let b_current = barrier_value_fallible(pref, &x, lb, ub, prices, income, mu)?;
193 let mut step = config.step_size;
194 let x_new = loop {
195 let candidate: Vec<f64> =
196 x.iter().zip(&grad).map(|(xi, gi)| xi + step * gi).collect();
197
198 if is_feasible(&candidate, lb, ub, prices, income) {
199 let b_new =
200 barrier_value_fallible(pref, &candidate, lb, ub, prices, income, mu)?;
201 if b_new > b_current {
202 break candidate;
203 }
204 }
205
206 step *= 0.5;
207 if step < 1e-15 {
208 break x.clone();
209 }
210 };
211
212 x = x_new;
213 }
214
215 mu *= config.mu_decay;
216 }
217
218 Ok(x)
219}
220
221fn barrier_value<F: Fn(&[f64]) -> f64>(
223 pref: &Preference<F>,
224 x: &[f64],
225 lb: &[f64],
226 ub: &[f64],
227 prices: &[f64],
228 income: f64,
229 mu: f64,
230) -> f64 {
231 let budget_slack = income - dot(prices, x);
232 let lb_slack: f64 = x.iter().zip(lb).map(|(xi, li)| (xi - li).ln()).sum();
233 let ub_slack: f64 = x.iter().zip(ub).map(|(xi, ui)| (ui - xi).ln()).sum();
234 pref.get_utility(x) + mu * (budget_slack.ln() + lb_slack + ub_slack)
235}
236
237fn barrier_gradient<F: Fn(&[f64]) -> f64>(
240 pref: &Preference<F>,
241 x: &[f64],
242 lb: &[f64],
243 ub: &[f64],
244 prices: &[f64],
245 income: f64,
246 mu: f64,
247) -> Vec<f64> {
248 let budget_slack = income - dot(prices, x);
249 (0..x.len())
250 .map(|i| {
251 let du = pref.get_mu(x, i);
252 let budget_term = -mu * prices[i] / budget_slack;
253 let lb_term = mu / (x[i] - lb[i]);
254 let ub_term = -mu / (ub[i] - x[i]);
255 du + budget_term + lb_term + ub_term
256 })
257 .collect()
258}
259
260fn barrier_value_fallible<F, E>(
261 pref: &FalliblePreference<F, E>,
262 x: &[f64],
263 lb: &[f64],
264 ub: &[f64],
265 prices: &[f64],
266 income: f64,
267 mu: f64,
268) -> Result<f64, PreferenceError<E>>
269where
270 F: Fn(&[f64]) -> Result<f64, E>,
271{
272 let budget_slack = income - dot(prices, x);
273 let lb_slack: f64 = x.iter().zip(lb).map(|(xi, li)| (xi - li).ln()).sum();
274 let ub_slack: f64 = x.iter().zip(ub).map(|(xi, ui)| (ui - xi).ln()).sum();
275 Ok(pref.get_utility(x)? + mu * (budget_slack.ln() + lb_slack + ub_slack))
276}
277
278fn barrier_gradient_fallible<F, E>(
279 pref: &FalliblePreference<F, E>,
280 x: &[f64],
281 lb: &[f64],
282 ub: &[f64],
283 prices: &[f64],
284 income: f64,
285 mu: f64,
286) -> Result<Vec<f64>, PreferenceError<E>>
287where
288 F: Fn(&[f64]) -> Result<f64, E>,
289{
290 let budget_slack = income - dot(prices, x);
291 (0..x.len())
292 .map(|i| {
293 let du = pref.get_mu(x, i)?;
294 let budget_term = -mu * prices[i] / budget_slack;
295 let lb_term = mu / (x[i] - lb[i]);
296 let ub_term = -mu / (ub[i] - x[i]);
297 Ok(du + budget_term + lb_term + ub_term)
298 })
299 .collect()
300}
301
302fn is_feasible(x: &[f64], lb: &[f64], ub: &[f64], prices: &[f64], income: f64) -> bool {
304 let budget_ok = dot(prices, x) < income;
305 let bounds_ok = x
306 .iter()
307 .zip(lb)
308 .zip(ub)
309 .all(|((xi, li), ui)| xi > li && xi < ui);
310 budget_ok && bounds_ok
311}
312
313fn dot(a: &[f64], b: &[f64]) -> f64 {
314 a.iter().zip(b).map(|(ai, bi)| ai * bi).sum()
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320 use crate::pref::Preference;
321
322 fn cobb_douglas(bundle: &[f64]) -> f64 {
324 bundle.iter().product::<f64>().sqrt()
325 }
326
327 fn linear(bundle: &[f64]) -> f64 {
329 bundle.iter().sum()
330 }
331
332 #[test]
333 fn test_optimal_bundle_cobb_douglas_returns_ok() {
334 let pref = Preference::new(cobb_douglas, vec![0.0, 0.0], vec![20.0, 20.0]).unwrap();
337 let constraint = BudgetConstraint {
338 prices: vec![1.0, 1.0],
339 income: 10.0,
340 };
341
342 let result = optimal_bundle(&pref, &constraint, OptimConfig::default());
343
344 assert!(result.is_ok(), "Optimisation failed: {:?}", result);
345 let x = result.unwrap();
346 assert!((x[0] - 5.0).abs() < 0.1, "Expected x ~= 5.0, got {}", x[0]);
347 assert!((x[1] - 5.0).abs() < 0.1, "Expected y ~= 5.0, got {}", x[1]);
348 }
349
350 #[test]
351 fn test_optimal_bundle_mismatched_prices_raises_err() {
352 let pref = Preference::new(linear, vec![0.0, 0.0], vec![10.0, 10.0]).unwrap();
353 let constraint = BudgetConstraint {
354 prices: vec![1.0],
355 income: 10.0,
356 };
357
358 let result = optimal_bundle(&pref, &constraint, OptimConfig::default());
359
360 assert!(result.is_err());
361 assert!(result.unwrap_err().contains("prices length"));
362 }
363
364 #[test]
365 fn test_optimal_bundle_zero_income_raises_err() {
366 let pref = Preference::new(linear, vec![0.0, 0.0], vec![10.0, 10.0]).unwrap();
367 let constraint = BudgetConstraint {
368 prices: vec![1.0, 1.0],
369 income: 0.0,
370 };
371
372 let result = optimal_bundle(&pref, &constraint, OptimConfig::default());
373
374 assert!(result.is_err());
375 assert!(result.unwrap_err().contains("income must be positive"));
376 }
377
378 #[test]
379 fn test_optimal_bundle_solution_satisfies_budget_constraint() {
380 let pref = Preference::new(cobb_douglas, vec![0.0, 0.0], vec![20.0, 20.0]).unwrap();
381 let prices = vec![2.0, 3.0];
382 let income = 12.0;
383 let constraint = BudgetConstraint {
384 prices: prices.clone(),
385 income,
386 };
387
388 let x = optimal_bundle(&pref, &constraint, OptimConfig::default()).unwrap();
389
390 let expenditure: f64 = x.iter().zip(&prices).map(|(xi, pi)| xi * pi).sum();
391 assert!(
392 expenditure <= income + 1e-6,
393 "Budget constraint violated: expenditure {} > income {}",
394 expenditure,
395 income
396 );
397 }
398}