APS开源源码解读: 排程工具 optaplanner

抽象层次非常好,广义优化工具。用于排产没有复杂的落地示例

安装

配置

  • xml配置
  • solutionClass
  • entityClass
  • constraintProviderClass
  • termination: 5min
  • constructionHeuristic: FIRST_FIT
  • localSearch:

也即是说,先定义对象"entityClass", 转化为约束"constraintProviderClass",然后运用 constructionHeuristic + localSearch的方式进行求解

其中,一个整体的任务叫做project, 资源有可再生,非可再生。

工序叫做Job,job跟着若干project。每个工序有自己的资源,ResourceRequirement. 执行模式Execution Mode. 分配allocation.

  • resourceRequirement和allocation都要设置execution mode
  • 每个工序JOb, 有自己的resourceRequirement, executation mode, allocation

基本概念

约束

比如排产中的工序依赖关系

import org.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore;
import org.timefold.solver.core.api.score.stream.ConstraintProvider;
import org.timefold.solver.core.api.score.stream.Constraint;
import org.timefold.solver.core.api.score.stream.ConstraintStream;
import org.timefold.solver.core.api.score.stream.Joiners;

public class JobShopConstraintProvider implements ConstraintProvider {

    @Override
    public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
        return new Constraint[] {
            // Ensure operations follow the sequence within each job
            constraintFactory.from(Operation.class)
                .join(Operation.class, Joiners.filteringEach(otherOp -> 
                    otherOp.getJob().equals(op.getJob()) && 
                    otherOp.getSequence() == op.getSequence() + 1
                ))
                .penalize("Operations must follow sequence",
                    HardSoftScore.ONE_HARD,
                    (op, otherOp) -> 1),
            
            // Ensure machine constraints are respected
            constraintFactory.from(Operation.class)
                .join(Operation.class, Joiners.filteringEach((op1, op2) ->
                    op1.getMachine().equals(op2.getMachine()) &&
                    op1.getEndTime() > op2.getStartTime() &&
                    op1.getStartTime() < op2.getEndTime() &&
                    !op1.equals(op2))
                )
                .penalize("Machine cannot process two operations at once",
                    HardSoftScore.ONE_HARD,
                    (op1, op2) -> 1)
        };
    }
}

官方示例

入口在APP的main

public static void main(String[] args) {
        prepareSwingEnvironment();
        new ProjectJobSchedulingApp().init();
    }

init

public void init() {
        init(null, true);
    }

    public void init(Component centerForComponent, boolean exitOnClose) {
        solutionBusiness = createSolutionBusiness();
        solverAndPersistenceFrame = new SolverAndPersistenceFrame<>(solutionBusiness, createSolutionPanel(),
                createExtraActions());
        solverAndPersistenceFrame
                .setDefaultCloseOperation(exitOnClose ? WindowConstants.EXIT_ON_CLOSE : WindowConstants.DISPOSE_ON_CLOSE);
        solverAndPersistenceFrame.init(centerForComponent);
        solverAndPersistenceFrame.setVisible(true);
    }

其中,solution business

  • SolverFactory.createFromXmlResource建立了solver

    public SolutionBusiness<Solution_, ?> createSolutionBusiness() {
    SolutionBusiness<Solution_, ?> solutionBusiness = new SolutionBusiness<>(this,
    SolverFactory.createFromXmlResource(solverConfigResource));
    solutionBusiness.setDataDir(determineDataDir(dataDirName));
    solutionBusiness.setSolutionFileIO(createSolutionFileIO());
    solutionBusiness.setImporters(createSolutionImporters());
    solutionBusiness.setExporters(createSolutionExporters());
    solutionBusiness.updateDataDirs();
    return solutionBusiness;
    }

在APP类继承的solution中,示例采用的是schedule,也就是planningsolution,作为问题和排产结果

package org.optaplanner.examples.projectjobscheduling.domain;

import java.util.List;

import org.optaplanner.core.api.domain.solution.PlanningEntityCollectionProperty;
import org.optaplanner.core.api.domain.solution.PlanningScore;
import org.optaplanner.core.api.domain.solution.PlanningSolution;
import org.optaplanner.core.api.domain.solution.ProblemFactCollectionProperty;
import org.optaplanner.core.api.score.buildin.hardmediumsoft.HardMediumSoftScore;
import org.optaplanner.examples.common.domain.AbstractPersistable;
import org.optaplanner.examples.projectjobscheduling.domain.resource.Resource;

@PlanningSolution
public class Schedule extends AbstractPersistable {

    private List<Project> projectList;
    private List<Job> jobList;
    private List<ExecutionMode> executionModeList;
    private List<Resource> resourceList;
    private List<ResourceRequirement> resourceRequirementList;

    private List<Allocation> allocationList;

    private HardMediumSoftScore score;

    public Schedule() {
    }

    public Schedule(long id) {
        super(id);
    }

    @ProblemFactCollectionProperty
    public List<Project> getProjectList() {
        return projectList;
    }

    public void setProjectList(List<Project> projectList) {
        this.projectList = projectList;
    }

    @ProblemFactCollectionProperty
    public List<Job> getJobList() {
        return jobList;
    }

    public void setJobList(List<Job> jobList) {
        this.jobList = jobList;
    }

    @ProblemFactCollectionProperty
    public List<ExecutionMode> getExecutionModeList() {
        return executionModeList;
    }

    public void setExecutionModeList(List<ExecutionMode> executionModeList) {
        this.executionModeList = executionModeList;
    }

    @ProblemFactCollectionProperty
    public List<Resource> getResourceList() {
        return resourceList;
    }

    public void setResourceList(List<Resource> resourceList) {
        this.resourceList = resourceList;
    }

    @ProblemFactCollectionProperty
    public List<ResourceRequirement> getResourceRequirementList() {
        return resourceRequirementList;
    }

    public void setResourceRequirementList(List<ResourceRequirement> resourceRequirementList) {
        this.resourceRequirementList = resourceRequirementList;
    }

    @PlanningEntityCollectionProperty
    public List<Allocation> getAllocationList() {
        return allocationList;
    }

    public void setAllocationList(List<Allocation> allocationList) {
        this.allocationList = allocationList;
    }

    @PlanningScore
    public HardMediumSoftScore getScore() {
        return score;
    }

    public void setScore(HardMediumSoftScore score) {
        this.score = score;
    }

    // ************************************************************************
    // Complex methods
    // ************************************************************************

}

Timefold 示例

Solver job接受到problem,开始run

@Deprecated(forRemoval = true, since = "1.6.0")
    default SolverJob<Solution_, ProblemId_> solve(ProblemId_ problemId,
            Solution_ problem, Consumer<? super Solution_> finalBestSolutionConsumer,
            BiConsumer<? super ProblemId_, ? super Throwable> exceptionHandler) {
        SolverJobBuilder<Solution_, ProblemId_> builder = solveBuilder()
                .withProblemId(problemId)
                .withProblem(problem);
        if (finalBestSolutionConsumer != null) {
            builder.withFinalBestSolutionConsumer(finalBestSolutionConsumer);
        }
        if (exceptionHandler != null) {
            builder.withExceptionHandler(exceptionHandler);
        }
        return builder.run();
    }

solverStatus = SolverStatus.SOLVING_ACTIVE;
            // Create the consumer thread pool only when this solver job is active.
            consumerSupport = new ConsumerSupport<>(getProblemId(), bestSolutionConsumer, finalBestSolutionConsumer,
                    firstInitializedSolutionConsumer, exceptionHandler, bestSolutionHolder);

            Solution_ problem = problemFinder.apply(problemId);
            // add a phase lifecycle listener that unlock the solver status lock when solving started
            solver.addPhaseLifecycleListener(new UnlockLockPhaseLifecycleListener());
            // add a phase lifecycle listener that consumes the first initialized solution
            solver.addPhaseLifecycleListener(new FirstInitializedSolutionPhaseLifecycleListener(consumerSupport));
            solver.addEventListener(this::onBestSolutionChangedEvent);
            final Solution_ finalBestSolution = solver.solve(problem);
            consumerSupport.consumeFinalBestSolution(finalBestSolution);
            return finalBestSolution;

理解

  • https://www.optaplanner.org/docs/optaplanner/latest/shadow-variable/shadow-variable.html

  • build_solver/ default_solver_factory

      public Solver<Solution_> buildSolver(SolverConfigOverride<Solution_> configOverride) {
          Objects.requireNonNull(configOverride, "Invalid configOverride (null) given to SolverFactory.");
          var isDaemon = Objects.requireNonNullElse(solverConfig.getDaemon(), false);
    
          var solverScope = new SolverScope<Solution_>();
          var monitoringConfig = solverConfig.determineMetricConfig();
          solverScope.setMonitoringTags(Tags.empty());
          var metricsRequiringConstraintMatchSet = Collections.<SolverMetric> emptyList();
          if (!monitoringConfig.getSolverMetricList().isEmpty()) {
              solverScope.setSolverMetricSet(EnumSet.copyOf(monitoringConfig.getSolverMetricList()));
              metricsRequiringConstraintMatchSet = solverScope.getSolverMetricSet().stream()
                      .filter(SolverMetric::isMetricConstraintMatchBased)
                      .filter(solverScope::isMetricEnabled)
                      .toList();
          } else {
              solverScope.setSolverMetricSet(EnumSet.noneOf(SolverMetric.class));
          }
    
          var environmentMode = solverConfig.determineEnvironmentMode();
          var constraintMatchEnabled = !metricsRequiringConstraintMatchSet.isEmpty() || environmentMode.isAsserted();
          if (constraintMatchEnabled && !environmentMode.isAsserted()) {
              LOGGER.info(
                      "Enabling constraint matching as required by the enabled metrics ({}). This will impact solver performance.",
                      metricsRequiringConstraintMatchSet);
          }
    
          var innerScoreDirector = scoreDirectorFactory.buildScoreDirector(true, constraintMatchEnabled);
          solverScope.setScoreDirector(innerScoreDirector);
          solverScope.setProblemChangeDirector(new DefaultProblemChangeDirector<>(innerScoreDirector));
    
          var moveThreadCount = resolveMoveThreadCount(true);
          var bestSolutionRecaller = BestSolutionRecallerFactory.create().<Solution_> buildBestSolutionRecaller(environmentMode);
          var randomFactory = buildRandomFactory(environmentMode);
    
          var configPolicy = new HeuristicConfigPolicy.Builder<>(
                  environmentMode,
                  moveThreadCount,
                  solverConfig.getMoveThreadBufferSize(),
                  solverConfig.getThreadFactoryClass(),
                  solverConfig.getNearbyDistanceMeterClass(),
                  randomFactory.createRandom(),
                  scoreDirectorFactory.getInitializingScoreTrend(),
                  solutionDescriptor,
                  ClassInstanceCache.create()).build();
          var basicPlumbingTermination = new BasicPlumbingTermination<Solution_>(isDaemon);
          var termination = buildTerminationConfig(basicPlumbingTermination, configPolicy, configOverride);
          var phaseList = buildPhaseList(configPolicy, bestSolutionRecaller, termination);
    
          return new DefaultSolver<>(environmentMode, randomFactory, bestSolutionRecaller, basicPlumbingTermination,
                  termination, phaseList, solverScope,
                  moveThreadCount == null ? SolverConfig.MOVE_THREAD_COUNT_NONE : Integer.toString(moveThreadCount));
      }
    

solver的主流程

@Override
    public final Solution_ solve(Solution_ problem) {
        if (problem == null) {
            throw new IllegalArgumentException("The problem (" + problem + ") must not be null.");
        }

        // No tags for these metrics; they are global
        LongTaskTimer solveLengthTimer = Metrics.more().longTaskTimer(SolverMetric.SOLVE_DURATION.getMeterId());
        Counter errorCounter = Metrics.counter(SolverMetric.ERROR_COUNT.getMeterId());

        solverScope.setBestSolution(problem);
        solverScope.setSolver(this);
        outerSolvingStarted(solverScope);
        boolean restartSolver = true;
        while (restartSolver) {
            LongTaskTimer.Sample sample = solveLengthTimer.start();
            try {
                // solvingStarted will call registerSolverSpecificMetrics(), since
                // the solverScope need to be fully initialized to calculate the
                // problem's scale metrics
                solvingStarted(solverScope);
                runPhases(solverScope);
                solvingEnded(solverScope);
            } catch (Exception e) {
                errorCounter.increment();
                solvingError(solverScope, e);
                throw e;
            } finally {
                sample.stop();
                unregisterSolverSpecificMetrics();
            }
            restartSolver = checkProblemFactChanges();
        }
        outerSolvingEnded(solverScope);
        return solverScope.getBestSolution();
    }
  • run_phase /abstract_solver

    protected void runPhases(SolverScope<Solution_> solverScope) {
    if (!solverScope.getSolutionDescriptor().hasMovableEntities(solverScope.getScoreDirector())) {
    logger.info("Skipped all phases ({}): out of {} planning entities, none are movable (non-pinned).",
    phaseList.size(), solverScope.getWorkingEntityCount());
    return;
    }
    Iterator<Phase<Solution_>> it = phaseList.iterator();
    while (!solverTermination.isSolverTerminated(solverScope) && it.hasNext()) {
    Phase<Solution_> phase = it.next();
    phase.solve(solverScope);
    // If there is a next phase, it starts from the best solution, which might differ from the working solution.
    // If there isn't, no need to planning clone the best solution to the working solution.
    if (it.hasNext()) {
    solverScope.setWorkingSolutionFromBestSolution();
    }
    }
    }

  • solver外面的phase, PhaseFactory

  • dostep

    局部搜索在当前解上尝试多个移动,并选择最佳的被接受的移动作为这一步。A step is the winning Move。在每一步,它尝试所有选定的移动,除非是选定的step,否则它不会进一步研究那个解。这就是局部搜索具有很高可扩展性的原因之一。

    private void doStep(CustomStepScope<Solution_> stepScope, CustomPhaseCommand<Solution_> customPhaseCommand) {
    InnerScoreDirector<Solution_, ?> scoreDirector = stepScope.getScoreDirector();
    customPhaseCommand.changeWorkingSolution(scoreDirector);
    calculateWorkingStepScore(stepScope, customPhaseCommand);
    solver.getBestSolutionRecaller().processWorkingSolutionDuringStep(stepScope);
    }

  • 决定下一步

    • A MoveSelector which selects the possible moves of the current solution. See the chapter move and neighborhood selection.

    • An Acceptor which filters out unacceptable moves.

    • A Forager which gathers accepted moves and picks the next step from them.

      <localSearch> <unionMoveSelector> ... </unionMoveSelector> <acceptor> ... </acceptor> <forager> ... </forager> </localSearch>

从底向上看,理解可能的move。如果是entity+value组合,或者是entity和entity进行新的组合。也许这就是叫做组合优化的原因?

相关推荐
lsjweiyi7 小时前
极简AI工具箱网站开源啦!
opencv·开源·微信支付·支付宝支付·百度ai·极简ai工具箱·ai图像处理
开源社8 小时前
一场开源视角的AI会议即将在南京举办
人工智能·开源
FreeIPCC8 小时前
谈一下开源生态对 AI人工智能大模型的促进作用
大数据·人工智能·机器人·开源
海害嗨8 小时前
阿里巴巴官方「SpringCloudAlibaba全彩学习手册」限时开源!
学习·开源
生命是有光的8 小时前
【开源风云】从若依系列脚手架汲取编程之道(八)
开源
HuggingFace10 小时前
Halo 正式开源: 使用可穿戴设备进行开源健康追踪
开源·健康追踪
时光追逐者14 小时前
.NET 9 中 LINQ 新增功能实操
开发语言·开源·c#·.net·.netcore·linq·微软技术
檀越剑指大厂16 小时前
Linux本地部署开源项目OpenHands基于AI的软件开发代理平台及公网访问
linux·人工智能·开源
胜天半子_王二_王半仙1 天前
c++源码阅读__ThreadPool__正文阅读
开发语言·c++·开源
FIT2CLOUD飞致云1 天前
案例研究|阿特斯的JumpServer分布式部署和多组织管理实践
运维·开源·堡垒机·jumpserver