Commit 8312e0fa by Ren Ping

feat:移除不满足工程师时间窗强约束的订单

1 parent f83b0d31
......@@ -76,6 +76,9 @@ public class DispatchConstraintProvider implements ConstraintProvider {
if (!in) {
// 到达时间在日历窗口外,惩罚得分
ret = true;
customer.setInTechnicianTimeWindows(false);
} else {
customer.setInTechnicianTimeWindows(true);
}
}
}
......@@ -93,18 +96,18 @@ public class DispatchConstraintProvider implements ConstraintProvider {
.asConstraint(ConstraintNameEnum.technicianCapacityMatch.name());
}
protected Constraint dispatchedMatch(ConstraintFactory factory) {
return factory.forEach(Customer.class).filter(customer ->
// 已分配但是分给了别人
(customer.getDispatchedTechnicianCode() != null && customer.getTechnician() != null
&& !StringUtils.equals(customer.getDispatchedTechnicianCode(), customer.getTechnician().getCode())) ||
// 已排除但是分给了这个人
(customer.getExclusiveTechnicianCode() != null && customer.getTechnician() != null
&& StringUtils.equals(customer.getExclusiveTechnicianCode(),
customer.getTechnician().getCode())))
.penalizeLong(HardSoftLongScore.ONE_HARD, customer -> 50)
.asConstraint(ConstraintNameEnum.dispatchedMatch.name());
}
protected Constraint dispatchedMatch(ConstraintFactory factory) {
return factory.forEach(Customer.class).filter(customer ->
// 已分配但是分给了别人
(customer.getDispatchedTechnicianCode() != null && customer.getTechnician() != null
&& !StringUtils.equals(customer.getDispatchedTechnicianCode(), customer.getTechnician().getCode())) ||
// 已排除但是分给了这个人
(customer.getExclusiveTechnicianCode() != null && customer.getTechnician() != null
&& StringUtils.equals(customer.getExclusiveTechnicianCode(),
customer.getTechnician().getCode())))
.penalizeLong(HardSoftLongScore.ONE_HARD, customer -> 50)
.asConstraint(ConstraintNameEnum.dispatchedMatch.name());
}
// protected Constraint customerTimeWindowsMatch1(ConstraintFactory factory) {
// return factory.forEach(Customer.class).filter(
......
......@@ -21,12 +21,12 @@ public class Customer {
private long id;
private String code;
// 已分配
private String dispatchedTechnicianCode;
// 已排除
private String exclusiveTechnicianCode;
// orderid(code)+dt 确定唯一一条工单
private String dt;
@JsonIgnore
......@@ -50,6 +50,8 @@ public class Customer {
// 离开时间
// private Integer departureTime;
private boolean isInTechnicianTimeWindows = true;
public Customer() {
}
......@@ -135,9 +137,9 @@ public class Customer {
// throw new IllegalStateException("This method must not be called when the shadow variables are not initialized yet.");
}
if (previousCustomer == null) {
return technician.getDepot().getLocation().getDistanceTo(technician.getVehicleType(),location);
return technician.getDepot().getLocation().getDistanceTo(technician.getVehicleType(), location);
}
return previousCustomer.getLocation().getDistanceTo(technician.getVehicleType(),location);
return previousCustomer.getLocation().getDistanceTo(technician.getVehicleType(), location);
}
/**
......@@ -151,9 +153,9 @@ public class Customer {
// throw new IllegalStateException("This method must not be called when the shadow variables are not initialized yet.");
}
if (previousCustomer == null) {
return technician.getDepot().getLocation().getPathTimeTo(technician.getVehicleType(),location);
return technician.getDepot().getLocation().getPathTimeTo(technician.getVehicleType(), location);
}
return previousCustomer.getLocation().getPathTimeTo(technician.getVehicleType(),location);
return previousCustomer.getLocation().getPathTimeTo(technician.getVehicleType(), location);
}
@Override
......
package com.dituhui.pea.dispatch.quartz.dispatch;
import com.dituhui.pea.dispatch.common.RedissonUtil;
import com.dituhui.pea.dispatch.service.SchedulerService;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
......@@ -23,7 +24,7 @@ import javax.annotation.Resource;
@DisallowConcurrentExecution
public class AutoDispatchJob extends QuartzJobBean {
public static final String TEAM_JOB_PREFIX="BOXI_TEAM_";
public static final String TEAM_JOB_PREFIX = "BOXI_TEAM_";
@Resource
private SchedulerService schedulerService;
......@@ -36,6 +37,9 @@ public class AutoDispatchJob extends QuartzJobBean {
String teamId = name.substring(TEAM_JOB_PREFIX.length());
long start = System.currentTimeMillis();
log.info(">>> 自动派工(teamId:{}) 自动任务开始", teamId);
/*RedissonUtil.lockOperation(AutoDispatchJob.TEAM_JOB_PREFIX + teamId, 60, () -> {
schedulerService.dispatchRun2(teamId);
});*/
schedulerService.dispatchRun2(teamId);
long end = System.currentTimeMillis();
log.info(">>> 自动派工(teamId:{}) 自动任务结束,耗时:{}", teamId, end - start);
......
......@@ -113,52 +113,45 @@ public class SchedulerServiceImpl implements SchedulerService {
for (int i = 1; i <= nextDaysLimit; i++) {
String currDay = LocalDate.now().plusDays(i).format(DateTimeFormatter.ISO_LOCAL_DATE);
boolean finalCutOff = cutOff;
RedissonUtil.lockOperation(AutoDispatchJob.TEAM_JOB_PREFIX + teamId, 60, () -> {
dispatchRun2OneDay(teamId, currDay, today, finalCutOff);
});
}
}
private void dispatchRun2OneDay(String teamId, String currDay, String today, boolean cutOff) {
Optional<DispatchBatch> optional = dispatchBatchRepository.findByTeamIdAndBatchDate(teamId, currDay);
if (optional.isPresent()
&& Objects.nonNull(optional.get().getCutoffedTime())
&& DateUtil.format(optional.get().getCutoffedTime(), "yyyy-MM-dd").equals(today)) {
//自动任务截止
log.error(">>> teamId:{}, day:{} 自动任务已截止", teamId, currDay);
return;
}
try {
log.info("dispatchRun begin----- teamId:{}, day:{}", teamId, currDay);
String batchNo = batchService.buildBatchData2(teamId, currDay, cutOff);
UUID problemId = solveService.generateProblemId(teamId, batchNo);
log.info("dispatchRun teamId:{}, day:{}, batch:{}, problemId:{}", teamId, currDay, batchNo, problemId);
DispatchSolution problem = solveService.prepareSolution2(teamId, batchNo, currDay);
if (problem.getCustomerList().size() <= 0) {
log.info("dispatchRun 当前批次没有待指派工单 , teamId:{}, day:{}, batch:{}, problemId:{}, order-size:{}", teamId, currDay, batchNo, problemId, problem.getCustomerList().size());
Optional<DispatchBatch> optional = dispatchBatchRepository.findByTeamIdAndBatchDate(teamId, currDay);
if (optional.isPresent()
&& Objects.nonNull(optional.get().getCutoffedTime())
&& DateUtil.format(optional.get().getCutoffedTime(), "yyyy-MM-dd").equals(today)) {
//自动任务截止
log.error(">>> teamId:{}, day:{} 自动任务已截止", teamId, currDay);
return;
}
log.info("dispatchRun prepare done, teamId:{}, day:{}, batch:{}, problemId:{}", teamId, currDay, batchNo, problemId);
Solver<DispatchSolution> solver = solverFactory.buildSolver();
DispatchSolution solution = solver.solve(problem);
DispatchSolutionUtils.removeHardConstraintCustomer(solution, solverFactory);
log.info("dispatchRun solve done, teamId:{}, day:{}, batch:{}, problemId:{}, score:{}", teamId, currDay, batchNo, problemId, solution.getScore().toShortString());
this.solveService.saveSolutionWrp2(solution);
this.extractService.extractDispatchToOrder2(teamId, batchNo, cutOff);
log.info("dispatchRun done ------ teamId:{}, day:{}", teamId, currDay);
JacksonSolutionFileIO<DispatchSolution> exporter = new JacksonSolutionFileIO<DispatchSolution>(DispatchSolution.class);
exporter.write(solution, new File(String.format("dispatchSolution_%s_%s.json", teamId, currDay)));
//log.info("dispatchRun group:{}, team:{} done", groupId, teamId);
} catch (Exception e) {
log.error(">>> (teamId:{}, day:{})自动排班失败:{}", teamId, currDay, e.getMessage(), e);
//throw e;
try {
log.info("dispatchRun begin----- teamId:{}, day:{}", teamId, currDay);
String batchNo = batchService.buildBatchData2(teamId, currDay, cutOff);
UUID problemId = solveService.generateProblemId(teamId, batchNo);
log.info("dispatchRun teamId:{}, day:{}, batch:{}, problemId:{}", teamId, currDay, batchNo, problemId);
DispatchSolution problem = solveService.prepareSolution2(teamId, batchNo, currDay);
if (problem.getCustomerList().size() <= 0) {
log.info("dispatchRun 当前批次没有待指派工单 , teamId:{}, day:{}, batch:{}, problemId:{}, order-size:{}", teamId, currDay, batchNo, problemId, problem.getCustomerList().size());
return;
}
log.info("dispatchRun prepare done, teamId:{}, day:{}, batch:{}, problemId:{}", teamId, currDay, batchNo, problemId);
Solver<DispatchSolution> solver = solverFactory.buildSolver();
DispatchSolution solution = solver.solve(problem);
DispatchSolutionUtils.removeHardConstraintCustomer(solution, solverFactory);
log.info("dispatchRun solve done, teamId:{}, day:{}, batch:{}, problemId:{}, score:{}", teamId, currDay, batchNo, problemId, solution.getScore().toShortString());
this.solveService.saveSolutionWrp2(solution);
this.extractService.extractDispatchToOrder2(teamId, batchNo, cutOff);
log.info("dispatchRun done ------ teamId:{}, day:{}", teamId, currDay);
JacksonSolutionFileIO<DispatchSolution> exporter = new JacksonSolutionFileIO<DispatchSolution>(DispatchSolution.class);
exporter.write(solution, new File(String.format("dispatchSolution_%s_%s.json", teamId, currDay)));
//log.info("dispatchRun group:{}, team:{} done", groupId, teamId);
} catch (Exception e) {
log.error(">>> (teamId:{}, day:{})自动排班失败:{}", teamId, currDay, e.getMessage(), e);
//throw e;
}
}
}
}
\ No newline at end of file
......@@ -553,7 +553,6 @@ public class SolveServiceImpl implements SolveService {
// Date end = Date.from(localEndTime.atZone(ZoneId.systemDefault()).toInstant());
log.info("算法结果回写dispatch, step3-逐个客户处理, teamId:{}, batchNo:{}, employ: {}, customer:{}, service-duration:{} ", teamId, batchNo, technician.getCode(), customer.getCode(), customer.getServiceDuration());
log.info(customer.toString());
LocalDateTime customDateTime = LocalDateTime.parse(customer.getDt() + " 00:00:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
LocalDateTime arriveTime = customDateTime.plusMinutes(customer.getArrivalTime());
LocalDateTime leaveTime = customDateTime.plusMinutes(customer.getDepartureTime());
......
......@@ -5,13 +5,12 @@ import static java.util.Comparator.comparing;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.optaplanner.constraint.streams.drools.DroolsConstraintStreamScoreDirector;
......@@ -38,423 +37,442 @@ import com.dituhui.pea.dispatch.pojo.Technician;
public class DispatchSolutionUtils {
/**
* 打印方案
*
* @param solution
*/
public static void printSolution(DispatchSolution solution) {
System.out.println("Score: " + solution.getScore().toShortString());
System.out.println("技能约束:");
solution.getTechnicianList().forEach(technician -> {
System.out.printf("技术员%s(%s) %s%n", technician.getId(), technician.getCode(), technician.getSkills());
for (Customer customer : technician.getCustomerList()) {
if (!technician.getSkills().contains(customer.getRequiredSkill())) {
// no match
System.err.printf(" 预约单%s(%s) %s%n", customer.getId(), customer.getCode(),
customer.getRequiredSkill());
} else {
System.out.printf(" 预约单%s(%s) %s%n", customer.getId(), customer.getCode(),
customer.getRequiredSkill());
}
}
});
AtomicInteger totalNum = new AtomicInteger(0);
solution.getTechnicianList().forEach(technician -> {
System.out.printf("技术员%s(%s) [%s,%s]%n", technician.getId(), technician.getCode(),
printTime(technician.getTimeWindows()[0][0]), printTime(technician.getTimeWindows()[0][1]));
totalNum.addAndGet(technician.getCustomerList().size());
for (Customer customer : technician.getCustomerList()) {
Customer previousCustomer = customer.getPreviousCustomer();
int startPath, endPath;// 路上时间
if (null == previousCustomer) {
startPath = technician.getDepot().getStartTime();
// endPath = startPath +
// customer.getLocation().getPathTimeTo(technician.getDepot().getLocation());
endPath = startPath + technician.getDepot().getLocation().getPathTimeTo(technician.getVehicleType(),customer.getLocation());
} else {
startPath = previousCustomer.getDepartureTime();
// endPath = startPath +
// customer.getLocation().getPathTimeTo(previousCustomer.getLocation());
endPath = startPath + previousCustomer.getLocation().getPathTimeTo(technician.getVehicleType(),customer.getLocation());
}
if (customer.getArrivalTime() > customer.getEndTime()) {
// 迟到
System.err.printf(
" 预约单%s(%s) 预约时间窗[%s=>%s] 路上时间[%s=>%s] 早到等待时间[%s=>%s] 派工时间[%s=>%s] 迟到时间[%s=>%s]%n",
customer.getId(), customer.getCode(),
// 预约时间窗
printTime(customer.getStartTime()), printTime(customer.getEndTime()),
// 路上时间
printTime(startPath), printTime(endPath),
// 早到等待时间
customer.getArrivalTime() < customer.getStartTime() ? printTime(endPath) : "",
customer.getArrivalTime() < customer.getStartTime() ? printTime(customer.getStartTime())
: "",
// 派工时间
printTime(customer.getArrivalTime()), printTime(customer.getDepartureTime()),
// 迟到时间
printTime(customer.getEndTime()), printTime(customer.getArrivalTime()));
} else {
System.out.printf(
" 预约单%s(%s) 预约时间窗[%s=>%s] 路上时间[%s=>%s] 早到等待时间[%s=>%s] 派工时间[%s=>%s] 迟到时间[%s=>%s]%n",
customer.getId(), customer.getCode(),
// 预约时间窗
printTime(customer.getStartTime()), printTime(customer.getEndTime()),
// 路上时间
printTime(startPath), printTime(endPath),
// 早到等待时间
customer.getArrivalTime() < customer.getStartTime() ? printTime(endPath) : "",
customer.getArrivalTime() < customer.getStartTime() ? printTime(customer.getStartTime())
: "",
// 派工时间
printTime(customer.getArrivalTime()), printTime(customer.getDepartureTime()),
// 迟到时间
"", "");
}
}
});
}
private static String printTime(int startTime) {
int hour = startTime / 60;
int minite = startTime % 60;
return StringUtils.leftPad("" + hour, 2, '0') + ":" + StringUtils.leftPad("" + minite, 2, '0');
}
/**
* 生成方案地图页
*
* @param solution
* @param filename
*/
public static void exportMapHtml(DispatchSolution solution, String filename) {
try {
// 仓库起点
String depot = "\"" + solution.getDepot().getLocation().getX() + ","
+ solution.getDepot().getLocation().getY() + "\"";
// 技术员路线
String lines_ = "[";
for (Technician technician : solution.getTechnicianList()) {
if (technician.getCustomerList().size() > 0) {
lines_ += "\"" + technician.getCustomerList().stream()
.map(c -> c.getLocation().getX() + "," + c.getLocation().getY())
.reduce((a, b) -> a + ";" + b).get() + "\",";
}
}
lines_ += "]";
final String lines = lines_;
// 技术员偏好中心点
String preferredlocation = "\"" + solution.getTechnicianList().stream()
.map(c -> (c.getPreferredlocation() == null ? "" : c.getPreferredlocation().getX()) + ","
+ (c.getPreferredlocation() == null ? "" : c.getPreferredlocation().getY()))
.reduce((a, b) -> a + ";" + b).get() + "\"";
// 技术员名称
String names = "\"" + solution.getTechnicianList().stream()
.map(c -> (c.getPreferredlocation() == null ? "" : c.getPreferredlocation().getX()) + ","
+ (c.getPreferredlocation() == null ? "" : c.getPreferredlocation().getY()) + ","
+ c.getCode())
.reduce((a, b) -> a + ";" + b).get() + "\"";
List<String> dispatchMapLines = IOUtils.readLines(new FileInputStream("data/dispatchMap.html"), "GBK");
dispatchMapLines = dispatchMapLines.stream().map(line -> {
if (StringUtils.startsWith(line, " var depot = ")) {
return " var depot = " + depot;
} else if (StringUtils.startsWith(line, " var preferredlocation = ")) {
return " var preferredlocation = " + preferredlocation;
} else if (StringUtils.startsWith(line, " var lines = ")) {
return " var lines = " + lines;
} else if (StringUtils.startsWith(line, " var names = ")) {
return " var names = " + names;
} else{
return line;
}
}).collect(Collectors.toList());
IOUtils.writeLines(dispatchMapLines, "\r\n", new FileOutputStream(filename + ".html"), "GBK");
System.out.println("output map : " + filename);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 计算方案分数
*
* @param solution
* @return
*/
public static HardSoftLongScore getScore(DispatchSolution solution) {
DefaultSolverFactory<DispatchSolution> solverFactory = getSolverFactory();
ScoreDirectorFactory<DispatchSolution> scoreDirectorFactory = solverFactory.getScoreDirectorFactory();
DroolsConstraintStreamScoreDirector<DispatchSolution, HardSoftLongScore> scoreDirector = (DroolsConstraintStreamScoreDirector<DispatchSolution, HardSoftLongScore>) scoreDirectorFactory
.buildScoreDirector();
// Set the working solution of the ScoreDirector object.
scoreDirector.setWorkingSolution(solution);
// Iterate over the constraints in your problem and call the
// `addConstraintMatch()` method
// for each constraint that is violated by the solution.
// for (Constraint constraint : myConstraints) {
// if (constraint.isViolated(mySolution)) {
// scoreDirector.addConstraintMatch(constraint, constraint.getScore());
// }
// }
// Calculate the score of the solution.
HardSoftLongScore score = (HardSoftLongScore) scoreDirector.calculateScore();
return score;
}
/**
* 移动某个技术员的订单,返回新方案<br>
* FIXME 没有成功
*
* @param solution
* @param source 技术员
* @param sourceIndex 0开始
* @param destinationIndex 0开始
* @return
*/
public static DispatchSolution moveCustomer(DefaultSolverFactory<DispatchSolution> solverFactory,
DispatchSolution solution, Technician source, int sourceIndex, int destinationIndex) {
if (null == solverFactory) {
solverFactory = getSolverFactory();
}
ScoreDirectorFactory<DispatchSolution> scoreDirectorFactory = solverFactory.getScoreDirectorFactory();
DroolsConstraintStreamScoreDirector<DispatchSolution, HardSoftLongScore> scoreDirector = (DroolsConstraintStreamScoreDirector<DispatchSolution, HardSoftLongScore>) scoreDirectorFactory
.buildScoreDirector();
// Make a change to the solution.
// doMove(), doSwap(), or doChangeVariable()
// Create a variable descriptor for the "customerList" list property
ListVariableDescriptor<DispatchSolution> customerDescriptor = (ListVariableDescriptor<DispatchSolution>) scoreDirector
.getSolutionDescriptor().getEntityDescriptorStrict(Technician.class)
.getVariableDescriptor("customerList");
// Create the ListChangeMove
ListChangeMove<DispatchSolution> move = new ListChangeMove<DispatchSolution>(customerDescriptor, source,
sourceIndex, source, destinationIndex);
// apply the move and re-evaluate the score
scoreDirector.doAndProcessMove(move, true);
return scoreDirector.getWorkingSolution();
}
public static DefaultSolverFactory<DispatchSolution> getSolverFactory() {
return getSolverFactory(0, 0);
}
public static DefaultSolverFactory<DispatchSolution> getSolverFactory(long unimprovedSecondsSpentLimit,
long secondsSpentLimit) {
// 创建求解器配置
// 创建 SolverConfig 对象,并设置求解器配置
SolverConfig solverConfig = new SolverConfig();
solverConfig.setSolutionClass(DispatchSolution.class);
solverConfig.withEntityClassList(Arrays.asList(Technician.class, Customer.class));// 这里不能漏掉,否则约束不生效
TerminationConfig terminationConfig = new TerminationConfig();
terminationConfig
.setUnimprovedSecondsSpentLimit(unimprovedSecondsSpentLimit <= 0 ? 5 : unimprovedSecondsSpentLimit);// XX秒没有找到更好方案
terminationConfig.setSecondsSpentLimit(secondsSpentLimit <= 0 ? 60 : secondsSpentLimit);// 总时间不能超过XXs
solverConfig.withTerminationConfig(terminationConfig);
// 约束条件
solverConfig.withConstraintProviderClass(DispatchConstraintProvider.class);
solverConfig.setScoreDirectorFactoryConfig(
new ScoreDirectorFactoryConfig().withConstraintProviderClass(DispatchConstraintProvider.class));
DefaultSolverFactory<DispatchSolution> solverFactory = (DefaultSolverFactory<DispatchSolution>) SolverFactory
.<DispatchSolution>create(solverConfig);
return solverFactory;
}
/**
* 打印详细约束得分
*
* @param solution
* @param solverFactory
*/
public static void explainSolutionConstraintDetail(DispatchSolution solution, DefaultSolverFactory<DispatchSolution> solverFactory) {
// Obtain a ScoreExplanation object for the best solution
// Using score calculation outside the Solver
// https://www.optaplanner.org/docs/optaplanner/latest/score-calculation/score-calculation.html
SolutionManager<DispatchSolution, HardSoftLongScore> scoreManager = SolutionManager.create(solverFactory);
ScoreExplanation<DispatchSolution, HardSoftLongScore> scoreExplanation = scoreManager.explain(solution);
// System.out.println(scoreExplanation.getSummary());
Map<String, ConstraintMatchTotal<HardSoftLongScore>> constraintMatchTotalMap = scoreExplanation
.getConstraintMatchTotalMap();
constraintMatchTotalMap.forEach((key, value) -> {
if (!value.getScore().isFeasible()) {
// 违反硬约束
System.out.printf("%s 匹配%s次 hard得分:%s%n", value.getConstraintName(), value.getConstraintMatchCount(),
value.getScore().hardScore());
} else {
// 软约束
System.out.printf("%s 匹配%s次 soft得分:%s%n", value.getConstraintName(), value.getConstraintMatchCount(),
value.getScore().softScore());
}
value.getConstraintMatchSet().stream().sorted(comparing(ConstraintMatch::getScore))
.forEach(constraintMatch -> {
String text = "";
switch (ConstraintNameEnum.valueOf(value.getConstraintName())) {
case skillMatch:
case customerTimeWindowsMatch:
case dispatchedMatch:
for (Object indictedObject : constraintMatch.getIndictedObjectList()) {
// 违反硬约束对象,根据具体约束返回不同类型对象
if (indictedObject instanceof Customer) {
Customer customer = (Customer) indictedObject;
text += customer.getCode() + ",";
}
}
System.out.printf(" 预约单(%s)违反约束,扣分%s%n", text, constraintMatch.getScore().toShortString());
break;
case totalDistance:
case preferredTotalDistance:
for (Object indictedObject : constraintMatch.getIndictedObjectList()) {
// 违反软约束对象,根据具体约束返回不同类型对象
if (indictedObject instanceof Technician) {
Technician technician = (Technician) indictedObject;
text += technician.getCode() + ",";
}
}
System.out.printf(" 技术员(%s)总路程得分%s%n", text, constraintMatch.getScore().toShortString());
break;
case technicianBalanceSoft:
if (constraintMatch.getScore().softScore() < 0) {
for (Object indictedObject : constraintMatch.getIndictedObjectList()) {
// 违反软约束对象,根据具体约束返回不同类型对象
if (indictedObject instanceof Technician) {
Technician technician = (Technician) indictedObject;
text += technician.getCode() + ",";
}
}
System.out.printf(" 技术员(%s)订单量差距得分%s%n", text,
constraintMatch.getScore().toShortString());
}
break;
default:
break;
}
});
});
}
/**
* 导出json结构
*
* @param solution
* @param fileName
*/
public static void exportSolutionJson(DispatchSolution solution, String fileName) {
// Create a JacksonSolutionFileIO instance.
JacksonSolutionFileIO<DispatchSolution> exporter = new JacksonSolutionFileIO<DispatchSolution>(
DispatchSolution.class);
// Set the output file.
exporter.write(solution, new File(fileName));
}
/**
* 移除hard约束元素订单
*
* @param solution
* @param solverFactory
*/
public static void removeHardConstraintCustomer(DispatchSolution solution,
SolverFactory<DispatchSolution> solverFactory) {
SolutionManager<DispatchSolution, HardSoftLongScore> scoreManager = SolutionManager.create(solverFactory);
ScoreExplanation<DispatchSolution, HardSoftLongScore> scoreExplanation = scoreManager.explain(solution);
Map<String, ConstraintMatchTotal<HardSoftLongScore>> constraintMatchTotalMap = scoreExplanation
.getConstraintMatchTotalMap();
constraintMatchTotalMap.forEach((key, value) -> {
if (!value.getScore().isFeasible()) {
// 违反硬约束
value.getConstraintMatchSet().stream().sorted(comparing(ConstraintMatch::getScore))
.forEach(constraintMatch -> {
switch (ConstraintNameEnum.valueOf(value.getConstraintName())) {
case skillMatch:
case customerTimeWindowsMatch:
for (Object indictedObject : constraintMatch.getIndictedObjectList()) {
// 违反硬约束对象,根据具体约束返回不同类型对象
if (indictedObject instanceof Customer) {
Customer customer = (Customer) indictedObject;
// 更新shadow变量
updateShadowVariable(customer);
// 移除技术员
customer.getTechnician().getCustomerList().remove(customer);
solution.getUnDispatchedCustomers().add(customer);
}
}
break;
default:
break;
}
});
}
});
}
private static void updateShadowVariable(Customer sourceCustomer) {
if (sourceCustomer.getTechnician() == null) {
if (sourceCustomer.getArrivalTime() != null) {
sourceCustomer.setArrivalTime(null);
}
return;
}
// 移除当前订单引用
Customer previousCustomer = sourceCustomer.getPreviousCustomer();
Customer nextCustomer = sourceCustomer.getNextCustomer();
if (previousCustomer == null) {
// 当前订单是第一个订单
if(null != nextCustomer) {
nextCustomer.setPreviousCustomer(null);
}
}
if (nextCustomer == null) {
// 当前订单是最后一个订单
if(null != previousCustomer) {
previousCustomer.setNextCustomer(null);
}
}
if (previousCustomer != null && null != nextCustomer) {
previousCustomer.setNextCustomer(nextCustomer);
nextCustomer.setPreviousCustomer(previousCustomer);
}
// 前一个的离开时间
Integer departureTime;
if (previousCustomer == null) {
departureTime = (sourceCustomer.getTechnician().getDepot()).getStartTime();
} else {
departureTime = previousCustomer.getDepartureTime();
}
if (nextCustomer != null) {
// 更新后续订单
Customer shadowCustomer = nextCustomer;
Integer arrivalTime = calculateArrivalTime(shadowCustomer, departureTime);
while (shadowCustomer != null && !Objects.equals(shadowCustomer.getArrivalTime(), arrivalTime)) {
shadowCustomer.setArrivalTime(arrivalTime);
departureTime = shadowCustomer.getDepartureTime();
shadowCustomer = shadowCustomer.getNextCustomer();
arrivalTime = calculateArrivalTime(shadowCustomer, departureTime);
}
}
}
private static Integer calculateArrivalTime(Customer customer, Integer previousDepartureTime) {
if (customer == null || previousDepartureTime == null) {
return null;
}
return previousDepartureTime + customer.getPathTimeFromPreviousStandstill();
}
/**
* 打印方案
*
* @param solution
*/
public static void printSolution(DispatchSolution solution) {
System.out.println("Score: " + solution.getScore().toShortString());
System.out.println("技能约束:");
solution.getTechnicianList().forEach(technician -> {
System.out.printf("技术员%s(%s) %s%n", technician.getId(), technician.getCode(), technician.getSkills());
for (Customer customer : technician.getCustomerList()) {
if (!technician.getSkills().contains(customer.getRequiredSkill())) {
// no match
System.err.printf(" 预约单%s(%s) %s%n", customer.getId(), customer.getCode(),
customer.getRequiredSkill());
} else {
System.out.printf(" 预约单%s(%s) %s%n", customer.getId(), customer.getCode(),
customer.getRequiredSkill());
}
}
});
AtomicInteger totalNum = new AtomicInteger(0);
solution.getTechnicianList().forEach(technician -> {
System.out.printf("技术员%s(%s) [%s,%s]%n", technician.getId(), technician.getCode(),
printTime(technician.getTimeWindows()[0][0]), printTime(technician.getTimeWindows()[0][1]));
totalNum.addAndGet(technician.getCustomerList().size());
for (Customer customer : technician.getCustomerList()) {
Customer previousCustomer = customer.getPreviousCustomer();
int startPath, endPath;// 路上时间
if (null == previousCustomer) {
startPath = technician.getDepot().getStartTime();
// endPath = startPath +
// customer.getLocation().getPathTimeTo(technician.getDepot().getLocation());
endPath = startPath + technician.getDepot().getLocation().getPathTimeTo(technician.getVehicleType(), customer.getLocation());
} else {
startPath = previousCustomer.getDepartureTime();
// endPath = startPath +
// customer.getLocation().getPathTimeTo(previousCustomer.getLocation());
endPath = startPath + previousCustomer.getLocation().getPathTimeTo(technician.getVehicleType(), customer.getLocation());
}
if (customer.getArrivalTime() > customer.getEndTime()) {
// 迟到
System.err.printf(
" 预约单%s(%s) 预约时间窗[%s=>%s] 路上时间[%s=>%s] 早到等待时间[%s=>%s] 派工时间[%s=>%s] 迟到时间[%s=>%s]%n",
customer.getId(), customer.getCode(),
// 预约时间窗
printTime(customer.getStartTime()), printTime(customer.getEndTime()),
// 路上时间
printTime(startPath), printTime(endPath),
// 早到等待时间
customer.getArrivalTime() < customer.getStartTime() ? printTime(endPath) : "",
customer.getArrivalTime() < customer.getStartTime() ? printTime(customer.getStartTime())
: "",
// 派工时间
printTime(customer.getArrivalTime()), printTime(customer.getDepartureTime()),
// 迟到时间
printTime(customer.getEndTime()), printTime(customer.getArrivalTime()));
} else {
System.out.printf(
" 预约单%s(%s) 预约时间窗[%s=>%s] 路上时间[%s=>%s] 早到等待时间[%s=>%s] 派工时间[%s=>%s] 迟到时间[%s=>%s]%n",
customer.getId(), customer.getCode(),
// 预约时间窗
printTime(customer.getStartTime()), printTime(customer.getEndTime()),
// 路上时间
printTime(startPath), printTime(endPath),
// 早到等待时间
customer.getArrivalTime() < customer.getStartTime() ? printTime(endPath) : "",
customer.getArrivalTime() < customer.getStartTime() ? printTime(customer.getStartTime())
: "",
// 派工时间
printTime(customer.getArrivalTime()), printTime(customer.getDepartureTime()),
// 迟到时间
"", "");
}
}
});
}
private static String printTime(int startTime) {
int hour = startTime / 60;
int minite = startTime % 60;
return StringUtils.leftPad("" + hour, 2, '0') + ":" + StringUtils.leftPad("" + minite, 2, '0');
}
/**
* 生成方案地图页
*
* @param solution
* @param filename
*/
public static void exportMapHtml(DispatchSolution solution, String filename) {
try {
// 仓库起点
String depot = "\"" + solution.getDepot().getLocation().getX() + ","
+ solution.getDepot().getLocation().getY() + "\"";
// 技术员路线
String lines_ = "[";
for (Technician technician : solution.getTechnicianList()) {
if (technician.getCustomerList().size() > 0) {
lines_ += "\"" + technician.getCustomerList().stream()
.map(c -> c.getLocation().getX() + "," + c.getLocation().getY())
.reduce((a, b) -> a + ";" + b).get() + "\",";
}
}
lines_ += "]";
final String lines = lines_;
// 技术员偏好中心点
String preferredlocation = "\"" + solution.getTechnicianList().stream()
.map(c -> (c.getPreferredlocation() == null ? "" : c.getPreferredlocation().getX()) + ","
+ (c.getPreferredlocation() == null ? "" : c.getPreferredlocation().getY()))
.reduce((a, b) -> a + ";" + b).get() + "\"";
// 技术员名称
String names = "\"" + solution.getTechnicianList().stream()
.map(c -> (c.getPreferredlocation() == null ? "" : c.getPreferredlocation().getX()) + ","
+ (c.getPreferredlocation() == null ? "" : c.getPreferredlocation().getY()) + ","
+ c.getCode())
.reduce((a, b) -> a + ";" + b).get() + "\"";
List<String> dispatchMapLines = IOUtils.readLines(new FileInputStream("data/dispatchMap.html"), "GBK");
dispatchMapLines = dispatchMapLines.stream().map(line -> {
if (StringUtils.startsWith(line, " var depot = ")) {
return " var depot = " + depot;
} else if (StringUtils.startsWith(line, " var preferredlocation = ")) {
return " var preferredlocation = " + preferredlocation;
} else if (StringUtils.startsWith(line, " var lines = ")) {
return " var lines = " + lines;
} else if (StringUtils.startsWith(line, " var names = ")) {
return " var names = " + names;
} else {
return line;
}
}).collect(Collectors.toList());
IOUtils.writeLines(dispatchMapLines, "\r\n", new FileOutputStream(filename + ".html"), "GBK");
System.out.println("output map : " + filename);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 计算方案分数
*
* @param solution
* @return
*/
public static HardSoftLongScore getScore(DispatchSolution solution) {
DefaultSolverFactory<DispatchSolution> solverFactory = getSolverFactory();
ScoreDirectorFactory<DispatchSolution> scoreDirectorFactory = solverFactory.getScoreDirectorFactory();
DroolsConstraintStreamScoreDirector<DispatchSolution, HardSoftLongScore> scoreDirector = (DroolsConstraintStreamScoreDirector<DispatchSolution, HardSoftLongScore>) scoreDirectorFactory
.buildScoreDirector();
// Set the working solution of the ScoreDirector object.
scoreDirector.setWorkingSolution(solution);
// Iterate over the constraints in your problem and call the
// `addConstraintMatch()` method
// for each constraint that is violated by the solution.
// for (Constraint constraint : myConstraints) {
// if (constraint.isViolated(mySolution)) {
// scoreDirector.addConstraintMatch(constraint, constraint.getScore());
// }
// }
// Calculate the score of the solution.
HardSoftLongScore score = (HardSoftLongScore) scoreDirector.calculateScore();
return score;
}
/**
* 移动某个技术员的订单,返回新方案<br>
* FIXME 没有成功
*
* @param solution
* @param source 技术员
* @param sourceIndex 0开始
* @param destinationIndex 0开始
* @return
*/
public static DispatchSolution moveCustomer(DefaultSolverFactory<DispatchSolution> solverFactory,
DispatchSolution solution, Technician source, int sourceIndex, int destinationIndex) {
if (null == solverFactory) {
solverFactory = getSolverFactory();
}
ScoreDirectorFactory<DispatchSolution> scoreDirectorFactory = solverFactory.getScoreDirectorFactory();
DroolsConstraintStreamScoreDirector<DispatchSolution, HardSoftLongScore> scoreDirector = (DroolsConstraintStreamScoreDirector<DispatchSolution, HardSoftLongScore>) scoreDirectorFactory
.buildScoreDirector();
// Make a change to the solution.
// doMove(), doSwap(), or doChangeVariable()
// Create a variable descriptor for the "customerList" list property
ListVariableDescriptor<DispatchSolution> customerDescriptor = (ListVariableDescriptor<DispatchSolution>) scoreDirector
.getSolutionDescriptor().getEntityDescriptorStrict(Technician.class)
.getVariableDescriptor("customerList");
// Create the ListChangeMove
ListChangeMove<DispatchSolution> move = new ListChangeMove<DispatchSolution>(customerDescriptor, source,
sourceIndex, source, destinationIndex);
// apply the move and re-evaluate the score
scoreDirector.doAndProcessMove(move, true);
return scoreDirector.getWorkingSolution();
}
public static DefaultSolverFactory<DispatchSolution> getSolverFactory() {
return getSolverFactory(0, 0);
}
public static DefaultSolverFactory<DispatchSolution> getSolverFactory(long unimprovedSecondsSpentLimit,
long secondsSpentLimit) {
// 创建求解器配置
// 创建 SolverConfig 对象,并设置求解器配置
SolverConfig solverConfig = new SolverConfig();
solverConfig.setSolutionClass(DispatchSolution.class);
solverConfig.withEntityClassList(Arrays.asList(Technician.class, Customer.class));// 这里不能漏掉,否则约束不生效
TerminationConfig terminationConfig = new TerminationConfig();
terminationConfig
.setUnimprovedSecondsSpentLimit(unimprovedSecondsSpentLimit <= 0 ? 5 : unimprovedSecondsSpentLimit);// XX秒没有找到更好方案
terminationConfig.setSecondsSpentLimit(secondsSpentLimit <= 0 ? 60 : secondsSpentLimit);// 总时间不能超过XXs
solverConfig.withTerminationConfig(terminationConfig);
// 约束条件
solverConfig.withConstraintProviderClass(DispatchConstraintProvider.class);
solverConfig.setScoreDirectorFactoryConfig(
new ScoreDirectorFactoryConfig().withConstraintProviderClass(DispatchConstraintProvider.class));
DefaultSolverFactory<DispatchSolution> solverFactory = (DefaultSolverFactory<DispatchSolution>) SolverFactory
.<DispatchSolution>create(solverConfig);
return solverFactory;
}
/**
* 打印详细约束得分
*
* @param solution
* @param solverFactory
*/
public static void explainSolutionConstraintDetail(DispatchSolution solution, DefaultSolverFactory<DispatchSolution> solverFactory) {
// Obtain a ScoreExplanation object for the best solution
// Using score calculation outside the Solver
// https://www.optaplanner.org/docs/optaplanner/latest/score-calculation/score-calculation.html
SolutionManager<DispatchSolution, HardSoftLongScore> scoreManager = SolutionManager.create(solverFactory);
ScoreExplanation<DispatchSolution, HardSoftLongScore> scoreExplanation = scoreManager.explain(solution);
// System.out.println(scoreExplanation.getSummary());
Map<String, ConstraintMatchTotal<HardSoftLongScore>> constraintMatchTotalMap = scoreExplanation
.getConstraintMatchTotalMap();
constraintMatchTotalMap.forEach((key, value) -> {
if (!value.getScore().isFeasible()) {
// 违反硬约束
System.out.printf("%s 匹配%s次 hard得分:%s%n", value.getConstraintName(), value.getConstraintMatchCount(),
value.getScore().hardScore());
} else {
// 软约束
System.out.printf("%s 匹配%s次 soft得分:%s%n", value.getConstraintName(), value.getConstraintMatchCount(),
value.getScore().softScore());
}
value.getConstraintMatchSet().stream().sorted(comparing(ConstraintMatch::getScore))
.forEach(constraintMatch -> {
String text = "";
switch (ConstraintNameEnum.valueOf(value.getConstraintName())) {
case skillMatch:
case customerTimeWindowsMatch:
case dispatchedMatch:
for (Object indictedObject : constraintMatch.getIndictedObjectList()) {
// 违反硬约束对象,根据具体约束返回不同类型对象
if (indictedObject instanceof Customer) {
Customer customer = (Customer) indictedObject;
text += customer.getCode() + ",";
}
}
System.out.printf(" 预约单(%s)违反约束,扣分%s%n", text, constraintMatch.getScore().toShortString());
break;
case totalDistance:
case preferredTotalDistance:
for (Object indictedObject : constraintMatch.getIndictedObjectList()) {
// 违反软约束对象,根据具体约束返回不同类型对象
if (indictedObject instanceof Technician) {
Technician technician = (Technician) indictedObject;
text += technician.getCode() + ",";
}
}
System.out.printf(" 技术员(%s)总路程得分%s%n", text, constraintMatch.getScore().toShortString());
break;
case technicianBalanceSoft:
if (constraintMatch.getScore().softScore() < 0) {
for (Object indictedObject : constraintMatch.getIndictedObjectList()) {
// 违反软约束对象,根据具体约束返回不同类型对象
if (indictedObject instanceof Technician) {
Technician technician = (Technician) indictedObject;
text += technician.getCode() + ",";
}
}
System.out.printf(" 技术员(%s)订单量差距得分%s%n", text,
constraintMatch.getScore().toShortString());
}
break;
default:
break;
}
});
});
}
/**
* 导出json结构
*
* @param solution
* @param fileName
*/
public static void exportSolutionJson(DispatchSolution solution, String fileName) {
// Create a JacksonSolutionFileIO instance.
JacksonSolutionFileIO<DispatchSolution> exporter = new JacksonSolutionFileIO<DispatchSolution>(
DispatchSolution.class);
// Set the output file.
exporter.write(solution, new File(fileName));
}
/**
* 移除hard约束元素订单
*
* @param solution
* @param solverFactory
*/
public static void removeHardConstraintCustomer(DispatchSolution solution,
SolverFactory<DispatchSolution> solverFactory) {
if ("1719308075152764928".equals(solution.getTeamId())) {
System.out.println(solution.getTeamId());
}
SolutionManager<DispatchSolution, HardSoftLongScore> scoreManager = SolutionManager.create(solverFactory);
ScoreExplanation<DispatchSolution, HardSoftLongScore> scoreExplanation = scoreManager.explain(solution);
Map<String, ConstraintMatchTotal<HardSoftLongScore>> constraintMatchTotalMap = scoreExplanation
.getConstraintMatchTotalMap();
constraintMatchTotalMap.forEach((key, value) -> {
if (!value.getScore().isFeasible()) {
// 违反硬约束
value.getConstraintMatchSet().stream().sorted(comparing(ConstraintMatch::getScore))
.forEach(constraintMatch -> {
switch (ConstraintNameEnum.valueOf(value.getConstraintName())) {
case skillMatch:
case customerTimeWindowsMatch:
for (Object indictedObject : constraintMatch.getIndictedObjectList()) {
// 违反硬约束对象,根据具体约束返回不同类型对象
if (indictedObject instanceof Customer) {
Customer customer = (Customer) indictedObject;
// 更新shadow变量
updateShadowVariable(customer);
// 移除技术员
customer.getTechnician().getCustomerList().remove(customer);
solution.getUnDispatchedCustomers().add(customer);
}
}
break;
case technicianTimeWindowsMatch:
for (Object indictedObject : constraintMatch.getIndictedObjectList()) {
// 违反硬约束对象,根据具体约束返回不同类型对象
if (indictedObject instanceof Technician
&& ObjectUtil.isNotEmpty(((Technician) indictedObject).getCustomerList())) {
List<Customer> customerList = new ArrayList<>();
customerList.addAll(((Technician) indictedObject).getCustomerList());
for (Customer customer : customerList) {
if (!customer.isInTechnicianTimeWindows()) {
// 更新shadow变量
updateShadowVariable(customer);
// 移除技术员
customer.getTechnician().getCustomerList().remove(customer);
solution.getUnDispatchedCustomers().add(customer);
}
}
}
}
break;
default:
break;
}
});
}
});
}
private static void updateShadowVariable(Customer sourceCustomer) {
if (sourceCustomer.getTechnician() == null) {
if (sourceCustomer.getArrivalTime() != null) {
sourceCustomer.setArrivalTime(null);
}
return;
}
// 移除当前订单引用
Customer previousCustomer = sourceCustomer.getPreviousCustomer();
Customer nextCustomer = sourceCustomer.getNextCustomer();
if (previousCustomer == null) {
// 当前订单是第一个订单
if (null != nextCustomer) {
nextCustomer.setPreviousCustomer(null);
}
}
if (nextCustomer == null) {
// 当前订单是最后一个订单
if (null != previousCustomer) {
previousCustomer.setNextCustomer(null);
}
}
if (previousCustomer != null && null != nextCustomer) {
previousCustomer.setNextCustomer(nextCustomer);
nextCustomer.setPreviousCustomer(previousCustomer);
}
// 前一个的离开时间
Integer departureTime;
if (previousCustomer == null) {
departureTime = (sourceCustomer.getTechnician().getDepot()).getStartTime();
} else {
departureTime = previousCustomer.getDepartureTime();
}
if (nextCustomer != null) {
// 更新后续订单
Customer shadowCustomer = nextCustomer;
Integer arrivalTime = calculateArrivalTime(shadowCustomer, departureTime);
while (shadowCustomer != null && !Objects.equals(shadowCustomer.getArrivalTime(), arrivalTime)) {
shadowCustomer.setArrivalTime(arrivalTime);
departureTime = shadowCustomer.getDepartureTime();
shadowCustomer = shadowCustomer.getNextCustomer();
arrivalTime = calculateArrivalTime(shadowCustomer, departureTime);
}
}
}
private static Integer calculateArrivalTime(Customer customer, Integer previousDepartureTime) {
if (customer == null || previousDepartureTime == null) {
return null;
}
return previousDepartureTime + customer.getPathTimeFromPreviousStandstill();
}
}
......@@ -3,8 +3,8 @@ server:
dispatch:
cron:
expr: 0 */3 8-23 * * ?
next-day-limit: 20
expr: 0 10 8-23 * * ?
next-day-limit: 2
scheduler:
init-engineer-capacity:
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!