Jump to: navigation, search

VIATRA/DSE/UserGuide/API

< VIATRA‎ | DSE

Usage of the VIATRA-DSE API

This page presents the basic API of the VIATRA-DSE framework. After reading this you will be able to define a DSE problem including transformation rules and objectives, run it with different built in strategies and use the solutions the exploration found.

It is highly recommended to get familiar with the EMF and VIATRA Query frameworks first as they are essential for using VIATRA-DSE.

API

VIATRA-DSE depends on the Eclipse platform thus using it one should create a plug-in project and add the org.eclipse.viatra.dse plug-in as a dependency for the plug-in project. To start using the framework type this line of code:

DesignSpaceExplorer dse = new DesignSpaceExplorer();

Defining the domain

The first thing required is a metamodel or ecore model created with EMF and an initial model. See the code below.

Notifier theInitialModel = createInitialModel();
dse.setInitialModel(theInitialModel);


The initial model must be set as the root EObject of the model (i.e.: it contains all other objects of the model via the containment hierarchy). From v0.13M1 Resource and ResourceSet is supported as well.

Transformation rules

Rules tell the engine how the initial model can be modified and treated as atomic steps. They consist of a left hand side (LHS or condition) and a right hand side (RHS or operation). The LHS is always a Viatra Query pattern (i.e. a model query) while the RHS is simple Java code. To define such a rule an instance of the BatchTransformationRule class must be created. While it is also possible to do this in the Java language, it is strongly recommended to use Xtend (which essentially compiles to Java code).

Let's say a pattern named myPattern with two parameters is already available and can be used as a LHS:

pattern myPattern(param1 : Type1, param2 : Type2) {
    // Constraints on param1 and param2
}

Then the following Xtend code creates a rule, and then adds it to the configuration:

val factory = new BatchTransformationRuleFactory
val myRule = factory.createRule
            .name("MyRule")
            .precondition(MyPatternQuerySpecification.instance)
            .action[
                // RHS manipulates the EMF model, e.g.:
                param1.target = param2
            ]
            .build
 
dse.addTransformationRule(myRule);

Note 1: It's a good practice to use a separate "RuleProvider" class and create the rules in the constructor, for easier readability of the DSE configuration. See the rules of the BPMN example.

Note 2: It's forbidden to apply two rules with the same LHS to be able to distinguish them based on name of the pattern. If two rules would use the same LHS, creating a new pattern and using the Viatra Query language keyword find can avoid code duplication.

Id generation for new objects

It may be important that an identifier is generated for every newly created object, either semantically (it is need for the problem at hand) or it is important for the state coder in order to distinguish different model states. There is a built-in helper for generating identifiers:

int id = DseIdPoolHelper.INSTANCE.getId(myRule);

The generated id is equivalent to how many times the rule was executed in a given trajectory (sequence of transformation rules), hence after backtracking and then re-executing the rule, it will return the same id.

For version 0.12 and before

DSETransformationRule<MyPatternMatch, MyPatternMatcher> myRule = 
    new DSETransformationRule<MyPatternMatch, MyPatternMatcher>(
        MyPatternQuerySpecification.instance(), new MyPatternMatchProcessor() {
 
        @Override
        public void process(Type param1, Type2 param2) {
            // RHS manipulates the EMF model
            param1.setTarget(param2);
        }
    });
 
dse.addTransformationRule(myRule);

Objectives

Objectives determine the quality of a solution represented in double values. The goal of a DSE problem can be a single objective with a binary check (valid or invalid model) or it can consist of multiple numerical objectives giving a multi-objective optimization problem. There are two types of objective: hard and soft. Hard objectives are used to decide whether the solution is valid and can be considered as a usable solution, while soft objectives determine the quality and enables the ordering of the solutions. Soft and hard objectives can be use in any combination.

The ConstraintsObjective uses VIATRA Query to specify both hard and as a soft constraints. Example for using it as a hard objective:

dse.addObjective(Objectives.createConstraintsObjective("MyHardObjective")
    .withHardConstraint(Pattern1QuerySpecification.instance()
    .withHardConstraint(Pattern2QuerySpecification.instance()));

For each hard constraint a name and type can also be specified. The default type is ModelQueryType.MUST_HAVE_MATCH and it means that the given pattern must have a match in the actual model. The other type is ModelQueryType.NO_MATCH, which means that the given pattern shouldn't have a match. The fitness value is unaffected whether the constraints are satisfied or not.

Note that if no hard constraint is given, the framework will consider the hard constraints unsatisfied. Use the DummyHardObjective (see below) to consider all model states valid.

Soft constraints can be added with the .withSoftConstraint(querySpecification, weight) method (a name can be also specified). In this case the fitness value will be determined in the following way: fitness = sum(pattern[i].countMatches() * weight[i]). Hard and soft constraints can be used simultaneously with the same objective.

Any objective that derives from the BaseObjective (e.g. the ConstraintsObjective) can define a hard constraint on the fitness value, i.e. a solution will be valid if its fitness value is better than the given value. For this, the .withHardConstraintOnFitness(0.5) method can be used. A custom Comparator implementation can also be defined.


Other built-in objective implementations (which can be instantiated with the static methods on Objectives) are :

  • CompositeObjective - can be configured with a list of objectives. It considers a solution valid, if all the contained hard objectives assess it as a valid solution. The fitness value will be the sum of each objective functions' result. Weights can be defined for each objective.
  • TrajectoryCostSoftObjective - calculates fitness from the trajectory. Fitness can be derived from fixed rules costs, rule activation costs or length of the trajectory.
  • NoRuleActivationsHardObjective - this hard objective is satisfied only if there are no more rule activations from the current state.
  • MinimalDepthHardObjective - this hard objective is satisfied only if the particular trajectory is longer than a predefined length.
  • DummyHardObjective - this hard objective considers all trajectories as a valid solution.

Objectives also have a comparator, which decides the relation between two solutions (which one is better) corresponding to the particular objective. For example, an objective could be a value to maximize, or a value to minimize the difference from a given number. This is dependent of the objective's comparator, which can be set in the following way:

objective.withComparator(Comparators.LOWER_IS_BETTER);


The default comparator of built-in objectives is Comparators.HIGHER_IS_BETTER.

A custom objective will be required for most problems. For that either implement the IObjective interface or preferably the BaseObjective abstract class.

Solution store

Solutions (that satisfy the hard objectives) can be handled in different way. The default configuration is that it stores all of them and that it will try to stop the exploration after the first solution is found.

The solution store can be configured by creating a new one:

dse.setSolutionStore(new SolutionStore(1)); // the default configuration

The number in the constructor means that it will try to stop the exploration after that many solution found. Zero or negative number means no such stop conditions.

dse.setSolutionStore(new SolutionStore(0)); // Find all solutions

Methods for configuration:

  • acceptAnySolutions() - if an exploration strategy (e.g. the evolutionary strategy) tries to save an invalid solution, the solution store will still accept it.
  • storeBestSolutionsOnly() - if a new solution is found during exploration, all dominated solutions will be removed from the solution store.
  • logSolutionsWhenFound() - logs any solution immediately if one is found.
  • registerSolutionFoundHandler(ISolutionFoundHandler) - solutions can be processed on the fly with a callback method. The context of the exploration is available, e.g. the exploration can be stopped.

State coding

A state coder is also necessary for the engine to start the exploration. By default there is a built-in general state coder which will be enough in most of the times but it can fail for certain problems and in that case a custom domain-specific state coder is required in order to use the framework. Domain-specific state coders usually also outperforms the general purpose state coder.

To use the built-in state coder, the metamodel must be given with the following code:

dse.addMetaModelPackage(MetaModelEPackage.eINSTANCE);
 
// The framework will implicitly instantiate the built-in state coder if it is not set. 
dse.setStateCoderFactory(new SimpleStateCoderFactory(dse.getMetaModelPacakges()));

For any more detail about state coders please see the state coding section of this wiki.

Starting the exploration

To start the exploration, a strategy must also be defined. Static methods of the Strategies class can be used to create a built-in strategy such as depth-first search, breadth-first search, fixed priority and hill climbing exploration strategies.

// Starting the exploration process with a depth first search limited to 5 step depth.
dse.startExploration(Strategies.createDfsStrategy(5));
 
// The exploration can also be started with a timeout or/and asynchronously, e.g.:
dse.startExplorationWithTimeout(Strategies.createDfsStrategy(5), 30000); // milisec
dse.startExplorationAsync(Strategies.createDfsStrategy(5));

Depth-first search

By default it will not explore the state space further if a valid solution is found (i.e., satisfies the hard objectives). It can be configured to explore further:

dse.startExploration(Strategies.createDfsStrategy(5).continueIfHardObjectivesFulfilled());

Evolutionary algorithms

With the org.eclipse.viatra.dse.genetic plug-in, fully customizable evolutionary algorithms are also available.

// Starting the exploration process with an NSGA-II exploration strategy with population size of 20.
dse.startExploration(EvolutionaryStrategyBuilder.createNsga2Strategy(20));

For configuration examples see the EvolutionaryStrategyBuilder class.

Stop the exploration

If exploration is started asynchronously, the following methods can be used to stop the exploration and/or wait for termination.

dse.stopExploration();
dse.stopExplorationAsync();
dse.waitForTerminaition();

Using the results

A solution of the exploration is a trajectory, a sequence of rules which if applied to the initial model, it satisfies the goals. It's important that the same goal state can be reached by different trajectories and this is also represented by the results: an instance of Solution class can have multiple SolutionTrajectory instances. A solution has at least one solution trajectory, but nothing more can be expected, as it is heavily depend on the traversal strategy and the actual traversal of the state space. The SolutionTrajectory can be used to transform the given model (should be the initial model) based on the trajectory.

Collection<Solution> solutions = dse.getSolutions();
if (!solutions.isEmpty()) {
    Solution solution = dse.getAllSolutions().iterator().next();
    SolutionTrajectory solutionTrajectory = solution.getArbitraryTrajectory();
    // Transform the model
    solutionTrajectory.doTransformation(theInitialModel);
}
 
// To get an arbitrary solution trajectory with simpler way, use the next method:
SolutionTrajectory solutionTrajectory = dse.getArbitrarySolution();
 
// To undo a transformation (e.g. for serializing all the solutions)
solutionTrajectory.doTransformationUndoable(theInitialModel);
// ... serialize "theInitialModel"
solutionTrajectory.undoTransformation();
 
// To easily print the solutions, use the following code:
System.out.println(dse.toStringSolutions());

Other functions of the API

Global constraints

If there are any global constraints added they must be satisfied in all of the intermediate model state of trajectory. If a global constraint is not satisfied the engine won't allow the exploration to go further from that state. It can be useful to reduce the state space, but it can also be a criteria for a usable solution.

dse.addGlobalConstraint(new ModelQueriesGlobalConstraint()
    .withConstraint(MyGlobalConstraintQuerySpecifiaction.instance());

Parallel execution

The engine allows the traversal strategy to explore the state space with multiple threads. By default it allows a number of threads equal to the number of logical cores in the processor. It can be overridden with the following code:

dse.setMaxNumberOfThreads(1);

Logging, debugging

DesignSpaceExplorer.turnOnLogging(DseLoggingLevel.BASIC);
// If log4j is not configured properly, just call
DesignSpaceExplorer.turnOnLoggingWithBasicConfig(DseLoggingLevel.BASIC);