1use crate::base::{CellRef, RangeRef, Value};
28use crate::model::{Cell, DefinedName, Sheet, Workbook};
29use serde_json::{Value as Json, json};
30
31use super::check_arrays;
32use super::whatif::Trial;
33use super::{CommandSpec, Ctx, DocState, EngineError, Result, Session, bad, bool_param, cmd, edit, f64_param, has_doc, str_param};
34use crate::base::split_sheet;
35
36mod evolutionary;
37mod lp;
38mod model;
39mod model_range;
40mod nlp;
41mod problem;
42mod reports;
43mod sensitivity;
44
45use evolutionary::{EvoEnd, evolutionary};
46use lp::{Outcome, SimplexProgram, branch_and_bound, simplex_program, solve_lp};
47use model::{Goal, Method, Model, model_params, parse_model, save_model};
48pub use model::{saved_model, sensitivity_applies, sensitivity_status};
49use model_range::{load_model_cells, reset_model, save_model_cells};
50use nlp::{nlp, project};
51use problem::{Evaluator, as_num, build_problem};
52use reports::{NotOptimal, ReportInput, insert_report_selections, prepare_reports, report, report_kinds, write_reports};
53
54const CMD: &str = "data.solver";
55const REPORT_CMD: &str = "data.solverReport";
56const SAVE_CMD: &str = "data.solverSaveModel";
57const LOAD_CMD: &str = "data.solverLoadModel";
58const MAX_VARS: usize = 200;
60const MAX_CONSTRAINTS: usize = 1000;
62const DEFAULT_EVALS: u64 = 20_000;
63const MAX_EVALS: u64 = 200_000;
64const LP_WORK: u64 = 2_000_000_000;
66const MAX_NODES_LP: usize = 10_000;
67const MAX_NODES_NLP: usize = 200;
68const MIN_POPULATION: usize = 10;
70const MAX_POPULATION: usize = 1000;
71const PRECISION: f64 = 1e-6;
73const EPS: f64 = 1e-9;
74
75pub fn specs() -> Vec<CommandSpec> {
76 vec![
77 cmd!(
78 "data.solver",
79 "Solver…",
80 ["Data", "Analysis"],
81 None,
82 "{objective: \"B10\" (formula cell), goal?: max|min|value (default max), value?: number (for goal value), variables: \"B2:B4,D1\" (≤200 constant cells, same sheet), constraints?: [\"B5 <= 10\", \"B2:B4 >= C2:C4\", \"B2 int\", \"B3 bin\", {lhs: \"B6\", op: \"=\", rhs: 3 | \"C6\"}], nonNegative?: bool (default true: variables without a lower bound get 0), method?: grg|simplex|evolutionary (default grg), populationSize?: 100, mutationRate?: 0.075, randomSeed?: 0 (a fixed seed, so runs repeat), convergence?: 0.0001, generationsWithoutImprovement?: 50, requireBounds?: bool (default true: every variable needs both bounds; false searches within ±100 of the start) (Evolutionary options), keep?: bool (default true; false restores the original values), saveModel?: bool (default true: hidden names, solver_* as Excel names them, with the options; the evaluation limit and generations without improvement, which Excel lacks, as gridcraft_solver_evals and gridcraft_solver_stall), maxEvaluations?: 20000 (at most 200000), integerOptimality?: percent (default 0), saved?: bool (solve the sheet's saved model; other params override), reports?: [answer|sensitivity|limits] (written on new sheets when a solution is found)} → {status: optimal|converged|cannotImprove|iterationLimit|infeasible|unbounded|notLinear, message, objective, values: {cell: value}, original: {cell: value}, model: the model as params, evaluations, changed, reports?: [sheet names], skippedReports?: [{report: \"sensitivity\", reason}] (a Sensitivity report left out because the run did not reach an optimum or the values are not one; the solve and the other reports still apply)}. No `objective` → {model: the saved model} (the menu opens the dialog).",
83 has_doc,
84 run
85 ),
86 cmd!(
87 "data.solverReport",
88 "Solver Reports",
89 [],
90 None,
91 "{reports: [answer|sensitivity|limits], values?: {cell: number} (the solution; default the variable cells' current values), original?: {cell: number} (the values before solving; default `values`), message?: text (the result line), skipNotOptimal?: bool (default false: a Sensitivity report for values that are not optimal refuses the call; true leaves it out and writes the others), model?: data.solver params (default the sheet's saved model, with any data.solver params given here over it)} → {sheets: [names], skippedReports?: [{report, reason}]}. Writes Answer, Sensitivity and Limits reports on new sheets before the model's sheet; Sensitivity and Limits need Simplex LP or GRG Nonlinear and no integer constraints, and Sensitivity a Max or Min objective. data.solver's result can be passed as is.",
92 has_doc,
93 report
94 ),
95 cmd!(
96 "data.solverSaveModel",
97 "Save Model",
98 [],
99 None,
100 "{to: \"H1\" (the first cell, on the model's sheet), model?: data.solver params (default the sheet's saved model), overwrite?: false} → {range}. Writes the model down a column as formulas: =MAX(objective), =MIN(objective) or =objective=value; =COUNT(variables); one =AND(relation) per constraint (=AND(x=INT(x)) for integer, =AND((x=0)+(x=1)) for binary), which shows TRUE while it holds; and the options as an array constant {method (1 GRG Nonlinear, 2 Simplex LP, 3 Evolutionary), non-negative (1 yes, 2 no), population size, mutation rate, random seed, require bounds (1 yes, 2 no), convergence, generations without improvement, maximum evaluations, integer optimality %}, which spills into the cells to its right. Refused, with the sheet unchanged, when the model's cells, content, another formula's spilled values, merged cells or a table are in the way of the options' spill, or it would run off the sheet, and when the column would cover another formula's spilled values. A column that holds a value or a formula (a model saved there before included) is replaced only with overwrite: true; without it the command is refused (\"already holds data\"), as Excel asks first. One undo step.",
101 has_doc,
102 save_model_cells
103 ),
104 cmd!(
105 "data.solverLoadModel",
106 "Load Model",
107 [],
108 None,
109 "{from: \"H1:H6\" (a range on the active sheet; one cell reads down to the first cell without a formula; a range needs a formula in every cell up to its last formula, and may end in empty cells)} → {model}. Reads a model written by Save Model and makes it the sheet's Solver model. The model must end with its options row ({…}), which Save Model writes last; a model cut short (a cell before the options row without a formula, or the options row cleared) is refused and the sheet's model stays as it was.",
110 has_doc,
111 load_model_cells
112 ),
113 cmd!(
114 "data.solverReset",
115 "Reset All",
116 [],
117 None,
118 "{} → {removed, masked}. Removes the active sheet's Solver model. A model read from workbook-level names stays for the other sheets and is masked on this one by an empty sheet-level solver_opt (masked: true).",
119 has_doc,
120 reset_model
121 ),
122 ]
123}
124
125struct Solve {
127 outcome: Outcome,
128 not_linear: bool,
129 evo_end: Option<EvoEnd>,
130}
131
132#[derive(Clone, Copy)]
134struct Bounds<'a> {
135 lo: &'a [f64],
136 hi: &'a [f64],
137 integer: &'a [bool],
138}
139
140fn optimise(ev: &mut Evaluator, m: &Model, goal: Goal, x0: &[f64], bounds: Bounds, gap: f64) -> Solve {
142 let Bounds { lo, hi, integer } = bounds;
143 let any_int = integer.iter().any(|b| *b);
144 let mut not_linear = false;
145 let mut evo_end = None;
146 let outcome = if lo.iter().zip(hi).any(|(l, h)| l > h) {
147 Outcome::Infeasible
148 } else {
149 match m.method {
150 Method::Simplex => match simplex_program(ev, goal, x0) {
151 Err("nonlinear") | Err("error") => {
152 not_linear = true;
153 Outcome::Infeasible
154 }
155 Err(_) => Outcome::Limit,
156 Ok(SimplexProgram { c, rows, mut work, .. }) => {
157 let mut relax = |l: &[f64], h: &[f64]| solve_lp(&c, &rows, l, h, &mut work);
158 if any_int { branch_and_bound(lo, hi, integer, MAX_NODES_LP, gap, &mut relax).0 } else { relax(lo, hi) }
159 }
160 },
161 Method::Grg => {
162 if any_int {
163 let mut relax = |l: &[f64], h: &[f64]| {
164 let mut start = x0.to_vec();
165 project(&mut start, l, h);
166 nlp(ev, &start, l, h, goal)
167 };
168 branch_and_bound(lo, hi, integer, MAX_NODES_NLP, gap, &mut relax).0
169 } else {
170 nlp(ev, x0, lo, hi, goal)
171 }
172 }
173 Method::Evolutionary => {
174 let (out, end) = evolutionary(ev, x0, lo, hi, integer, goal, &m.evo);
175 evo_end = Some(end);
176 out
177 }
178 }
179 };
180 Solve { outcome, not_linear, evo_end }
181}
182
183fn run(s: &mut Session, p: &Json) -> Result<Json> {
186 let d = s.doc()?;
187 let active = d.wb.active_sheet();
188 let mut params = p.clone();
189 if bool_param(p, "saved").unwrap_or(false) {
190 let saved = saved_model(&d.wb, active).ok_or_else(|| EngineError::Other("This sheet has no saved Solver model.".into()))?;
191 if let (Some(base), Some(over)) = (saved.as_object(), p.as_object()) {
192 let mut merged = base.clone();
193 for (k, v) in over {
194 merged.insert(k.clone(), v.clone());
195 }
196 params = Json::Object(merged);
197 }
198 }
199 let has_objective = ["objective", "setObjective", "set"].iter().any(|k| str_param(¶ms, k).is_some_and(|t| !t.trim().is_empty()));
200 if !has_objective {
201 let model = saved_model(&d.wb, active);
202 s.ui_requests.push(crate::engine::UiRequest::Dialog("solver".into(), json!({})));
203 return Ok(json!({"model": model}));
204 }
205 let model = parse_model(s, ¶ms)?;
206 let (budget, gap) = (model.max_evals, model.gap);
207 let keep = bool_param(¶ms, "keep").unwrap_or(true);
208 let save = bool_param(¶ms, "saveModel").unwrap_or(true);
209 let reports = report_kinds(CMD, ¶ms)?;
210 let prob = build_problem(&model);
211 if let Some(kinds) = &reports
214 && kinds.contains(&reports::ReportKind::Sensitivity)
215 && let Some(why) = sensitivity::sensitivity_refusal(&model, &prob)
216 {
217 return Err(EngineError::Other(why.into()));
218 }
219 let d = s.doc()?;
220 let sh = d.wb.sheet(model.sheet).ok_or(EngineError::NoDocument)?;
221 let x0: Vec<f64> = model.vars.iter().map(|c| as_num(&sh.value(*c))).map(|v| if v.is_finite() { v } else { 0.0 }).collect();
222 let mut ev = Evaluator {
223 trial: Trial::new(d),
224 sheet: model.sheet,
225 vars: model.vars.clone(),
226 objective: model.objective,
227 rows: prob.rows.clone(),
228 evals: 0,
229 budget,
230 target: None,
231 };
232 if model.method == Method::Evolutionary && model.evo.require_bounds && prob.lo.iter().chain(&prob.hi).any(|b| !b.is_finite()) {
233 return Err(EngineError::Other(
234 "The Evolutionary method needs a lower and an upper bound on every variable cell (constraints such as \"A1:A3 <= 10\"), or requireBounds: false."
235 .into(),
236 ));
237 }
238 let Solve { outcome, not_linear, evo_end } =
239 optimise(&mut ev, &model, model.goal, &x0, Bounds { lo: &prob.lo, hi: &prob.hi, integer: &prob.integer }, gap);
240 let (status, message, solution) = match outcome {
241 _ if not_linear => ("notLinear", "The model isn't linear in the variable cells, so Simplex LP can't solve it. Try GRG Nonlinear.", None),
242 Outcome::Solved(x, _, true) if model.method == Method::Evolutionary => match evo_end {
243 Some(EvoEnd::Stalled) => {
244 ("cannotImprove", "Solver stopped when the best solution stopped improving. All constraints are satisfied.", Some(x))
245 }
246 _ => ("converged", "The population converged. The best solution found satisfies all constraints.", Some(x)),
247 },
248 Outcome::Solved(x, _, true) => (
249 if model.method == Method::Simplex { "optimal" } else { "converged" },
250 if model.method == Method::Simplex {
251 "Solver found an optimal solution. All constraints are satisfied."
252 } else {
253 "Solver converged to a locally optimal solution. All constraints are satisfied."
254 },
255 Some(x),
256 ),
257 Outcome::Solved(x, _, false) => {
258 ("iterationLimit", "Solver stopped at its evaluation limit; the best solution found satisfies the constraints.", Some(x))
259 }
260 Outcome::Limit => ("iterationLimit", "Solver stopped at its evaluation limit without finding a feasible solution.", None),
261 Outcome::Infeasible => ("infeasible", "Solver could not find a solution that satisfies all constraints.", None),
262 Outcome::Unbounded => ("unbounded", "The objective can grow without limit: the problem is unbounded.", None),
263 };
264 let evaluations = ev.evals;
265 let objective = solution.as_ref().and_then(|x| {
267 ev.budget = ev.budget.saturating_add(1);
268 ev.eval(x).map(|p| p.f)
269 });
270 let mut values = serde_json::Map::new();
271 if let Some(x) = &solution {
272 for (c, v) in model.vars.iter().zip(x) {
273 values.insert(c.a1(), json!(v));
274 }
275 }
276 let original: serde_json::Map<String, Json> = model.vars.iter().zip(&x0).map(|(c, v)| (c.a1(), json!(v))).collect();
277 let changed = keep && solution.is_some();
278 let (built, skipped) = match (&reports, &solution) {
281 (Some(kinds), Some(x)) => {
282 let inp = ReportInput { model: &model, prob: &prob, original: x0, solution: x.clone(), message: message.to_string(), budget };
283 prepare_reports(s.doc()?, kinds, &inp, NotOptimal::Skip { optimum: sensitivity_status(status) })?
284 }
285 _ => (Vec::new(), Vec::new()),
286 };
287 let count = built.len();
288 let before = s.doc()?.wb.clone();
289 let mut out = json!({
290 "status": status,
291 "message": message,
292 "objective": objective,
293 "values": values,
294 "original": original,
295 "model": model_params(&before, &model),
296 "evaluations": evaluations,
297 "changed": changed,
298 });
299 let names = if changed || save || count > 0 {
300 let sheet = model.sheet;
301 edit(s, |cx| {
302 if changed && let Some(x) = &solution {
303 let sh = cx.sheet_mut(sheet)?;
304 for (c, v) in model.vars.iter().zip(x) {
305 let style = sh.style_id(*c);
306 sh.set_cell(*c, Cell { value: Value::number(*v), formula: None, style, py: None });
307 }
308 for c in &model.vars {
309 cx.touch(sheet + count, *c);
310 }
311 }
312 if save {
313 save_model(&mut cx.wb, &model);
314 }
315 if count > 0 {
316 check_arrays(&before, &cx.wb)?;
318 }
319 Ok(write_reports(cx, sheet, built))
320 })?
321 } else {
322 Vec::new()
323 };
324 insert_report_selections(s, model.sheet, count);
325 if reports.is_some() && solution.is_some() {
326 out["reports"] = json!(names);
327 }
328 if !skipped.is_empty() {
329 out["skippedReports"] = json!(skipped);
330 }
331 Ok(out)
332}
333
334#[cfg(test)]
335mod tests;