/* gtkconstraintsolver.c: Constraint solver based on the Cassowary method * Copyright 2019 GNOME Foundation * * SPDX-License-Identifier: LGPL-2.1-or-later * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see <http://www.gnu.org/licenses/>. * * Author: Emmanuele Bassi */ /*< private > * GtkConstraintSolver * * GtkConstraintSolver is an object that encodes constraints into a tableau * of linear equations and solves them, using an incremental optimization * algorithm known as the "Cassowary Linear Arithmetic Constraint Solving * Algorithm" (Badros, Borning & Stuckey 2001). * * Each constraint is expressed as a linear equation, whose terms are variables * containing widget attributes like the width, height, or position; the simplex * solver takes all the constraints and incrementally optimizes the tableau to * replace known terms; additionally, the algorithm will try to assign a value * to all remaining variables in order to satisfy the various constraints. * * Each constraint is given a "strength", which determines whether satisfying * the constraint is required in order to solve the tableau or not. * * A typical example of GtkConstraintSolver use is solving the following * system of constraints: * * - [required] right = left + 10 * - [required] right ≤ 100 * - [required] middle = left + right / 2 * - [required] left ≥ 0 * * Once we create a GtkConstraintSolver instance, we need to create the * various variables and expressions that represent the values we want to * compute and the constraints we wish to impose on the solutions: * * |[ * GtkConstraintSolver *solver = gtk_constraint_solver_new (); * * // Our variables * GtkConstraintVariable *left = * gtk_constraint_solver_create_variable (solver, NULL, "left", 0.0); * GtkConstraintVariable *middle = * gtk_constraint_solver_create_variable (solver, NULL, "middle", 0.0); * GtkConstraintVariable *right = * gtk_constraint_solver_create_variable (solver, NULL, "right", 0.0); * * // Our constraints * GtkConstraintExpressionBuilder builder; * GtkConstraintExpression *e; * * // right = left + 10 * gtk_constraint_expression_builder_init (&builder, solver); * gtk_constraint_expression_builder_term (&builder, left); * gtk_constraint_expression_builder_plus (&builder); * gtk_constraint_expression_builder_constant (&builder, 10.0); * e = gtk_constraint_expression_builder_finish (&builder); * gtk_constraint_solver_add_constraint (solver, * right, GTK_CONSTRAINT_RELATION_EQ, e, * GTK_CONSTRAINT_STRENGTH_REQUIRED); * * // right ≤ 100 * gtk_constraint_expression_builder_constant (&builder, 100.0); * e = gtk_constraint_expression_builder_finish (&builder); * gtk_constraint_solver_add_constraint (solver, * right, GTK_CONSTRAINT_RELATION_LE, e, * GTK_CONSTRAINT_STRENGTH_REQUIRED); * * // middle = (left + right) / 2 * gtk_constraint_expression_builder_term (&builder, left); * gtk_constraint_expression_builder_plus (&builder) * gtk_constraint_expression_builder_term (&builder, right); * gtk_constraint_expression_builder_divide_by (&builder); * gtk_constraint_expression_builder_constant (&builder, 2.0); * e = gtk_constraint_expression_builder_finish (&builder); * gtk_constraint_solver_add_constraint (solver * middle, GTK_CONSTRAINT_RELATION_EQ, e, * GTK_CONSTRAINT_STRENGTH_REQUIRED); * * // left ≥ 0 * gtk_constraint_expression_builder_constant (&builder, 0.0); * e = gtk_constraint_expression_builder_finish (&builder); * gtk_constraint_solver_add_constraint (solver, * left, GTK_CONSTRAINT_RELATION_GE, e, * GTK_CONSTRAINT_STRENGTH_REQUIRED); * ]| * * Now that we have all our constraints in place, suppose we wish to find * the values of `left` and `right` if we specify the value of `middle`. In * order to do that, we need to add an additional "stay" constraint, i.e. * a constraint that tells the solver to optimize for the solution that keeps * the variable in place: * * |[ * // Set the value first * gtk_constraint_variable_set_value (middle, 45.0); * // and then add the stay constraint, with a weak strength * gtk_constraint_solver_add_stay_variable (solver, middle, GTK_CONSTRAINT_STRENGTH_WEAK); * ]| * * GtkConstraintSolver incrementally solves the system every time a constraint * is added or removed, so it's possible to query the values of the variables * immediately afterward: * * |[ * double left_val = gtk_constraint_variable_get_value (left); * double right_val = gtk_constraint_variable_get_value (right); * double middle_val = gtk_constraint_variable_get_value (middle); * * // These are the values computed by the solver: * g_assert_cmpfloat_with_epsilon (left_val, 40.0, 0.001); * g_assert_cmpfloat_with_epsilon (middle_val, 45.0, 0.001); * g_assert_cmpfloat_with_epsilon (right_val, 50.0, 0.001); * ]| * * As you can see: * * - the middle value hasn't changed * - the left value is ≥ 0 * - the right value is ≤ 100 * - the right value is left + 10 * - the middle value is (left + right) / 2.0 * * For more information about the Cassowary constraint solving algorithm and * toolkit, see the following papers: * * - Badros G & Borning A, 1998, 'Cassowary Linear Arithmetic Constraint * Solving Algorithm: Interface and Implementation', Technical Report * UW-CSE-98-06-04, June 1998 (revised July 1999) * https://constraints.cs.washington.edu/cassowary/cassowary-tr.pdf * - Badros G, Borning A & Stuckey P, 2001, 'Cassowary Linear Arithmetic * Constraint Solving Algorithm', ACM Transactions on Computer-Human * Interaction, vol. 8 no. 4, December 2001, pages 267-306 * https://constraints.cs.washington.edu/solvers/cassowary-tochi.pdf * * The following implementation is based on these projects: * * - the original [C++ implementation](https://sourceforge.net/projects/cassowary/) * - the JavaScript port [Cassowary.js](https://github.com/slightlyoff/cassowary.js) * - the Python port [Cassowary](https://github.com/pybee/cassowary) */ #include "config.h" #include "gtkconstraintsolverprivate.h" #include "gtkconstraintexpressionprivate.h" #include "gtkdebug.h" #include <glib.h> #include <string.h> #include <math.h> #include <float.h> struct _GtkConstraintRef { /* The constraint's normal form inside the solver: * * x - (y × coefficient + constant) = 0 * * We only use equalities, and replace inequalities with slack * variables. */ GtkConstraintExpression *expression; /* A constraint variable, only used by stay and edit constraints */ GtkConstraintVariable *variable; /* The original relation used when creating the constraint */ GtkConstraintRelation relation; /* The strength of the constraint; this value is used to strengthen * or weaken a constraint weight in the tableau when coming to a * solution */ int strength; GtkConstraintSolver *solver; guint is_edit : 1; guint is_stay : 1; }; typedef struct { GtkConstraintRef *constraint; GtkConstraintVariable *eplus; GtkConstraintVariable *eminus; double prev_constant; } EditInfo; typedef struct { GtkConstraintRef *constraint; } StayInfo; struct _GtkConstraintSolver { GObject parent_instance; /* HashTable<Variable, VariableSet>; owns keys and values */ GHashTable *columns; /* HashTable<Variable, Expression>; owns keys and values */ GHashTable *rows; /* Set<Variable>; does not own keys */ GHashTable *external_rows; /* Set<Variable>; does not own keys */ GHashTable *external_parametric_vars; /* Vec<Variable> */ GPtrArray *infeasible_rows; /* Vec<VariablePair>; owns the pair */ GPtrArray *stay_error_vars; /* HashTable<Constraint, VariableSet>; owns the set */ GHashTable *error_vars; /* HashTable<Constraint, Variable> */ GHashTable *marker_vars; /* HashTable<Variable, EditInfo>; does not own keys, but owns values */ GHashTable *edit_var_map; /* HashTable<Variable, StayInfo>; does not own keys, but owns values */ GHashTable *stay_var_map; GtkConstraintVariable *objective; /* Set<Constraint>; owns the key */ GHashTable *constraints; /* Counters */ int var_counter; int slack_counter; int artificial_counter; int dummy_counter; int optimize_count; int freeze_count; /* Bitfields; keep at the end */ guint auto_solve : 1; guint needs_solving : 1; guint in_edit_phase : 1; }; static void gtk_constraint_ref_free (GtkConstraintRef *ref); static void edit_info_free (gpointer data); G_DEFINE_TYPE (GtkConstraintSolver, gtk_constraint_solver, G_TYPE_OBJECT) static void gtk_constraint_solver_finalize (GObject *gobject) { GtkConstraintSolver *self = GTK_CONSTRAINT_SOLVER (gobject); g_hash_table_remove_all (self->constraints); g_clear_pointer (&self->constraints, g_hash_table_unref); g_clear_pointer (&self->stay_error_vars, g_ptr_array_unref); g_clear_pointer (&self->infeasible_rows, g_ptr_array_unref); g_clear_pointer (&self->external_rows, g_hash_table_unref); g_clear_pointer (&self->external_parametric_vars, g_hash_table_unref); g_clear_pointer (&self->error_vars, g_hash_table_unref); g_clear_pointer (&self->marker_vars, g_hash_table_unref); g_clear_pointer (&self->edit_var_map, g_hash_table_unref); g_clear_pointer (&self->stay_var_map, g_hash_table_unref); g_clear_pointer (&self->rows, g_hash_table_unref); g_clear_pointer (&self->columns, g_hash_table_unref); G_OBJECT_CLASS (gtk_constraint_solver_parent_class)->finalize (gobject); } static void gtk_constraint_solver_class_init (GtkConstraintSolverClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); gobject_class->finalize = gtk_constraint_solver_finalize; } static void gtk_constraint_solver_init (GtkConstraintSolver *self) { self->columns = g_hash_table_new_full (NULL, NULL, (GDestroyNotify) gtk_constraint_variable_unref, (GDestroyNotify) gtk_constraint_variable_set_free); self->rows = g_hash_table_new_full (NULL, NULL, (GDestroyNotify) gtk_constraint_variable_unref, (GDestroyNotify) gtk_constraint_expression_unref); self->external_rows = g_hash_table_new (NULL, NULL); self->external_parametric_vars = g_hash_table_new (NULL, NULL); self->infeasible_rows = g_ptr_array_new (); self->stay_error_vars = g_ptr_array_new_with_free_func ((GDestroyNotify) gtk_constraint_variable_pair_free); self->error_vars = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify) gtk_constraint_variable_set_free); self->marker_vars = g_hash_table_new (NULL, NULL); self->edit_var_map = g_hash_table_new_full (NULL, NULL, NULL, edit_info_free); self->stay_var_map = g_hash_table_new_full (NULL, NULL, NULL, g_free); /* The rows table owns the objective variable */ self->objective = gtk_constraint_variable_new_objective ("Z"); g_hash_table_insert (self->rows, self->objective, gtk_constraint_expression_new (0.0)); self->constraints = g_hash_table_new_full (NULL, NULL, (GDestroyNotify) gtk_constraint_ref_free, NULL); self->slack_counter = 0; self->dummy_counter = 0; self->artificial_counter = 0; self->freeze_count = 0; self->needs_solving = FALSE; self->auto_solve = TRUE; } static void gtk_constraint_ref_free (GtkConstraintRef *self) { gtk_constraint_solver_remove_constraint (self->solver, self); gtk_constraint_expression_unref (self->expression); if (self->is_edit || self->is_stay) { g_assert (self->variable != NULL); gtk_constraint_variable_unref (self->variable); } g_free (self); } static gboolean gtk_constraint_ref_is_inequality (const GtkConstraintRef *self) { return self->relation != GTK_CONSTRAINT_RELATION_EQ; } static gboolean gtk_constraint_ref_is_required (const GtkConstraintRef *self) { return self->strength == GTK_CONSTRAINT_STRENGTH_REQUIRED; } static const char *relations[] = { "<=", "==", ">=", }; static const char * relation_to_string (GtkConstraintRelation r) { return relations[r + 1]; } static const char * strength_to_string (int s) { if (s >= GTK_CONSTRAINT_STRENGTH_STRONG) return "strong"; if (s >= GTK_CONSTRAINT_STRENGTH_MEDIUM) return "medium"; return "weak"; } static char * gtk_constraint_ref_to_string (const GtkConstraintRef *self) { GString *buf = g_string_new (NULL); char *str; if (self->is_stay) g_string_append (buf, "[stay]"); else if (self->is_edit) g_string_append (buf, "[edit]"); str = gtk_constraint_expression_to_string (self->expression); g_string_append (buf, str); g_free (str); g_string_append_c (buf, ' '); g_string_append (buf, relation_to_string (self->relation)); g_string_append (buf, " 0.0"); if (gtk_constraint_ref_is_required (self)) g_string_append (buf, " [strength:required]"); else g_string_append_printf (buf, " [strength:%d (%s)]", self->strength, strength_to_string (self->strength)); return g_string_free (buf, FALSE); } static GtkConstraintVariableSet * gtk_constraint_solver_get_column_set (GtkConstraintSolver *self, GtkConstraintVariable *param_var) { return g_hash_table_lookup (self->columns, param_var); } static gboolean gtk_constraint_solver_column_has_key (GtkConstraintSolver *self, GtkConstraintVariable *subject) { return g_hash_table_contains (self->columns, subject); } static void gtk_constraint_solver_insert_column_variable (GtkConstraintSolver *self, GtkConstraintVariable *param_var, GtkConstraintVariable *row_var) { GtkConstraintVariableSet *cset = g_hash_table_lookup (self->columns, param_var); if (cset == NULL) { cset = gtk_constraint_variable_set_new (); g_hash_table_insert (self->columns, gtk_constraint_variable_ref (param_var), cset); } if (row_var != NULL) gtk_constraint_variable_set_add (cset, row_var); } static void gtk_constraint_solver_insert_error_variable (GtkConstraintSolver *self, GtkConstraintRef *constraint, GtkConstraintVariable *variable) { GtkConstraintVariableSet *cset = g_hash_table_lookup (self->error_vars, constraint); if (cset == NULL) { cset = gtk_constraint_variable_set_new (); g_hash_table_insert (self->error_vars, constraint, cset); } gtk_constraint_variable_set_add (cset, variable); } static void gtk_constraint_solver_reset_stay_constants (GtkConstraintSolver *self) { int i; for (i = 0; i < self->stay_error_vars->len; i++) { GtkConstraintVariablePair *pair = g_ptr_array_index (self->stay_error_vars, i); GtkConstraintExpression *expression; expression = g_hash_table_lookup (self->rows, pair->first); if (expression == NULL) expression = g_hash_table_lookup (self->rows, pair->second); if (expression != NULL) gtk_constraint_expression_set_constant (expression, 0.0); } } static void gtk_constraint_solver_set_external_variables (GtkConstraintSolver *self) { GHashTableIter iter; gpointer key_p; g_hash_table_iter_init (&iter, self->external_parametric_vars); while (g_hash_table_iter_next (&iter, &key_p, NULL)) { GtkConstraintVariable *variable = key_p; if (g_hash_table_contains (self->rows, variable)) continue; gtk_constraint_variable_set_value (variable, 0.0); } g_hash_table_iter_init (&iter, self->external_rows); while (g_hash_table_iter_next (&iter, &key_p, NULL)) { GtkConstraintVariable *variable = key_p; GtkConstraintExpression *expression; double constant; expression = g_hash_table_lookup (self->rows, variable); constant = gtk_constraint_expression_get_constant (expression); gtk_constraint_variable_set_value (variable, constant); } self->needs_solving = FALSE; } static void gtk_constraint_solver_add_row (GtkConstraintSolver *self, GtkConstraintVariable *variable, GtkConstraintExpression *expression) { GtkConstraintExpressionIter iter; GtkConstraintVariable *t_v; double t_c; g_hash_table_insert (self->rows, gtk_constraint_variable_ref (variable), gtk_constraint_expression_ref (expression)); gtk_constraint_expression_iter_init (&iter, expression); while (gtk_constraint_expression_iter_next (&iter, &t_v, &t_c)) { gtk_constraint_solver_insert_column_variable (self, t_v, variable); if (gtk_constraint_variable_is_external (t_v)) g_hash_table_add (self->external_parametric_vars, t_v); } if (gtk_constraint_variable_is_external (variable)) g_hash_table_add (self->external_rows, variable); } static void gtk_constraint_solver_remove_column (GtkConstraintSolver *self, GtkConstraintVariable *variable) { GtkConstraintVariable *v; GtkConstraintVariableSetIter iter; GtkConstraintVariableSet *cset; /* Take a reference on the variable, as we're going to remove it * from various maps and we want to guarantee the pointer is * valid until we leave this function */ gtk_constraint_variable_ref (variable); cset = g_hash_table_lookup (self->columns, variable); if (cset == NULL) goto out; gtk_constraint_variable_set_iter_init (&iter, cset); while (gtk_constraint_variable_set_iter_next (&iter, &v)) { GtkConstraintExpression *e = g_hash_table_lookup (self->rows, v); gtk_constraint_expression_remove_variable (e, variable); } g_hash_table_remove (self->columns, variable); out: if (gtk_constraint_variable_is_external (variable)) { g_hash_table_remove (self->external_rows, variable); g_hash_table_remove (self->external_parametric_vars, variable); } gtk_constraint_variable_unref (variable); } static GtkConstraintExpression * gtk_constraint_solver_remove_row (GtkConstraintSolver *self, GtkConstraintVariable *variable, gboolean free_res) { GtkConstraintExpression *e; GtkConstraintExpressionIter iter; GtkConstraintVariable *t_v; double t_c; e = g_hash_table_lookup (self->rows, variable); g_assert (e != NULL); gtk_constraint_expression_ref (e); gtk_constraint_expression_iter_init (&iter, e); while (gtk_constraint_expression_iter_next (&iter, &t_v, &t_c)) { GtkConstraintVariableSet *cset = g_hash_table_lookup (self->columns, t_v); if (cset != NULL) gtk_constraint_variable_set_remove (cset, variable); } g_ptr_array_remove (self->infeasible_rows, variable); if (gtk_constraint_variable_is_external (variable)) g_hash_table_remove (self->external_rows, variable); g_hash_table_remove (self->rows, variable); if (free_res) { gtk_constraint_expression_unref (e); return NULL; } return e; } /*< private > * gtk_constraint_solver_substitute_out: * @self: a `GtkConstraintSolver` * @old_variable: a `GtkConstraintVariable` * @expression: a `GtkConstraintExpression` * * Replaces @old_variable in every row of the tableau with @expression. */ static void gtk_constraint_solver_substitute_out (GtkConstraintSolver *self, GtkConstraintVariable *old_variable, GtkConstraintExpression *expression) { GtkConstraintVariableSet *cset = g_hash_table_lookup (self->columns, old_variable); if (cset != NULL) { GtkConstraintVariableSetIter iter; GtkConstraintVariable *v; gtk_constraint_variable_set_iter_init (&iter, cset); while (gtk_constraint_variable_set_iter_next (&iter, &v)) { GtkConstraintExpression *row = g_hash_table_lookup (self->rows, v); gtk_constraint_expression_substitute_out (row, old_variable, expression, v, self); if (gtk_constraint_variable_is_restricted (v) && gtk_constraint_expression_get_constant (row) < 0) g_ptr_array_add (self->infeasible_rows, v); } } if (gtk_constraint_variable_is_external (old_variable)) { g_hash_table_add (self->external_rows, old_variable); g_hash_table_remove (self->external_parametric_vars, old_variable); } g_hash_table_remove (self->columns, old_variable); } /*< private > * gtk_constraint_solver_pivot: * @self: a `GtkConstraintSolver` * @entry_var: a `GtkConstraintVariable` * @exit_var: a `GtkConstraintVariable` * * Pivots the `GtkConstraintSolver`. * * This function will move @entry_var into the basis of the tableau, * making it a basic variable; and move @exit_var out of the basis of * the tableau, making it a parametric variable. */ static void gtk_constraint_solver_pivot (GtkConstraintSolver *self, GtkConstraintVariable *entry_var, GtkConstraintVariable *exit_var) { GtkConstraintExpression *expr; if (entry_var != NULL) gtk_constraint_variable_ref (entry_var); else g_critical ("INTERNAL: invalid entry variable during pivot"); if (exit_var != NULL) gtk_constraint_variable_ref (exit_var); else g_critical ("INTERNAL: invalid exit variable during pivot"); /* We keep a reference to the expression */ expr = gtk_constraint_solver_remove_row (self, exit_var, FALSE); gtk_constraint_expression_change_subject (expr, exit_var, entry_var); gtk_constraint_solver_substitute_out (self, entry_var, expr); if (gtk_constraint_variable_is_external (entry_var)) g_hash_table_remove (self->external_parametric_vars, entry_var); gtk_constraint_solver_add_row (self, entry_var, expr); gtk_constraint_variable_unref (entry_var); gtk_constraint_variable_unref (exit_var); gtk_constraint_expression_unref (expr); } static void gtk_constraint_solver_optimize (GtkConstraintSolver *self, GtkConstraintVariable *z) { GtkConstraintVariable *entry = NULL, *exit = NULL; GtkConstraintExpression *z_row = g_hash_table_lookup (self->rows, z); #ifdef G_ENABLE_DEBUG gint64 start_time = g_get_monotonic_time (); #endif g_assert (z_row != NULL); self->optimize_count += 1; #ifdef G_ENABLE_DEBUG if (GTK_DEBUG_CHECK (CONSTRAINTS)) { char *str = gtk_constraint_variable_to_string (z); g_message ("optimize: %s", str); g_free (str); } #endif while (TRUE) { GtkConstraintVariableSet *column_vars; GtkConstraintVariableSetIter viter; GtkConstraintExpressionIter eiter; GtkConstraintVariable *t_v, *v; double t_c; double objective_coefficient = 0.0; double min_ratio; double r; gtk_constraint_expression_iter_init (&eiter, z_row); while (gtk_constraint_expression_iter_prev (&eiter, &t_v, &t_c)) { if (gtk_constraint_variable_is_pivotable (t_v) && t_c < objective_coefficient) { entry = t_v; objective_coefficient = t_c; break; } } if (objective_coefficient >= -1e-8) break; min_ratio = DBL_MAX; r = 0; column_vars = gtk_constraint_solver_get_column_set (self, entry); gtk_constraint_variable_set_iter_init (&viter, column_vars); while (gtk_constraint_variable_set_iter_next (&viter, &v)) { if (gtk_constraint_variable_is_pivotable (v)) { GtkConstraintExpression *expr = g_hash_table_lookup (self->rows, v); double coeff = gtk_constraint_expression_get_coefficient (expr, entry); if (coeff < 0.0) { double constant = gtk_constraint_expression_get_constant (expr); r = -1.0 * constant / coeff; if (r < min_ratio) { min_ratio = r; exit = v; } } } } if (min_ratio == DBL_MAX) { GTK_NOTE (CONSTRAINTS, g_message ("Unbounded objective variable during optimization")); break; } #ifdef G_ENABLE_DEBUG if (GTK_DEBUG_CHECK (CONSTRAINTS)) { char *entry_s = gtk_constraint_variable_to_string (entry); char *exit_s = gtk_constraint_variable_to_string (exit); g_message ("pivot(entry: %s, exit: %s)", entry_s, exit_s); g_free (entry_s); g_free (exit_s); } #endif gtk_constraint_solver_pivot (self, entry, exit); } GTK_NOTE (CONSTRAINTS, g_message ("solver.optimize.time := %.3f ms (pass: %d)", (float) (g_get_monotonic_time () - start_time) / 1000.f, self->optimize_count)); } /*< private > * gtk_constraint_solver_new_expression: * @self: a `GtkConstraintSolver` * @constraint: a `GtkConstraintRef` * @eplus_p: (out) (optional): the positive error variable * @eminus_p: (out) (optional): the negative error variable * @prev_constant_p: the constant part of the @constraint's expression * * Creates a new expression for the @constraint, replacing * any basic variable with their expressions, and normalizing * the terms to avoid a negative constant. * * If the @constraint is not required, this function will add * error variables with the appropriate weight to the tableau. * * Returns: (transfer full): the new expression for the constraint */ static GtkConstraintExpression * gtk_constraint_solver_new_expression (GtkConstraintSolver *self, GtkConstraintRef *constraint, GtkConstraintVariable **eplus_p, GtkConstraintVariable **eminus_p, double *prev_constant_p) { GtkConstraintExpression *cn_expr = constraint->expression; GtkConstraintExpression *expr; GtkConstraintExpressionIter eiter; GtkConstraintVariable *t_v; double t_c; if (eplus_p != NULL) *eplus_p = NULL; if (eminus_p != NULL) *eminus_p = NULL; if (prev_constant_p != NULL) *prev_constant_p = 0.0; expr = gtk_constraint_expression_new (gtk_constraint_expression_get_constant (cn_expr)); gtk_constraint_expression_iter_init (&eiter, cn_expr); while (gtk_constraint_expression_iter_next (&eiter, &t_v, &t_c)) { GtkConstraintExpression *e = g_hash_table_lookup (self->rows, t_v); if (e == NULL) gtk_constraint_expression_add_variable (expr, t_v, t_c, NULL, self); else gtk_constraint_expression_add_expression (expr, e, t_c, NULL, self); } if (gtk_constraint_ref_is_inequality (constraint)) { GtkConstraintVariable *slack_var; /* If the constraint is an inequality, we add a slack variable to * turn it into an equality, e.g. from * * expr ≥ 0 * * to * * expr - slack = 0 * * Additionally, if the constraint is not required we add an * error variable with the weight of the constraint: * * expr - slack + error = 0 */ self->slack_counter += 1; slack_var = gtk_constraint_variable_new_slack ("s"); gtk_constraint_expression_set_variable (expr, slack_var, -1.0); gtk_constraint_variable_unref (slack_var); g_hash_table_insert (self->marker_vars, constraint, slack_var); if (!gtk_constraint_ref_is_required (constraint)) { GtkConstraintExpression *z_row; GtkConstraintVariable *eminus; self->slack_counter += 1; eminus = gtk_constraint_variable_new_slack ("em"); gtk_constraint_expression_set_variable (expr, eminus, 1.0); gtk_constraint_variable_unref (eminus); z_row = g_hash_table_lookup (self->rows, self->objective); gtk_constraint_expression_set_variable (z_row, eminus, constraint->strength); gtk_constraint_solver_insert_error_variable (self, constraint, eminus); gtk_constraint_solver_note_added_variable (self, eminus, self->objective); gtk_constraint_variable_unref (eminus); } } else { GtkConstraintVariable *dummy_var; if (gtk_constraint_ref_is_required (constraint)) { /* If the constraint is required, we use a dummy marker variable; * the dummy won't be allowed to enter the basis of the tableau * when pivoting. */ self->dummy_counter += 1; dummy_var = gtk_constraint_variable_new_dummy ("dummy"); if (eplus_p != NULL) *eplus_p = dummy_var; if (eminus_p != NULL) *eminus_p = dummy_var; if (prev_constant_p != NULL) *prev_constant_p = gtk_constraint_expression_get_constant (cn_expr); gtk_constraint_expression_set_variable (expr, dummy_var, 1.0); g_hash_table_insert (self->marker_vars, constraint, dummy_var); gtk_constraint_variable_unref (dummy_var); } else { GtkConstraintVariable *eplus, *eminus; GtkConstraintExpression *z_row; /* Since the constraint is a non-required equality, we need to * add error variables around it, i.e. turn it from: * * expr = 0 * * to: * * expr - eplus + eminus = 0 */ self->slack_counter += 1; eplus = gtk_constraint_variable_new_slack ("ep"); eminus = gtk_constraint_variable_new_slack ("em"); gtk_constraint_expression_set_variable (expr, eplus, -1.0); gtk_constraint_expression_set_variable (expr, eminus, 1.0); g_hash_table_insert (self->marker_vars, constraint, eplus); z_row = g_hash_table_lookup (self->rows, self->objective); gtk_constraint_expression_set_variable (z_row, eplus, constraint->strength); gtk_constraint_expression_set_variable (z_row, eminus, constraint->strength); gtk_constraint_solver_note_added_variable (self, eplus, self->objective); gtk_constraint_solver_note_added_variable (self, eminus, self->objective); gtk_constraint_solver_insert_error_variable (self, constraint, eplus); gtk_constraint_solver_insert_error_variable (self, constraint, eminus); if (constraint->is_stay) { g_ptr_array_add (self->stay_error_vars, gtk_constraint_variable_pair_new (eplus, eminus)); } else if (constraint->is_edit) { if (eplus_p != NULL) *eplus_p = eplus; if (eminus_p != NULL) *eminus_p = eminus; if (prev_constant_p != NULL) *prev_constant_p = gtk_constraint_expression_get_constant (cn_expr); } gtk_constraint_variable_unref (eplus); gtk_constraint_variable_unref (eminus); } } if (gtk_constraint_expression_get_constant (expr) < 0.0) gtk_constraint_expression_multiply_by (expr, -1.0); return expr; } static void gtk_constraint_solver_dual_optimize (GtkConstraintSolver *self) { GtkConstraintExpression *z_row = g_hash_table_lookup (self->rows, self->objective); #ifdef G_ENABLE_DEBUG gint64 start_time = g_get_monotonic_time (); #endif /* We iterate until we don't have any more infeasible rows; the pivot() * at the end of the loop iteration may add or remove infeasible rows * as well */ while (self->infeasible_rows->len != 0) { GtkConstraintVariable *entry_var, *exit_var, *t_v; GtkConstraintExpressionIter eiter; GtkConstraintExpression *expr; double ratio, t_c; /* Pop the last element of the array */ exit_var = g_ptr_array_index (self->infeasible_rows, self->infeasible_rows->len - 1); g_ptr_array_set_size (self->infeasible_rows, self->infeasible_rows->len - 1); expr = g_hash_table_lookup (self->rows, exit_var); if (expr == NULL) continue; if (gtk_constraint_expression_get_constant (expr) >= 0.0) continue; ratio = DBL_MAX; entry_var = NULL; gtk_constraint_expression_iter_init (&eiter, expr); while (gtk_constraint_expression_iter_next (&eiter, &t_v, &t_c)) { if (t_c > 0.0 && gtk_constraint_variable_is_pivotable (t_v)) { double zc = gtk_constraint_expression_get_coefficient (z_row, t_v); double r = zc / t_c; if (r < ratio) { entry_var = t_v; ratio = r; } } } if (ratio == DBL_MAX) g_critical ("INTERNAL: ratio == DBL_MAX in dual_optimize"); gtk_constraint_solver_pivot (self, entry_var, exit_var); } GTK_NOTE (CONSTRAINTS, g_message ("dual_optimize.time := %.3f ms", (float) (g_get_monotonic_time () - start_time) / 1000.f)); } static void gtk_constraint_solver_delta_edit_constant (GtkConstraintSolver *self, double delta, GtkConstraintVariable *plus_error_var, GtkConstraintVariable *minus_error_var) { GtkConstraintExpression *plus_expr, *minus_expr; GtkConstraintVariable *basic_var; GtkConstraintVariableSet *column_set; GtkConstraintVariableSetIter iter; plus_expr = g_hash_table_lookup (self->rows, plus_error_var); if (plus_expr != NULL) { double new_constant = gtk_constraint_expression_get_constant (plus_expr) + delta; gtk_constraint_expression_set_constant (plus_expr, new_constant); if (new_constant < 0.0) g_ptr_array_add (self->infeasible_rows, plus_error_var); return; } minus_expr = g_hash_table_lookup (self->rows, minus_error_var); if (minus_expr != NULL) { double new_constant = gtk_constraint_expression_get_constant (minus_expr) - delta; gtk_constraint_expression_set_constant (minus_expr, new_constant); if (new_constant < 0.0) g_ptr_array_add (self->infeasible_rows, minus_error_var); return; } column_set = g_hash_table_lookup (self->columns, minus_error_var); if (column_set == NULL) { g_critical ("INTERNAL: Columns are unset during delta edit"); return; } gtk_constraint_variable_set_iter_init (&iter, column_set); while (gtk_constraint_variable_set_iter_next (&iter, &basic_var)) { GtkConstraintExpression *expr; double c, new_constant; expr = g_hash_table_lookup (self->rows, basic_var); c = gtk_constraint_expression_get_coefficient (expr, minus_error_var); new_constant = gtk_constraint_expression_get_constant (expr) + (c * delta); gtk_constraint_expression_set_constant (expr, new_constant); if (gtk_constraint_variable_is_restricted (basic_var) && new_constant < 0.0) g_ptr_array_add (self->infeasible_rows, basic_var); } } static GtkConstraintVariable * gtk_constraint_solver_choose_subject (GtkConstraintSolver *self, GtkConstraintExpression *expression) { GtkConstraintExpressionIter eiter; GtkConstraintVariable *subject = NULL; GtkConstraintVariable *retval = NULL; GtkConstraintVariable *t_v; gboolean found_unrestricted = FALSE; gboolean found_new_restricted = FALSE; gboolean retval_found = FALSE; double coeff = 0.0; double t_c; gtk_constraint_expression_iter_init (&eiter, expression); while (gtk_constraint_expression_iter_prev (&eiter, &t_v, &t_c)) { if (found_unrestricted) { if (!gtk_constraint_variable_is_restricted (t_v)) { if (!g_hash_table_contains (self->columns, t_v)) { retval_found = TRUE; retval = t_v; break; } } } else { if (gtk_constraint_variable_is_restricted (t_v)) { if (!found_new_restricted && !gtk_constraint_variable_is_dummy (t_v) && t_c < 0.0) { GtkConstraintVariableSet *cset = g_hash_table_lookup (self->columns, t_v); if (cset == NULL || (gtk_constraint_variable_set_is_singleton (cset) && g_hash_table_contains (self->columns, self->objective))) { subject = t_v; found_new_restricted = TRUE; } } } else { subject = t_v; found_unrestricted = TRUE; } } } if (retval_found) return retval; if (subject != NULL) return subject; gtk_constraint_expression_iter_init (&eiter, expression); while (gtk_constraint_expression_iter_prev (&eiter, &t_v, &t_c)) { if (!gtk_constraint_variable_is_dummy (t_v)) return NULL; if (!g_hash_table_contains (self->columns, t_v)) { subject = t_v; coeff = t_c; } } if (!G_APPROX_VALUE (gtk_constraint_expression_get_constant (expression), 0.0, 0.001)) { GTK_NOTE (CONSTRAINTS, g_message ("Unable to satisfy required constraint (choose_subject)")); return NULL; } if (coeff > 0) gtk_constraint_expression_multiply_by (expression, -1.0); return subject; } static gboolean gtk_constraint_solver_try_adding_directly (GtkConstraintSolver *self, GtkConstraintExpression *expression) { GtkConstraintVariable *subject; subject = gtk_constraint_solver_choose_subject (self, expression); if (subject == NULL) return FALSE; gtk_constraint_variable_ref (subject); gtk_constraint_expression_new_subject (expression, subject); if (gtk_constraint_solver_column_has_key (self, subject)) gtk_constraint_solver_substitute_out (self, subject, expression); gtk_constraint_solver_add_row (self, subject, expression); gtk_constraint_variable_unref (subject); return TRUE; } static void gtk_constraint_solver_add_with_artificial_variable (GtkConstraintSolver *self, GtkConstraintExpression *expression) { GtkConstraintVariable *av, *az; GtkConstraintExpression *az_row; GtkConstraintExpression *az_tableau_row; GtkConstraintExpression *e; av = gtk_constraint_variable_new_slack ("a"); self->artificial_counter += 1; az = gtk_constraint_variable_new_objective ("az"); az_row = gtk_constraint_expression_clone (expression); gtk_constraint_solver_add_row (self, az, az_row); gtk_constraint_solver_add_row (self, av, expression); gtk_constraint_expression_unref (az_row); gtk_constraint_variable_unref (av); gtk_constraint_variable_unref (az); gtk_constraint_solver_optimize (self, az); az_tableau_row = g_hash_table_lookup (self->rows, az); if (!G_APPROX_VALUE (gtk_constraint_expression_get_constant (az_tableau_row), 0.0, 0.001)) { gtk_constraint_solver_remove_column (self, av); gtk_constraint_solver_remove_row (self, az, TRUE); #ifdef G_ENABLE_DEBUG if (GTK_DEBUG_CHECK (CONSTRAINTS)) { char *str = gtk_constraint_expression_to_string (expression); g_message ("Unable to satisfy a required constraint (add): %s", str); g_free (str); } #endif return; } e = g_hash_table_lookup (self->rows, av); if (e != NULL) { GtkConstraintVariable *entry_var; if (gtk_constraint_expression_is_constant (e)) { gtk_constraint_solver_remove_row (self, av, TRUE); gtk_constraint_solver_remove_row (self, az, TRUE); return; } entry_var = gtk_constraint_expression_get_pivotable_variable (e); if (entry_var == NULL) return; gtk_constraint_solver_pivot (self, entry_var, av); } g_assert (!g_hash_table_contains (self->rows, av)); gtk_constraint_solver_remove_column (self, av); gtk_constraint_solver_remove_row (self, az, TRUE); } static void gtk_constraint_solver_add_constraint_internal (GtkConstraintSolver *self, GtkConstraintRef *constraint) { GtkConstraintExpression *expr; GtkConstraintVariable *eplus; GtkConstraintVariable *eminus; double prev_constant; expr = gtk_constraint_solver_new_expression (self, constraint, &eplus, &eminus, &prev_constant); #ifdef G_ENABLE_DEBUG if (GTK_DEBUG_CHECK (CONSTRAINTS)) { char *expr_s = gtk_constraint_expression_to_string (expr); char *ref_s = gtk_constraint_ref_to_string (constraint); g_message ("Adding constraint '%s' (normalized expression: '%s')", ref_s, expr_s); g_free (ref_s); g_free (expr_s); } #endif if (constraint->is_stay) { StayInfo *si = g_new (StayInfo, 1); si->constraint = constraint; g_hash_table_insert (self->stay_var_map, constraint->variable, si); } else if (constraint->is_edit) { EditInfo *ei = g_new (EditInfo, 1); ei->constraint = constraint; ei->eplus = eplus; ei->eminus = eminus; ei->prev_constant = prev_constant; g_hash_table_insert (self->edit_var_map, constraint->variable, ei); } if (!gtk_constraint_solver_try_adding_directly (self, expr)) gtk_constraint_solver_add_with_artificial_variable (self, expr); gtk_constraint_expression_unref (expr); self->needs_solving = TRUE; if (self->auto_solve) { gtk_constraint_solver_optimize (self, self->objective); gtk_constraint_solver_set_external_variables (self); } constraint->solver = self; g_hash_table_add (self->constraints, constraint); } /*< private > * gtk_constraint_solver_new: * * Creates a new `GtkConstraintSolver` instance. * * Returns: the newly created `GtkConstraintSolver` */ GtkConstraintSolver * gtk_constraint_solver_new (void) { return g_object_new (GTK_TYPE_CONSTRAINT_SOLVER, NULL); } /*< private > * gtk_constraint_solver_freeze: * @solver: a `GtkConstraintSolver` * * Freezes the solver; any constraint addition or removal will not * be automatically solved until gtk_constraint_solver_thaw() is * called. */ void gtk_constraint_solver_freeze (GtkConstraintSolver *solver) { g_return_if_fail (GTK_IS_CONSTRAINT_SOLVER (solver)); solver->freeze_count += 1; if (solver->freeze_count > 0) solver->auto_solve = FALSE; } /*< private > * gtk_constraint_solver_thaw: * @solver: a `GtkConstraintSolver` * * Thaws a frozen `GtkConstraintSolver`. */ void gtk_constraint_solver_thaw (GtkConstraintSolver *solver) { g_return_if_fail (GTK_IS_CONSTRAINT_SOLVER (solver)); g_return_if_fail (solver->freeze_count > 0); solver->freeze_count -= 1; if (solver->freeze_count == 0) { solver->auto_solve = TRUE; gtk_constraint_solver_resolve (solver); } } /*< private > * gtk_constraint_solver_note_added_variable: * @self: a `GtkConstraintSolver` * @variable: a `GtkConstraintVariable` * @subject: a `GtkConstraintVariable` * * Adds a new @variable into the tableau of a `GtkConstraintSolver`. * * This function is typically called by `GtkConstraintExpression`, and * should never be directly called. */ void gtk_constraint_solver_note_added_variable (GtkConstraintSolver *self, GtkConstraintVariable *variable, GtkConstraintVariable *subject) { if (subject != NULL) gtk_constraint_solver_insert_column_variable (self, variable, subject); } /*< private > * gtk_constraint_solver_note_removed_variable: * @self: a `GtkConstraintSolver` * @variable: a `GtkConstraintVariable` * @subject: a `GtkConstraintVariable` * * Removes a @variable from the tableau of a `GtkConstraintSolver`. * * This function is typically called by `GtkConstraintExpression`, and * should never be directly called. */ void gtk_constraint_solver_note_removed_variable (GtkConstraintSolver *self, GtkConstraintVariable *variable, GtkConstraintVariable *subject) { GtkConstraintVariableSet *set; set = g_hash_table_lookup (self->columns, variable); if (set != NULL && subject != NULL) gtk_constraint_variable_set_remove (set, subject); } /*< private > * gtk_constraint_solver_create_variable: * @self: a `GtkConstraintSolver` * @prefix: (nullable): the prefix of the variable * @name: (nullable): the name of the variable * @value: the initial value of the variable * * Creates a new variable inside the @solver. * * Returns: (transfer full): the newly created variable */ GtkConstraintVariable * gtk_constraint_solver_create_variable (GtkConstraintSolver *self, const char *prefix, const char *name, double value) { GtkConstraintVariable *res; res = gtk_constraint_variable_new (prefix, name); gtk_constraint_variable_set_value (res, value); self->var_counter++; return res; } /*< private > * gtk_constraint_solver_resolve: * @solver: a `GtkConstraintSolver` * * Resolves the constraints currently stored in @solver. */ void gtk_constraint_solver_resolve (GtkConstraintSolver *solver) { #ifdef G_ENABLE_DEBUG gint64 start_time = g_get_monotonic_time (); #endif g_return_if_fail (GTK_IS_CONSTRAINT_SOLVER (solver)); gtk_constraint_solver_dual_optimize (solver); gtk_constraint_solver_set_external_variables (solver); g_ptr_array_set_size (solver->infeasible_rows, 0); gtk_constraint_solver_reset_stay_constants (solver); GTK_NOTE (CONSTRAINTS, g_message ("resolve.time := %.3f ms", (float) (g_get_monotonic_time () - start_time) / 1000.f)); solver->needs_solving = FALSE; } /*< private > * gtk_constraint_solver_add_constraint: * @self: a `GtkConstraintSolver` * @variable: the subject of the constraint * @relation: the relation of the constraint * @expression: the expression of the constraint * @strength: the strength of the constraint * * Adds a new constraint in the form of: * * |[ * variable relation expression (strength) * |] * * into the `GtkConstraintSolver`. * * Returns: (transfer none): a reference to the newly created * constraint; you can use the reference to remove the * constraint from the solver */ GtkConstraintRef * gtk_constraint_solver_add_constraint (GtkConstraintSolver *self, GtkConstraintVariable *variable, GtkConstraintRelation relation, GtkConstraintExpression *expression, int strength) { GtkConstraintRef *res = g_new0 (GtkConstraintRef, 1); res->solver = self; res->strength = strength; res->is_edit = FALSE; res->is_stay = FALSE; res->relation = relation; if (expression == NULL) res->expression = gtk_constraint_expression_new_from_variable (variable); else { res->expression = expression; if (variable != NULL) { switch (res->relation) { case GTK_CONSTRAINT_RELATION_EQ: gtk_constraint_expression_add_variable (res->expression, variable, -1.0, NULL, self); break; case GTK_CONSTRAINT_RELATION_LE: gtk_constraint_expression_add_variable (res->expression, variable, -1.0, NULL, self); break; case GTK_CONSTRAINT_RELATION_GE: gtk_constraint_expression_multiply_by (res->expression, -1.0); gtk_constraint_expression_add_variable (res->expression, variable, 1.0, NULL, self); break; default: g_assert_not_reached (); } } } gtk_constraint_solver_add_constraint_internal (self, res); return res; } /*< private > * gtk_constraint_solver_add_stay_variable: * @self: a `GtkConstraintSolver` * @variable: a stay `GtkConstraintVariable` * @strength: the strength of the constraint * * Adds a constraint on a stay @variable with the given @strength. * * A stay variable is an "anchor" in the system: a variable that is * supposed to stay at the same value. * * Returns: (transfer none): a reference to the newly created * constraint; you can use the reference to remove the * constraint from the solver */ GtkConstraintRef * gtk_constraint_solver_add_stay_variable (GtkConstraintSolver *self, GtkConstraintVariable *variable, int strength) { GtkConstraintRef *res = g_new0 (GtkConstraintRef, 1); res->solver = self; res->variable = gtk_constraint_variable_ref (variable); res->relation = GTK_CONSTRAINT_RELATION_EQ; res->strength = strength; res->is_stay = TRUE; res->is_edit = FALSE; res->expression = gtk_constraint_expression_new (gtk_constraint_variable_get_value (res->variable)); gtk_constraint_expression_add_variable (res->expression, res->variable, -1.0, NULL, self); #ifdef G_ENABLE_DEBUG if (GTK_DEBUG_CHECK (CONSTRAINTS)) { char *str = gtk_constraint_expression_to_string (res->expression); g_message ("Adding stay variable: %s", str); g_free (str); } #endif gtk_constraint_solver_add_constraint_internal (self, res); return res; } /*< private > * gtk_constraint_solver_remove_stay_variable: * @self: a `GtkConstraintSolver` * @variable: a stay variable * * Removes the stay constraint associated to @variable. * * This is a convenience function for gtk_constraint_solver_remove_constraint(). */ void gtk_constraint_solver_remove_stay_variable (GtkConstraintSolver *self, GtkConstraintVariable *variable) { StayInfo *si = g_hash_table_lookup (self->stay_var_map, variable); if (si == NULL) { char *str = gtk_constraint_variable_to_string (variable); g_critical ("Unknown stay variable '%s'", str); g_free (str); return; } gtk_constraint_solver_remove_constraint (self, si->constraint); } /*< private > * gtk_constraint_solver_add_edit_variable: * @self: a `GtkConstraintSolver` * @variable: an edit variable * @strength: the strength of the constraint * * Adds an editable constraint to the @solver. * * Editable constraints can be used to suggest values to a * `GtkConstraintSolver` inside an edit phase, for instance: if * you want to change the value of a variable without necessarily * insert a new constraint every time. * * See also: gtk_constraint_solver_suggest_value() * * Returns: (transfer none): a reference to the newly added constraint */ GtkConstraintRef * gtk_constraint_solver_add_edit_variable (GtkConstraintSolver *self, GtkConstraintVariable *variable, int strength) { GtkConstraintRef *res = g_new0 (GtkConstraintRef, 1); res->solver = self; res->variable = gtk_constraint_variable_ref (variable); res->relation = GTK_CONSTRAINT_RELATION_EQ; res->strength = strength; res->is_stay = FALSE; res->is_edit = TRUE; res->expression = gtk_constraint_expression_new (gtk_constraint_variable_get_value (variable)); gtk_constraint_expression_add_variable (res->expression, variable, -1.0, NULL, self); gtk_constraint_solver_add_constraint_internal (self, res); return res; } /*< private > * gtk_constraint_solver_remove_edit_variable: * @self: a `GtkConstraintSolver` * @variable: an edit variable * * Removes the edit constraint associated to @variable. * * This is a convenience function around gtk_constraint_solver_remove_constraint(). */ void gtk_constraint_solver_remove_edit_variable (GtkConstraintSolver *self, GtkConstraintVariable *variable) { EditInfo *ei = g_hash_table_lookup (self->edit_var_map, variable); if (ei == NULL) { char *str = gtk_constraint_variable_to_string (variable); g_critical ("Unknown edit variable '%s'", str); g_free (str); return; } gtk_constraint_solver_remove_constraint (self, ei->constraint); } /*< private > * gtk_constraint_solver_remove_constraint: * @self: a `GtkConstraintSolver` * @constraint: a constraint reference * * Removes a @constraint from the @solver. */ void gtk_constraint_solver_remove_constraint (GtkConstraintSolver *self, GtkConstraintRef *constraint) { GtkConstraintExpression *z_row; GtkConstraintVariable *marker; GtkConstraintVariableSet *error_vars; GtkConstraintVariableSetIter iter; if (!g_hash_table_contains (self->constraints, constraint)) return; self->needs_solving = TRUE; gtk_constraint_solver_reset_stay_constants (self); z_row = g_hash_table_lookup (self->rows, self->objective); error_vars = g_hash_table_lookup (self->error_vars, constraint); if (error_vars != NULL) { GtkConstraintVariable *v; gtk_constraint_variable_set_iter_init (&iter, error_vars); while (gtk_constraint_variable_set_iter_next (&iter, &v)) { GtkConstraintExpression *e = g_hash_table_lookup (self->rows, v); if (e == NULL) { gtk_constraint_expression_add_variable (z_row, v, constraint->strength, self->objective, self); } else { gtk_constraint_expression_add_expression (z_row, e, constraint->strength, self->objective, self); } } } marker = g_hash_table_lookup (self->marker_vars, constraint); if (marker == NULL) { g_critical ("Constraint %p not found", constraint); return; } g_hash_table_remove (self->marker_vars, constraint); if (g_hash_table_lookup (self->rows, marker) == NULL) { GtkConstraintVariableSet *set = g_hash_table_lookup (self->columns, marker); GtkConstraintVariable *exit_var = NULL; GtkConstraintVariable *v; double min_ratio = 0; if (set == NULL) goto no_columns; gtk_constraint_variable_set_iter_init (&iter, set); while (gtk_constraint_variable_set_iter_next (&iter, &v)) { if (gtk_constraint_variable_is_restricted (v)) { GtkConstraintExpression *e = g_hash_table_lookup (self->rows, v); double coeff = gtk_constraint_expression_get_coefficient (e, marker); if (coeff < 0.0) { double r = -gtk_constraint_expression_get_constant (e) / coeff; if (exit_var == NULL || r < min_ratio || G_APPROX_VALUE (r, min_ratio, 0.0001)) { min_ratio = r; exit_var = v; } } } } if (exit_var == NULL) { gtk_constraint_variable_set_iter_init (&iter, set); while (gtk_constraint_variable_set_iter_next (&iter, &v)) { if (gtk_constraint_variable_is_restricted (v)) { GtkConstraintExpression *e = g_hash_table_lookup (self->rows, v); double coeff = gtk_constraint_expression_get_coefficient (e, marker); double r = 0.0; if (!G_APPROX_VALUE (coeff, 0.0, 0.0001)) r = gtk_constraint_expression_get_constant (e) / coeff; if (exit_var == NULL || r < min_ratio) { min_ratio = r; exit_var = v; } } } } if (exit_var == NULL) { if (gtk_constraint_variable_set_is_empty (set)) gtk_constraint_solver_remove_column (self, marker); else { gtk_constraint_variable_set_iter_init (&iter, set); while (gtk_constraint_variable_set_iter_next (&iter, &v)) { if (v != self->objective) { exit_var = v; break; } } } } if (exit_var != NULL) gtk_constraint_solver_pivot (self, marker, exit_var); } no_columns: if (g_hash_table_lookup (self->rows, marker) != NULL) gtk_constraint_solver_remove_row (self, marker, TRUE); else gtk_constraint_variable_unref (marker); if (error_vars != NULL) { GtkConstraintVariable *v; gtk_constraint_variable_set_iter_init (&iter, error_vars); while (gtk_constraint_variable_set_iter_next (&iter, &v)) { if (v != marker) gtk_constraint_solver_remove_column (self, v); } } if (constraint->is_stay) { if (error_vars != NULL) { GPtrArray *remaining = g_ptr_array_new_with_free_func ((GDestroyNotify) gtk_constraint_variable_pair_free); int i = 0; for (i = 0; i < self->stay_error_vars->len; i++) { GtkConstraintVariablePair *pair = g_ptr_array_index (self->stay_error_vars, i); gboolean found = FALSE; if (gtk_constraint_variable_set_remove (error_vars, pair->first)) found = TRUE; if (gtk_constraint_variable_set_remove (error_vars, pair->second)) found = FALSE; if (!found) g_ptr_array_add (remaining, gtk_constraint_variable_pair_new (pair->first, pair->second)); } g_clear_pointer (&self->stay_error_vars, g_ptr_array_unref); self->stay_error_vars = remaining; } g_hash_table_remove (self->stay_var_map, constraint->variable); } else if (constraint->is_edit) { EditInfo *ei = g_hash_table_lookup (self->edit_var_map, constraint->variable); gtk_constraint_solver_remove_column (self, ei->eminus); g_hash_table_remove (self->edit_var_map, constraint->variable); } if (error_vars != NULL) g_hash_table_remove (self->error_vars, constraint); if (self->auto_solve) { gtk_constraint_solver_optimize (self, self->objective); gtk_constraint_solver_set_external_variables (self); } g_hash_table_remove (self->constraints, constraint); } /*< private > * gtk_constraint_solver_suggest_value: * @self: a `GtkConstraintSolver` * @variable: a `GtkConstraintVariable` * @value: the suggested value for @variable * * Suggests a new @value for an edit @variable. * * The @variable must be an edit variable, and the solver must be * in an edit phase. */ void gtk_constraint_solver_suggest_value (GtkConstraintSolver *self, GtkConstraintVariable *variable, double value) { EditInfo *ei = g_hash_table_lookup (self->edit_var_map, variable); double delta; if (ei == NULL) { g_critical ("Suggesting value '%g' but variable %p is not editable", value, variable); return; } if (!self->in_edit_phase) { g_critical ("Suggesting value '%g' for variable '%p' but solver is " "not in an edit phase", value, variable); return; } delta = value - ei->prev_constant; ei->prev_constant = value; gtk_constraint_solver_delta_edit_constant (self, delta, ei->eplus, ei->eminus); } /*< private > * gtk_constraint_solver_has_stay_variable: * @solver: a `GtkConstraintSolver` * @variable: a `GtkConstraintVariable` * * Checks whether @variable is a stay variable. * * Returns: %TRUE if the variable is a stay variable */ gboolean gtk_constraint_solver_has_stay_variable (GtkConstraintSolver *solver, GtkConstraintVariable *variable) { g_return_val_if_fail (GTK_IS_CONSTRAINT_SOLVER (solver), FALSE); g_return_val_if_fail (variable != NULL, FALSE); return g_hash_table_contains (solver->stay_var_map, variable); } /*< private > * gtk_constraint_solver_has_edit_variable: * @solver: a `GtkConstraintSolver` * @variable: a `GtkConstraintVariable` * * Checks whether @variable is an edit variable. * * Returns: %TRUE if the variable is an edit variable */ gboolean gtk_constraint_solver_has_edit_variable (GtkConstraintSolver *solver, GtkConstraintVariable *variable) { g_return_val_if_fail (GTK_IS_CONSTRAINT_SOLVER (solver), FALSE); g_return_val_if_fail (variable != NULL, FALSE); return g_hash_table_contains (solver->edit_var_map, variable); } /*< private > * gtk_constraint_solver_begin_edit: * @solver: a `GtkConstraintSolver` * * Begins the edit phase for a constraint system. * * Typically, you need to add new edit constraints for a variable to the * system, using gtk_constraint_solver_add_edit_variable(); then you * call this function and suggest values for the edit variables, using * gtk_constraint_solver_suggest_value(). After you suggested a value * for all the variables you need to edit, you will need to call * gtk_constraint_solver_resolve() to solve the system, and get the value * of the various variables that you're interested in. * * Once you completed the edit phase, call gtk_constraint_solver_end_edit() * to remove all the edit variables. */ void gtk_constraint_solver_begin_edit (GtkConstraintSolver *solver) { g_return_if_fail (GTK_IS_CONSTRAINT_SOLVER (solver)); if (g_hash_table_size (solver->edit_var_map) == 0) { g_critical ("Solver %p does not have editable variables.", solver); return; } g_ptr_array_set_size (solver->infeasible_rows, 0); gtk_constraint_solver_reset_stay_constants (solver); solver->in_edit_phase = TRUE; } static void edit_info_free (gpointer data) { g_free (data); } /*< private > * gtk_constraint_solver_end_edit: * @solver: a `GtkConstraintSolver` * * Ends the edit phase for a constraint system, and clears * all the edit variables introduced. */ void gtk_constraint_solver_end_edit (GtkConstraintSolver *solver) { solver->in_edit_phase = FALSE; gtk_constraint_solver_resolve (solver); g_hash_table_remove_all (solver->edit_var_map); } void gtk_constraint_solver_clear (GtkConstraintSolver *solver) { g_return_if_fail (GTK_IS_CONSTRAINT_SOLVER (solver)); g_hash_table_remove_all (solver->constraints); g_hash_table_remove_all (solver->external_rows); g_hash_table_remove_all (solver->external_parametric_vars); g_hash_table_remove_all (solver->error_vars); g_hash_table_remove_all (solver->marker_vars); g_hash_table_remove_all (solver->edit_var_map); g_hash_table_remove_all (solver->stay_var_map); g_ptr_array_set_size (solver->infeasible_rows, 0); g_ptr_array_set_size (solver->stay_error_vars, 0); g_hash_table_remove_all (solver->rows); g_hash_table_remove_all (solver->columns); /* The rows table owns the objective variable */ solver->objective = gtk_constraint_variable_new_objective ("Z"); g_hash_table_insert (solver->rows, solver->objective, gtk_constraint_expression_new (0.0)); solver->slack_counter = 0; solver->dummy_counter = 0; solver->artificial_counter = 0; solver->freeze_count = 0; solver->needs_solving = FALSE; solver->auto_solve = TRUE; } char * gtk_constraint_solver_to_string (GtkConstraintSolver *solver) { GString *buf = g_string_new (NULL); g_string_append (buf, "Tableau info:\n"); g_string_append_printf (buf, "Rows: %d (= %d constraints)\n", g_hash_table_size (solver->rows), g_hash_table_size (solver->rows) - 1); g_string_append_printf (buf, "Columns: %d\n", g_hash_table_size (solver->columns)); g_string_append_printf (buf, "Infeasible rows: %d\n", solver->infeasible_rows->len); g_string_append_printf (buf, "External basic variables: %d\n", g_hash_table_size (solver->external_rows)); g_string_append_printf (buf, "External parametric variables: %d\n", g_hash_table_size (solver->external_parametric_vars)); g_string_append (buf, "Constraints:"); if (g_hash_table_size (solver->constraints) == 0) g_string_append (buf, " <empty>\n"); else { GHashTableIter iter; gpointer key_p; g_string_append (buf, "\n"); g_hash_table_iter_init (&iter, solver->constraints); while (g_hash_table_iter_next (&iter, &key_p, NULL)) { char *ref = gtk_constraint_ref_to_string (key_p); g_string_append_printf (buf, " %s\n", ref); g_free (ref); } } g_string_append (buf, "Stay error vars:"); if (solver->stay_error_vars->len == 0) g_string_append (buf, " <empty>\n"); else { g_string_append (buf, "\n"); for (int i = 0; i < solver->stay_error_vars->len; i++) { const GtkConstraintVariablePair *pair = g_ptr_array_index (solver->stay_error_vars, i); char *first_s = gtk_constraint_variable_to_string (pair->first); char *second_s = gtk_constraint_variable_to_string (pair->second); g_string_append_printf (buf, " (%s, %s)\n", first_s, second_s); g_free (first_s); g_free (second_s); } } g_string_append (buf, "Edit var map:"); if (g_hash_table_size (solver->edit_var_map) == 0) g_string_append (buf, " <empty>\n"); else { GHashTableIter iter; gpointer key_p, value_p; g_string_append (buf, "\n"); g_hash_table_iter_init (&iter, solver->edit_var_map); while (g_hash_table_iter_next (&iter, &key_p, &value_p)) { char *var = gtk_constraint_variable_to_string (key_p); const EditInfo *ei = value_p; char *c = gtk_constraint_ref_to_string (ei->constraint); g_string_append_printf (buf, " %s => %s\n", var, c); g_free (var); g_free (c); } } return g_string_free (buf, FALSE); } char * gtk_constraint_solver_statistics (GtkConstraintSolver *solver) { GString *buf = g_string_new (NULL); g_string_append_printf (buf, "Variables: %d\n", solver->var_counter); g_string_append_printf (buf, "Slack vars: %d\n", solver->slack_counter); g_string_append_printf (buf, "Artificial vars: %d\n", solver->artificial_counter); g_string_append_printf (buf, "Dummy vars: %d\n", solver->dummy_counter); g_string_append_printf (buf, "Stay vars: %d\n", g_hash_table_size (solver->stay_var_map)); g_string_append_printf (buf, "Optimize count: %d\n", solver->optimize_count); g_string_append_printf (buf, "Rows: %d\n", g_hash_table_size (solver->rows)); g_string_append_printf (buf, "Columns: %d\n", g_hash_table_size (solver->columns)); if (g_hash_table_size (solver->columns) > 0) { GHashTableIter iter; gpointer val; double sum = 0; g_hash_table_iter_init (&iter, solver->columns); while (g_hash_table_iter_next (&iter, NULL, &val)) { GtkConstraintVariableSet *set = val; sum += gtk_constraint_variable_set_size (set); } g_string_append_printf (buf, "Avg column size: %g\n", sum / g_hash_table_size (solver->columns)); } g_string_append_printf (buf, "Infeasible rows: %d\n", solver->infeasible_rows->len); g_string_append_printf (buf, "External basic variables: %d\n", g_hash_table_size (solver->external_rows)); g_string_append_printf (buf, "External parametric variables: %d\n", g_hash_table_size (solver->external_parametric_vars)); return g_string_free (buf, FALSE); }