The context is Employee Shift Assignment with OptaPlanner using Drools rules for calculating scores.
My Employees cannot work for, say, for more than three consecutive days without a rest day.
I implement such a constraint very stupidly as:
rule "No more than three consecutive working days"
when
ShiftAssignment(
$id1 : id,
$empoloyee : empoloyee != null,
$shift1 : shift
)
ShiftAssignment(
id > $id1,
empoloyee == $empoloyee,
shift.isConsecutiveDay($shift1),
$id2 : id,
$shift2 : shift
)
ShiftAssignment(
id > $id2,
empoloyee == $empoloyee,
shift.isConsecutiveDay($shift2),
$id3 : id,
$shift3 : shift
)
ShiftAssignment(
id > $id3,
empoloyee == $empoloyee,
shift.isConsecutiveDay($shift10)
)
then
scoreHolder.penalize(kcontext);
end
I hope the name of the methods/variables clearly reveal what they do/mean.
Is there a more convenient and smart way to implement such a rule? Keep in mind that the three days above may need to change to a bigger number (I used three to avoid a more realistic ten and more lines of code in the rule). Thanks.
If we can assume an employee takes up to a single shift per day and the shift.isConsecutiveDay() may be replaced by something like shift.day == $shift1.day + 1, exists can be used:
when
ShiftAssignment($employee : empoloyee != null, $shift1 : shift)
exists ShiftAssignment(employee == $employee, shift.day == $shift1.day + 1)
exists ShiftAssignment(employee == $employee, shift.day == $shift1.day + 2)
exists ShiftAssignment(employee == $employee, shift.day == $shift1.day + 3)
then
If such an assumption cannot be made, your solution should work, with one potential corner case to think about:
The rule tries to filter out combinations of the same shifts by the condition id > $id1. This condition works, but the IDs must be generated ascendingly by the time of the shift, otherwise, it clashes with shift.isConsecutiveDay(...). In case this property cannot be guaranteed, checking for ID inequality could be preferable.
I used a combination of rules to achieve this. First rule sets up the start of a consecutive work sequence, second one sets up the end, 3rd rule creates a "Work Sequence" to fit between the start and end. Finally the "Max Consecutive Days" rule actually checks your "Work Sequence" against a limit on number of consecutive days.
This paradigm is actually in the nurse rostering example:
https://github.com/kiegroup/optaplanner/blob/master/optaplanner-examples/src/main/resources/org/optaplanner/examples/nurserostering/solver/nurseRosteringConstraints.drl
Related
Hello I it is my first time involved in drools project. I have created some simple rules that work fine, however I have trouble with more complex rules that use the accumulate function. Below I have this rule.
rule "1"
no-loop
when
$msg : Declaration(header.totalGrossMassMeasure != null,
header.totalGrossMassMeasure.compareTo(BigDecimal.ZERO) > 0 )
result : ValidationResult()
$netValue : Number() from accumulate (
GoodsItemsType($net : netNetWeightMeasure),
sum($net.doubleValue())
)
eval($netValue.doubleValue() > ($msg.getHeader().getTotalGrossMassMeasure().doubleValue() + (0.45 * $msg.getGoodsItems().size())))
then
RulesValidationError error = new RulesValidationError();
error.setErrorType(ErrorType.INCORECT_VALUE);
result.getErrorsList().add(error);
end
the concept is to sum the net value from a list of goodsItemType object and compare the sum to the total gross mass measure multiplied by one buffer number. The problem is I have been trying last couple of days not being able to fire the rule with anything. Could someone please help me?
I understand the basics of writing drools rules now but i can't seem to understand in the examples that i've seen (optaplanner), there are comparisons of IDs. Is this necessary? Why is it there?
// RoomOccupancy: Two lectures in the same room at the same period.
// Any extra lecture in the same period and room counts as one more violation.
rule "roomOccupancy"
when
Lecture($leftId : id, period != null, $period : period, room != null, $room : room)
// $leftLecture has lowest id of the period+room combo
not Lecture(period == $period, room == $room, id < $leftId)
// rightLecture has the same period
Lecture(period == $period, room == $room, id > $leftId, $rightId : id)
then
scoreHolder.addHardConstraintMatch(kcontext, -1);
end
From my understanding deleting the line with not Lecture(.. and leaving Lecture(period == $period, room == $room) should do the trick. Is my understanding correct or am I missing some use cases here?
You should understand that a pattern such as
$a: Lecture()
$b: Lecture()
with two Lecture facts A an B in the system will produce the following matches and firings:
$a-A, $b-B (1)
$a-B, $b-A (2)
$a-A, $b-A
$a-B, $b-B
Therefore, to reduce the unwanted combinations you need have a way to ascertain to have not identical facts matching (bound to) $a and $b:
$a: Lecture( $ida: id )
$b: Lecture( $idb: id != $ida )
However, using not equal still produces combinations (1) and (2).
Given 2 queens A and B, the id comparison in the "no 2 queens on the same horizontal row" constraint makes sure that we only match A-B and not B-A, A-A and B-B.
Same principle for lectures.
I'm currently reading the course curriculum example of optaplanner and I can't seem to understand this:
rule "conflictingLecturesSameCourseInSamePeriod"
when
// line 1
Lecture($leftId : id, $leftCourse : course, $period : period, period != null)
// line 2
Lecture(course == $leftCourse, period == $period, id > $leftId)
then
scoreHolder.addHardConstraintMatch(kcontext, -1);
end
Questions are:
- What's the difference between the Lecture() in line 1 and the Lecture in line 2?
- I understand the variable assignements are happening in Line 1 but in Line 2, what's the difference between course and $leftCourse, period and $period and id and $leftId
So far, I can't seem to find any explanation in the documentation
A binding such as $leftCourse : course establishes $leftCourse as a variable that refers to the field course of a Lecture object.
The lecture in line 1 is identified by its id; the one in line 2 has an id that is greater. Assuming that id values are primary keys, this combination would basically match all possible unordered pairs of Lectures, but ...
... the constraint course == $leftCourse restricts the pairings to those of identical courses and ...
... the constraint period == $period furthermore restricts it to equal (non-null) periods.
In other words, the planning rules out an assignment for two different Lectures for the same course in the same period.
Is there a built-in feature in Drools, selecting the latest n events, matching a certain pattern? I've read about sliding length windows in the documentation and the stock tick example seemed to be exactly what I wanted:
"For instance, if the user wants to consider only the last 10 RHT Stock Ticks, independent of how old they are, the pattern would look like this:"
StockTick( company == "RHT" ) over window:length( 10 )
When testing the example, it seems to me that it is evaluted more like a
StockTick( company == "RHT" ) from StockTick() over window:length( 10 )
selecting the latest 10 StockTick events and afterwards filtering them by company == "RTH", resulting in 0 to 10 RHT-Ticks, event though the stream contains more then 10 RTH-events.
A workaround is something like:
$tick : StockTick( company == "RHT" )
accumulate(
$other : StockTick(this after $tick, company == "RHT" );
$cnt : count(other);
$cnt < 10)
which has bad performance and readability.
Most likely you are seeing an initial phase where the count of events in the window and according to the constraints hasn't reached the length specified in window:length yet. For instance,
rule "Your First Rule"
when
accumulate( $st : Applicant($age: age > 5) over window:length(10)
from entry-point X,
$avg: average ( $age ), $cnt: count( $st ))
then
System.out.println("~~~~~avg~~~~~");
System.out.println($avg + ", count=" + $cnt);
System.out.println("~~~~~avg~~~~~");
end
displays an output even before there are 10 matching Applicants but later on, $cnt never falls below 10, even though $age ranges from 0 to 9, periodically.
If you do think you have found an example supporting your claim, please provide full code for reproduction and make sure to indicate the Drools version.
Your workaround is very bad indeed, as it accumulates for each StockTick. But a window:length(n) can be very efficiently implemented by using an auxiliary fact maintaining a list of n events. This may even be more advantageous than window:length.
I'm new to Drools and have hit a problem.
I've simplified the rule to exhibit the problem:
rule "test"
when
$ev : TestEvent()
$evList : ArrayList( size >= 3 ) from collect
(
TestEvent(linkId == $ev.getLinkId())
)
then
System.out.println("Rule fired!")
end
Basically, I want to count Events occurring on a particular Link (a Link is a section of road). When 3 events occur on the same Link I want the rule to fire.
The rule above is almost working, but when it fires, it fires 3 times, once for each event. I only want it to fire once.
What am I missing?
Many thanks in advance.
The first pattern picks any TestEvent irrespective of its linkId. If there are n TestEvent facts with a certain linkId, the acivation proceeds n times.
To restrict this rule to fire once you could select a single TestEvent out of any such group of n. Any attribute with a unique ordered value may be used, and if you have events also the event timestamp is available.
rule "test"
when
$ev: TestEvent( $lid: linkId )
not TestEvent( linkId == $lid, this before $ev )
$evList : ArrayList( size >= 3 ) from collect
(
TestEvent(linkId == $lid)
)
then
System.out.println("Rule fired!")
end
I got this working by changing my approach to the problem. I've created Link objects now and then tie the events back to the Link.
The rule ends up
rule "test"
when
$link : Link()
$events : ArrayList( size >= 3 ) from collect (TestEvent(link == $link))
then
System.out.println("Rule fired!")
end
This only fires once per link which is what I need.