lpsolve 1.0.1

High-level lpsolve wrapper
Documentation
//! Diet problem example: Minimize cost while meeting nutritional requirements
//!
//! Classic optimization problem where we want to find the cheapest combination
//! of foods that meets all nutritional requirements.

use lpsolve::prelude::*;

#[derive(Debug)]
struct Food {
    name: &'static str,
    cost: f64,
    calories: f64,
    protein: f64,
    calcium: f64,
    iron: f64,
}

#[derive(Debug)]
struct NutritionalRequirement {
    name: &'static str,
    minimum: f64,
    maximum: Option<f64>,
}

fn main() -> Result<()> {
    println!("=== Diet Problem ===\n");

    // Define available foods with nutritional content per unit
    let foods = vec![
        Food { name: "Bread", cost: 2.0, calories: 100.0, protein: 4.0, calcium: 2.0, iron: 1.0 },
        Food { name: "Milk", cost: 3.0, calories: 120.0, protein: 8.0, calcium: 15.0, iron: 0.5 },
        Food { name: "Eggs", cost: 4.0, calories: 150.0, protein: 12.0, calcium: 2.0, iron: 2.0 },
        Food { name: "Meat", cost: 8.0, calories: 200.0, protein: 20.0, calcium: 1.0, iron: 3.0 },
        Food { name: "Vegetables", cost: 1.5, calories: 50.0, protein: 2.0, calcium: 4.0, iron: 1.5 },
    ];

    // Define nutritional requirements
    let requirements = vec![
        NutritionalRequirement { name: "Calories", minimum: 2000.0, maximum: Some(2500.0) },
        NutritionalRequirement { name: "Protein", minimum: 50.0, maximum: None },
        NutritionalRequirement { name: "Calcium", minimum: 100.0, maximum: None },
        NutritionalRequirement { name: "Iron", minimum: 10.0, maximum: Some(20.0) },
    ];

    let n_foods = foods.len() as i32;

    // Extract nutrition vectors
    let costs: Vec<f64> = foods.iter().map(|f| f.cost).collect();
    let calories: Vec<f64> = foods.iter().map(|f| f.calories).collect();
    let protein: Vec<f64> = foods.iter().map(|f| f.protein).collect();
    let calcium: Vec<f64> = foods.iter().map(|f| f.calcium).collect();
    let iron: Vec<f64> = foods.iter().map(|f| f.iron).collect();
    let names: Vec<&str> = foods.iter().map(|f| f.name).collect();

    // Build using terse API
    let solution = Problem::builder()
        .name("Diet Optimization")?
        .cols(n_foods)
        .min(&costs)
        // Calorie constraints
        .named_constraint("Min Calories", &calories, Ge, requirements[0].minimum)?
        .named_constraint("Max Calories", &calories, Le, requirements[0].maximum.unwrap())?
        // Other nutrition constraints
        .named_constraint("Min Protein", &protein, Ge, requirements[1].minimum)?
        .named_constraint("Min Calcium", &calcium, Ge, requirements[2].minimum)?
        .named_constraint("Min Iron", &iron, Ge, requirements[3].minimum)?
        .named_constraint("Max Iron", &iron, Le, requirements[3].maximum.unwrap())?
        // Variable configuration
        .bounds_all(0.0, 10.0)  // All foods: 0-10 units
        .variable_names(&names)?
        .typical_lp()
        .solve()?;

    // Display results
    if solution.is_optimal() {
        println!("✓ Found optimal diet!");
        println!("  Total daily cost: ${:.2}\n", solution.objective_value());

        println!("  Food quantities (units per day):");
        let mut total_nutrients = vec![0.0; 4];  // calories, protein, calcium, iron

        for (i, food) in foods.iter().enumerate() {
            if let Some(quantity) = solution.variable((i + 1) as i32) {
                if quantity > 0.01 {  // Only show foods actually in the diet
                    println!("    {:<12} {:.2} units @ ${:.2}/unit = ${:.2}",
                        food.name, quantity, food.cost, quantity * food.cost);

                    // Calculate total nutrients
                    total_nutrients[0] += quantity * food.calories;
                    total_nutrients[1] += quantity * food.protein;
                    total_nutrients[2] += quantity * food.calcium;
                    total_nutrients[3] += quantity * food.iron;
                }
            }
        }

        println!("\n  Nutritional content:");
        println!("    Calories: {:.1} (required: {:.0}-{:.0})",
            total_nutrients[0], requirements[0].minimum,
            requirements[0].maximum.unwrap_or(f64::INFINITY));
        println!("    Protein:  {:.1}g (required: >= {:.0}g)",
            total_nutrients[1], requirements[1].minimum);
        println!("    Calcium:  {:.1}mg (required: >= {:.0}mg)",
            total_nutrients[2], requirements[2].minimum);
        println!("    Iron:     {:.1}mg (required: {:.0}-{:.0}mg)",
            total_nutrients[3], requirements[3].minimum,
            requirements[3].maximum.unwrap_or(f64::INFINITY));

        // Show shadow prices (sensitivity analysis)
        if let Some(duals) = solution.dual_values() {
            println!("\n  Shadow prices (marginal cost of constraints):");
            let constraint_names = [
                "Min Calories", "Max Calories", "Min Protein",
                "Min Calcium", "Min Iron", "Max Iron"
            ];
            for (i, name) in constraint_names.iter().enumerate() {
                if i < duals.len() && duals[i].abs() > 0.001 {
                    println!("    {}: ${:.4}", name, duals[i].abs());
                }
            }
        }

        println!("\n  Solver statistics:");
        println!("    Iterations: {}", solution.iterations());
        println!("    Time: {:.4}s", solution.time_elapsed());
    } else {
        println!("✗ Could not find optimal diet: {:?}", solution.status());

        // Provide helpful debugging for infeasible problems
        if solution.status() == Infeasible {
            println!("\n  The nutritional requirements might be impossible to meet");
            println!("  with the given foods. Try:");
            println!("  - Relaxing some constraints");
            println!("  - Adding more food options");
            println!("  - Adjusting the bounds");
        }
    }

    Ok(())
}