LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
decomposition.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_OPT_DECOMPOSITION_H
6#define LINE_OPT_DECOMPOSITION_H
7
8/**
9 * @file
10 * @ingroup line_opt
11 * Solving a large problem as a sequence of smaller ones.
12 *
13 * A `SubProblem` names a subset of the decision variables and fixes the rest.
14 * `DecompositionWorkflow::solve_sequential` cycles over the sub-problems,
15 * solving each with the others held at their current values and feeding the
16 * result forward, until the objective stops improving by more than the tolerance
17 * or the cycle limit is reached.
18 *
19 * This is a heuristic, not a decomposition theorem: it converges to a point that
20 * is optimal in each block separately, which need not be the joint optimum. It
21 * earns its place when the joint search space is too large for the population to
22 * cover, and `WorkflowResult` reports the per-cycle history so the trade is
23 * visible rather than assumed.
24 */
25
26#include <algorithm>
27#include <chrono>
28#include <cmath>
29#include <limits>
30#include <map>
31#include <set>
32#include <string>
33#include <utility>
34#include <vector>
35
37
38namespace line {
39namespace opt {
40
41struct SubProblem {
42 std::string name;
43 std::string variable_type;
44 std::vector<VariablePtr> variables;
46
47 std::vector<std::string> variable_names() const {
48 std::vector<std::string> names;
49 names.reserve(variables.size());
50 for (const VariablePtr& variable : variables) names.push_back(variable->name());
51 return names;
52 }
53};
54
60
62 double final_objective = std::numeric_limits<double>::infinity();
63 std::map<std::string, SubProblemResult> subproblem_results;
64 std::size_t cycles_completed = 0;
65 bool converged = false;
66 double total_solve_time = 0.0;
67 std::vector<double> objective_history;
69 std::vector<std::string> frozen_layers;
70 std::size_t model_evaluations = 0;
71
72 const SubProblemResult* subproblem_result(const std::string& name) const {
73 const auto found = subproblem_results.find(name);
74 return found == subproblem_results.end() ? nullptr : &found->second;
75 }
76 const Value* final_variable_value(const std::string& name) const {
77 const auto found = final_variable_values.find(name);
78 return found == final_variable_values.end() ? nullptr : &found->second;
79 }
80};
81
83public:
84 explicit DecompositionWorkflow(const OptimizationProblem& problem) : problem_(problem) {}
85
86 static const std::vector<std::string>& default_order() {
87 static const std::vector<std::string> order = {
88 "server_allocation", "station_replicas", "service_rate", "job_population",
89 "class_priority", "routing", "class_mapping", "processor_multiplicity",
90 "task_multiplicity", "task_replication", "host_demand", "think_time"};
91 return order;
92 }
93
95 solver_options_ = std::move(options);
96 return *this;
97 }
98
100 std::map<std::string, std::vector<VariablePtr>> by_type;
101 std::vector<std::string> discovered;
102 for (const VariablePtr& variable : problem_.variables()) {
103 const std::string type = variable->type();
104 if (by_type.find(type) == by_type.end()) discovered.push_back(type);
105 by_type[type].push_back(variable);
106 }
107 subproblems_.clear();
108 std::set<std::string> used;
109 for (const std::string& type : default_order()) {
110 const auto found = by_type.find(type);
111 if (found == by_type.end()) continue;
112 subproblems_.push_back({type, type, found->second, {}});
113 used.insert(type);
114 }
115 for (const std::string& type : discovered)
116 if (used.find(type) == used.end())
117 subproblems_.push_back({type, type, by_type.at(type), {}});
118 return *this;
119 }
120
121 DecompositionWorkflow& set_dependency(const std::string& from_problem,
122 const std::string& to_problem) {
123 dependencies_[to_problem].push_back(from_problem);
124 return *this;
125 }
126
128 std::vector<VariablePtr> variables,
129 std::vector<std::string> after = {}) {
130 const std::string type = variables.empty() ? "custom" : variables.front()->type();
131 const std::string dependency_target = name;
132 subproblems_.push_back({std::move(name), type, std::move(variables), {}});
133 for (const std::string& dependency : after)
134 set_dependency(dependency, dependency_target);
135 return *this;
136 }
137
138 const std::vector<SubProblem>& subproblems() const { return subproblems_; }
139
140 std::vector<SubProblem> execution_order() const {
141 if (dependencies_.empty()) return subproblems_;
142 std::map<std::string, std::size_t> indegree;
143 std::map<std::string, std::vector<std::string>> adjacent;
144 std::map<std::string, SubProblem> by_name;
145 for (const SubProblem& subproblem : subproblems_) {
146 indegree[subproblem.name] = 0;
147 adjacent[subproblem.name] = {};
148 by_name[subproblem.name] = subproblem;
149 }
150 for (const auto& target : dependencies_) {
151 if (indegree.find(target.first) == indegree.end()) continue;
152 for (const std::string& source : target.second) {
153 if (adjacent.find(source) == adjacent.end()) continue;
154 adjacent[source].push_back(target.first);
155 ++indegree[target.first];
156 }
157 }
158 std::vector<std::string> queue;
159 for (const SubProblem& subproblem : subproblems_)
160 if (indegree[subproblem.name] == 0) queue.push_back(subproblem.name);
161 std::vector<SubProblem> ordered;
162 for (std::size_t head = 0; head < queue.size(); ++head) {
163 const std::string current = queue[head];
164 ordered.push_back(by_name.at(current));
165 for (const std::string& successor : adjacent[current])
166 if (--indegree[successor] == 0) queue.push_back(successor);
167 }
168 return ordered.size() == subproblems_.size() ? ordered : subproblems_;
169 }
170
171 WorkflowResult solve_sequential(std::size_t max_cycles = 10, double tolerance = 0.01) const {
172 const auto start = std::chrono::steady_clock::now();
173 WorkflowResult out;
174 if (subproblems_.empty()) {
175 out.converged = true;
176 return out;
177 }
178 const std::vector<SubProblem> ordered = execution_order();
179 VariableValues fixed_values;
180 double previous_objective = std::numeric_limits<double>::infinity();
181 for (std::size_t cycle = 1; cycle <= max_cycles; ++cycle) {
182 for (const SubProblem& subproblem : ordered) {
183 OptimizationProblem partial = create_partial_problem(subproblem, fixed_values);
184 OptimizationResult solved = LineOptSolver(partial, solver_options_).solve();
185 SubProblemResult subresult{subproblem.name, solved, fixed_values};
187 out.subproblem_results[subproblem.name] = std::move(subresult);
188 for (const auto& value : solved.variable_values)
189 fixed_values[value.first] = value.second;
190 }
191 const double current_objective = evaluate_full_objective(fixed_values);
192 ++out.model_evaluations;
193 out.objective_history.push_back(current_objective);
194 if (std::abs(current_objective - previous_objective) < tolerance) {
195 out.converged = true;
196 break;
197 }
198 previous_objective = current_objective;
199 out.cycles_completed = cycle;
200 }
201 if (!out.objective_history.empty()) out.final_objective = out.objective_history.back();
202 out.final_variable_values = std::move(fixed_values);
203 out.total_solve_time = std::chrono::duration<double>(
204 std::chrono::steady_clock::now() - start).count();
205 return out;
206 }
207
209
210 WorkflowResult solve_layered(std::size_t max_cycles = 10, double tolerance = 0.01,
211 bool auto_freeze = true, double freeze_tolerance = 1e-3,
212 std::vector<std::string> frozen_layers = {}) const {
213 if (!problem_.is_layered()) return solve_sequential(max_cycles, tolerance);
214 const auto start = std::chrono::steady_clock::now();
215 WorkflowResult out;
216 const lqn::LqnModel<double>& model = problem_.layered_model();
217 std::map<std::string, std::vector<VariablePtr>> groups;
218 std::vector<std::string> group_order;
219 for (const VariablePtr& variable : problem_.variables()) {
220 const std::vector<std::string> owned = variable->layers(model);
221 const std::string layer = owned.empty() ? "_nolayer" : owned.front();
222 if (groups.find(layer) == groups.end()) group_order.push_back(layer);
223 groups[layer].push_back(variable);
224 }
225
226 VariableValues fixed_values;
227 std::map<std::string, std::vector<double>> previous_signatures;
228 bool have_previous_signatures = false;
229 double previous_objective = std::numeric_limits<double>::infinity();
230 LineEvaluator full_evaluator(problem_.model_variant(), problem_.variables(),
231 problem_.fixed_variables());
232 for (std::size_t cycle = 1; cycle <= max_cycles; ++cycle) {
233 for (const std::string& layer : group_order) {
234 if (std::find(frozen_layers.begin(), frozen_layers.end(), layer) !=
235 frozen_layers.end())
236 continue;
237 const SubProblem subproblem{layer, groups[layer].front()->type(),
238 groups[layer], {}};
239 OptimizationProblem partial = create_partial_problem(subproblem, fixed_values);
240 OptimizationResult solved = LineOptSolver(partial, solver_options_).solve();
241 out.model_evaluations += solved.model_evaluations;
242 out.subproblem_results[layer] = {layer, solved, fixed_values};
243 for (const auto& value : solved.variable_values)
244 fixed_values[value.first] = value.second;
245 }
246
247 const EvaluationResult evaluation = full_evaluator.evaluate(fixed_values);
248 ++out.model_evaluations;
249 double current_objective = std::numeric_limits<double>::infinity();
250 std::map<std::string, std::vector<double>> signatures;
251 if (evaluation.feasible) {
252 VariableValues all_values;
253 for (const auto& fixed : problem_.fixed_variables())
254 all_values[fixed.first->name()] = fixed.second;
255 for (const auto& value : fixed_values) all_values[value.first] = value.second;
256 current_objective = problem_.objective()->evaluate_with_penalty(
257 evaluation, all_values, solver_options_.penalty_weight);
258 for (const ConstraintPtr& constraint : problem_.constraints())
259 current_objective += constraint->evaluate(evaluation, all_values) *
260 solver_options_.penalty_weight;
261 signatures = layer_signatures(evaluation, group_order);
262 }
263
264 if (auto_freeze && have_previous_signatures) {
265 std::map<std::string, double> moved;
266 bool active_moved = false;
267 for (const std::string& layer : group_order) {
268 const auto before = previous_signatures.find(layer);
269 const auto after = signatures.find(layer);
270 moved[layer] = signature_delta(
271 before == previous_signatures.end() ? std::vector<double>()
272 : before->second,
273 after == signatures.end() ? std::vector<double>() : after->second);
274 if (std::find(frozen_layers.begin(), frozen_layers.end(), layer) ==
275 frozen_layers.end() &&
276 moved[layer] > freeze_tolerance)
277 active_moved = true;
278 }
279 for (const std::string& layer : group_order) {
280 const auto frozen = std::find(frozen_layers.begin(), frozen_layers.end(), layer);
281 if (frozen != frozen_layers.end()) {
282 if (active_moved) frozen_layers.erase(frozen);
283 } else if (moved[layer] < freeze_tolerance) {
284 frozen_layers.push_back(layer);
285 }
286 }
287 }
288
289 previous_signatures = std::move(signatures);
290 have_previous_signatures = true;
291 out.objective_history.push_back(current_objective);
292 if (std::abs(current_objective - previous_objective) < tolerance) {
293 out.converged = true;
294 break;
295 }
296 previous_objective = current_objective;
297 out.cycles_completed = cycle;
298 if (frozen_layers.size() >= group_order.size()) {
299 out.converged = true;
300 break;
301 }
302 }
303 if (!out.objective_history.empty()) out.final_objective = out.objective_history.back();
304 out.final_variable_values = std::move(fixed_values);
305 std::sort(frozen_layers.begin(), frozen_layers.end());
306 frozen_layers.erase(std::unique(frozen_layers.begin(), frozen_layers.end()),
307 frozen_layers.end());
308 out.frozen_layers = std::move(frozen_layers);
309 out.total_solve_time = std::chrono::duration<double>(
310 std::chrono::steady_clock::now() - start).count();
311 return out;
312 }
313
314private:
315 static std::map<std::string, std::vector<double>> layer_signatures(
316 const EvaluationResult& result, const std::vector<std::string>& layers) {
317 std::map<std::string, std::vector<double>> out;
318 for (const std::string& layer : layers) {
319 const double utilization = result.utilization(layer);
320 const double queue_length = result.queue_length(layer);
321 const double throughput = result.throughput(layer);
322 const double response_time = result.response_time(layer);
323 const double probes[] = {utilization, queue_length, throughput};
324 bool present = false;
325 for (double probe : probes)
326 if (probe != 0.0 && !std::isinf(probe)) present = true;
327 if (present)
328 out[layer] = {utilization, queue_length, throughput, response_time};
329 }
330 return out;
331 }
332
333 static double signature_delta(const std::vector<double>& before,
334 const std::vector<double>& after) {
335 if (before.empty() || after.empty()) return std::numeric_limits<double>::infinity();
336 double delta = 0.0;
337 const std::size_t size = std::min(before.size(), after.size());
338 for (std::size_t i = 0; i < size; ++i) {
339 if (!std::isfinite(before[i]) || !std::isfinite(after[i])) continue;
340 delta = std::max(delta, std::abs(after[i] - before[i]) /
341 (std::abs(before[i]) + 1e-12));
342 }
343 return delta;
344 }
345
346 OptimizationProblem create_partial_problem(const SubProblem& subproblem,
347 const VariableValues& fixed_values) const {
348 OptimizationProblem partial(problem_.model_variant());
349 for (const VariablePtr& variable : subproblem.variables) partial.add_variable(variable);
350 std::set<std::string> subproblem_names;
351 for (const VariablePtr& variable : subproblem.variables)
352 subproblem_names.insert(variable->name());
353 std::vector<std::pair<VariablePtr, Value>> fixed = problem_.fixed_variables();
354 for (const VariablePtr& variable : problem_.variables()) {
355 if (subproblem_names.find(variable->name()) != subproblem_names.end()) continue;
356 const auto found = fixed_values.find(variable->name());
357 if (found != fixed_values.end()) fixed.push_back({variable, found->second});
358 }
359 partial.set_fixed_variables(std::move(fixed));
360 partial.set_objective(problem_.objective());
361 for (const ConstraintPtr& constraint : problem_.constraints())
362 partial.add_constraint(constraint);
363 for (const Scenario& scenario : problem_.scenarios())
364 partial.add_scenario(scenario.model, scenario.weight);
365 return partial;
366 }
367
368 double evaluate_full_objective(const VariableValues& values) const {
369 LineEvaluator evaluator(problem_.model_variant(), problem_.variables(),
370 problem_.fixed_variables());
371 const EvaluationResult result = evaluator.evaluate(values);
372 if (!result.feasible) return std::numeric_limits<double>::infinity();
373 VariableValues all_values;
374 for (const auto& fixed : problem_.fixed_variables())
375 all_values[fixed.first->name()] = fixed.second;
376 for (const auto& value : values) all_values[value.first] = value.second;
377 double objective = problem_.objective()->evaluate_with_penalty(
378 result, all_values, solver_options_.penalty_weight);
379 for (const ConstraintPtr& constraint : problem_.constraints())
380 objective += constraint->evaluate(result, all_values) * solver_options_.penalty_weight;
381 return objective;
382 }
383
384 const OptimizationProblem& problem_;
385 std::vector<SubProblem> subproblems_;
386 std::map<std::string, std::vector<std::string>> dependencies_;
387 LineOptSolverOptions solver_options_;
388};
389
390} // namespace opt
391} // namespace line
392
393#endif
DecompositionWorkflow & add_subproblem(std::string name, std::vector< VariablePtr > variables, std::vector< std::string > after={})
std::vector< SubProblem > execution_order() const
const std::vector< SubProblem > & subproblems() const
static const std::vector< std::string > & default_order()
WorkflowResult solve_hierarchical() const
DecompositionWorkflow(const OptimizationProblem &problem)
DecompositionWorkflow & set_dependency(const std::string &from_problem, const std::string &to_problem)
WorkflowResult solve_layered(std::size_t max_cycles=10, double tolerance=0.01, bool auto_freeze=true, double freeze_tolerance=1e-3, std::vector< std::string > frozen_layers={}) const
WorkflowResult solve_sequential(std::size_t max_cycles=10, double tolerance=0.01) const
DecompositionWorkflow & auto_decompose()
DecompositionWorkflow & set_solver_options(LineOptSolverOptions options)
LineOptSolver(const OptimizationProblem &problem, LineOptSolverOptions options={}, LineEvaluator::SolveFunction solve={})
const std::vector< VariablePtr > & variables() const
Definition problem.h:109
const lqn::LqnModel< double > & layered_model() const
Definition problem.h:105
const OptimizationModel & model_variant() const
Definition problem.h:108
const std::vector< std::pair< VariablePtr, Value > > & fixed_variables() const
Definition problem.h:112
The optimizer: search the variable space for the best feasible point.
std::shared_ptr< Constraint > ConstraintPtr
Definition objectives.h:34
std::shared_ptr< DecisionVariable > VariablePtr
Definition variables.h:160
std::map< std::string, Value > VariableValues
Definition results.h:33
std::vector< double > Value
Definition results.h:32
The intermediate model, and the second stage that flattens it.
Definition lqn_reader.h:399
VariableValues variable_values
Definition results.h:100
OptimizationResult result
VariableValues fixed_values
std::vector< VariablePtr > variables
std::string variable_type
std::vector< std::string > variable_names() const
std::vector< double > objective_history
const SubProblemResult * subproblem_result(const std::string &name) const
std::map< std::string, SubProblemResult > subproblem_results
VariableValues final_variable_values
std::vector< std::string > frozen_layers
const Value * final_variable_value(const std::string &name) const