156 : problem_(problem), options_(std::move(options)), solve_(std::move(
solve)) {
157 const std::vector<std::string> errors = problem_.
validate();
158 if (!errors.empty())
throw InputError(
"LineOptSolver: invalid optimization problem");
162 for (
const auto& item : fixed_variables_)
163 fixed_values_[item.first->name()] = item.second;
165 evaluators_.emplace_back(problem_.
model_variant(), free_variables_, fixed_variables_, solve_);
166 scenario_weights_.push_back(1.0);
167 for (
const Scenario& scenario : problem_.
scenarios()) {
168 evaluators_.emplace_back(scenario.model, free_variables_, fixed_variables_, solve_);
169 scenario_weights_.push_back(scenario.weight);
171 caches_.resize(evaluators_.size());
175 start_ = Clock::now();
177 best_value_ = std::numeric_limits<double>::infinity();
179 convergence_history_.clear();
181 lqn_sensitivity_cache_.clear();
182 resolved_seed_ = options_.seed.value_or(
static_cast<std::uint64_t
>(
183 std::chrono::duration_cast<std::chrono::nanoseconds>(
184 Clock::now().time_since_epoch()).count()) & UINT64_C(0x7fffffff));
187 const std::vector<std::pair<double, double>> bounds = evaluators_[0].bounds();
188 if (bounds.empty())
return empty_result();
189 if (should_use_gradient())
return solve_gradient(bounds.size());
190 return solve_evolution(bounds.size());
194 using Clock = std::chrono::steady_clock;
197 double elapsed()
const {
198 return std::chrono::duration<double>(Clock::now() - start_).count();
203 for (
const auto& item : values) out[item.first] = item.second;
207 const EvaluationResult& evaluate(std::size_t scenario,
const VariableValues& values) {
208 auto found = caches_[scenario].find(values);
209 if (found != caches_[scenario].end())
return found->second;
210 EvaluationResult result = evaluators_[scenario].evaluate(values);
211 return caches_[scenario].emplace(values, std::move(result)).first->second;
214 double aggregate_scenarios(
const std::vector<double>& values)
const {
215 if (values.size() == 1)
return values[0];
216 if (options_.scenario_aggregation ==
"mean") {
217 double weighted = 0.0, weights = 0.0;
218 for (std::size_t i = 0; i < values.size(); ++i) {
219 weighted += scenario_weights_[i] * values[i];
220 weights += scenario_weights_[i];
222 return weighted / weights;
224 if (options_.scenario_aggregation !=
"worst")
225 throw InputError(
"LineOptSolver: unknown scenario aggregation '" +
226 options_.scenario_aggregation +
"'");
227 return *std::max_element(values.begin(), values.end());
230 double objective_function(
const std::vector<double>& x) {
231 if (elapsed() >= options_.time_limit)
throw TimeLimit();
234 std::vector<double> scenario_values(evaluators_.size(), 0.0);
235 for (std::size_t i = 0; i < evaluators_.size(); ++i) {
236 const EvaluationResult& result = evaluate(i, values);
237 if (!result.feasible)
return std::numeric_limits<double>::infinity();
238 double value = problem_.objective()->evaluate_with_penalty(
239 result, all_values, options_.penalty_weight);
240 for (
const ConstraintPtr& constraint : problem_.constraints())
241 value += constraint->evaluate(result, all_values) * options_.penalty_weight;
242 scenario_values[i] = value;
244 const double total = aggregate_scenarios(scenario_values);
245 if (total < best_value_) {
252 OptimizationResult solve_evolution(std::size_t dimension) {
253 std::vector<double> low(dimension, 0.0), high(dimension, 1.0);
254 de::DifferentialEvolution engine(
255 [
this](
const std::vector<double>& x) {
return objective_function(x); }, low, high,
256 options_.strategy, options_.popsize, options_.max_iterations,
257 options_.mutation_low, options_.mutation_high, options_.recombination,
258 options_.tolerance, resolved_seed_);
259 engine.set_callback([
this](
const std::vector<double>&, std::size_t nit) {
261 convergence_history_.push_back(best_value_);
262 return elapsed() >= options_.time_limit;
265 bool timed_out =
false;
266 std::vector<double> x;
267 double value = std::numeric_limits<double>::infinity();
269 const de::DifferentialEvolutionResult result = engine.solve();
270 if (!best_x_.empty() && best_value_ <= result.fun) {
277 }
catch (
const TimeLimit&) {
279 if (best_x_.empty())
return empty_result();
283 OptimizationResult result = build_result(x, value);
284 if (timed_out) result.terminated_by =
"time_limit";
288 bool all_continuous()
const {
289 if (free_variables_.empty())
return false;
290 for (
const VariablePtr& variable : free_variables_) {
291 const std::string type = variable->type();
292 if (type !=
"service_rate" && type !=
"routing" && type !=
"host_demand" &&
293 type !=
"think_time")
299 bool should_use_gradient()
const {
300 if (options_.optimizer ==
"gradient")
return true;
301 if (options_.optimizer ==
"evolution")
return false;
302 if (options_.optimizer ==
"auto")
return all_continuous();
303 throw InputError(
"LineOptSolver: unknown optimizer '" + options_.optimizer +
"'");
306 std::vector<double> finite_difference_gradient(
const std::vector<double>& x) {
307 const double h = evaluators_[0].is_layered() ? options_.fd_step_layered
309 std::vector<double> gradient(x.size(), 0.0);
310 std::optional<double> centre;
311 for (std::size_t i = 0; i < x.size(); ++i) {
312 std::vector<double> plus = x, minus = x;
313 plus[i] = std::min(1.0, x[i] + h);
314 minus[i] = std::max(0.0, x[i] - h);
315 const double fp = objective_function(plus);
316 const double fm = objective_function(minus);
317 if (std::isfinite(fp) && std::isfinite(fm) && plus[i] > minus[i]) {
318 gradient[i] = (fp - fm) / (plus[i] - minus[i]);
321 if (!centre) centre = objective_function(x);
322 if (std::isfinite(fp) && std::isfinite(*centre) && plus[i] > x[i])
323 gradient[i] = (fp - *centre) / (plus[i] - x[i]);
324 else if (std::isfinite(fm) && std::isfinite(*centre) && x[i] > minus[i])
325 gradient[i] = (*centre - fm) / (x[i] - minus[i]);
330 std::vector<double> projected_gradient_descent(std::vector<double> x) {
331 for (
double& value : x) value = std::clamp(value, 0.0, 1.0);
332 double f = objective_function(x);
333 for (std::size_t iter = 0; iter < options_.max_iterations; ++iter) {
334 const std::vector<double> gradient = objective_gradient(x);
336 for (
double value : gradient) norm2 += value * value;
337 if (std::sqrt(norm2) < 1e-9)
break;
339 bool improved =
false;
340 for (std::size_t search = 0; search < 30; ++search) {
341 std::vector<double> next(x.size());
342 for (std::size_t i = 0; i < x.size(); ++i)
343 next[i] = std::clamp(x[i] - step * gradient[i], 0.0, 1.0);
344 const double fn = objective_function(next);
345 if (std::isfinite(fn) && fn < f - 1e-12) {
353 if (!improved)
break;
358 OptimizationResult solve_gradient(std::size_t dimension) {
359 const std::size_t starts = std::max<std::size_t>(1, options_.gradient_restarts);
360 de::NumpyRandomState random(resolved_seed_);
361 std::vector<double> best;
362 double best_fun = std::numeric_limits<double>::infinity();
363 bool timed_out =
false;
364 for (std::size_t restart = 0; restart < starts; ++restart) {
365 if (elapsed() >= options_.time_limit) {
369 std::vector<double> initial =
370 restart == 0 ? std::vector<double>(dimension, 0.5)
371 : random.uniform(0.0, 1.0, dimension);
373 std::vector<double> candidate = projected_gradient_descent(initial);
374 const double value = objective_function(candidate);
375 if (std::isfinite(value) && value < best_fun) {
376 best = std::move(candidate);
379 }
catch (
const TimeLimit&) {
384 if (best.empty() || (!best_x_.empty() && best_value_ < best_fun)) {
385 if (!best_x_.empty()) {
387 best_fun = best_value_;
389 return empty_result();
392 OptimizationResult result = build_result(best, best_fun);
393 if (timed_out) result.terminated_by =
"time_limit";
397 void freeze_layers() {
398 if (!problem_.is_layered() || options_.frozen_layers.empty())
return;
399 const lqn::LqnModel<double>& model = problem_.layered_model();
400 std::set<std::string> frozen(options_.frozen_layers.begin(), options_.frozen_layers.end());
401 std::set<std::string> already;
402 for (
const auto& fixed : fixed_variables_) already.insert(fixed.first->name());
403 std::vector<VariablePtr> kept;
404 for (
const VariablePtr& variable : free_variables_) {
405 const std::vector<std::string> variable_layers = variable->layers(model);
406 bool intersects =
false;
407 for (
const std::string& layer : variable_layers)
408 if (frozen.find(layer) != frozen.end()) intersects =
true;
410 kept.push_back(variable);
413 if (already.find(variable->name()) != already.end())
continue;
414 const std::optional<Value> value = variable->current_value(model);
416 fixed_variables_.push_back({variable, *value});
417 already.insert(variable->name());
420 free_variables_ = std::move(kept);
423 std::vector<double> objective_gradient(
const std::vector<double>& x) {
424 if (!evaluators_[0].is_layered())
return finite_difference_gradient(x);
425 const std::string& mode = options_.lqn_gradient;
426 if (mode ==
"partial_sens" || mode ==
"partial_plus_fd") {
428 const std::size_t refresh = std::max<std::size_t>(1, options_.fd_refresh);
429 if (!(mode ==
"partial_plus_fd" && gradient_calls_ % refresh == 0)) {
430 const std::optional<std::vector<double>> gradient =
431 lqn_analytic_gradient(x);
432 if (gradient)
return *gradient;
435 return finite_difference_gradient(x);
438 double scalar_objective(
const EvaluationResult& result,
440 double out = problem_.objective()->evaluate_with_penalty(
441 result, values, options_.penalty_weight);
442 for (
const ConstraintPtr& constraint : problem_.constraints())
443 out += constraint->evaluate(result, values) * options_.penalty_weight;
447 double scalar_metric_derivative(
const EvaluationResult& base,
449 const std::string& kind,
450 const std::string& key)
const {
451 const double h = options_.fd_step;
452 EvaluationResult plus = base, minus = base;
453 std::map<std::string, double>* plus_map =
nullptr;
454 std::map<std::string, double>* minus_map =
nullptr;
455 if (kind ==
"RespT") {
456 plus_map = &plus.response_times;
457 minus_map = &minus.response_times;
458 }
else if (kind ==
"QLen") {
459 plus_map = &plus.queue_lengths;
460 minus_map = &minus.queue_lengths;
461 }
else if (kind ==
"Tput") {
462 plus_map = &plus.throughputs;
463 minus_map = &minus.throughputs;
464 }
else if (kind ==
"Util") {
465 plus_map = &plus.utilizations;
466 minus_map = &minus.utilizations;
470 const auto found = plus_map->find(key);
471 if (found == plus_map->end())
return 0.0;
472 (*plus_map)[key] = found->second + h;
473 (*minus_map)[key] = found->second - h;
474 return (scalar_objective(plus, values) - scalar_objective(minus, values)) /
478 std::optional<std::vector<double>> lqn_analytic_gradient(
479 const std::vector<double>& x) {
480 if (evaluators_.size() != 1 || free_variables_.empty())
return std::nullopt;
481 for (
const VariablePtr& variable : free_variables_)
482 if (!variable->supports_sensitivity())
return std::nullopt;
485 const EvaluationResult& result = evaluate(0, values);
486 if (!result.feasible)
return std::nullopt;
488 auto cached = lqn_sensitivity_cache_.find(values);
489 if (cached == lqn_sensitivity_cache_.end())
490 cached = lqn_sensitivity_cache_.emplace(
491 values, evaluators_[0].evaluate_layered_sensitivities(values)).first;
492 if (!cached->second)
return std::nullopt;
494 const lqn::LqnModel<double>& model = problem_.layered_model();
495 std::vector<double> gradient(x.size(), 0.0);
496 std::size_t offset = 0;
497 for (
const VariablePtr& variable : free_variables_) {
498 const auto row = sensitivities.find(variable->sensitivity_key(model));
499 if (row != sensitivities.end()) {
500 const std::map<std::string, std::string> targets =
501 variable->sensitivity_metric_targets(model);
502 double scalar_rate = 0.0;
505 double LayerSensitivity::*field;
510 for (
const auto& metric : metrics) {
511 const auto target = targets.find(metric.kind);
512 if (target == targets.end())
continue;
513 scalar_rate += scalar_metric_derivative(
514 result, all_values, metric.kind, target->second) *
515 row->second.*(metric.field);
517 const auto value = all_values.find(variable->name());
518 if (value != all_values.end())
519 gradient[offset] = scalar_rate * variable->rate_jacobian(value->second) *
520 variable->decode_jacobian(x[offset]);
522 offset += variable->dimension();
527 std::vector<ConstraintPtr> all_constraints()
const {
528 std::vector<ConstraintPtr> constraints = problem_.objective()->constraints;
529 constraints.insert(constraints.end(), problem_.constraints().begin(),
530 problem_.constraints().end());
534 OptimizationResult build_result(
const std::vector<double>& x,
double objective_value) {
535 OptimizationResult out;
536 out.objective_value = objective_value;
537 out.variable_values = evaluators_[0].decode(x);
538 out.iterations = iterations_;
539 out.solve_time = elapsed();
540 out.convergence_history = convergence_history_;
541 for (
const LineEvaluator& evaluator : evaluators_)
542 out.model_evaluations += evaluator.evaluation_count();
544 const VariableValues all_values = merge_values(out.variable_values);
546 for (std::size_t i = 0; i < evaluators_.size(); ++i) {
547 const EvaluationResult& result = evaluate(i, out.variable_values);
548 if (!result.feasible) {
549 out.feasible =
false;
553 const double violation = constraint->evaluate(result, all_values);
554 if (violation > 0.0) {
555 out.feasible =
false;
556 out.constraint_violations[constraint->name()] = std::max(
557 out.constraint_violations[constraint->name()], violation);
561 if (elapsed() >= options_.time_limit)
562 out.terminated_by =
"time_limit";
563 else if (iterations_ >= options_.max_iterations)
564 out.terminated_by =
"iterations";
566 out.terminated_by =
"convergence";
570 OptimizationResult empty_result()
const {
571 OptimizationResult out;
572 out.objective_value = 0.0;
574 out.terminated_by =
"empty";
578 const OptimizationProblem& problem_;
579 LineOptSolverOptions options_;
581 std::vector<VariablePtr> free_variables_;
582 std::vector<std::pair<VariablePtr, Value>> fixed_variables_;
584 std::vector<LineEvaluator> evaluators_;
585 std::vector<double> scenario_weights_;
586 std::vector<std::map<VariableValues, EvaluationResult>> caches_;
587 Clock::time_point start_;
588 std::size_t iterations_ = 0;
589 double best_value_ = std::numeric_limits<double>::infinity();
590 std::vector<double> best_x_;
591 std::vector<double> convergence_history_;
592 std::size_t gradient_calls_ = 0;
593 std::uint64_t resolved_seed_ = 0;
594 std::map<VariableValues, std::optional<LineEvaluator::LayerSensitivityMap>>
595 lqn_sensitivity_cache_;