CS 2360 Lecture 14: February 28

Constraints

A good deal of this material (both in textual and idea forms) are due to Scott Hudson.
Constraints offer the ability to describe, in a high-level and declarative fashion, a set of relationships that are to hold among data items. Given a set of values and a set of relationships between them expressed as equations, a constraint maintenance or evaluation system can be employed to automatically maintain the proper relationships between these values in the face of changes. The automatic nature of this update allows very high level specifications to be employed for a number of tasks in the user interface software domain. These include, for example, controlling user interface presentations [Huds89, Huds90, Vand90, Myer93], making connections between data objects and interactive views of those objects [Huds88, Hill92], support for construction of graphical documents [VanW82], and controlling animation [Born86].

Because of their inherent advantages, constraint systems of various sorts have been used in a number of user interface systems (see for example [Bart86, Born86, Myer90, Huds93b]). However, constraints are still considered an "exotic" technology in many circles. One reason for this is that, in the compiled languages that would typically be used for production user interface development (i.e., languages like Pascal, C, C++, Eiffel, etc.), there have been no systems that allow truly convenient and efficient integration of constraints with the rest of the user interface and application code. Many such systems however exist for our target language: Lisp. More the details of this in a minute.

One Way or Many Ways?

The general problem of constraint satisfaction is unsolvable (I believe its no to be the same as solving systems of Boolean equations which is known to be NP hard). Consequently, all constraint systems place some limitations on the classes of constraints that they can deal with. Among systems supporting user interface applications both so called "one-way" and "multi-way" constraint satisfaction systems have been used.

One-way systems (such as the one described here) place limitations on the form of equations that may be used such that information always flows only from the right hand side of a constraint rule to the left hand side hence the designation as "one-way". In general, these systems support rules of the form (in a pascal-like notation):

A = F(B,C,D); This rule states that the value of attribute A will always be equal to the value obtained by applying function F to (the updated values of) attributes B, C, and D. This form of rule allows only a single attribute on the left hand side and does not allow any given attribute to be defined by more than one equation. Typically these systems also require that there are no circular dependencies between attributes, however, this restriction is lifted in various ways in some systems.

Consider if this were not the case. Here is a system of cyclic constrains which could cause you a problem:

A= B -2 ; B= A -2 ; What values satisfy these equations? If a constraint system was processing these values what might happen? (Hint: Think about substituting the equations in for the values in the above equations.) It is important that you understand that in the constraint

A = F (B,C,D); says nothing about the values of B, C, and D. It says how to compute A from the values in question not the reverse. This is why the system is called "one-way." Note that a one-way system which guarantees that the constraints will be satisfied will adjust the value of A to meet the constraint above but if A is modified it will not modify B, C, or D to insure the constraint is met. Multi-way constraints normally remove the asymmetry from rules and allow information flow in different directions at different times. In general they may support rules such as:

F(A,B) = G(C,D); or

F (A,B) < G(C,D); However, except in special cases (such as multi-linear equations) the system designer is typically responsible for providing a series of solution methods (or factorizations of the rule equation), each of which allows one attribute's value to be computed as a function of the remaining attributes.

In general, multi-way constraints are more powerful and can support some constructions that one-way constraints cannot. However, with this generality comes some cost.

First, multi-way constraint satisfaction systems can fail to find a solution to a system of constraints that arise at runtime either because no such solution exists, or because the particular solution mechanism being employed cannot find one. Currently few analysis techniques exist for detecting or characterizing this possibility in advance. Consequently the system designer may need to explicitly provide for this possibility in all aspects of a system that uses constraints (see [Born87, Free90] for one approach to this). In contrast, one-way constraints are guaranteed to find a solution in all cases (so long as the code for each individual constraint equation completes in finite time).

Second, multi-way constraint systems are much less predictable and understandable than one-way systems. For example, unlike one-way systems, multi-way systems can find themselves in under-constrained situations where more than one solution is valid. Robust approaches for specifying how the system is to respond in these cases have been developed (see [Born87, Free90]). However, it is can still be difficult to understand at design or specification time, just how the system will react in all cases at run-time. Again, this is in contrast to one-way systems which, because of their more restricted form, are very easy to understand and predict.

Why In The World Would I Want To Use This Stuff

My chief interest in the use of constraints is to make user interface programming easier. However, since I realize many of you have probably never built user interface software, I'll try to motivate my examples by giving you cases where people do stupid or ugly things in interfaces that you can see, and/or programming examples you are familiar with.

Geometry Management
How many times have you resized a window on a Mac, Pc w/windows, or a unix box running X and had it, "not do what you wanted." Somehow, the person who wrote the code just didn't "do the right thing" with resizing. The reason this happens is because sophisticated geometry management is hard without constraints. Consider an expression which can make things stay spaced correctly (in a C++ or Modula style notation) as well as as aligned:

object2-left := object1-right + 2 object2-top := object1-top
Connect 4 Display
Suppose you had a terminal package which could control a particular character on the screen based on the contents of a two dimensional array called "screenDisplay." Further suppose you knew that the upper left corner of the connect 4 board should be displayed at (screen location) 5,6. Consider these constraints (again using Pascal or Modula3 notation): screenDisplay(5,6) := ValueAtBoardPosition(0,0) screenDisplay(5,7) := ValueAtBoardPosition(0,2) screenDisplay(5,8) := ValueAtBoardPosition(0,3) ... Now your board is always syncrhonized with your positions, and you never need to write a function which "updates" the screen to reflect new board values.

Now suppose that you have a two player version of connect four which the players were connected to each other (and your program) via a network. You want to make sure that they each get correct (up to date) displays and that you don't to do very much work. Just reuse your constraints above for each players display:

screenDisplayPlayer2(5,6) := screenDisplay(0,0) ... Shazam, now both players have displays that are synchronized to your data structures!

Now suppose each player wants to play red but they can agree who will will go first. How could you use constraints to make sure that each player felt happy (saw the color red (or a little 'R') on their display for their pieces) and yet you didn't have to do very much work? Well, write a function called c4-invert-color which changes red to black and vice versa, but doesn't affect the empty spaces. Now rewrite your constraints for player two's screen to be:

screenDisplay(5,6) := c4-invert-color(ValueAtBoardPosition(0,0)) screenDisplay(5,7) := c4-invert-color(ValueAtBoardPosition(0,2)) screenDisplay(5,8) := c4-invert-color(ValueAtBoardPosition(0,3)) ...

Connect 4 Data Structure

Someone came to my office and said:

Sometimes you want a column-based data structure and sometimes you want a row-based data structure, maybe I should just keep both?
I said, "No, that'll be difficult, because everytime you modify one, you'll have to go modify the other. Further, it requires double the code because every modification (or access) routine now must be written once for each data structure. If you have constraints, this problem goes away. Say you decide to use columns, but might find it convenient to have rows sometimes. You write all of your code to modify/access columns and have one function "extract row from board" which will walk across the columns to extract a row. Now consider these constraints:

row1 := ExtractRowFromBoard(some-board,1); Now you are guaranteed that anytime you need access to row1, it will be "synchronized" with the other (column-based) representation.

The astute observer here will realize that there is a problem with the above. It cannot compute a general transformation of the data structure (i.e. on boards of arbitrary size) because we need to know in advance what values to compute. We would need to enumerate the row values to compute. What we really want to say is:

for i from 0 to number-of-rows(some-board) : row[i]:=ExtractRowFromBoard(some-board,i)
Bank Example
Suppose you wanted to build an interactive system which could constantly display the monthly payments for potential car-buyers. They would input some values such as the amount of the car, the down-payment, the finance rate, the number of months, etc., and at all times the monthly payment should be kept up to date on the screen. This is useful so people can explore what the payment schedule looks like. If you can express the monthly payment as a function of the variables, you are home free with constraints! monthlyPayment:=F(amountOfCar,amountOfDownpayment,rate,numberOfMonths); Now, you never have to worry about recalculating the monthly payment!

Implementing Constraints

Well, all along here we have been assuming that we had a constraint system which could do this for us... Now, the real problem! How would we construct a system which could automatically perform the work of updating values to make sure constraints are always met.

For this discussion, we'll assume that a set of constraints operates over a set of variables. We need to be sure that our system can handle changes to both the set of variables and to the set of constraints.

This is a declarative system. You say (as the programmer) what is to happen, not how it is to happen. The system takes care of the how. Note that "constraints" is a loaded word. There is a whole community of people of who do "Constraint Logic Programming" which is not what we are doing here, nor is this what some scientific programming people call constraint satisfaction (which tends to be numerical in nature).

Some assumptions we will make to make our situation a little more restricted and thus easier to deal with are:

Dependency Graphs
Before we get very far we will need to build a dependency graph. This a graph which expresses which variables "depend on" other variables to compute their values. The nodes in the graph are varaibles, the edges are dependencies. An edge from A to B indicates that B is dependent on A. Consider the constraint: A := B + C ; This implies two edges, B->A and C->A. Note: you can reverse the edge direction if you wish, as long as you are consistant.

Simple Approach
Topological Sort
A topological sort (for our purposes) is a sort of the nodes of the graph based on the outgoing dependency edges. This sort says that a node X is in front of node Y in the sorted list if there is an edge from Y to X. A reverse topological sort says that X is in front of Y if there is an edge from X to Y.

Note: this does not produce a total order, but sufficiently good partial order. Example:

   b 
 /   \ 
a     d
 \  / 
  c   
   \  
    e 
Edges here are from B to A, C to A, D to C, D to B, and E to C. A toplogical sort gives you :

A, B, C, D, E, F. There are other possible orderings, such as A, C, B, F, E. A reverse topological sort gives you: F, E, D, C, B, A Now, returning to our discussion of bringing constraints up to date, you should see that it is useful to topologically sort as if you didn't, you might end up calculating some values more than once. Consider an order like F, E, B, C, D, A. This order is terrible because to make sure the constraints hold you'll end up calculating the value of B and C multiple times!

Approach 2: Data Driven
Here is a better algorithm which updates things in a more intelligent manner. It should be called whenever a variable (or set of variables) is modified.

Consider this example:
       (A)
       / \                (all edges are up)
     (B) (C)              Change G
     / \   \              Assume B does not result in a new value
   (D) (E)  (F)           Therefore A,C, & F never get in work list
    |
   (G)
Note that if the values that a variable depends on never get changed, it never ends up in the work list and never (needlessly) gets evaluated.

The big problem here is that variables may end up in the work list multiple times. In fact, they may end up there an exponential number of times! This is a result of a variable gettting "intermediate" values before it gets its final value. Consider modifying the graph above to have an edge from D to E and reconsider our example. (B will end up on the queue multiple times.)

Approach 3: A simple demand driven algorithm
A simple demand driven approach is to start at what you want, and work in the other direction. Note: This approach does not modify the values when the values change, but when they are demanded (asked for) . Consider this algorithm with the assumption that every variable can be marked with a bit that says if it is "out of date". Note: this is really just a topological sort with the evaluations being done along the way.

Note: This alogorithm doesn't take into account what has changed. It could potentially reevaluate the entire graph.

Sketch Of A Combined Data and Demand Driven Approach
This is just the basic idea of how to combine the data and demand approaches to get an optimal solution. You will need to have a bit stored on each variable to know if it is "out of date" or not.


Back To The CS2360 Home Page
Ian Smith (iansmith@cc.gatech.edu)

Last Modified 27 Feb 95 by Ian Smith (iansmith@cc.gatech.edu)