/*
* Copyright 2014 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.optaplanner.examples.cheaptime.solver.score;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.optaplanner.core.api.score.buildin.hardmediumsoftlong.HardMediumSoftLongScore;
import org.optaplanner.core.api.score.constraint.ConstraintMatchTotal;
import org.optaplanner.core.api.score.constraint.Indictment;
import org.optaplanner.core.impl.score.director.incremental.AbstractIncrementalScoreCalculator;
import org.optaplanner.core.impl.score.director.incremental.ConstraintMatchAwareIncrementalScoreCalculator;
import org.optaplanner.examples.cheaptime.domain.CheapTimeSolution;
import org.optaplanner.examples.cheaptime.domain.Machine;
import org.optaplanner.examples.cheaptime.domain.PeriodPowerPrice;
import org.optaplanner.examples.cheaptime.domain.Resource;
import org.optaplanner.examples.cheaptime.domain.Task;
import org.optaplanner.examples.cheaptime.domain.TaskAssignment;
import org.optaplanner.examples.cheaptime.domain.TaskRequirement;
import org.optaplanner.examples.cheaptime.solver.CheapTimeCostCalculator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CheapTimeIncrementalScoreCalculator extends AbstractIncrementalScoreCalculator<CheapTimeSolution>
implements ConstraintMatchAwareIncrementalScoreCalculator<CheapTimeSolution> {
protected static final String CONSTRAINT_PACKAGE = "org.optaplanner.examples.cheaptime.solver";
protected final transient Logger logger = LoggerFactory.getLogger(getClass());
private CheapTimeSolution cheapTimeSolution;
private int resourceListSize;
private int globalPeriodRangeTo;
private MachinePeriodPart[][] machineToMachinePeriodListMap; // Map<List<>> replaced by array[][] for performance
private MachinePeriodPart[] unassignedMachinePeriodList; // List<> replaced by array[] for performance
private long hardScore;
private long mediumScore;
private long softScore;
private Machine oldMachine = null;
private Integer oldStartPeriod = null;
// ************************************************************************
// Lifecycle methods
// ************************************************************************
@Override
public void resetWorkingSolution(CheapTimeSolution solution) {
this.cheapTimeSolution = solution;
hardScore = 0L;
mediumScore = 0L;
softScore = 0L;
if (solution.getGlobalPeriodRangeFrom() != 0) {
throw new IllegalStateException("The globalPeriodRangeFrom (" + solution.getGlobalPeriodRangeFrom()
+ ") should be 0.");
}
resourceListSize = solution.getResourceList().size();
globalPeriodRangeTo = solution.getGlobalPeriodRangeTo();
List<Machine> machineList = solution.getMachineList();
List<PeriodPowerPrice> periodPowerPriceList = solution.getPeriodPowerPriceList();
machineToMachinePeriodListMap = new MachinePeriodPart[machineList.size()][];
for (Machine machine : machineList) {
MachinePeriodPart[] machinePeriodList = new MachinePeriodPart[globalPeriodRangeTo];
for (int period = 0; period < globalPeriodRangeTo; period++) {
machinePeriodList[period] = new MachinePeriodPart(machine, periodPowerPriceList.get(period));
}
machineToMachinePeriodListMap[machine.getIndex()] = machinePeriodList;
}
unassignedMachinePeriodList = new MachinePeriodPart[globalPeriodRangeTo];
for (int period = 0; period < globalPeriodRangeTo; period++) {
unassignedMachinePeriodList[period] = new MachinePeriodPart(null, periodPowerPriceList.get(period));
}
for (TaskAssignment taskAssignment : solution.getTaskAssignmentList()) {
// Do not do modifyMachine(taskAssignment, null, taskAssignment.getMachine());
// because modifyStartPeriod does all it's effects too
modifyStartPeriod(taskAssignment, null, taskAssignment.getStartPeriod());
}
}
@Override
public void beforeEntityAdded(Object entity) {
// Do nothing
}
@Override
public void afterEntityAdded(Object entity) {
TaskAssignment taskAssignment = (TaskAssignment) entity;
// Do not do modifyMachine(taskAssignment, null, taskAssignment.getMachine());
// because modifyStartPeriod does all it's effects too
modifyStartPeriod(taskAssignment, null, taskAssignment.getStartPeriod());
}
@Override
public void beforeVariableChanged(Object entity, String variableName) {
TaskAssignment taskAssignment = (TaskAssignment) entity;
switch (variableName) {
case "machine":
oldMachine = taskAssignment.getMachine();
break;
case "startPeriod":
oldStartPeriod = taskAssignment.getStartPeriod();
break;
default:
throw new IllegalArgumentException("The variableName (" + variableName + ") is not supported.");
}
}
@Override
public void afterVariableChanged(Object entity, String variableName) {
TaskAssignment taskAssignment = (TaskAssignment) entity;
switch (variableName) {
case "machine":
modifyMachine(taskAssignment, oldMachine, taskAssignment.getMachine());
break;
case "startPeriod":
modifyStartPeriod(taskAssignment, oldStartPeriod, taskAssignment.getStartPeriod());
break;
default:
throw new IllegalArgumentException("The variableName (" + variableName + ") is not supported.");
}
}
@Override
public void beforeEntityRemoved(Object entity) {
TaskAssignment taskAssignment = (TaskAssignment) entity;
oldMachine = taskAssignment.getMachine();
oldStartPeriod = taskAssignment.getStartPeriod();
}
@Override
public void afterEntityRemoved(Object entity) {
TaskAssignment taskAssignment = (TaskAssignment) entity;
// Do not do modifyMachine(taskAssignment, oldMachine, null);
// because modifyStartPeriod does all it's effects too
modifyStartPeriod(taskAssignment, oldStartPeriod, null);
}
// ************************************************************************
// Modify methods
// ************************************************************************
private void modifyMachine(TaskAssignment taskAssignment, Machine oldMachine, Machine newMachine) {
if (Objects.equals(oldMachine, newMachine)) {
return;
}
Integer startPeriod = taskAssignment.getStartPeriod();
if (startPeriod == null) {
return;
}
Integer endPeriod = taskAssignment.getEndPeriod();
if (oldMachine != null) {
MachinePeriodPart[] machinePeriodList = machineToMachinePeriodListMap[oldMachine.getIndex()];
retractRange(taskAssignment, machinePeriodList, startPeriod, endPeriod, false);
}
if (newMachine != null) {
MachinePeriodPart[] machinePeriodList = machineToMachinePeriodListMap[newMachine.getIndex()];
insertRange(taskAssignment, machinePeriodList, startPeriod, endPeriod, false);
}
}
private void modifyStartPeriod(TaskAssignment taskAssignment, Integer oldStartPeriod, Integer newStartPeriod) {
if (Objects.equals(oldStartPeriod, newStartPeriod)) {
return;
}
Task task = taskAssignment.getTask();
int duration = task.getDuration();
int retractStart;
int retractEnd;
int insertStart;
int insertEnd;
if (oldStartPeriod == null) {
retractStart = -1;
retractEnd = -1;
insertStart = newStartPeriod;
insertEnd = insertStart + duration;
} else if (newStartPeriod == null) {
retractStart = oldStartPeriod;
retractEnd = retractStart + duration;
insertStart = -1;
insertEnd = -1;
} else {
retractStart = oldStartPeriod;
retractEnd = retractStart + duration;
insertStart = newStartPeriod;
insertEnd = insertStart + duration;
if (oldStartPeriod < newStartPeriod) {
if (insertStart < retractEnd) {
int overlap = retractEnd - insertStart;
retractEnd -= overlap;
insertStart += overlap;
}
} else {
if (retractStart < insertEnd) {
int overlap = insertEnd - retractStart;
insertEnd -= overlap;
retractStart += overlap;
}
}
}
if (oldStartPeriod != null) {
softScore += oldStartPeriod;
}
if (newStartPeriod != null) {
softScore -= newStartPeriod;
}
Machine machine = taskAssignment.getMachine();
MachinePeriodPart[] machinePeriodList;
if (machine != null) {
machinePeriodList = machineToMachinePeriodListMap[machine.getIndex()];
} else {
machinePeriodList = unassignedMachinePeriodList;
}
if (retractStart != retractEnd) {
retractRange(taskAssignment, machinePeriodList, retractStart, retractEnd, true);
}
if (insertStart != insertEnd) {
insertRange(taskAssignment, machinePeriodList, insertStart, insertEnd, true);
}
}
private void retractRange(TaskAssignment taskAssignment, MachinePeriodPart[] machinePeriodList,
int startPeriod, int endPeriod, boolean retractTaskCost) {
long powerConsumptionMicros = taskAssignment.getTask().getPowerConsumptionMicros();
long spinUpDownCostMicros = taskAssignment.getMachine().getSpinUpDownCostMicros();
MachinePeriodStatus previousStatus;
int idlePeriodStart = Integer.MIN_VALUE;
long idleAvailable;
if (startPeriod == 0) {
previousStatus = MachinePeriodStatus.OFF;
idleAvailable = Long.MIN_VALUE;
} else {
previousStatus = machinePeriodList[startPeriod - 1].status;
if (previousStatus == MachinePeriodStatus.IDLE) {
idleAvailable = spinUpDownCostMicros;
for (int i = startPeriod - 1; i >= 0; i--) {
MachinePeriodPart machinePeriod = machinePeriodList[i];
if (machinePeriod.status.isActive()) {
idlePeriodStart = i + 1;
break;
}
machinePeriod.undoMakeIdle();
idleAvailable -= machinePeriod.machineCostMicros;
}
if (idleAvailable < 0L) {
throw new IllegalStateException("The range of idlePeriodStart (" + idlePeriodStart
+ ") to startPeriod (" + startPeriod
+ ") should have been IDLE because the idleAvailable (" + idleAvailable
+ ") is negative.");
}
} else {
idleAvailable = Long.MIN_VALUE;
}
}
for (int i = startPeriod; i < endPeriod; i++) {
MachinePeriodPart machinePeriod = machinePeriodList[i];
machinePeriod.retractTaskAssignment(taskAssignment);
if (retractTaskCost) {
mediumScore += CheapTimeCostCalculator.multiplyTwoMicros(powerConsumptionMicros,
machinePeriod.periodPowerPriceMicros);
}
// SpinUp vs idle
if (machinePeriod.status.isActive()) {
if (previousStatus == MachinePeriodStatus.OFF) {
// Only if (startPeriod == i), it could be SPIN_UP_AND_ACTIVE
if (machinePeriod.status != MachinePeriodStatus.SPIN_UP_AND_ACTIVE) {
machinePeriod.spinUp();
}
} else if (previousStatus == MachinePeriodStatus.IDLE) {
// Create idle period
for (int j = idlePeriodStart; j < i; j++) {
machinePeriodList[j].makeIdle();
}
idlePeriodStart = Integer.MIN_VALUE;
idleAvailable = Long.MIN_VALUE;
}
previousStatus = MachinePeriodStatus.STILL_ACTIVE;
} else if (machinePeriod.status == MachinePeriodStatus.OFF) {
if (previousStatus != MachinePeriodStatus.OFF) {
if (previousStatus.isActive()) {
idlePeriodStart = i;
idleAvailable = spinUpDownCostMicros;
}
idleAvailable -= machinePeriod.machineCostMicros;
if (idleAvailable < 0) {
previousStatus = MachinePeriodStatus.OFF;
idlePeriodStart = Integer.MIN_VALUE;
idleAvailable = Long.MIN_VALUE;
} else {
previousStatus = MachinePeriodStatus.IDLE;
}
}
} else {
throw new IllegalStateException("Impossible status (" + machinePeriod.status + ").");
}
}
if (endPeriod < globalPeriodRangeTo && machinePeriodList[endPeriod].status != MachinePeriodStatus.OFF
&& !previousStatus.isActive()) {
for (int i = endPeriod; i < globalPeriodRangeTo; i++) {
MachinePeriodPart machinePeriod = machinePeriodList[i];
if (machinePeriod.status.isActive()) {
if (previousStatus == MachinePeriodStatus.OFF) {
machinePeriod.spinUp();
} else if (previousStatus == MachinePeriodStatus.IDLE) {
// Create idle period
for (int j = idlePeriodStart; j < i; j++) {
machinePeriodList[j].makeIdle();
}
}
break;
} else if (machinePeriod.status == MachinePeriodStatus.IDLE) {
machinePeriod.undoMakeIdle();
if (previousStatus == MachinePeriodStatus.IDLE) {
idleAvailable -= machinePeriod.machineCostMicros;
if (idleAvailable < 0) {
previousStatus = MachinePeriodStatus.OFF;
idlePeriodStart = Integer.MIN_VALUE;
idleAvailable = Long.MIN_VALUE;
}
}
} else {
throw new IllegalStateException("Impossible status (" + machinePeriod.status + ").");
}
}
}
}
private void insertRange(TaskAssignment taskAssignment, MachinePeriodPart[] machinePeriodList,
int startPeriod, int endPeriod, boolean insertTaskCost) {
long powerConsumptionMicros = taskAssignment.getTask().getPowerConsumptionMicros();
MachinePeriodPart startMachinePeriod = machinePeriodList[startPeriod];
boolean startIsOff = startMachinePeriod.status == MachinePeriodStatus.OFF;
boolean lastIsOff = machinePeriodList[endPeriod - 1].status == MachinePeriodStatus.OFF;
for (int i = startPeriod; i < endPeriod; i++) {
MachinePeriodPart machinePeriod = machinePeriodList[i];
machinePeriod.insertTaskAssignment(taskAssignment);
if (insertTaskCost) {
mediumScore -= CheapTimeCostCalculator.multiplyTwoMicros(powerConsumptionMicros,
machinePeriod.periodPowerPriceMicros);
}
// SpinUp vs idle
if (machinePeriod.status == MachinePeriodStatus.SPIN_UP_AND_ACTIVE && i != startPeriod) {
machinePeriod.undoSpinUp();
}
}
// SpinUp vs idle
if (startIsOff) {
long idleAvailable = taskAssignment.getMachine().getSpinUpDownCostMicros();
int idlePeriodStart = Integer.MIN_VALUE;
for (int i = startPeriod - 1; i >= 0 && idleAvailable >= 0L; i--) {
MachinePeriodPart machinePeriod = machinePeriodList[i];
if (machinePeriod.status.isActive()) {
idlePeriodStart = i + 1;
break;
}
idleAvailable -= machinePeriod.machineCostMicros;
}
if (idlePeriodStart >= 0) {
// Create idle period
for (int i = idlePeriodStart; i < startPeriod; i++) {
machinePeriodList[i].makeIdle();
}
} else {
startMachinePeriod.spinUp();
}
}
if (lastIsOff) {
long idleAvailable = taskAssignment.getMachine().getSpinUpDownCostMicros();
int idlePeriodEnd = Integer.MIN_VALUE;
for (int i = endPeriod; i < globalPeriodRangeTo && idleAvailable >= 0L; i++) {
MachinePeriodPart machinePeriod = machinePeriodList[i];
if (machinePeriod.status.isActive()) {
idlePeriodEnd = i;
machinePeriod.undoSpinUp();
break;
}
idleAvailable -= machinePeriod.machineCostMicros;
}
if (idlePeriodEnd >= 0) {
// Create idle period
for (int i = endPeriod; i < idlePeriodEnd; i++) {
machinePeriodList[i].makeIdle();
}
}
}
}
@Override
public HardMediumSoftLongScore calculateScore() {
return HardMediumSoftLongScore.valueOf(hardScore, mediumScore, softScore);
}
private class MachinePeriodPart {
private final Machine machine;
private final int period;
private final long periodPowerPriceMicros;
private final long machineCostMicros;
private int taskCount;
private MachinePeriodStatus status;
private int[] resourceAvailableList; // List<> replaced by array[] for performance
private MachinePeriodPart(Machine machine, PeriodPowerPrice periodPowerPrice) {
this.machine = machine;
this.period = periodPowerPrice.getPeriod();
this.periodPowerPriceMicros = periodPowerPrice.getPowerPriceMicros();
taskCount = 0;
status = MachinePeriodStatus.OFF;
if (machine != null) {
resourceAvailableList = new int[resourceListSize];
for (int i = 0; i < resourceListSize; i++) {
resourceAvailableList[i] = machine.getMachineCapacityList().get(i).getCapacity();
}
machineCostMicros = CheapTimeCostCalculator.multiplyTwoMicros(machine.getPowerConsumptionMicros(),
periodPowerPriceMicros);
} else {
machineCostMicros = Long.MIN_VALUE;
}
}
public void spinUp() {
if (status != MachinePeriodStatus.STILL_ACTIVE) {
throw new IllegalStateException("Impossible status (" + status + ").");
}
mediumScore -= machine.getSpinUpDownCostMicros();
status = MachinePeriodStatus.SPIN_UP_AND_ACTIVE;
}
public void undoSpinUp() {
if (status != MachinePeriodStatus.SPIN_UP_AND_ACTIVE) {
throw new IllegalStateException("Impossible status (" + status + ").");
}
mediumScore += machine.getSpinUpDownCostMicros();
status = MachinePeriodStatus.STILL_ACTIVE;
}
public void makeIdle() {
if (status != MachinePeriodStatus.OFF) {
throw new IllegalStateException("Impossible status (" + status + ").");
}
mediumScore -= machineCostMicros;
status = MachinePeriodStatus.IDLE;
}
public void undoMakeIdle() {
if (status != MachinePeriodStatus.IDLE) {
throw new IllegalStateException("Impossible status (" + status + ").");
}
mediumScore += machineCostMicros;
status = MachinePeriodStatus.OFF;
}
public void insertTaskAssignment(TaskAssignment taskAssignment) {
if (machine == null) {
return;
}
Task task = taskAssignment.getTask();
if (status == MachinePeriodStatus.OFF) {
mediumScore -= machineCostMicros;
status = MachinePeriodStatus.STILL_ACTIVE;
} else if (status == MachinePeriodStatus.IDLE) {
status = MachinePeriodStatus.STILL_ACTIVE;
}
taskCount++;
for (int i = 0; i < resourceAvailableList.length; i++) {
int resourceAvailable = resourceAvailableList[i];
TaskRequirement taskRequirement = task.getTaskRequirementList().get(i);
if (resourceAvailable < 0) {
hardScore -= resourceAvailable;
}
resourceAvailable -= taskRequirement.getResourceUsage();
if (resourceAvailable < 0) {
hardScore += resourceAvailable;
}
resourceAvailableList[i] = resourceAvailable;
}
}
public void retractTaskAssignment(TaskAssignment taskAssignment) {
if (machine == null) {
return;
}
Task task = taskAssignment.getTask();
if (status == MachinePeriodStatus.OFF || status == MachinePeriodStatus.IDLE) {
throw new IllegalStateException("Impossible status (" + status + ").");
}
taskCount--;
if (taskCount == 0) {
mediumScore += machineCostMicros;
if (status == MachinePeriodStatus.SPIN_UP_AND_ACTIVE) {
mediumScore += machine.getSpinUpDownCostMicros();
}
status = MachinePeriodStatus.OFF;
}
for (int i = 0; i < resourceAvailableList.length; i++) {
int resourceAvailable = resourceAvailableList[i];
TaskRequirement taskRequirement = task.getTaskRequirementList().get(i);
if (resourceAvailable < 0) {
hardScore -= resourceAvailable;
}
resourceAvailable += taskRequirement.getResourceUsage();
if (resourceAvailable < 0) {
hardScore += resourceAvailable;
}
resourceAvailableList[i] = resourceAvailable;
}
}
@Override
public String toString() {
return status.name() + " (" + taskCount + " tasks)";
}
}
// ************************************************************************
// ConstraintMatchAwareIncrementalScoreCalculator methods
// ************************************************************************
@Override
public void resetWorkingSolution(CheapTimeSolution workingSolution, boolean constraintMatchEnabled) {
resetWorkingSolution(workingSolution);
// ignore constraintMatchEnabled, it is always presumed enabled
}
@Override
public Collection<ConstraintMatchTotal> getConstraintMatchTotals() {
List<Resource> resourceList = cheapTimeSolution.getResourceList();
ConstraintMatchTotal resourceCapacityMatchTotal = new ConstraintMatchTotal(
CONSTRAINT_PACKAGE, "resourceCapacity", HardMediumSoftLongScore.ZERO);
ConstraintMatchTotal spinUpDownMatchTotal = new ConstraintMatchTotal(
CONSTRAINT_PACKAGE, "spinUpDown", HardMediumSoftLongScore.ZERO);
ConstraintMatchTotal machineConsumptionMatchTotal = new ConstraintMatchTotal(
CONSTRAINT_PACKAGE, "machineConsumption", HardMediumSoftLongScore.ZERO);
ConstraintMatchTotal taskConsumptionMatchTotal = new ConstraintMatchTotal(
CONSTRAINT_PACKAGE, "taskConsumption", HardMediumSoftLongScore.ZERO);
ConstraintMatchTotal minimizeTaskStartPeriodMatchTotal = new ConstraintMatchTotal(
CONSTRAINT_PACKAGE, "minimizeTaskStartPeriod", HardMediumSoftLongScore.ZERO);
long taskConsumptionWeight = mediumScore;
for (Machine machine : cheapTimeSolution.getMachineList()) {
for (int period = 0; period < globalPeriodRangeTo; period++) {
MachinePeriodPart machinePeriod = machineToMachinePeriodListMap[machine.getIndex()][period];
for (int i = 0; i < machinePeriod.resourceAvailableList.length; i++) {
int resourceAvailable = machinePeriod.resourceAvailableList[i];
if (resourceAvailable < 0) {
resourceCapacityMatchTotal.addConstraintMatch(
Arrays.asList(machine, period, resourceList.get(i)),
HardMediumSoftLongScore.valueOf(resourceAvailable, 0, 0));
}
}
if (machinePeriod.status == MachinePeriodStatus.SPIN_UP_AND_ACTIVE) {
spinUpDownMatchTotal.addConstraintMatch(
Arrays.asList(machine, period),
HardMediumSoftLongScore.valueOf(0, - machine.getSpinUpDownCostMicros(), 0));
taskConsumptionWeight += machine.getSpinUpDownCostMicros();
}
if (machinePeriod.status != MachinePeriodStatus.OFF) {
machineConsumptionMatchTotal.addConstraintMatch(
Arrays.asList(machine, period),
HardMediumSoftLongScore.valueOf(0, - machinePeriod.machineCostMicros, 0));
taskConsumptionWeight += machinePeriod.machineCostMicros;
}
}
}
// Individual taskConsumption isn't tracked for performance
taskConsumptionMatchTotal.addConstraintMatch(
Arrays.asList(),
HardMediumSoftLongScore.valueOf(0, taskConsumptionWeight, 0));
// Individual taskStartPeriod isn't tracked for performance
// but we mimic it
for (TaskAssignment taskAssignment : cheapTimeSolution.getTaskAssignmentList()) {
Integer startPeriod = taskAssignment.getStartPeriod();
if (startPeriod != null) {
minimizeTaskStartPeriodMatchTotal.addConstraintMatch(
Arrays.asList(taskAssignment),
HardMediumSoftLongScore.valueOf(0, 0, - startPeriod));
}
}
List<ConstraintMatchTotal> constraintMatchTotalList = new ArrayList<>(4);
constraintMatchTotalList.add(resourceCapacityMatchTotal);
constraintMatchTotalList.add(spinUpDownMatchTotal);
constraintMatchTotalList.add(machineConsumptionMatchTotal);
constraintMatchTotalList.add(taskConsumptionMatchTotal);
constraintMatchTotalList.add(minimizeTaskStartPeriodMatchTotal);
return constraintMatchTotalList;
}
@Override
public Map<Object, Indictment> getIndictmentMap() {
return null; // Calculate it non-incrementally from getConstraintMatchTotals()
}
private enum MachinePeriodStatus {
OFF,
IDLE,
SPIN_UP_AND_ACTIVE,
STILL_ACTIVE;
public boolean isActive() {
return this == MachinePeriodStatus.STILL_ACTIVE || this == MachinePeriodStatus.SPIN_UP_AND_ACTIVE;
}
}
}