Distribute Each Shifts Equally amongst Similarly Skilled Employees Using Optaplanner in Employee Roster - optaplanner

rule "Fairness: all similar skilled employees should work about the type of shifts"
when
$f : Employee(skill=="SR")
$s : Shift(shiftType=="NSR")
accumulate(
$a : ShiftAssignment(employee == $f, shiftType == $s);
$total :count($a)
)
then
// Fairness and load balancing trick (see docs): squared to assure correctness in corner cases
// Negative to balance it across employees
scoreHolder.addHardConstraintMatch(kcontext, - ($total.intValue() * $total.intValue()));
end
I have written the above logical Constraint . Could someone guide me the right direction or point out the error in this logic

Related

OptaPlanner - Explain score of non-optimal solutions

We have a use-case where we want to present the user with some human-readable message with why an "assignment" was rejected based on the score of the constraints.
For e.g. in the CloudBalancing problem with 3 computers (Computer-1,2,3) and 1 process (Process-1) we ended up with the below result:
Computer-1 broke a hard constraint (requiredCpu)
Computer-2 lost due to a soft constraint (min cost)
Computer-3 assigned to Process-1 --> (Optimal solution)
We had implemented the BestSolutionChanged listener where we used solution.explainScore() to get some info and enabled DEBUG logging which provided us the OptaPlanner internal logs for intermediate moves and their scores. But the requirement is to provide some custom human readable information on why all the non-optimal solutions (Computer-1, Computer-2) were rejected even if they were infeasible (basically explanation of scores of these two solutions).
So wanted to know how can we achieve the above ?
We did not want to rely on listening to BestSolutionChanged event as
it might not get triggered for other solutions if the LS/CH
phase starts with a solution which is already a "best solution"
(Computer-3). Is this a valid assumption ?
DEBUG logs do provide us with the
information but building a custom message from this log does not seem
like a good idea so was wondering if there is another
listener/OptaPlanner concept which can be used to achieve this.
By "all the non-optimal solutions", do you mean instead a particular non-optimal solution? Search space can get very large very quickly, and OptaPlanner itself probably won't evaluate the majority of those solutions (simply because the search space is so large).
You are correct that BestSolutionChanged event will not fire again if the problem/solution given to the Solver is already the optimal solution (since by definition, there are no solutions better than it).
Of particular interest is ScoreManager, which allows you to calculate and explain the score of any problem/solution:
(Examples taken from https://www.optaplanner.org/docs/optaplanner/latest/score-calculation/score-calculation.html#usingScoreCalculationOutsideTheSolver)
To create it and get a ScoreExplanation do:
ScoreManager<CloudBalance, HardSoftScore> scoreManager = ScoreManager.create(solverFactory);
ScoreExplanation<CloudBalance, HardSoftScore> scoreExplanation = scoreManager.explainScore(cloudBalance);
Where cloudBalance is the problem/solution you want to explain. With the
score explanation you can:
Get the score
HardSoftScore score = scoreExplanation.getScore();
Break down the score by constraint
Collection<ConstraintMatchTotal<HardSoftScore>> constraintMatchTotals = scoreExplanation.getConstraintMatchTotalMap().values();
for (ConstraintMatchTotal<HardSoftScore> constraintMatchTotal : constraintMatchTotals) {
String constraintName = constraintMatchTotal.getConstraintName();
// The score impact of that constraint
HardSoftScore totalScore = constraintMatchTotal.getScore();
for (ConstraintMatch<HardSoftScore> constraintMatch : constraintMatchTotal.getConstraintMatchSet()) {
List<Object> justificationList = constraintMatch.getJustificationList();
HardSoftScore score = constraintMatch.getScore();
...
}
}
and get the impact of individual entities and problem facts:
Map<Object, Indictment<HardSoftScore>> indictmentMap = scoreExplanation.getIndictmentMap();
for (CloudProcess process : cloudBalance.getProcessList()) {
Indictment<HardSoftScore> indictment = indictmentMap.get(process);
if (indictment == null) {
continue;
}
// The score impact of that planning entity
HardSoftScore totalScore = indictment.getScore();
for (ConstraintMatch<HardSoftScore> constraintMatch : indictment.getConstraintMatchSet()) {
String constraintName = constraintMatch.getConstraintName();
HardSoftScore score = constraintMatch.getScore();
...
}
}

Opta planner incorrect best score

I am trying out the optaplanner for a shift assignment problem.
It is a many to many relationship since one shift can have many employees.
In the trial run , I have two employees and three shifts .
One of the shift needs two employees.
So I have created a new ShiftAssignment class to handle the many to many relationship . ShiftAssignment is the planning entity and employee is the planning variable.
I pass the two employees and four shift assignment class ( because one shift needs two employees )
to the planning solution
I have only one hard rule in the score calculator which is basically the employee should
have the necessary skill needed for the shift
When I run the solver , I print the score in my code below ( I dont have any soft constraints so I have hard coded it to zero )
public HardSoftScore calculateScore(AuditAllocationSolution auditAllocationSolution) {
int hardScore = 0;
for (Auditor auditor : auditAllocationSolution.getAuditors()) {
for (AuditAssignment auditAssignment : auditAllocationSolution.getAuditAssignments()) {
if (auditor.equals(auditAssignment.getAuditor())) {
List<String> auditorSkils = auditor.getQualifications().stream().map(skill -> skill.getSkillName())
.collect(Collectors.toList());
String requiredSkillForThisAuditInstance = auditAssignment.getRequiredSkill().getSkillName();
if ( !auditorSkils.contains(requiredSkillForThisAuditInstance))
{
// increement hard score since skill match contraint is violated
hardScore = hardScore + 1;
}
}
}
}
System.out.println(" hardScore " + hardScore);
return HardSoftScore.valueOf(hardScore, 0);
}
When I print the values of the solution class in the score calculator , I can see that there are few solutions where hard score is zero. The solution satisfies the rules and matches the expected results . But it is not accepted as per the logs
08:16:35.549 [main] TRACE o.o.c.i.l.decider.LocalSearchDecider - Move index (0), score (0hard/0soft), accepted (false), move (AuditAssignment-2 {Auditor-1} <-> AuditAssignment-3 {Auditor-0}).
08:16:35.549 [main] TRACE o.o.c.i.l.decider.LocalSearchDecider - Move index (0), score (0hard/0soft), accepted (false), move (AuditAssignment-2 {Auditor-1} <-> AuditAssignment-3 {Auditor-0}).
One another observation which I want to clarify in the logs.
I understand that every new solution , which is the outcome of each step , is passed to score calculator . But sometimes I see that for a single step , score calculator is invoked more than once with different solution. This is my observation from the logs. Assuming this is single threaded and log sequencing is correct , why does that happen ?
The final output is incorrect . The best score that is selected is something with high hard score. And the solutions with the best score are not accepted
I also see the below line in the logs which I am not able to comprehend. Is there anything wrong in my configuration ?
23:53:01.242 [main] DEBUG o.o.c.i.l.DefaultLocalSearchPhase - LS step (26), time spent (121), score (2hard/0soft), best score (4hard/0soft), accepted/selected move count (1/1), picked move (AuditAssignment-2 {Auditor-1} <-> AuditAssignment-0 {Auditor-0}).
23:53:01.242 [main] DEBUG o.o.c.i.l.DefaultLocalSearchPhase - LS step (26), time spent (121), score (2hard/0soft), best score (4hard/0soft), accepted/selected move count (1/1), picked move (AuditAssignment-2 {Auditor-1} <-> AuditAssignment-0 {Auditor-0}).
This is a small problem size and I feel I have not set it up right . Kindly suggest.
Hard Score has to be decremented when a constraint is violated. In the above code , I had incremented the hard score which probably had led to the erroneous result.
It worked as expected once I fixed the above.

I want to add lower limit constraints in portfolio optimization in Optaplanner exercise

I want to add lower limit constraints in portfolio optimization in Optaplanner
exercise.
In Portfolio optimization in Optaplanner, we can control ratio limit by controlling sector constraints, and can only control upper limit value which is given as Quantity Maximum.
I've tried 'add lower limit as Quantity Minimum, and change Hardscore by quantity minimum similarly as quantity maximum.' However, I failed to apply it.
Anyway, is there any better way to add lower limit constraints in portfolio optimization in Optaplanner?
Plz Help me!
If it's for example per region, just add quantityMillisMinimum field on Region:
public class Region extends AbstractPersistable {
private String name;
private Long quantityMillisMaximum; // In milli's (so multiplied by 1000)
// New field
private Long quantityMillisMinimum; // In milli's (so multiplied by 1000)
}
and a score rule to add the hard constraint:
// New rule
rule "Region quantity minimum"
when
$region : Region($quantityMillisMinimum : quantityMillisMinimum)
accumulate(
AssetClassAllocation(region == $region, quantityMillis != null, $quantityMillis : quantityMillis);
$quantityMillisTotal : sum($quantityMillis);
$quantityMillisTotal < $quantityMillisMinimum
)
then
scoreHolder.addHardConstraintMatch(kcontext,
$quantityMillisTotal - $quantityMillisMinimum);
end
Are you using portfolio optimization for anything serious? :)

optaplanner custom implementation of ScoreHolder

My main question is, if I can use in OptaPlanner custom implementation of ScoreHolder in drools score calculation? And if yes, how I can do it, as the scoreHolder object is injected implicitly to global variable? Below you can find details, why I would like to use custom implementation of ScoreHolder.
I have found one problem during my work on an application that need to optimize value of some production. I have devices and based on forecasts I calculate production for each device.
I use OptaPlanner and I have rule like following:
when
$device : Device($deviceId : id)
$forecast : Forecast(deviceId == $deviceId)
then
int deviceProduction = $device.calculateProduction($forecast);
scoreHolder.addSoftConstraintMatch(kcontext, deviceProduction);
end
So after this, soft score will contain amount of overall production (overallProduction). What I want to have, is to keep VALUE of overall production in the soft score. The problem is, that value of production is not just overallProduction multiplied by price. There is a limit of production and everything over the limit have negative price. So we have given limit and two prices: positive(positive_price) and negative(negative_price). The schema of value calculation looks like following:
if( overallProduction <= limit)
value = positive_price * overallProduction;
else
value = positive_price * limit + negative_price * (overallProduction - limit);
So I think, I can't include calculation of this value inside rule calculated production for single device, because I'm able to calculate this value of overall production, only after collection of production from all devices.
My only idea how I can handle it, is to have custom implementation of scoreHolder extending HardSoftScoreHolder. This custom ScoreHolder would keep internally, in soft sore, overall production. It will be counted by rule given at the beginning. And then, the proper value will be calculated by custom ScoreHolder, just before returning result from extractScore method.
The question is, if I can use my custom ScoreHolder inside drools score calculation? As global scoreHolder object is injected implicitly..
Or if there is other way how I can put value of overall production into score object?
Thanks for your help and best regards,
Instead of hacking the ScoreHolder, I done it like Geoffrey De Smet suggested, thanks!
rule "SingleProduction"
when
$device : Device($deviceId : id)
$forecast : Forecast(deviceId == $deviceId)
then
int deviceProduction = $device.calculateProduction($forecast);
insertLogical(new SingleProduction(deviceProduction));
end
rule "ValueOfTotalProduction"
when
ProductionLimit( $limit : value )
PositivePrice( $positivePrice : value )
NegativePrice( $negativePrice : value )
Number( $totalProduction : intValue() )
from accumulate( SingleProduction( $value : value ),
sum( $value ) )
then
int productionValue;
if($totalProduction <= $limit)
productionValue = $totalProduction * $positivePrice;
else
productionValue = $limit * $positivePrice +
($totalProduction - $limit) * $negativePrice;
scoreHolder.addSoftConstraintMatch(kcontext, productionValue);
end
Hacking the ScoreHolder is not the way to do this (although by creating a custom ScoreDefinition you can do it that way).
Instead, your rule can use an insertLogical() to insert the intermediate results as a new fact, instead of modifying the ScoreHolder directly. Then another rule matches on that intermediate result (probably with a lower salience) and modifies the ScoreHolder accordingly. See the nurse rostering example for an example.
Laune's way would also work.

Using less than (or equal to) in Drools

I am trying to write a Drools file for a java project I am working on, and one of my rules is heavily reliant on less than or equal to. I have read in a couple of places that you shouldn't use < and should in fact use <. Presumably this means that <= would become <=?
Netbeans highlights my < in red as well, which suggests there's something amiss.
This seems completely mad to me - does this mean the code below changes as follows:
($T1.getValue()<$T2getValue)&&($T1.getOtherValue()<=$T2getOtherValue)
becomes
($T1.getValue()<$T2getValue)&&($T1.getOtherValue()<=$T2getOtherValue)
What is the explanation for this?
In *.drl files you can safely use < and >. It doesn't require XML or HTML escaping.
For example, notice the > in this rule from one of the optaplanner examples:
rule "requiredCpuPowerTotal"
when
$computer : CloudComputer($cpuPower : cpuPower)
$requiredCpuPowerTotal : Number(intValue > $cpuPower) from accumulate(
CloudProcess(
computer == $computer,
$requiredCpuPower : requiredCpuPower),
sum($requiredCpuPower)
)
then
scoreHolder.addHardConstraintMatch(kcontext, $cpuPower - $requiredCpuPowerTotal.intValue());
end
I would write your code something like this:
T1($t1Value : value, $t1OtherValue : otherValue)
T2(value < $t1Value, otherValue <= $t1OtherValue)