I'm writing a school timetable in optaplanner, and if I change my constraints slightly, the memory use increases without bound (hundreds of gigs).
The following constraint works fine (memory use on 4 threads tops out at about 1g):
private Constraint teachingLoadConflict(ConstraintFactory constraintFactory)
{
return constraintFactory
.forEach(Lecture.class)
.groupBy(Lecture::getInstructor, ConstraintCollectors.count())
.filter((instructor, count)-> count>instructor.getTeachingLoad() )
.penalize("Exceeds teaching load", HardSoftScore.ofHard(100), (instructor,count)->count-instructor.getTeachingLoad());
}
However, if I add in a filter to only check those courses in the fall the memory usage increases without bound (eventually leading to an exception):
private Constraint teachingLoadConflict(ConstraintFactory constraintFactory)
{
return constraintFactory
.forEach(Lecture.class)
.filter(Lecture::getIsFall)
.groupBy(Lecture::getInstructor, ConstraintCollectors.count())
.filter((instructor, count)-> count>instructor.getTeachingLoad() )
.penalize("Exceeds teaching load", HardSoftScore.ofHard(100), (instructor,count)->count-instructor.getTeachingLoad());
}
How should I implement this idea? I've tried various things for that filter. Right now, getIsFall is implemented as:
#ProblemFactProperty
public boolean getIsFall(){return isFall;}
We have recently fixed a bug that had very similar, if not the same, symptoms. I suggest you wait for the release of OptaPlanner 8.23.0.Final, and let us know if the issue is still there.
Related
I'm using solverManager to continually save the best score:
solverManager.solveAndListen(
SINGLETON_TIME_TABLE_ID,
this::findById,
this::save
)
My save() method just updates a global reference to the best solution and nothing else happens to it.
However, when I go to retrieve the best solution and get the score details, it does not always get the best solution:
val solution: MySolution = findById(SINGLETON_TIME_TABLE_ID)
println(solution.score) // prints 1100
scoreManager.updateScore(solution) // Sets the score
println(solution.score) // prints 1020 (it's now worse than before)
Simply calling scoreManager.updateScore(solution) seems to bring back a worse solution. In the above example, the score might go from 1100 down to 1020. Is there an obvious explanation for this?
I'm using SimpleScore and this happens well after all planning variables are assigned.
I'm using a variable listener ArrivalTimeUpdatingVariableListener : VariableListener.
Not sure what else is relevant. Thanks
This has all warning signs of a score corruption. Please run your solver for a couple minutes with <environmentMode>FULL_ASSERT</environmentMode>. If you'll see exceptions being thrown, you know that your constraints have a bug in them. What that bug is, that is impossible for me to know unless I see those constraints.
I have a following time scheduling optimisation problem:
There are n breaks to be scheduled. A break takes up k time grains of 15 minutes each. The total horizon I am looking at is of m time grains. Each time grain has a desired amount of breaks to optimise for. The range to start a break at is defined per break, you cannot freely pick the range.
To make it more general - there is a distribution of breaks over time as a goal. I need to output a result which would align with this desired distribution as much as possible. I am allowed to move each break within certain boundaries, e.g. 1 hour boundary.
I had a look at the TimeGrain pattern as a starting point which is described here: https://www.optaplanner.org/blog/2015/12/01/TimeSchedulingDesignPatterns.html and in this video: https://youtu.be/wLK2-4IGtWY. I am trying to use Constraint Streams for incremental optimisation.
My approach so far is following:
Break.scala:
case class Break(vehicleId: String, durationInGrains: Int)
TimeGrain.scala:
#PlanningEntity
case class TimeGrain(desiredBreaks: Int,
instant: Instant,
#CustomShadowVariable(...), // Dummy annotation, I want to use the entity in constraint stream
var breaks: Set[Break])
BreakAssignment:
#PlanningEntity
case class BreakAssignment(
break: Break,
#PlanningVariable(valueRangeProviderRefs = Array("timeGrainRange"))
var startingTimeGrain: TimeGrain,
#ValueRangeProvider(id = "timeGrainRange")
#ProblemFactCollectionProperty #field
timeGrainRange: java.util.List[TimeGrain],
#CustomShadowVariable(
variableListenerClass = classOf[StartingTimeGrainVariableListener],
sources = Array(new PlanningVariableReference(variableName = "startingTimeGrain"))
)
var timeGrains: util.Set[TimeGrain]
)
object BreakAssignment {
class StartingTimeGrainVariableListener extends VariableListener[Solution, BreakAssignment] {
override def afterVariableChanged(scoreDirector: ScoreDirector[Solution], entity: BreakAssignment): Unit = {
val end = entity.startingTimeGrain.instant
.plusSeconds((entity.break.durationInGrains * TimeGrain.grainLength).toSeconds)
scoreDirector.getWorkingSolution.timeGrains.asScala
.filter(
timeGrain =>
timeGrain.instant == entity.startingTimeGrain.instant ||
entity.startingTimeGrain.instant.isBefore(timeGrain.instant) && end
.isAfter(timeGrain.instant)
)
.foreach { timeGrain =>
scoreDirector.beforeVariableChanged(timeGrain, "breaks")
timeGrain.breaks = timeGrain.breaks + entity.break
scoreDirector.afterVariableChanged(timeGrain, "breaks")
}
}
}
}
Constraints.scala:
private def constraint(constraintFactory: ConstraintFactory) =
constraintFactory
.from(classOf[TimeGrain])
.filter(timeGrain => timeGrain.breaks.nonEmpty)
.penalize(
"Constraint",
HardSoftScore.ONE_SOFT,
(timeGrain: TimeGrain) => {
math.abs(timeGrain.desiredBreaks - timeGrain.breaks.size)
}
)
As you can see I need to iterate over all the grains in order to find out which ones needs to be updated to hold the break which was just moved in time. This somewhat negates the idea of Constraint Streams.
A different way to look at the issue I am facing is that I cannot come up with an approach to link the BreakAssignment planning entity with respective TimeGrains via e.g. Shadow Variables. A break assignment is spanning multiple time grains. A time grain in return contains multiple break assignments. For the soft constraint I need to group all the assignments within the same grain, accessing the desired target break count of a grain, while doing this for all the grains of my time horizon. My approach therefore is having each grain as a planning entity, so I can store the information of all the breaks on each change of the starting time grain of the assignment himself. What I end up is basically a many-to-many relationship between assignments and time grains. This does not fit into the inverse mechanism of a shadow variable from my understanding as it needs to be a one-to-many relationship.
Am I going in the wrong direction while trying to come up with the correct model?
If I understand properly what you mean, then conceptually, in the TimeGrain class, I would keep a (custom) shadow variable keeping (only) the count of Break instances that are overlapping that TimeGrain (instance). Let me call it breakCount for simplicity. Let me call x the number of TimeGrains a Break spans.
So, upon the solver assigning a Break instance to a TimeGrain instance, I would increment that TimeGrain instance's breakCount. Not only thát TimeGrain instance's breakCount, but also the breakCount of the next few (x-1) TimeGrain instances. Beware to wrap each of those incrementations in a "scoreDirector.beforeVariableChanged()"-"scoreDirector.afterVariableChanged()" bracket.
The score calculation would do the rest. But do note that I myself would moreover also square the difference of a TimeGrain's ideal breakCount and it's "real" breakCount (i.e. the shadow variable), like explained in OptaPlanner's documentation, in order to enforce more "fairness".
Edit : of course also decrement a TimeGrain's breakCount upon removing a Break instance from a Timegrain instance...
What I Have
Using OptaPlanner 7.45.0.Final on Java 8.
I am working on a proof-of-concept for scheduling of shifts, where I have created CrewAssignment object as a #PlanningEntity.
I have created my own EasyScoreCalculator implementation (from the new non-deprecated EasyScoreCalculator).
The score function returns a HardMediumSoftScore, with Hard representing physical possibility (can't be in 2 places at once, can't take on an activity that starts in a different place than the previous activity ended), Medium representing legal issues (maximum work day, etc.), and Soft roughly representing financial concerns.
The behavior I am getting
The schedule seemed to work well for small datasets where there are many good possible Solutions. However, when I moved to larger datasets, it had a harder time returning good final schedules. There would be scores like -60hard/-390060medium/-1280457soft, which seems really bad. Moreover, if I increased the time available, the score would sometimes get worse!
Things I have tried
I put a print in my score function for the score being calculated. It would often give scores such as 0hard/0medium/-2250411soft, which in comparison to the final scores is great! However, the final result would still be a bad score.
I added a SolverEventListener to the Solver, and it is only called once at the very end of the solving.
I thought maybe there was a problem with cloning of the Solution, so I created my own SolutionCloner. It is called once at the beginning, and twice at the end. Since it is called so infrequently, I suspect the solver uses the first Solution clone as the best score, tries to copy the values from the current Solution iteration to the best Solution, but given that the SolverEventListener is only called once, that may indicate that it is not recognizing the best solution.
I tried simplifying to a HardSoftScore by combining Hard and Medium values, but the behavior is the same.
I tried calling solverConfig.setEnvironmentMode(EnvironmentMode.NON_INTRUSIVE_FULL_ASSERT); (yes, I'm programatically creating the SolverConfig, but the behavior does not change.
Relevant Code Snippets
Subset of the CrewAssignment #PlanningEntity:
#PlanningEntity
public final class CrewAssignment
{
private Long crewMemberId;
#PlanningVariable(valueRangeProviderRefs = {"crewMemberId"})
public Long getCrewMemberId() { return crewMemberId; }
public void setCrewMemberId(Long value) { crewMemberId = value; }
#Override
protected CrewAssignment clone()
{
CrewAssignment newAssignment = new CrewAssignment(getActivities());
newAssignment.setCrewMemberId(crewMemberId);
return newAssignment;
}
}
Subset of the Solution:
#PlanningSolution(solutionCloner = CrewSchedSolutionCloner.class)
public final class CrewSchedSolution
{
private final List<Long> crewMemberIds;
#ValueRangeProvider(id="crewMemberId")
#ProblemFactCollectionProperty
public List<Long> getCrewMemberIds() { return crewMemberIds; }
// assignments
private List<CrewAssignment> crewAssignments = new ArrayList<>();
#PlanningEntityCollectionProperty
public List<CrewAssignment> getCrewAssignments() { return crewAssignments; }
HardSoftScore score;
#PlanningScore
public HardSoftScore getScore() { return score; }
public void setScore(HardSoftScore value) { score = value; }
public CrewSchedSolution cloneSolution()
{
List<CrewAssignment> newCrewAssignments = new ArrayList<>(crewAssignments);
for (int i = 0; i < newCrewAssignments.size(); i++)
{
CrewAssignment existingAssignment = newCrewAssignments.get(i);
CrewAssignment newAssignment = existingAssignment.clone();
newCrewAssignments.set(i, newAssignment);
}
return new CrewSchedSolution(/* Various additional data */,
crewMemberIds,
newCrewAssignments, score);
}
}
The SolutionCloner:
public final class CrewSchedSolutionCloner
implements SolutionCloner<CrewSchedSolution>
{
#Override
public CrewSchedSolution cloneSolution(CrewSchedSolution originalSolution)
{
return originalSolution.cloneSolution();
}
}
Summary
I have run out of obvious (to me) ways to debug this further. It kind of feels like the good values are getting written over, but I can't prove that yet. I might be able to come up with a way of saving the values set in the CrewAssignment #PlanningEntity just to get it to work, but it seems like that is something OptaPlanner was designed to be able to handle.
The problem is too large to post a SSCCE. I have posted what I think might be relevant. Let me know if there is any other part of the code you would need to see. Thanks in advance!
P.S. I realize this may be a duplicate of OptaPlanner return the best score but not its associated solution, but the original poster never followed up with the posted answer.
if I increased the time available, the score would sometimes get worse! That is impossible (if it gets more steps in the second run, which it should, and it is a reproducible run, which it is).
Does the DEBUG log show it's running more "LS step" lines? If it does, you might have score corruption, but NON_INTRUSIVE_FULL_ASSERT or FULL_ASSERT would detect that if they run long enough (much longer than without because they are slower).
Normally, optaplanner runs are 100% reproducible, given the same amount of steps (~ same amount of time give or take a few steps). Check your DEBUG log if that's the case. Simulated Annealing isn't reproducible, but that's off by default.
OptaPlanner Benchmark is your best friend. Especially the BEST_SCORE graph and the score calculation speed number. Run it 4 times longer, so you can see how the BEST_SCORE graph behaves if given more time. Also, subSingleCount might be an interesting thing to turn on here, to see how sensitive the optimization is to a good random seed (it shouldn't be).
Ow, wait. You wrote your own solution cloner? That's probably it, because solution cloners are extremely difficult to write correctly. Disable that and see what happens.
OptaPlanner was (of course) working correctly. I had a conceptual misunderstanding.
I was focusing on the Scores I was calculating, and not focusing on the scores that OptaPlanner was actually using. OptaPlanner can augment the Scores that are returned from the scoring function by adding an "init" value representing the number of uninitialized #PlanningVariables. Scores that had 0 Hard and Medium scores may have looked reasonable, but may still be uninitialized, which takes precedence over Hard/Medium/Soft values.
Once the duration of the simulation was set sufficiently high, all #PlanningVariables were being set. No scores with init of 0 were created after that, and completed optimized solutions (whether infeasible or feasible) were computed.
See the steps Geoffrey outlined for debugging. If you are using JDK logging, you may have to add slf4j-api and slf4j-jdk14 jars to your path.
See the documentation, section 2.3.6 "Gather the domain objects in a planning solution", where it describes the difference between an "uninitialized", "infeasible", and "feasible" solution.
Note: It appears that the solver does not notify SolverEventListeners when the best solution so far is uninitialized.
What are some code examples demonstrating KeY’s strength?
Details
With so many Formal Method tools available, I was wondering where KeY is better than its competition, and how? Some readable code examples would be quite helpful for comparison and understanding.
Updates
Searching through the KeY website, I found code examples from the book — is there a suitable code example in there somewhere?
Furthermore, I found a paper about the bug that KeY found in Java 8’s mergeCollapse in TimSort. What is a minimal code from TimSort that demonstrates KeY’s strength? I do not understand, however, why model checking supposedly cannot find the bug — a bit array with 64 elements should not be too large to handle. Are other deductive verification tools just as capable of finding the bug?
Is there an established verification competition with suitable code examples?
This is a very hard question, which is why it hasn't yet been answered after having already been asked more than one year ago (and although we from the KeY community are well aware of it...).
The Power of Interaction
First, I'd like to point out that KeY is basically the only tool out there allowing for interactive proofs of Java programs. Although many proofs work automatically and we have quite powerful automatic strategies at hand, sometimes interaction is required to understand why a proof fails (too weak or even wrong specifications, wrong code or "just" a prover incapacity) and to add suitable corrections or strengthenings.
Feedback from Proof Inspection
Especially in the case of a prover incapacity (specification and program are OK, but the problem is too hard for the prover to succeed automatically), interaction is a powerful feature. Many program provers (like OpenJML, Dafny, Frama-C etc.) rely on SMT solvers in the backend which they feed with many more or less small verification conditions. The verification status for these conditions is then reported back to the user, basically as pass or fail -- or timeout. When an assertion failed, a user can change the program or refine the specifications, but cannot inspect the state of the proof to deduct information about why something went wrong; this style is sometimes called "auto-active" as opposed to interactive. While this can be quite convenient in many cases (especially when proofs pass, since the SMT solvers can be really quick in proving something), it can be hard to mine SMT solver output for information. Not even the SMT solvers themselves know why something went wrong (although they can produce a counterexample), as they just are fed a set of formulas for which they attempt to find a contradiction.
TimSort: A Complicated Algorithmic Problem
For the TimSort proof which you mentioned, we had to use a lot of interaction to make them pass. Take, for instance, the mergeHi method of the sorting algorithm which has been proven by one of the most experienced KeY power users known to me. In this proof of 460K proof nodes, 3K user interactions were necessary, consisting of quite a lot of simple ones like the hiding of distracting formulas, but also of 478 quantifier instantiations and about 300 cuts (on-line lemma introduction). The code of that method features many difficult Java features like nested loops with labeled breaks, integer overflows, bit arithmetic and so on; especially, there are a lot of potential exceptions and other reasons for branching in the proof tree (which is why in addition, also five manual state merging rule applications have been used in the proof). The workflow for proving this method basically was to give the strategies a try for some time, check the proof state afterward, prune back the proof and introduce a helpful lemma to reduce the overall proof work and to start again; occasionally, quantifiers were instantiated manually if the strategies failed to find the right instantiation directly by themselves, and proof tree branches were merged to tackle state explosion. I would just claim here that proving this code is (at least currently) not possible with auto-active tools, where you cannot guide the prover in that way, and also cannot obtain the right feedback for knowing how to guide it.
Strength of KeY
Concluding, I'd say that KeY's strong in proving hard algorithmic problems (like sorting etc.) where you have complicated quantified invariants and integer arithmetic with overflows, and where you need to find quantifier instantiations and small lemmas on the fly by inspecting and interacting with the proof state. The KeY approach of semi-interactive verification also excels in general for cases where SMT solvers time out, such that a user cannot tell whether something is wrong or an additional lemma is required.
KeY can of course also proof "simple" problems, however there you need to take care that your program does not contain an unsupported Java feature like floating point numbers or multithreading; also, library methods can be quite a problem if they're not yet specified in JML (but this problem applies to other approaches as well).
Ongoing Developments
As a side remark, I also would like to point out that KeY is now more and more being transformed to a platform for static analysis of different kinds of program properties (not only functional correctness of Java programs). On the one hand, we have developed tools such as the Symbolic Execution Debugger which can be used also by non-experts to examine the behavior of a sequential Java program. On the other hand, we are currently busy in refactoring the architecture of the system for making it possible to add frontends for languages different than Java (in our internal project "KeY-RED"); furthermore, there are ongoing efforts to modernize the Java frontend such that also newer language features like Lambdas and so on are supported. We are also looking into relational properties like compiler correctness. And while we already support the integration of third-party SMT solvers, our integrated logic core will still be there to support understanding proof situations and manual interactions for cases where SMT and automation fails.
TimSort Code Example
Since you asked for a code example... I cannot right know think of "the" code example showing KeY's strength, but maybe for giving you a flavor of the complexity of mergeHi in the TimSort algorithm, here a shortened excerpt with some comments (the full method has about 100 lines of code):
private void mergeHi(int base1, int len1, int base2, int len2) {
// ...
T[] tmp = ensureCapacity(len2); // Method call by contract
System.arraycopy(a, base2, tmp, 0, len2); // Manually specified library method
// ...
a[dest--] = a[cursor1--]; // potential overflow, NullPointerException, ArrayIndexOutOfBoundsException
if (--len1 == 0) {
System.arraycopy(tmp, 0, a, dest - (len2 - 1), len2);
return; // Proof branching
}
if (len2 == 1) {
// ...
return; // Proof branching
}
// ...
outer: // Loop labels...
while (true) {
// ...
do { // Nested loop
if (c.compare(tmp[cursor2], a[cursor1]) < 0) {
// ...
if (--len1 == 0)
break outer; // Labeled break
} else {
// ...
if (--len2 == 1)
break outer; // Labeled break
}
} while ((count1 | count2) < minGallop); // Bit arithmetic
do { // 2nd nested loop
// That's one complex statement below...
count1 = len1 - gallopRight(tmp[cursor2], a, base1, len1, len1 - 1, c);
if (count1 != 0) {
// ...
if (len1 == 0)
break outer;
}
// ...
if (--len2 == 1)
break outer;
count2 = len2 - gallopLeft(a[cursor1], tmp, 0, len2, len2 - 1, c);
if (count2 != 0) {
// ...
if (len2 <= 1)
break outer;
}
a[dest--] = a[cursor1--];
if (--len1 == 0)
break outer;
// ...
} while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP);
// ...
} // End of "outer" loop
this.minGallop = minGallop < 1 ? 1 : minGallop; // Write back to field
if (len2 == 1) {
// ...
} else if (len2 == 0) {
throw new IllegalArgumentException(
"Comparison method violates its general contract!");
} else {
System.arraycopy(tmp, 0, a, dest - (len2 - 1), len2);
}
}
Verification Competition
VerifyThis is an established competition for logic-based verification tools which will have its 7th iteration in 2019. The concrete challenges for past events can be downloaded from the "archive" section of the website I linked. Two KeY teams participated there in 2017. The overall winner that year was Why3. An interesting observation is that there was one problem, Pair Insertion Sort, which came as a simplified and as an optimized Java version, for which no team succeeded in verifying the real-world optimized version on site. However, a KeY team finished that proof in the weeks after the event. I think that highlights my point: KeY proofs of difficult algorithmic problems take their time and require expertise, but they're likely to succeed due to the combined power of strategies and interaction.
I'm trying to make a Sudoku game, and I gathered the following validations to each number inserted:
Number must be between 1 and 9;
Number must be unique in the line;
Number must be unique in the column;
Number must be unique in the sub-matrix.
As I'm repeating too much the "Number must be unique in..." rule, I made the following design:
There are 3 kinds of groups, ColumnGroup, LineGroup, and SubMatrixGroup (all of them implement the GroupInterface);
GroupInterface has a method public boolean validate(Integer number);
Each cell is related to 3 groups, and it must be unique between the groups, if any of them doesn't evaluate to true, number isn't allowed;
Each cell is an observable, making the group an observer, that reacts to one Cell change attempt.
And that s*cks.
I can't find what's wrong with my design. I just got stuck with it.
Any ideas of how I can make it work?
Where is it over-objectified? I can feel it too, maybe there is another solution that would be more simple than that...
Instead of having 3 validator classes, an abstract GroupInterface, an observable, etc., you can do it with a single function.
Pseudocode ahead:
bool setCell(int cellX, int cellY, int cellValue)
{
m_cells[x][y] = cellValue;
if (!isRowValid(y) || !isColumnValid(x) || !isSubMatrixValid(x, y))
{
m_cells[x][y] = null; // or 0 or however you represent an empty cell
return false;
}
return true;
}
What is the difference between a ColumnGroup, LineGroup and SubMatrixGroup? IMO, these three should simply be instances of a generic "Group" type, as the type of the group changes nothing - it doesn't even need to be noted.
It sounds like you want to create a checker ("user attempted to write number X"), not a solver. For this, your observable pattern sounds OK (with the change mentioned above).
Here (link) is an example of a simple sudoku solver using the above-mentioned "group" approach.