[英]OptaPlanner ignoring constraint
我在 VehicleRoutingConstraintProvider 中創建了一個timeToIdealDate
約束。
此約束的目標是將訪問的車輛訪問分組,訪問的idealDate
最接近車輛的start
日期。
我遇到的問題與分配給start
和idealDate
的日期無關。 訪問似乎是隨機分配給車輛的,忽略了軟約束。
我期望求解器將訪問從一輛車切換到另一輛車,直到分數變得越來越低,因為start
日期更接近車輛的所有 idealDate` 的總和。
有任何想法嗎?
鑒於我們有開始日期為2022-10-05
的“車輛 A”和開始日期為2022-01-05
的“車輛 B”。 理想日期為2022-11-23
的訪問應分配給“車輛 B”。
start
日期分布在一年內)idealDate
在一年內隨機分布) PlanningVisit 有idealDate
:
PlanningVehicle 已經start
:
vehiclerouting/plugin/planner/VehicleRoutingConstraintProvider.java
:
package org.optaweb.vehiclerouting.plugin.planner;
import static org.optaplanner.core.api.score.stream.ConstraintCollectors.sumLong;
import org.optaplanner.core.api.score.buildin.hardmediumsoftlong.HardMediumSoftLongScore;
import org.optaplanner.core.api.score.stream.Constraint;
import org.optaplanner.core.api.score.stream.ConstraintFactory;
import org.optaplanner.core.api.score.stream.ConstraintProvider;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit;
public class VehicleRoutingConstraintProvider implements ConstraintProvider {
@Override
public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
return new Constraint[] {
vehicleCapacity(constraintFactory),
timeToIdealDate(constraintFactory)
};
}
Constraint vehicleCapacity(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(PlanningVisit.class)
.groupBy(PlanningVisit::getVehicle, sum(PlanningVisit::getDemand))
.filter((vehicle, demand) -> demand > vehicle.getCapacity())
.penalizeLong(
"vehicle capacity",
HardMediumSoftLongScore.ONE_HARD,
(vehicle, demand) -> demand - vehicle.getCapacity());
}
Constraint timeToIdealDate(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(PlanningVisit.class)
.penalizeLong(
"time to ideal date",
HardMediumSoftLongScore.ONE_SOFT,
PlanningVisit::absoluteDistanceToIdealDate);
}
}
vehiclerouting/plugin/planner/domain/PlanningVisit.java
:
package org.optaweb.vehiclerouting.plugin.planner.domain;
import org.optaplanner.core.api.domain.entity.PlanningEntity;
import org.optaplanner.core.api.domain.lookup.PlanningId;
import org.optaplanner.core.api.domain.variable.AnchorShadowVariable;
import org.optaplanner.core.api.domain.variable.PlanningVariable;
import org.optaplanner.core.api.domain.variable.PlanningVariableGraphType;
import org.optaweb.vehiclerouting.plugin.planner.weight.DepotAngleVisitDifficultyWeightFactory;
@PlanningEntity(difficultyWeightFactoryClass = DepotAngleVisitDifficultyWeightFactory.class)
public class PlanningVisit implements Standstill {
@PlanningId
private long id;
private PlanningLocation location;
private int idealDate;
// Planning variable: changes during planning, between score calculations.
@PlanningVariable(valueRangeProviderRefs = { "vehicleRange", "visitRange" },
graphType = PlanningVariableGraphType.CHAINED)
private Standstill previousStandstill;
// Shadow variables
private PlanningVisit nextVisit;
@AnchorShadowVariable(sourceVariableName = "previousStandstill")
private PlanningVehicle vehicle;
PlanningVisit() {
// Hide public constructor in favor of the factory.
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
@Override
public PlanningLocation getLocation() {
return location;
}
public void setLocation(PlanningLocation location) {
this.location = location;
}
public int getIdealDate() {
return idealDate;
}
public void setIdealDate(int idealDate) {
this.idealDate = idealDate;
}
public Standstill getPreviousStandstill() {
return previousStandstill;
}
public void setPreviousStandstill(Standstill previousStandstill) {
this.previousStandstill = previousStandstill;
}
@Override
public PlanningVisit getNextVisit() {
return nextVisit;
}
@Override
public void setNextVisit(PlanningVisit nextVisit) {
this.nextVisit = nextVisit;
}
public PlanningVehicle getVehicle() {
return vehicle;
}
public void setVehicle(PlanningVehicle vehicle) {
this.vehicle = vehicle;
}
// ************************************************************************
// Complex methods
// ************************************************************************
/**
* Distance from the previous standstill to this visit. This is used to calculate the travel cost of a chain
* beginning with a vehicle (at a depot) and ending with the {@link #isLast() last} visit.
* The chain ends with a visit, not a depot so the cost of returning from the last visit back to the depot
* has to be added in a separate step using {@link #distanceToDepot()}.
*
* @return distance from previous standstill to this visit
*/
public long distanceFromPreviousStandstill() {
if (previousStandstill == null) {
throw new IllegalStateException(
"This method must not be called when the previousStandstill (null) is not initialized yet.");
}
return previousStandstill.getLocation().distanceTo(location);
}
/**
* Distance from this visit back to the depot.
*
* @return distance from this visit back its vehicle's depot
*/
public long distanceToDepot() {
return location.distanceTo(vehicle.getLocation());
}
public long absoluteDistanceToIdealDate() {
long vehicleStart = vehicle.getStart();
if (idealDate > 0 && vehicleStart > 0) {
return Math.abs(idealDate - vehicleStart);
}
return 0;
}
/**
* Whether this visit is the last in a chain.
*
* @return true, if this visit has no {@link #getNextVisit() next} visit
*/
public boolean isLast() {
return nextVisit == null;
}
@Override
public String toString() {
return "PlanningVisit{" +
(location == null ? "" : "location=" + location.getId()) +
",demand=" + demand +
",idealDate=" + idealDate +
(previousStandstill == null ? "" : ",previousStandstill='" + previousStandstill.getLocation().getId()) +
(nextVisit == null ? "" : ",nextVisit=" + nextVisit.getId()) +
(vehicle == null ? "" : ",vehicle=" + vehicle.getId()) +
",id=" + id +
'}';
}
}
vehiclerouting/plugin/planner/domain/PlanningVehicle.java
:
package org.optaweb.vehiclerouting.plugin.planner.domain;
import java.util.Iterator;
import java.util.NoSuchElementException;
import org.optaplanner.core.api.domain.lookup.PlanningId;
public class PlanningVehicle implements Standstill {
@PlanningId
private long id;
private int capacity;
private int start;
private PlanningDepot depot;
// Shadow variables
private PlanningVisit nextVisit;
PlanningVehicle() {
// Hide public constructor in favor of the factory.
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public int getCapacity() {
return capacity;
}
public void setCapacity(int capacity) {
this.capacity = capacity;
}
public int getStart() {
return start;
}
public void setStart(int start) {
this.start = start;
}
public PlanningDepot getDepot() {
return depot;
}
public void setDepot(PlanningDepot depot) {
this.depot = depot;
}
@Override
public PlanningVisit getNextVisit() {
return nextVisit;
}
@Override
public void setNextVisit(PlanningVisit nextVisit) {
this.nextVisit = nextVisit;
}
public Iterable<PlanningVisit> getFutureVisits() {
return () -> new Iterator<PlanningVisit>() {
PlanningVisit nextVisit = getNextVisit();
@Override
public boolean hasNext() {
return nextVisit != null;
}
@Override
public PlanningVisit next() {
if (nextVisit == null) {
throw new NoSuchElementException();
}
PlanningVisit out = nextVisit;
nextVisit = nextVisit.getNextVisit();
return out;
}
};
}
@Override
public PlanningLocation getLocation() {
return depot.getLocation();
}
@Override
public String toString() {
return "PlanningVehicle{" +
"start=" + start +
"capacity=" + capacity +
(depot == null ? "" : ",depot=" + depot.getId()) +
(nextVisit == null ? "" : ",nextVisit=" + nextVisit.getId()) +
",id=" + id +
'}';
}
}
timeToIdealDate
約束看起來是正確的,盡管它可以通過刪除groupBy()
並將PlanningVisit::absoluteDistanceToIdealDate
直接傳遞給 penalizeLong penalizeLong()
) 來簡化:
Constraint timeToIdealDate(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(PlanningVisit.class)
.penalizeLong(
"time to ideal date",
HardMediumSoftLongScore.ONE_SOFT,
PlanningVisit::absoluteDistanceToIdealDate);
}
}
就調試約束而言,以下步驟可能有所幫助:
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.