IBM

The Grouping and Sequencing Application Development Environment

Contents

Introduction
Overview
ADE Objects
Comparison Methods
Base elements
Group elements
Constraints
Evaluators
Sequences
Iterators
Iterator Groups
Sets
Interface to the EBB Search
Software architecture
Code Development with the ADE
Example: Constrained Bin Packing
Code Maintenance with the ADE
Generic GUI
Code generator
Object Specification Files
Summary
ADE Software


Last modification $Date: 1996/09/27 15:20:17 $ by $Author: trumbo $.

Introduction

This work was motivated by the similarity between many grouping and sequence problems we have encountered so far. The ADE classes were designed around this experience base to facilitate rapid application development and provide for easy maintainability and modification. In addition to having built-in constraint manipulation techniques and complex data structure navigation, the ADE class library allows user interfaces to be developed which will automatically adapt to additions, removals, and changes in the physical data attributes of the objects used in implementing the algorithms without any recompilation of the interfaces.

At its primary level, the ADE consists of a set of classes designed specifically for forming the foundation of large, industrial strength applications in which high performance search techniques and constraint manipulation are required. They provide the developer with the primitive operations necessary to implement many of the common requirements which we have found to be shared across large scheduling and planning problems such as: sequencing, grouping, navigation and constraint enforcement. In addition to this, all base classes and classes derived from them are persistent. This allows other applications, like graphical user interfaces, the ability to recreate an exact memory image of the application during any stage of its execution.

A developer utilizes the base classes of the library by deriving their own application specific classes from them. This is done by utilizing the ADE object specification language to describe all object names and attributes along with problem constraints. These scripts are then given to the ADE class code generator which generates proper C++ header and source files for implementing the objects that the developer described, along with additional classes for navigation of those specified objects. The code generator also creates source code for each constraint specified. These constraints will be compiled and linked into a shared library which will be dynamically loaded by the ADE library to perform constraint checking. This allows the developer to add, delete or modify constraint code without recompiling any of the application code. Finally, any non-ADE solution specific code, such as heuristics, is compiled and linked with the ADE generated library to create an executable.

This scheme allows for quick development and modification because the object specification language is very easy to understand and most changes can be seen merely by recompiling changed code and re-running the application. The end user can even alter constraint code after all development is finished and the system is installed. Also, the classes generated by the ADE are fully functional C++ classes that the developers can use as they would any class.

Once the solution application is developed, we can use the ADE browser to look at particular ADE objects. This is done by writing the objects out at some point during the execution of the solution application and then reading them in with the browser. The browser doesn't simply read in the object data, though. It actually instantiates the particular objects through a method we call dynamic construction. The newly instantiated object then reads its own information back in from the file created by the solution application. So all static class data along with member functions for the object is available to the browser and whenever the ADE object's attributes are changed, by modifying the object specification file for example, the browser will automatically know about them and be able to display them without any recoding or recompilation.

By using the browser, end users are able to view object attributes, types and descriptions. If the object is derived from a container type base class, (AdeSet, AdeSequence, AdeGroupManager), then all contained objects can be displayed. The user has the ability to alter what information is to be displayed for a particular object type and can define tables and graphs to better view the data. These modifications are saved in an options file which can be unique to every user.

The ability to alter the actual display of the browser and to define tables and graphs allows the developer quickly create a usable interface to the problem application's solutions. It also permits the end user to browse multiple object databases from entirely different solution applications during the same session.

In the same way that the ADE browser re-instantiates the objects of a problem, the developer can write any program to process the ADE object database file and use the objects. Any application for this might be an interface to the main plant database and/or computer control systems.


Overview

The ADE consists of a project editor, a code generator, and a generic GUI. The developer uses the project editor to define the objects and constraints in the application. The code generator translates this input into C++ classes. The generated code is linked with additional developer code. The solutions generated are in a form usable by the generic GUI.

ADE Architecture


ADE Objects

Many of the objects defined in the ADE are of the type AdeObject. This is a C++ abstract base class that defines several pure virtual functions, specifically those required for runtime attribute information, persistence, and relational operators.


Comparison Methods

An object has some subset of its attributes that contribute to comparing its goodness to another object of the same type. Each attribute, in turn has a non-negative weight associated with it. We can arrange the attributes into an attribute vector and the weights into a weight vector. Given these two vectors, we can define several distinct comparison methods

Just having one relational operator, operator<() defined is sufficient to determine whether an object is better than, worse than, or is as good as another.

    A < B   is    A < B
    A > B   is    B < A
    A == B  is    !( (A < B) || (B < A) )

In the examples that follow, assume that there are three objects, A, B, and C. The weights and the attribute vectors are

   weights = [3 1 0 2]

   A = [ 2  1  7  2 ]
   B = [ 3  1 -1  4 ]
   C = [ 3  0  5  2 ]

Single

To compare two objects using the single comparison method, we first form the inner product (called the score of the attribute vector and the weight vector for each element. Hence, for the single comparison method, each of the elements in the attribute vector must either be an arithmetic type or automatically convertible to an arithmetic type. The comparison between the two object is then just a comparison of these two scalar values.

   score A = 11
   score B = 18
   score C = 11

   A < B true
   A < C false

   B < A false
   B < C false

   C < A false
   C < B true

Priority

A priority comparison uses an evaluation order that depends on the weight vector. An attribute with a non-zero weight is enabled, and indicates the importance of the element. Comparisons are made in order of decreasing importance, the most important enabled attribute being compared first.

   the evaluation order is [ 1 4 2 ]

   A < B               true  (2 < 3; stop)
   A < C               true  (2 < 3; stop)

   B < A               false (3 < 2; stop)
   B < C               false (3 < 3; 4 < 2; stop)

   C < A               false (3 < 2; stop)
   C < B               true  (3 < 3; 2 < 4; stop)

Multiple

Multiple comparisons are based on dominance. An object is better than another if it dominates the other object. An object is as good as another it neither dominates nor is dominated by the other object.

For an object to dominate another object, at least one of its attributes must be better than the corresponding attribute of the other attribute, and none of its attributes can be worse than the corresponding attribute of the other object. Thus, an object can have some set of attributes that are better than the corresponding attributes of another object, but if one attribute is worse, than neither object dominates the other.

A multiple comparison uses an enabled vector. A non-zero element in the enabled vector indicates that the attribute is considered in determining dominance.

   the enabled vector is [1 1 0 1 ]

   B dominates A
   B dominates C
   A does not dominate C
   C does not dominate A

   A < B               true
   B < A               false

   A < C               false
   C < A               false

   B < C               false
   C < B               true

The possible comparison methods for an object are determined at compile time, and the particular comparison method from the set of enabled methods can be set at run time.


Base elements

Base elements are the lowest level sequenceable elements. The developer, using the application development environment user interface, or by editing an ADE object specification file using a text editor, defines the properties of the base elements. The code generator uses this definition to generate appropriate C++ code for the class.


Group elements

The characteristics and boundaries of groups of base elements are defined by group elements. Like the base elements, the group elements can have developer-defined attributes.

A group is usually built by specifying the group identifier and two base elements that define the endpoints of the group. A base element is said to be in the scope of a group if bit corresponding to the group is set in the base element's flags.

In addition to the standard behaviour of all ADE objects, groups have the following additional member functions

Note 1: Since an base element can be out of scope of a group, it is the responsibility of the developer to check that a particular element is actually in the group when, for example, determining and setting properties about the group in the selfEvaluate function.

Note 2: Forcing a base element out of scope has no effect on iterators currently pointing to the group. That is, whether an element is in scope of a group is only checked when an iterator moves.


Sequences

In grouping and sequencing applications, inefficiency may result when there are several levels of grouping and it is necessary to operate simultaneously at different levels.

Unfortunately, standard collection classes (like the flat collections in the IBM class library or in Rogue Wave) make it difficult to do this easily. Sometimes sequences have to be unrolled from their groups in order to operate on them which is very expensive.

Consider the following multiply-grouped sequence

An example sequence

In this example there are 14 base elements. There are three possible groupings. The first grouping has three groups, the second has three also, and the third grouping has four groups. In the first group of the first grouping, we see that element 4 is ignored. The same is true of element 10 in the second group of the first grouping, elements 6 and 7 in the second group of the third grouping, and element 14 in the last group of the third grouping.

With standard collection classes that force one to implement strict hierarchies of elements, even though elements 5 and 6 are adjacent, there is no way to get to 6 from 5 except by going out to the containing and then getting the first element. When we are at 5 there can be no nextElement method that returns 6 because it is isolated by the hierarchical organization. There are some algorithms that require frequent jumps between groups like this.

Instead, it is arguably better to represent this hierarchical structure as a completely flat structure that somehow maintains the grouping information and is not confined by the notion of a strict hierarchy.

Since one of the primary operations on the sequences is sequential traversal (both forward and backward directions) the sequences of the should probably be some form of linked lists of elements.

Another frequent operation is destructive concatenation, in which two sequences are concatenated to form a third and the component sequences no longer exist.

In most collection class libraries, there is no notion of destructive concatenation, so to concatenate a sequence all elements must be copied and the component sequences destroyed, which is more expensive than it has to be.


Iterators

Iterators are modeled loosely after the GNU C++ pseudo-index. An iterator for a sequence can iterate over any one of its groups or over its base elements. An iterator over a group only iterates over its base elements.

The setToFirst() and setToLast() members set the iterator to the first and last element in the sequence, respectively. The next() and previous() members set the iterator to the next and previous members, respectively. The operators operator++() and operator--() overload next() and previous(). When used as an boolean value, an iterator returns true if the iterator is valid, and false otherwise. The dereferencing operators return the object that the iterator is currently pointing to.


Iterator Groups

An iterator group is a collection of related iterators that all move in lock-step. It is created with a specification of forward or backward traversal of the list. For example, if the sequence is undirected and the iterator group i is created in the backward direction, then gi = i.group1(); gi.first() will actually return the last group of the list and gi++ moves backward through the groups in the list

An iterator group simultaneously points to multiple groups. Because all iterators in an iterator groups move in lockstep there is no need for a function that returns the containing group for a base element or a the group with a common base element for a particular group.

The following example shows the initial situation of the iterator group for the example sequence.

The initial situation.

If we increment the cursor for the base element element twice, it now points to element 3. The other iterators have not changed because we haven't changed groups in any grouping.

After incrementing twice.

If we increment the cursor for the base element to element 4, we notice two things. First, the iterator for the second grouping becomes invalid, because element 4 was marked as ignored for the second group. Also, the iterator for the third grouping has changed from the first group in the grouping to the second.

After incrementing three times.

Incrementing the base element iterator again to element 5 makes the iterator for the second grouping valid again.

After incrementing four times.

Incrementing the base element iterator to element 6 forces a change of group in grouping 1, and invalidates the iterator for grouping 3 because element 6 is ignored for grouping 3.

After incrementing five times.


Evaluators

Each object known to the ADE has to have an evaluator defined, although an evaluator might not do anything in particular. For a group object, the evaluator is called whenever there is a change to its contained base elements (an insertion or deletion for example) or when there is a concatenation of two sequences that results in some group element being merged. Additionally, evaluators can be invoked to update the state of an object that depends on global data.


Sets

In addition to sequences, set are required in grouping and sequencing applications to represent solutions. An AdeSet manages a collection of pointers to ADE objects. Its behavior is different depending on its state which is set when the object is created. The state consists of a bound and flags.

The bound indicates the maximum number of elements that can be contained. The bound defaults to zero indicating that the set is unbounded. Otherwise, the set is restricted to contain no more than bound elements, although it may contain as few as one element.

The first set of flags determine the memory management characteristics

If CloneOnAdd is set, the AdeObject is cloned and the pointer to the clone is added to the set. Otherwise, the actual pointer is added to the set. If CloneOnCopy is set, when the set is copied, each individual object is cloned. Otherwise only the pointers to the objects are copied. If DeleteOnDestroy is set, the set calls delete on the objects when the set is destroyed.

The second set of flags relates to the relational behavior of the elements in the set.

If DoComparisons is enabled, the elements are compared for uniqueness. Otherwise no comparisons are done to ensure that the set is unique. If the NonDominated flag is set, the set tries to maintain a non-dominated set of elements. Otherwise, the set maintains the best elements.

The last flag, EvaluateOnAdd, determines whether the set invokes its self-evaluation function after an object has been added.


Interface to the EBB Search

The current EBBSolution class specifies a behavior of a set of sequences of elements but it does not specify the implementation of the set, the sequences, or the elements.

The EbbAdeSolution is a template class inherited from the EbbSolution class. The template takes an AdeObject representing the link, an AdeSet representing the solution, an AdeSequence representing the sequences that make up the set, and an AdeIterator that iterates over the sequences.

The developer only needs to generate the initial solution (typically by adding singleton sequences to the set), enable the appropriate constraints, and invoke the search. A set of solutions will be returned. These solutions can be written to disk using the persistent object methods and then viewed with the browser.


Code Development with the ADE

The following diagram is an overview of how one might use the ADE for development of a grouping and sequencing application.

Using the ADE for Application Development.


Example: Constrained Bin Packing

Description of the Problem

Assume that we have a set of elements, each of which has a non-zero length less than some fixed maximum length, and an identifier.

CBP

There exists a precedence constraint between any two elements, such that a positive cost cost(a,b) is incurred when element a follows item b. This cost can be infinite, indicating that item a cannot follow item b in a feasible solution.

CBP

The path cost of a sequence of elements is the sum of the cost between all adjacent pairs of the elements in the sequence. By this definition, the path cost of a one-element sequence is zero. The length of a sequence is the sum of the lengths of the elements.

Assume further that we have unlimited number of bins, each of which has the same fixed capacity. Our primary goal is to sequence the items into as few bins as possible. The secondary goal is to minimize the sum of the path costs of all sequenced bins.

CBP

The Solution Approach

This problem lends itself well to the grouping/sequencing abstraction. More...

Using the ADE for the Solution Implementation

To implement an application that solves this problem we make use of the ADE and the ADE to EBB Search bridge.

Define the base element

The base element has a length and identifier, both of which are integer valued quantities. We define an AdeBase object called (arbitrarily but appropriately) CbpElement.

Define the group

There is only grouping in this application, the bin. We define a AdeGroup called CbpBin to represent the bin.

The bin only has one attribute, the length which is an integer value. An auxilliary function, getUtilization is provided as a convenience function to calculate the length of the bin compared to the capacity of the bin.

The bin's evaluator determines the bin attributes. This code goes through the base elements and sums the length and sets the corresponding component in the length attribute.

Note that we don't define the cost of the sequence here because cost is a direction-dependent quantity in this problem and groupings are directionless. It is only the underlying sequence that has any sense of direction. Hence, the cost is calculated as a member of the sequence.

Define the link

Since we are using the EBB search to generate solutions for this problem, we must define an object that represents the "goodness" of linking two yet unlinked sequences. In this case, the link is called CbpLink and has a sole attribute, the cost between the last element in the first sequence and the first element in the second sequence.

Because links need to be compared, we must enable some comparison protocols for the object. In this case we enable the multiple and priority protocols. Additionally, we have to indicate that the cost is a comparable attribute. The cost is given negative polarity because we consider higher costs to be "worse". Finally, if we do choose to compare links with a weighted or priority comparison method, we assign the arbitrary weight of one to the attribute. Had there been more attributes, we could have assigned different weights to the different attributes, thus giving them different importance in the comparison.

Define the globals

The globals are managed by a plain ADE object called CbpGlobals. By including the header file CbpGlobals.h generated by the code generator, these globals are made visible. The global parameters can be conveniently initialized from a parameters files since all static attributes can be read set using the readStatic function.

Define the sequence

The sequence CbpSequence pulls together all of the other pieces -- the link CbpLink, the base element CbpElement, and the group CbpBin.

Sequences cannot have any member data, but they may have any number of member functions and constraints. In this case, there are two functions, getCost and getIdentifier. The former sums the costs between adjacent elements and returns the value. The latter simply returns the identifier of the first element, which, in this formulation, has a one to one corresondence with and uniquely identifies the sequence.

Define the constraints

The constraints are represented and managed by an AdeSeqenceConstraintManager object called CbpConstraint.

Each constraint checking code fragment for the base element has a unique name within the object specification file and it receives two iterators. The iterators are initialized and passed to the constraint, saving the developer some effort and reducing opportunities for creating bugs.

The code is wrapped to look like the appropriate function (passing in the two iterators with a name that is determined from the name of the constraint) stored in a file name that the developer selects, and compiled.

The first constraint, costCheck, pulls the identity from the last element of the first sequence and from the first element of the second sequence. It uses the cost matrix in CbpGlobals to determine the cost and sets this into the cost element of the link merit.

The second constraint, lengthCheck, pulls the length from each sequence and determines whether the sum of the length exceeds the global value BinCapacity.

Define the solution

In this formulation, a solution is simply a set of CbpSequence objects. We declare the AdeSet representing the solution, giving it the name CbpSolution.

Since the EBB search needs to make comparisons between solutions, as with the link, we must enable comparison protocols for the CbpSolution object. The attribute that serves as the sole component of the comparison is the pathCost. The path cost is set by the evaluator by summing the individual path cost of each CbpSequence in the set. Again, the cost has a negative polarity.

Two additional member functions are required by the EBB search, tryToFix and isGoodEnough.

Write the main application

The main application starts all the pieces working together. It consists of configuring the initial solution (a set of elements, each of which is a sequence) and invoking the search.

To initialize the solution we need to generate the cost matrix and the random quantity for each of the elements and place them into the set of sequences of elements.

Write the makefile

Any non-trivial makefile is always a bit mysterious. A makefile is generated for doing much of the work but a top-level makefile still needs to be written to link in any developer-defined code. The makefile given for this example could serve as a boilerplate example for other applications because they all tend to look fairly similar.

Compile the application

Luckily, all the work is done in the makefile. The developer needs only to invoke it with make.

Run the application

The resulting application is named cbp. The parameters are set in the configuration file cbp.config in which one has control over all the attributs in the CbpGlobals object.

When the code has finished running, an ADE persistent database file called cbp_objectdump is created.

Inspect the results

Invoke the browser by entering ade-browser and then use the file dialog to load the persistent database file cbp_objectdump.


Constraints

The developer can define an arbitrary number of object code modules that implement constraint checks. Constraints optionally modify the state of an object passed by reference to the constraint, and return a boolean value indicating whether or not the constraint was satisfied.

The constraint code is dynamically linked so that only tiny pieces of code need to be recompiled, and without even relinking the application, the changes will be reflected. Dynamic linking also facilitates enabling and disabling of constraints.

Currently, constraints can be defined for both sets and sequences.

General Constraints

For a sequence, a constraint takes two iterator groups and an ADE object as arguments. The constraint determines whether the sequence represented by the first iterator group can be concatenated with the sequence represented by the second iterator group to form a new sequence. If the constraint is satisfied the constraint function returns true, otherwise is returns false. If the constraint is satisfied, the constraint function can optionally modify attributes of the ADE object that was passed as the third argument to give a more quantitative indication of the nature of the constraint satisfaction.

For a set, a constraint takes an AdeSet, and two ADE objects as arguments. The constraint determines whether the ADE object specified as the second argument can be added to the set specified in the first argument. If the constraint is satisfied the constraint function returns true, otherwise is returns false. Again, as in the case of sequence constraints, if the constraint is satisfied, the constraint function can optionally modify attributes of the ADE object that was passed as the third argument to give a more quantitative indication of the nature of the constraint satisfaction.

Convolutional Constraints

One important high level component of the ADE should be convolutional constraints for sequences. To implement general convolutional constraints requires requires some sort of structured language that operates over sequences and groups.

Given a sequence of elements, A, a general convolutional constraint over A consists of

The extent function and constraint constraint function C++ code fragments. The steps size is the name of the base element or a group.

There may be performance problems with this approach but we gain a lot by again concentrating the information in a small number of functions.


Generic GUI

The regular structure of the ADE sequences and sets and rich information available from all ADE objects allows a fairly generic user interface that provides an intuitive way of browsing all the information about the objects in a grouping and sequencing application.

The GUI reads an ADE persistent object database which can contain any number of ADE objects. Depending on the type of the object (AdeObject, AdeSequence, or AdeSet) the format of the display is different.

The browser is invoked by simplifying issuing the ade-browser command. From the browser, the database of interest can be selected from menus. The browser is currently capable of displaying sets, sequences, and plain objects.

Once the database has been selected an loaded, the browser displays the object contained in the database and their type.

Initial browser dialog.

Double clicking on an object name brings up more detail. A set of objects is represented by a set of buttons on a canvas. In this example, the set of solutions contained one element which itself it a set.

Displaying a set of sets.

Double clicking on a set element shows more detail about the contents of the set. This solution was found to contain several sequences.

Displaying a set of sequences.

In this example, double clicking on the set element reveals the sequence details. The base elements of the sequence are represented by squares that run along the bottom of the window. Successive groups are indicated by rectangles. A rectangle which spans the same horizontal distances as a set of squares is a metaphor for a group that contains a range of base elements.

Displaying a sequence.

Double clicking on a group or base element button reveals information about the base or the group. In this case, we show information about a base element.

Displaying an object.

We can define graphs that display characteristics of sequences. Both line graphs and bar graphs can be configured.

Defining a graph.

The data contained in the graph is selectable from menus of the sequence, group, and base element attributes.

Defining a graph.

An example of a bar graph showing the weight of the base elements in the constrained bin packing example is shown below.

A sample graph.


Object Specification Files

An object specification file is a method for generating C++ classes. The files are not intended to replace the need for writing C++, but rather to simplify and complement it. As such, the object specification file is not as general as C++ but allows some access to C++ constructs.

Arguably, some of the constructs in the ADE should really become language extensions, specifically, the run-time attribute inquiries and persistent objects. However, this effort is out of the scope of this project.

The syntax of object specification files is relatively straightforward. One object can be defined per specification file. Each object can have several attributes. An attribute may be either a function or a variable, and either static or automatic.

If a variable is declared with the name foo, the only way to access it is via the generated const and non-const functions getFoo() and the (non-const) function setFoo(). A variable may be another ADE object. If the variable is declared static, then there will be one instance that is shared between all members of the class. In this case, the access is still limited to the accessor and mutator functions but there is no const version of getFoo(). All attributes not marked as invisible are available to the browser.

A function can be any valid C++ function definition. They can be const or non-const, static or automatic. The functions have the same level of access as normal C++ member functions. Note, however, that only const functions that take no arguments are available to the browser.

Standard C/C++ comments are allowed anywhere in the object specification file. Standard C/C++ #include directives are also supported. They may anywhere before the declaration of the object.

The syntax rules are given below.

object-specification-file:

include
comment
object-declaration

include:

#include <file-name>
#include "file-name"

file-name:

standard UNIX file naming rules
object-declaration:
AdeObject identifier object-property-list { attribute-block }
AdeBase identifier object-property-list { attribute-block }
AdeGroup identifier [identifier] object-property-list { attribute-block }
AdeSet identifier [identifier] object-property-list { attribute-block }
AdeSequence identifier [identifier][identifier-list][identifier][identifier] object-property-list { function-attribute-block }

identifier:

variable name satisfying standard C/C++ naming rules

identifier-list:

identifier
identifier-list , identifier

attribute-block:

empty
attribute attribute-block

function-attribute-block:

empty
function-attribute function-attribute-block

attribute:

data-attribute
function-attribute

object-property:

description
single
multiple
priority

object-property-list:

empty
: object-property
object-property-list , object-property

attribute-property:

description
unit
default-value
default-weight
comparable
invisible
negative

attribute-property-list:

empty
: attribute-property
attribute-property-list , attribute-property

data-attribute:

c++-type name attribute-property-list;

function-attribute:

c++-type name ( arguments ) constness attribute-property-list { function body }

arguments:

standard C++ function argument declaration

constness:

empty
const

string:

standard C++ double-quote delimited character string

description:

description string;

default-weight:

weight arithmetic-expression

arithmetic-expression:

anything that resolves to a C++ arithmetic value

default-value:

value expression

unit:

unit string

Further description of the keywords

description
Optional for an AdeObject or an attribute.

The description specifies a character string that is associated with the object or attribute. It is intended for providing information which can be used by the generic GUI.

comparable
Optional for an attribute.

If an attribute is comparable, its weight and its value will be considered by all comparison methods.

negative
optional for an attribute.

This polarity keyword indicates that smaller values of this attribute are better. Attributes are positive by default (larger values of the attribute are better) unless specified as negative.

single
Optional for an AdeObject.

This comparison method keyword indicates that the object is enabled for single comparisons. A single comparison uses the inner product of an object's comparable attributes and their weights as basis of comparison between two objects.

multiple
Optional for an AdeObject.

This comparison method keyword indicates that the object is enabled for multiple comparisons. A multiple comparison uses the object's comparable attributes to determine whether one object dominates another. A non-zero weight on an attribute indicates that the attribute is considered in the dominance determination.

priority
Optional for an AdeObject.

This comparison method keyword indicates that the object is enabled for priority comparisons. A priority comparison uses the inner product of an object's comparable attributes and their weights as basis of comparison between two objects.

value
Optional for an attribute.

If the attribute is intrinsic, it can have a default value.

weight
Optional for an attribute.

If the attribute is comparable, it can have a default weight.

unit
Optional for an attribute.

The unit specifies a character string that is associated with the attribute. It is intended for use with the generic GUI.

invisible
Optional for an attribute.

If the attribute is invisible, then it is assumed that either an operator<<() is not defined or that it is impossible or undesirable to display the attribute. Attributes are displayable by default unless they are function attributes that either take arguments, are non-const, or both.

#include
Optional for an object specification file.

Pull source code into the generated files.

Restrictions on Object Specification Files...

There are several restrictions to the function and data members that are managed by the ADE:

...And How to Get Around Them

Because the ADE is strongly dependent on object persistence, each attribute must itself be an object that is either a base type or a derived type that minimally knows how to read and write itself.

Hence, instead of a pointer, one will often find oneself using a reference counted AdeObject.


Code Generator

The code generator operates on an object specification file to generate C++ source code which can be compiled and linked with the ADE libraries, the EBB search libraries, and developer code, to form an application.

The code generator usage is ade-parser [options] filename where the options are

   -d        debug mode
   -M        generate dependencies
   -v        show version information

For any ADE object named Abc123, the following files are generated

Abc123.h
Any #include directives are copied into this file. Depending on the object declaration, a class is generated that derives from either AdeObject, AdeBase, AdeGroup, AdeSequence, or AdeSet. In this class are declarations for
Abc123.C
Definitions for the data and methods declared in Abc123.h.
Abc123DynCtor.C
The dynamic constructor source for the object.
Abc123DynCtor.make
A makefile to build a dynamic link library from the dynamic constructor source. For an object named Abc123, the dynamic constructor DLL is called Abc123DynCtor.dll.

For an AdeSequenceConstraint or AdeSetConstraint object named Abc123, the following file is generated

Abc123.C
Any constraints that may have been defined in the ADE file reside here. This is a separate compilation unit because the constraints are compile into dynamically linked libraries. Additionally, it is assumed that more constraints will be added over the development cycle and we don't want to have to recompile the entire application each time a constraint is added.

For an AdeBase or AdeGroup object named Abc123, the following files are also generated

Abc123-adegen.C
Declares the iterator over the object.
Abc123-adegen.h
Defines the iterator for the object.

For an AdeSequence object named Abc123, the following additional files are generated

Abc123iterg-adegen.h
Declares the iterator group for the AdeSequence which contains one iterator object for the base element and one for each of the group elements.
Abc123iterg-adegen.C
Defines the iterator group behavior.
Abc123.make
Builds all the code for the groups and base elements managed by the sequence, as well as for the sequence itself.


Software architecture

A sequence of base elements such may be divided into n groups of type k, where k is a level of grouping. If j is the maximum number of groupings, then 1 <= k <= j.

An AdeIterator is constructed from two endpoints. Given an iterator, one can use operator++() and operator--() to walk up and down a chain of base elements or groups.

This approach is fine for a single grouping, but when using iterators for multiple groupings simultaneously, there must be a mechanism for automatically incrementing and decrementing the iterator of a certain grouping when the movement of an iterator of another grouping goes over a boundary.

To implement this, we took the following approach: a group is nothing more than an iterator over a specified subsequence of base elements. Since iterators have a direction we do not need to be concerned about direction while traversing the sequence because it will be taken care of automatically. But we do need to worry about the base elements that fall on group boundaries because the movement of a high level group iterator may create a situation in which the new current base element has skipped over the boundaries of other containing groups. If this occurs, then the only way to properly restore the other containing groups' iterators is to have the new base element be able to say what contains it. So each boundary element has a vector of pointers to all of its containing groups. We call the process of resolving group pointers forward-patching and back-patching. For example

The base elements are {0,1,2,3,4,5}. The inner grouping has two groups, { a = {0,1,2}, b = {3,4,5} }. The outer grouping also has two groups, { A = {0,1,2,3}, B = {4,5} }. Each of the boundary base elements, namely {0,2,3,4,5} would have a vector of pointers to their containing groups. In this case, the first component of the vector would be a pointer to the inner grouping object (either a or b) and the second component would be pointer to the outer grouping object (either A or B).

The base elements that fall on the boundary of group A are 0 and 3. and both of these elements have a pointer to group A. Similarly, elements 4 and 5 have pointers to B, 0 and 2 have pointers to a, and 3 and 5 have pointers to b. These pointers were identified by forward patching.

To fill in the remaining components of the vector, element 2 points to A, and element 4 points to b. These pointers were obtained by back-patching because in each case, the element does not fall on the boundary of the group to which it points.

When a group level iterator is moved, we can adjust all other iterators within the group just by looking at the new base element and setting them. Each base element has a vector of grouping pointers. If the element is not a boundary element, then this vector is NULL, else this vector should have an entry for each grouping level.

When a new group is defined over a sequence, the two boundary elements of the group can have the entry in their group vector updated immediately (forward-patched) for the new group. However, it may be the case that by defining this new group we have split another grouping. In this case we will need to patch the non-overlapping boundary elements (back-patch) with the appropriate group pointers. Because it can't be done immediately, so we'll take care of it later.

The actual time when all back-patching is performed is when the an iterator is moved to a boundary element whose group vector is only partially full. At that time the AdeSequence will search for the proper groupings to fill in the vector. This has the advantage of not back-patching elements that the developer might not be concerned about. Also, once an element in a sequence is back patched, it won't need to be back-patched again.

Upon deletion of a boundary element from a sequence, all information contained within its group vector is copied to its adjacent element within the same group.

Base and Group Objects

We define two abstract base classes, namely, AdeBase and AdeGroup.

Solution-specific base element and groups are derived from these two classes. There can only be one base element class defined in a sequence. There can, however, be up to 255 distinct groupings. These base classes only provide a mechanism for chaining like objects together. This mechanism is invisible to derived classes and does not need to concern the developer.

The classes that are derived from AdeBase and AdeGroup can have any number of methods and variables that the developer wishes. They should also contain appropriate construction and destruction methods. To avoid the complexity of defining the classes by hand, it is recommended that the developer uses the object specification language.

Here is an example of using the AdeBase and AdeGroup classes.

class Element : public AdeBase
{
  Element ( int a1, int a2 ) : attribute1(a1), attribute2(a2) {};
  int attribute1;
  int attribute2;
};

class Group1 : public AdeGroup
{
  Group1 ( AdeBase *ep1, AdeBase *ep2 ) : AdeGroup(ep1,ep2) {};
  float attribute1;
  int   attribute2;
};

class Group2 : public AdeGroup
{
  Group2 ( BaseElement *ep1, BaseElement *ep2 ) : AdeGroup(ep1,ep2) {};
  String attribute1;
  float  attribute2;
};

Groupings are defined by instantiating an object of the proper group type by passing the constructor the two endpoints of the group. An element within a group can be removed from the scope of that group by using the unScopeElement method. Note, however, that this does not physically remove the element from the group, it merely tells the group to ignore it. This element can subsequently be added back into the scope of the group by calling the scopeElement method. A group must have at least one element in its scope. If this last element is to be removed from the group, then the group object needs to be removed from the sequence.

Sequence Objects

In order to encapsulate the actual base object sequencing for the developer and also allow for correct iterator initialization and manipulation, we define an abstract base class called AdeSequence. This class provides method for adding and deleting both base elements and groupings to/from the sequence, along with protected methods for forward- and back-patching. To use the AdeSequence, one must create a new class called in which methods for solution-specific groupings of the base elements are defined. Again, it is recommended that the developer uses the object specification language and allow the code generator to handle the complexity of defining the derived classes.

The developer defines a new group on a sequence by first instantiating and initializing the appropriate group object then calling the addGroup method of the sequence. Likewise, the developer can remove that grouping by calling the removeGroup method of the sequence. Notice that this scheme gives the developer a lot of freedom in declaring groups. For example, the developer may declare two groups at the same level which are non-disjoint, or the developer may leave gaps in the grouping of a certain level. The only operation that the sequence object will deny is defining a group on a boundary element that is already a boundary element of another grouping at the same level. Therefore, it is important that the developer ensure that the logic of the grouping sequences is correct for the particular application.

A sequence can either be directed or undirected. If a sequence is directed, then this means that we can only traverse the base elements and groups in a single set direction. Whenever an iterator group is initialized via a directed sequence, the developer has no control over setting the traversal direction of the group. If a sequence is undirected, then it may be traversed starting from either endpoint. When an iterator group is instantiated via an undirected sequence, then the traversal direction will be set to the mode specified by the developer. The default direction will be forward traversal, meaning the direction in which the sequence was built.

class Sequence : public AdeSequence
{
  void addGroup    ( Group1 * );     // Add a group1 to sequence
  void addGroup    ( Group2 *   );   // Add a group2 to sequence

  void removeGroup ( Group1 *   );   // Remove a group2 to sequence
  void removeGroup ( Group2 *   );   // Remove a group2 to sequence
};

int main ( void )
{
  Element e1, e2, e3, e4, e5;
  Sequence;

  sequence.add ( &e1 );
  sequence.add ( &e2 );
  sequence.add ( &e3 );
  sequence.add ( &e4 );
  sequence.add ( &e5 );

  sequence.addGroup ( new Group1(&e1, &e3) );  // G1 = {e1,e2,e3}
  sequence.addGroup ( new Group1(&e4, &e5) );  // G1 = {e1,e2,e3}, {e4,e5}
  sequence.addGroup ( new Group2(&e1, &e5) );  // G2 = {e1,e2,e3,e4,e5}
}

Elements that are appended or prepended to a sequence are set to be not included within any group's scope. The developer must specifically add new elements to group scope by calling scopeElement method or by creating a new grouping which includes the element. An element cannot be physically removed from a sequence until it has been properly removed from the scope of all its containing groups. Groupings may be added and removed from a sequence at any time.

Iterator Objects

As described above, iterators are objects that walk up and down a chain of base elements. Iterators may be moved by using pre and post increment/decrement operators. The actual object being iterated over can be dereferenced by operator*() and the data within the objects can be manipulated by using operator->().

The abstract base class for iterators is called AdeIterator. This base class has an associated direction (forward or backward) which is set upon instantiation and dictates which way the iterator will move along the chain. There is also a boolean operator to test if an iterator is valid. An iterator becomes invalid when it has reached the final element in the chain along the direction which it has been moved. An iterator also becomes invalid if the current element is set to be ignored by the iterator. This happens, for example, when an element is removed from the scope of a group. In this case, however, the iterator is still able to be incremented and decremented. An iterator can be reset at any time back to either of its endpoints by using the setToFirst and setToLast methods.

The ADE will inherit from AdeIterator to define a specific iterator class for each group and base element object that is defined by the developer. These specific iterator classes will know the type of element that they iterate over, along with having a specific copy constructor and assignment operator to those element types.

class ElementIterator : public AdeIterator < Element >
{
  ElementIterator ( Element *e1, Element *e2 );
  ElementIterator &operator=( ElementIterator *Src );
};

class Group1Iterator : public AdeIterator < Group1 >
{
  Group1Iterator ( Group1 *e1, Group1 *e2 );
  Group1Iterator &operator=( Group1Iterator *Src );
};

class Group2Iterator : public AdeIterator < Group2 >
{
  Group2Iterator ( Group2 *e1, Group2 *e2 );
  Group2Iterator &operator=( Group2Iterator *Src );
};

int main ( void )
{
  Element e1, e2, e3, e4, e5;

  link(e1, e2);
  link(e2, e3);
  link(e3, e4);
  link(e4, e5);

  ElementIterator ei( &e1, &e5 );

  while (ei) {
    cout << "Height :" << ei -> height << endl;
    cout << "Width : " << ei -> width << endl;
    ei++;
  }
}

Iterator Group Objects

An iterator group is a set of iterators that work together to iterate over a sequence. The group will consist of an iterator for each level of grouping in the sequence along with an iterator for the base elements of the sequence. The developer may utilize a specific iterator within the group by obtaining a reference to it via the appropriate group method. The iterators within a specific iterator group work together by propagating the movement of one of the iterators to any other iterator within the same group that may be affected by that movement. In other words, all iterators within a group move with respect to one another. Note, however, that multiple objects of the same iterator class will not have an effect on each other.

An iterator group is instantiated by passing the constructor a reference to an AdeSequence and an optional direction. From that point forward all iterator manipulations of that group will be done with respect to the appropriate direction of the group. The ADE will inherit from the abstract base class AdeIteratorGroup class to define a specific IteratorGroup class which will know how to set the iterators correctly with respect to the solution dependent groups.

It is important to note that if the sequence from which an iterator group was instantiated physically changes, then the iterator group will be left in an undefined state and needs to be reinitialized before using it. This can be done by calling the reInitialize method for each active iterator group.

class IteratorGroup : public AdeIteratorGroup
{
  ElementIterator Element;
  Group1Iterator  Group1;
  Group2Iterator  Group2;
};

int main ( void )
{
  IteratorGroup ig ( sequence );

  ElementIterator &ei = ig.Element;  // *ei = e1
  Group1Iterator &g1i = ig.Group1;   // *g1i = {e1,e2,e3}
  Group2Iterator &g2i = ig.Group2;   // *g2i = {e1,e2,e3,e4,e5}

  g1i++;                        // *ei = e4
  // *g1i = {e4,e5}
  // *g2i = {e1,e2,e3,e4,e5}

  ei--;                              // *ei = e3
  // *g1i = {e1,e2,e3}
  // *g2i = {e1,e2,e3,e4,e5}
}

An iterator group can also be initialized by passing the constructor a reference to another iterator group along with a group reference. This will have the effect of creating an iterator group which is frozen at the given group level. What this means is that any operation performed on an iterator of this group which moves outside of the current frozen group will invalidate all iterators. As with normal iterator groups, if the sequence should physically change while this iterator group is active, then the entire iterator group will be in an undefined state and will need to be reinitialized.

Class Diagram

The class library has been designed to implement the scheme that is described above.

Class Diagram


Code Maintenance with the ADE

Imagine that we have to write a simple application suite. The application involves an algorithm that generates a few objects and writes them to a file. A graphical user interface application reads from the file and displays the generated objects.

The developer defines the objects, writes the algorithm, and the output function. These files are compiled and linked to form an application.

Initial development without ADE

The application is run to generate an output database containing several objects of class AppClass. The GUI must be written to read the objects from the file and then display them.

Initial development without ADE

With the ADE, the initial development of the application that generates the object is very much simplified. The input and output of the objects is performed using the built-in persistence (so writeResults is a one-liner) and there is no need to write a graphical user interface at all.

The objects are specified using the ADE object specification language which is parsed to generate C++ code which is compiled and linked with the rest of the application.

Initial development with ADE

Now imagine that a change needs to be made such that the objects generated have an additional property. We would like this property to also be stored in the file and displayed. Without the ADE, the following modifications need to be made. We must modify application.h to add the attribute, writeResults to output the new information, and any part of the algorithm that is affected by the new attribute. For the GUI, both readResults and the code that displays the attribute must be updated.

Maintenance without ADE

Maintenance without ADE

With the ADE, however, only a single modification needs to be made. The application still has to be compiled and linked. Because persistence is built into the ADE objects, the input and output code in writeResults is unchanged. And because of the run time attribute inquiry capability, the objects themselves tell the browser how to change what it displayed. Finally, any parts of the algorithm that uses relational operators on objects of type AppClass are unchanged.

Maintenance with ADE

Of course, this is an extremely trivial example but it shows the kind of reduction in development and maintenance time that can be achieved using the ADE.


Summary

The ADE is a synthesis of several standard object-oriented programming techniques (persistent objects and the abstract factory design pattern), a few non-standard techniques which arguably should be standard (run-time type and attribute inquiries) and some novel techniques specific to the domain of grouping and sequencing applications (configurable relational operators, smart iterators over multiply grouped sequences, and constraint manipulation).


ADE Software

The ADE is written in C++ and depends on the IBM Collection Class Library and on OSF/Motif. It currently runs only under AIX.

To obtain a copy of the ADE package, please contact us by sending mail to schedule@watson.ibm.com.


[ IBM home page | Order | Search | Contact IBM | Help | (C) | (TM) ]