Numbers

Optimization

Linear programming in pure Rust: the best plan under constraints, with whole-number variables, plus knapsack and assignment problems ready made.

Version 1.0.0 · 3 functions · 1 crate

#maths#planning#scheduling#optimization

Functions

What use optimize.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use optimize_lp, optimize_knapsack from optimize (Quillo 0.6).

optimize_lp(problem: any) → anyor an error

Solves a linear program: { maximize: { chairs: 45, tables: 80 }, constraints: [{ name, terms: { chairs: 5, tables: 20 }, op: "<=", value: 400 }], variables: { tables: { max: 15, integer: true } } } (minimize in place of maximize; variables are 0 or more unless min says, min: null for none; integer or binary make them whole). Gives { status: "optimal", objective, values, constraints: [{ name, used, value, binding }] }, or status "infeasible" or "unbounded" with null values. Up to 500 variables.

optimize_knapsack(items: list, capacity: float) → anyor an error

The 0/1 knapsack: which items ([{ name, value, weight }]) to take for the most value within capacity. Gives { take, value, weight }. Up to 500 items.

optimize_assign(cost: list) → anyor an error

The assignment problem: cost[i][j] is what giving task j to worker i costs; each worker gets one task and each task one worker (as many as the smaller side), for the least total cost. Gives { pairs: [[worker, task], ...], cost } (indexes from 0). Up to 20 by 20.

Get it

$ quillo install optimize               # optimize.qwer into this folder$ quillo install optimize --example     # and its example, optimize.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/optimize.qwer$ curl -O https://quillolang.org/plugins/files/optimize.qwe

Then, in your program, all of it or only what you call:

main.qwe
use optimize.qweruse optimize_lp, optimize_knapsack from optimize      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
good_lp = { version = "1.15", default-features = false, features = ["microlp"] }

Example

optimize.qwe
// A furniture workshop's week: how many chairs and tables to make for the most profit with the// wood and hours there are; what fits in the delivery van; and who drives which route.////   quillo run optimize.qwe      then open http://localhost:8080/planuse "optimize.qwer"route GET /plan ? { wood: float = 400.0, hours: float = 450.0 } {    let r = optimize_lp({        maximize: { chairs: 45, tables: 80 },        constraints: [            { name: "wood (kg)", terms: { chairs: 5, tables: 20 }, op: "<=", value: wood },            { name: "hours", terms: { chairs: 10, tables: 15 }, op: "<=", value: hours },        ],        variables: { chairs: { integer: true }, tables: { integer: true } },    })?    { status: r.status, make: r.values, profit: r.objective, limits: r.constraints | filter((c) => c.binding) | map((c) => c.name) }}route GET /van = optimize_knapsack([    { name: "sofa", value: 900, weight: 60 }, { name: "table", value: 500, weight: 35 },    { name: "chairs", value: 300, weight: 20 }, { name: "lamp", value: 120, weight: 5 }, { name: "bookcase", value: 450, weight: 40 },], 100.0)?route GET /routes {    let drivers = ["Ada", "Bas", "Cleo"]    let routes = ["north", "centre", "south"]    let minutes = [[40, 25, 60], [35, 30, 20], [50, 45, 40]]    let a = optimize_assign(minutes)?    { minutes: a.cost, plan: a.pairs | map((p) => `${drivers[p[0]]}: ${routes[p[1]]}`) }}route GET /impossible = optimize_lp({ minimize: { x: 1 }, constraints: [{ terms: { x: 1 }, op: ">=", value: 5 }, { terms: { x: 1 }, op: "<=", value: 3 }] })?.status
$ quillo run optimize.qwe$ curl 'localhost:8080/plan'$ curl 'localhost:8080/van'$ curl 'localhost:8080/routes'

Source

optimize.qwer
//! Linear programming in pure Rust: the best plan under constraints (production mix, diet, shift staffing, ad budgets), with whole-number variables when needed, plus knapsack and assignment problems ready made.//! version: 1.0.0//!//! [dependencies]//! good_lp = { version = "1.15", default-features = false, features = ["microlp"] }use good_lp::{microlp, variable, Expression, ProblemVariables, ResolutionError, Solution, SolverModel, Variable};use serde_json::{json, Map, Value};use std::collections::BTreeMap;const MAX_VARS: usize = 500;const MAX_CONSTRAINTS: usize = 2000;fn num(v: &Value, what: &str) -> Result<f64, String> {    v.as_f64().filter(|f| f.is_finite()).ok_or(format!("{} is a number, not {}", what, v))}/// A result rounded to 6 decimals, whole ones as ints.fn r(x: f64) -> Value {    let y = (x * 1e6).round() / 1e6;    if y.fract() == 0.0 && y.abs() < 9e15 { json!(y as i64) } else { json!(y) }}fn solve(problem: &Value) -> Result<Value, String> {    let (sense, objective) = match (problem.get("maximize"), problem.get("minimize")) {        (Some(o), None) => ("max", o),        (None, Some(o)) => ("min", o),        _ => return Err("give `maximize` or `minimize`: a map of variable to coefficient".into()),    };    let obj = objective.as_object().ok_or("the objective is a map of variable to coefficient")?;    let cons = problem.get("constraints").and_then(Value::as_array).cloned().unwrap_or_default();    if cons.len() > MAX_CONSTRAINTS {        return Err(format!("at most {} constraints", MAX_CONSTRAINTS));    }    let specs = problem.get("variables").and_then(Value::as_object).cloned().unwrap_or_default();    let mut names: BTreeMap<String, ()> = BTreeMap::new();    for k in obj.keys().chain(specs.keys()) {        names.insert(k.clone(), ());    }    for c in &cons {        for k in c.get("terms").and_then(Value::as_object).ok_or("each constraint has `terms`: a map of variable to coefficient")?.keys() {            names.insert(k.clone(), ());        }    }    if names.len() > MAX_VARS {        return Err(format!("at most {} variables", MAX_VARS));    }    let mut pv = ProblemVariables::new();    let mut vars: BTreeMap<String, Variable> = BTreeMap::new();    for name in names.keys() {        let s = specs.get(name).cloned().unwrap_or(Value::Null);        let mut d = variable().name(name.clone());        d = match s.get("min") {            Some(Value::Null) => d,            Some(m) => d.min(num(m, "min")?),            None => d.min(0.0),        };        if let Some(m) = s.get("max").filter(|m| !m.is_null()) {            d = d.max(num(m, "max")?);        }        if s.get("integer") == Some(&Value::Bool(true)) {            d = d.integer();        }        if s.get("binary") == Some(&Value::Bool(true)) {            d = d.binary();        }        vars.insert(name.clone(), pv.add(d));    }    let expr = |terms: &Map<String, Value>| -> Result<Expression, String> {        let mut e = Expression::from(0.0);        for (k, c) in terms {            e.add_mul(num(c, k)?, vars[k]);        }        Ok(e)    };    let goal = expr(obj)?;    let mut model = if sense == "max" { pv.maximise(goal.clone()) } else { pv.minimise(goal.clone()) }.using(microlp);    for c in &cons {        let e = expr(c["terms"].as_object().expect("checked above"))?;        let v = num(c.get("value").ok_or("each constraint has a `value`")?, "value")?;        let op = c.get("op").and_then(Value::as_str).unwrap_or("<=");        model = model.with(match op {            "<=" => e.leq(v),            ">=" => e.geq(v),            "=" | "==" => e.eq(v),            other => return Err(format!("`{}` is not a constraint: <=, >= or =", other)),        });    }    match model.solve() {        Ok(sol) => {            let values: Map<String, Value> = vars.iter().map(|(k, v)| (k.clone(), r(sol.value(*v)))).collect();            let slack: Vec<Value> = cons                .iter()                .map(|c| {                    let lhs: f64 = c["terms"].as_object().map(|t| t.iter().map(|(k, x)| x.as_f64().unwrap_or(0.0) * sol.value(vars[k])).sum()).unwrap_or(0.0);                    json!({ "name": c.get("name"), "used": r(lhs), "value": c["value"], "binding": (lhs - c["value"].as_f64().unwrap_or(0.0)).abs() < 1e-6 })                })                .collect();            Ok(json!({ "status": "optimal", "objective": r(sol.eval(goal)), "values": values, "constraints": slack }))        }        Err(ResolutionError::Infeasible) => Ok(json!({ "status": "infeasible", "objective": null, "values": null })),        Err(ResolutionError::Unbounded) => Ok(json!({ "status": "unbounded", "objective": null, "values": null })),        Err(e) => Err(e.to_string()),    }}/// Solves a linear program: `{ maximize: { chairs: 45, tables: 80 }, constraints: [{ name, terms: { chairs: 5, tables: 20 }, op: "<=", value: 400 }], variables: { tables: { max: 15, integer: true } } }`/// (`minimize` in place of `maximize`; variables are 0 or more unless `min` says, `min: null` for none; `integer` or `binary` make them whole)./// Gives `{ status: "optimal", objective, values, constraints: [{ name, used, value, binding }] }`, or `status` "infeasible" or "unbounded" with null values. Up to 500 variables.pub fn optimize_lp(problem: serde_json::Value) -> Result<Value, String> {    solve(&problem)}/// The 0/1 knapsack: which `items` (`[{ name, value, weight }]`) to take for the most value within `capacity`. Gives `{ take, value, weight }`. Up to 500 items.pub fn optimize_knapsack(items: Vec<serde_json::Value>, capacity: f64) -> Result<Value, String> {    let mut obj = Map::new();    let mut w = Map::new();    let mut vars = Map::new();    for (i, it) in items.iter().enumerate() {        let k = format!("i{}", i);        obj.insert(k.clone(), json!(num(it.get("value").unwrap_or(&Value::Null), "value")?));        w.insert(k.clone(), json!(num(it.get("weight").unwrap_or(&Value::Null), "weight")?));        vars.insert(k, json!({ "binary": true }));    }    let s = solve(&json!({ "maximize": obj, "constraints": [{ "terms": w, "op": "<=", "value": capacity }], "variables": vars }))?;    if s["status"] != "optimal" {        return Ok(json!({ "take": [], "value": 0, "weight": 0 }));    }    let take: Vec<&Value> = items.iter().enumerate().filter(|(i, _)| s["values"][format!("i{}", i)].as_f64().unwrap_or(0.0) > 0.5).map(|(_, it)| it).collect();    let weight: f64 = take.iter().map(|it| it["weight"].as_f64().unwrap_or(0.0)).sum();    Ok(json!({ "take": take.iter().map(|it| it.get("name").cloned().unwrap_or(Value::Null)).collect::<Vec<_>>(), "value": s["objective"], "weight": r(weight) }))}/// The assignment problem: `cost[i][j]` is what giving task j to worker i costs; each worker gets one task and each task one worker (as many as the smaller side),/// for the least total cost. Gives `{ pairs: [[worker, task], ...], cost }` (indexes from 0). Up to 20 by 20.pub fn optimize_assign(cost: Vec<Vec<f64>>) -> Result<Value, String> {    let n = cost.len();    let m = cost.first().map(Vec::len).unwrap_or(0);    if n == 0 || m == 0 || n > 20 || m > 20 || cost.iter().any(|row| row.len() != m) {        return Err("cost is a rectangle of numbers, at most 20 by 20".into());    }    let mut obj = Map::new();    let mut vars = Map::new();    for (i, row) in cost.iter().enumerate() {        for (j, c) in row.iter().enumerate() {            obj.insert(format!("x{}_{}", i, j), json!(c));            vars.insert(format!("x{}_{}", i, j), json!({ "binary": true }));        }    }    let k = n.min(m) as f64;    let mut cons = Vec::new();    for i in 0..n {        cons.push(json!({ "terms": (0..m).map(|j| (format!("x{}_{}", i, j), json!(1))).collect::<Map<_, _>>(), "op": "<=", "value": 1 }));    }    for j in 0..m {        cons.push(json!({ "terms": (0..n).map(|i| (format!("x{}_{}", i, j), json!(1))).collect::<Map<_, _>>(), "op": "<=", "value": 1 }));    }    cons.push(json!({ "terms": obj.keys().map(|key| (key.clone(), json!(1))).collect::<Map<_, _>>(), "op": "=", "value": k }));    let s = solve(&json!({ "minimize": obj, "constraints": cons, "variables": vars }))?;    let mut pairs = Vec::new();    for i in 0..n {        for j in 0..m {            if s["values"][format!("x{}_{}", i, j)].as_f64().unwrap_or(0.0) > 0.5 {                pairs.push(json!([i, j]));            }        }    }    Ok(json!({ "pairs": pairs, "cost": s["objective"] }))}
Big numbers bignum.qwer · 1.0.0Integers and fractions of any size, exact: factorials, powers, gcd, modular powers, and fractions such as 1/3 + 1/6 = 1/2 with no rounding.mathsmoneyexactfractions8 views · 7 downloadsClustering clustering.qwer · 1.0.0Data put into groups: k-means with k chosen by the elbow and silhouette, DBSCAN for groups of any shape and their outliers (in metres for latitude and longitude), and hierarchical clustering, on rows of numbers or maps.statisticsanalyticssegmentsgeomaths7 views · 7 downloadsFinance finance.qwer · 1.0.0Loan payments and amortization tables, compound growth with deposits, net present value and internal rate of return, in cents.moneyloanscalculatorsmaths7 views · 6 downloadsForecasting forecast.qwer · 1.0.0Forecasts from a series of numbers: moving averages, exponential smoothing, Holt's trend and Holt-Winters seasons with their weights fitted, the best model chosen on held-out data, prediction intervals, and accuracy measures.forecastingstatisticsanalyticsplanning8 views · 9 downloadsFractions fractions.qwer · 1.0.0Exact fractions for recipes, carpentry and school maths: "1 1/2", "3/8" and "12.5%" read and calculated without rounding, expressions worked out, decimals turned into simple fractions, measures rounded to 1/16 inch, and recipes scaled.fractionsmathscalculatorsexact7 views · 9 downloadsFrequency spectrum fft.qwer · 1.0.0The frequency spectrum of samples from a microphone or sensor: frequencies and their strength, the dominant ones found between bins, and the signal's level.signalsaudiosensorsmaths8 views · 7 downloads
Thread
79Useful? Like it, or add to its thread below.

Thread 0 posts

On Optimization: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.