gtk2/gtk/gtkconstraintsolver.c
2021-09-18 12:53:41 +02:00

2244 lines
69 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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;
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;
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);
double 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);
}