简体   繁体   中英

Incremental score calculation bug?

I've been dealing with a score corruption error for few days with no apparent reason. The error appears only on FULL_ASSERT mode and it is not related to the constraints defined on the drools file.

Following is the error :

014-07-02 14:51:49,037 [SwingWorker-pool-1-thread-4] TRACE         Move index (0), score (-4/-2450/-240/-170), accepted (false) for move (EMP4@START => EMP2).
       java.util.concurrent.ExecutionException: java.lang.IllegalStateException: Score corruption: the workingScore (-3/-1890/-640/-170) is not the uncorruptedScore (-3/-1890/-640/-250) after completedAction (EMP3@EMP4 => EMP4):
      The corrupted scoreDirector has 1 ConstraintMatch(s) which are in excess (and should not be there):
        com.abcdl.be.solver/MinimizeTotalTime/level3/[org.drools.core.reteoo.InitialFactImpl@4dde85f0]=-170
      The corrupted scoreDirector has 1 ConstraintMatch(s) which are missing:
        com.abcdl.be.solver/MinimizeTotalTime/level3/[org.drools.core.reteoo.InitialFactImpl@4dde85f0]=-250
      Check your score constraints.

The error appears every time after several steps are completed for no apparent reason.

I'm developing a software to schedule several tasks considering time and resources constraints. The whole process is represented by a directed tree diagram such that the nodes of the graph represent the tasks and the edges, the dependencies between the tasks.

To do this, the planner change the parent node of each node until he finds the best solution.

The node is the planning entity and its parent the planning variable :

    @PlanningEntity(difficultyComparatorClass = NodeDifficultyComparator.class)
public class Node extends ProcessChain {

    private Node parent; // Planning variable: changes during planning, between score calculations.

    private String delay; // Used to display the delay for nodes of type "And" 

    private int id; // Used as an identifier for each node. Different nodes cannot have the same id

    public Node(String name, String type, int time, int resources, String md, int id)
    {
        super(name, "", time, resources, type, md); 
        this.id = id;
    }

    public Node()
    {
        super();
        this.delay = "";
    }

    public String getDelay() {
        return delay;
    }

    public void setDelay(String delay) {
        this.delay = delay;
    }


    @PlanningVariable(valueRangeProviderRefs = {"parentRange"}, strengthComparatorClass = ParentStrengthComparator.class, nullable = false) 
    public Node getParent() {
        return parent;
    }

    public void setParent(Node parent) {
        this.parent = parent;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    /*public String toString()
    {
        if(this.type.equals("AND"))
            return delay;
        if(!this.md.isEmpty())
            return Tools.excerpt(name+" : "+this.md);

         return Tools.excerpt(name);
    }*/


    public String toString()
    {
        if(parent!= null)
            return Tools.excerpt(name) +"@"+parent;
        else 
            return Tools.excerpt(name);
    }
    public boolean equals( Object o ) {
        if (this == o) {
            return true;
        } else if (o instanceof Node) {
            Node other = (Node) o;
            return new EqualsBuilder()
                    .append(name, other.name)
                    .append(id, other.id)
                    .isEquals();
        } else {
            return false;
        }
    }

    public int hashCode() {
        return new HashCodeBuilder()
                .append(name)
                .append(id)
                .toHashCode();
    }


     // ************************************************************************
    // Complex methods
    // ************************************************************************

     public int getStartTime()
     {
         try{
             return  Graph.getInstance().getNode2times().get(this).getFirst();
            }
         catch(NullPointerException e)
         {
             System.out.println("getStartTime() is null for " + this);
         }
         return 10;
     }

     public int getEndTime()
     {  try{
         return  Graph.getInstance().getNode2times().get(this).getSecond();
        }
     catch(NullPointerException e)
     {
         System.out.println("getEndTime() is null for " + this);
     }
     return 10;
     }

     @ValueRangeProvider(id = "parentRange")
     public Collection<Node> getPossibleParents()
     {  
         Collection<Node> nodes = new ArrayList<Node>(Graph.getInstance().getNodes());

         nodes.remove(this); // We remove this node from the list

         if(Graph.getInstance().getParentsCount(this) > 0) 
             nodes.remove(Graph.getInstance().getParents(this)); // We remove its parents from the list

         if(Graph.getInstance().getChildCount(this) > 0)
             nodes.remove(Graph.getInstance().getChildren(this)); // We remove its children from the list

         if(!nodes.contains(Graph.getInstance().getNt()))
             nodes.add(Graph.getInstance().getNt());

         return nodes;
     }

    /**
     * The normal methods {@link #equals(Object)} and {@link #hashCode()} cannot be used because the rule engine already
     * requires them (for performance in their original state).
     * @see #solutionHashCode()
     */
    public boolean solutionEquals(Object o) {
        if (this == o) {
            return true;
        } else if (o instanceof Node) {
            Node other = (Node) o;
            return new EqualsBuilder()
                    .append(name, other.name)
                    .append(id, other.id)
                    .isEquals();
        } else {
            return false;
        }
    }

    /**
     * The normal methods {@link #equals(Object)} and {@link #hashCode()} cannot be used because the rule engine already
     * requires them (for performance in their original state).
     * @see #solutionEquals(Object)
     */
    public int solutionHashCode() {
        return new HashCodeBuilder()
                .append(name)
                .append(id)
                .toHashCode();
    }


}

Each move must update the graph by removing the previous edge and adding the new edge from the node to its parent, so i'm using a custom change move :

public class ParentChangeMove implements Move{

    private Node node;
    private Node parent;

    private Graph g  = Graph.getInstance();

    public ParentChangeMove(Node node, Node parent) {
        this.node = node;
        this.parent = parent;
    }

    public boolean isMoveDoable(ScoreDirector scoreDirector) {  
        List<Dependency> dep = new ArrayList<Dependency>(g.getDependencies());
        dep.add(new Dependency(parent.getName(), node.getName()));

        return !ObjectUtils.equals(node.getParent(), parent) && !g.detectCycles(dep) && !g.getParents(node).contains(parent);
    }

    public Move createUndoMove(ScoreDirector scoreDirector) {
        return new ParentChangeMove(node, node.getParent());
    }


    public void doMove(ScoreDirector scoreDirector) {

        scoreDirector.beforeVariableChanged(node, "parent"); // before changes are made

        //The previous edge is removed from the graph
        if(node.getParent() != null)
        {
            Dependency d = new Dependency(node.getParent().getName(), node.getName());
            g.removeEdge(g.getDep2link().get(d)); 
            g.getDependencies().remove(d);
            g.getDep2link().remove(d);
        }

        node.setParent(parent); // the move

        //The new edge is added on the graph (parent ==> node)
        Link link = new Link();
        Dependency d = new Dependency(parent.getName(), node.getName());
        g.addEdge(link, parent, node); 
        g.getDependencies().add(d);
        g.getDep2link().put(d, link);

        g.setStepTimes();

        scoreDirector.afterVariableChanged(node, "parent"); // after changes are made
    }


    public Collection<? extends Object> getPlanningEntities() {
        return Collections.singletonList(node);
    }


    public Collection<? extends Object> getPlanningValues() {
        return Collections.singletonList(parent);
    }

     public boolean equals(Object o) {
            if (this == o) {
                return true;
            } else if (o instanceof ParentChangeMove) {
                ParentChangeMove other = (ParentChangeMove) o;
                return new EqualsBuilder()
                        .append(node, other.node)
                        .append(parent, other.parent)
                        .isEquals();
            } else {
                return false;
            }
        }

        public int hashCode() {
            return new HashCodeBuilder()
                    .append(node)
                    .append(parent)
                    .toHashCode();
        }

        public String toString() {
            return node + " => " + parent;
        }

}

The graph does define multiple methods that are used by the constraints to calculate the score for each solution like the following :

    rule "MinimizeTotalTime" // Minimize the total process time
    when
        eval(true)
    then
        scoreHolder.addSoftConstraintMatch(kcontext, 1, -Graph.getInstance().totalTime());
end

On other environment modes, the error does not appear but the best score calculated is not equal to the actual score.

I don't have any clue as to where the problem could come from. Note that i already checked all my equals and hashcode methods.


EDIT : Following ge0ffrey's proposition, I used collect CE in "MinimizeTotalTime" rule to check if the error comes again :

rule "MinimizeTotalTime" // Minimize the total process time
    when
        ArrayList() from  collect(Node())
    then
        scoreHolder.addSoftConstraintMatch(kcontext, 0, -Graph.getInstance().totalTime());
end

At this point, no error appears and everything seems ok. But when I use "terminate early", I get the following error :

java.util.concurrent.ExecutionException: java.lang.IllegalStateException: Score corruption: the solution's score (-9133) is not the uncorruptedScore (-9765).

Also, I have a rule that doesn't use any method from the Graph class and seems to respect the incremental score calculation but returns another score corruption error.

The purpose of the rule is to make sure that we don't use more resources that available:

   rule "addMarks" //insert a Mark each time a task starts or ends

    when
        Node($startTime : getStartTime(), $endTime : getEndTime())

    then
        insertLogical(new Mark($startTime));
        insertLogical(new Mark($endTime));

end 

rule "resourcesLimit" // At any time, The number of resources used must not exceed the total number of resources available

    when
        Mark($startTime: time)
        Mark(time > $startTime, $endTime : time)
        not Mark(time > $startTime, time < $endTime)
        $total : Number(intValue > Global.getInstance().getAvailableResources() ) from  
             accumulate(Node(getEndTime() >=$endTime, getStartTime()<= $startTime, $res : resources), sum($res))
    then
            scoreHolder.addHardConstraintMatch(kcontext, 0, (Global.getInstance().getAvailableResources() - $total.intValue()) * ($endTime - $startTime) );             
end

Following is the error :

    java.util.concurrent.ExecutionException: java.lang.IllegalStateException: Score corruption: the workingScore (-193595) is not the uncorruptedScore (-193574) after completedAction (DWL_CM_XX_101@DWL_PA_XX_180 => DWL_PA_XX_180):
  The corrupted scoreDirector has 4 ConstraintMatch(s) which are in excess (and should not be there):
    com.abcdl.be.solver/resourcesLimit/level0/[43.0, 2012, 1891]=-2783
    com.abcdl.be.solver/resourcesLimit/level0/[45.0, 1870, 1805]=-1625
    com.abcdl.be.solver/resourcesLimit/level0/[46.0, 1805, 1774]=-806
    com.abcdl.be.solver/resourcesLimit/level0/[45.0, 1774, 1762]=-300
  The corrupted scoreDirector has 3 ConstraintMatch(s) which are missing:
    com.abcdl.be.solver/resourcesLimit/level0/[43.0, 2012, 1901]=-2553
    com.abcdl.be.solver/resourcesLimit/level0/[45.0, 1870, 1762]=-2700
    com.abcdl.be.solver/resourcesLimit/level0/[44.0, 1901, 1891]=-240
  Check your score constraints.

A score rule that has a LHS of just " eval(true) " is inherently broken. Either that constraint is always broken, for the exact same weight, and there really is no reason to evaluate it. Or it is sometimes broken (or always broken but for different weights) and then the rule needs to refire accordingly.

Problem: the return value of Graph.getInstance().totalTime() changes as the planning variables change value. But Drools just looks at the LHS as planning variables change and it sees that nothing in the LHS has changed so there's no need to re-evaluate that score rule, when the planning variables change. Note: this is called incremental score calculation (see docs), which is a huge performance speedup.

Subproblem: The method Graph.getInstance().totalTime() is inherently not incremental.

Fix: translate that totalTime() function into a DRL function based on Node selections. You 'll probably need to use accumulate . If that's too hard (because it's a complex calculation of the critical path or so), try it anyway (for incremental score calculation's sake) or try a LHS that does a collect over all Node s (which is like eval(true) but it will be refired every time.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM