use super::pref::{FalliblePreference, Preference, PreferenceError, StandardConfig};
pub struct BudgetConstraint {
pub prices: Vec<f64>,
pub income: f64,
}
#[derive(Clone, Debug)]
pub struct OptimConfig {
pub mu_init: f64,
pub mu_decay: f64,
pub outer_iters: usize,
pub inner_iters: usize,
pub step_size: f64,
pub tol: f64,
}
impl Default for OptimConfig {
fn default() -> Self {
let standard = StandardConfig::get();
Self {
mu_init: standard.optimisation.optim_mu_init,
mu_decay: standard.optimisation.optim_mu_decay,
outer_iters: standard.optimisation.optim_outer_iters,
inner_iters: standard.optimisation.optim_inner_iters,
step_size: standard.optimisation.optim_step_size,
tol: standard.optimisation.optim_tol,
}
}
}
pub fn optimal_bundle<F: Fn(&[f64]) -> f64>(
pref: &Preference<F>,
constraint: &BudgetConstraint,
config: OptimConfig,
) -> Result<Vec<f64>, String> {
let lb = pref.min_bounds();
let ub = pref.max_bounds();
let dims = lb.len();
let prices = &constraint.prices;
let income = constraint.income;
if prices.len() != dims {
return Err("prices length must match the number of goods in the Preference".into());
}
if income <= 0.0 {
return Err("income must be positive".into());
}
if prices.iter().any(|&p| p <= 0.0) {
return Err("all prices must be positive".into());
}
let mut x: Vec<f64> = (0..dims)
.map(|i| {
let equal_share = income / (dims as f64 * prices[i]);
lb[i] + 0.5 * (equal_share.min(ub[i]) - lb[i])
})
.collect();
if !is_feasible(&x, lb, ub, prices, income) {
return Err("Could not find a feasible interior starting point".into());
}
let mut mu = config.mu_init;
for _ in 0..config.outer_iters {
for _ in 0..config.inner_iters {
let grad = barrier_gradient(pref, &x, lb, ub, prices, income, mu);
let grad_norm: f64 = grad.iter().map(|g| g * g).sum::<f64>().sqrt();
if grad_norm < config.tol {
return Ok(x);
}
let b_current = barrier_value(pref, &x, lb, ub, prices, income, mu);
let mut step = config.step_size;
let x_new = loop {
let candidate: Vec<f64> =
x.iter().zip(&grad).map(|(xi, gi)| xi + step * gi).collect();
if is_feasible(&candidate, lb, ub, prices, income) {
let b_new = barrier_value(pref, &candidate, lb, ub, prices, income, mu);
if b_new > b_current {
break candidate;
}
}
step *= 0.5;
if step < 1e-15 {
break x.clone();
}
};
x = x_new;
}
mu *= config.mu_decay;
}
Ok(x)
}
pub fn optimal_bundle_fallible<F, E>(
pref: &FalliblePreference<F, E>,
constraint: &BudgetConstraint,
config: OptimConfig,
) -> Result<Vec<f64>, PreferenceError<E>>
where
F: Fn(&[f64]) -> Result<f64, E>,
{
let lb = pref.min_bounds();
let ub = pref.max_bounds();
let dims = lb.len();
let prices = &constraint.prices;
let income = constraint.income;
if prices.len() != dims {
return Err(PreferenceError::Config(
"prices length must match the number of goods in the Preference".into(),
));
}
if income <= 0.0 {
return Err(PreferenceError::Config("income must be positive".into()));
}
if prices.iter().any(|&p| p <= 0.0) {
return Err(PreferenceError::Config(
"all prices must be positive".into(),
));
}
let mut x: Vec<f64> = (0..dims)
.map(|i| {
let equal_share = income / (dims as f64 * prices[i]);
lb[i] + 0.5 * (equal_share.min(ub[i]) - lb[i])
})
.collect();
if !is_feasible(&x, lb, ub, prices, income) {
return Err(PreferenceError::Config(
"Could not find a feasible interior starting point".into(),
));
}
let mut mu = config.mu_init;
for _ in 0..config.outer_iters {
for _ in 0..config.inner_iters {
let grad = barrier_gradient_fallible(pref, &x, lb, ub, prices, income, mu)?;
let grad_norm: f64 = grad.iter().map(|g| g * g).sum::<f64>().sqrt();
if grad_norm < config.tol {
return Ok(x);
}
let b_current = barrier_value_fallible(pref, &x, lb, ub, prices, income, mu)?;
let mut step = config.step_size;
let x_new = loop {
let candidate: Vec<f64> =
x.iter().zip(&grad).map(|(xi, gi)| xi + step * gi).collect();
if is_feasible(&candidate, lb, ub, prices, income) {
let b_new =
barrier_value_fallible(pref, &candidate, lb, ub, prices, income, mu)?;
if b_new > b_current {
break candidate;
}
}
step *= 0.5;
if step < 1e-15 {
break x.clone();
}
};
x = x_new;
}
mu *= config.mu_decay;
}
Ok(x)
}
fn barrier_value<F: Fn(&[f64]) -> f64>(
pref: &Preference<F>,
x: &[f64],
lb: &[f64],
ub: &[f64],
prices: &[f64],
income: f64,
mu: f64,
) -> f64 {
let budget_slack = income - dot(prices, x);
let lb_slack: f64 = x.iter().zip(lb).map(|(xi, li)| (xi - li).ln()).sum();
let ub_slack: f64 = x.iter().zip(ub).map(|(xi, ui)| (ui - xi).ln()).sum();
pref.get_utility(x) + mu * (budget_slack.ln() + lb_slack + ub_slack)
}
fn barrier_gradient<F: Fn(&[f64]) -> f64>(
pref: &Preference<F>,
x: &[f64],
lb: &[f64],
ub: &[f64],
prices: &[f64],
income: f64,
mu: f64,
) -> Vec<f64> {
let budget_slack = income - dot(prices, x);
(0..x.len())
.map(|i| {
let du = pref.get_mu(x, i);
let budget_term = -mu * prices[i] / budget_slack;
let lb_term = mu / (x[i] - lb[i]);
let ub_term = -mu / (ub[i] - x[i]);
du + budget_term + lb_term + ub_term
})
.collect()
}
fn barrier_value_fallible<F, E>(
pref: &FalliblePreference<F, E>,
x: &[f64],
lb: &[f64],
ub: &[f64],
prices: &[f64],
income: f64,
mu: f64,
) -> Result<f64, PreferenceError<E>>
where
F: Fn(&[f64]) -> Result<f64, E>,
{
let budget_slack = income - dot(prices, x);
let lb_slack: f64 = x.iter().zip(lb).map(|(xi, li)| (xi - li).ln()).sum();
let ub_slack: f64 = x.iter().zip(ub).map(|(xi, ui)| (ui - xi).ln()).sum();
Ok(pref.get_utility(x)? + mu * (budget_slack.ln() + lb_slack + ub_slack))
}
fn barrier_gradient_fallible<F, E>(
pref: &FalliblePreference<F, E>,
x: &[f64],
lb: &[f64],
ub: &[f64],
prices: &[f64],
income: f64,
mu: f64,
) -> Result<Vec<f64>, PreferenceError<E>>
where
F: Fn(&[f64]) -> Result<f64, E>,
{
let budget_slack = income - dot(prices, x);
(0..x.len())
.map(|i| {
let du = pref.get_mu(x, i)?;
let budget_term = -mu * prices[i] / budget_slack;
let lb_term = mu / (x[i] - lb[i]);
let ub_term = -mu / (ub[i] - x[i]);
Ok(du + budget_term + lb_term + ub_term)
})
.collect()
}
fn is_feasible(x: &[f64], lb: &[f64], ub: &[f64], prices: &[f64], income: f64) -> bool {
let budget_ok = dot(prices, x) < income;
let bounds_ok = x
.iter()
.zip(lb)
.zip(ub)
.all(|((xi, li), ui)| xi > li && xi < ui);
budget_ok && bounds_ok
}
fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b).map(|(ai, bi)| ai * bi).sum()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pref::Preference;
fn cobb_douglas(bundle: &[f64]) -> f64 {
bundle.iter().product::<f64>().sqrt()
}
fn linear(bundle: &[f64]) -> f64 {
bundle.iter().sum()
}
#[test]
fn test_optimal_bundle_cobb_douglas_returns_ok() {
let pref = Preference::new(cobb_douglas, vec![0.0, 0.0], vec![20.0, 20.0]).unwrap();
let constraint = BudgetConstraint {
prices: vec![1.0, 1.0],
income: 10.0,
};
let result = optimal_bundle(&pref, &constraint, OptimConfig::default());
assert!(result.is_ok(), "Optimisation failed: {:?}", result);
let x = result.unwrap();
assert!((x[0] - 5.0).abs() < 0.1, "Expected x ~= 5.0, got {}", x[0]);
assert!((x[1] - 5.0).abs() < 0.1, "Expected y ~= 5.0, got {}", x[1]);
}
#[test]
fn test_optimal_bundle_mismatched_prices_raises_err() {
let pref = Preference::new(linear, vec![0.0, 0.0], vec![10.0, 10.0]).unwrap();
let constraint = BudgetConstraint {
prices: vec![1.0],
income: 10.0,
};
let result = optimal_bundle(&pref, &constraint, OptimConfig::default());
assert!(result.is_err());
assert!(result.unwrap_err().contains("prices length"));
}
#[test]
fn test_optimal_bundle_zero_income_raises_err() {
let pref = Preference::new(linear, vec![0.0, 0.0], vec![10.0, 10.0]).unwrap();
let constraint = BudgetConstraint {
prices: vec![1.0, 1.0],
income: 0.0,
};
let result = optimal_bundle(&pref, &constraint, OptimConfig::default());
assert!(result.is_err());
assert!(result.unwrap_err().contains("income must be positive"));
}
#[test]
fn test_optimal_bundle_solution_satisfies_budget_constraint() {
let pref = Preference::new(cobb_douglas, vec![0.0, 0.0], vec![20.0, 20.0]).unwrap();
let prices = vec![2.0, 3.0];
let income = 12.0;
let constraint = BudgetConstraint {
prices: prices.clone(),
income,
};
let x = optimal_bundle(&pref, &constraint, OptimConfig::default()).unwrap();
let expenditure: f64 = x.iter().zip(&prices).map(|(xi, pi)| xi * pi).sum();
assert!(
expenditure <= income + 1e-6,
"Budget constraint violated: expenditure {} > income {}",
expenditure,
income
);
}
}