Search code examples
timeoptaplannerplanning

Planning Task's StartTime is Later then EndTime


I am making a time planning for 162 produce tasks by optaPlanner( version 8.9.1.final). My planning wants these tasks to execute on some different produce equipments and execute within one month(between 12/01/2023 and 12/31/2023), here are my Java codes :

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.jeecg.common.aspect.annotation.Dict;
import org.jeecg.common.util.oConvertUtils;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.optaplanner.core.api.domain.entity.PlanningEntity;
import org.optaplanner.core.api.domain.lookup.PlanningId;
import org.optaplanner.core.api.domain.variable.PlanningVariable;
import org.springframework.format.annotation.DateTimeFormat;

import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@PlanningEntity
@Data
@EqualsAndHashCode(of = {"id"})
@TableName("produce_order_task")
public class ProduceOrderTask implements Serializable {

    private static final long serialVersionUID = -480523240175919145L;

    @PlanningId
    @TableId(type = IdType.ASSIGN_ID)
    private String id;

    @PlanningVariable(valueRangeProviderRefs = "allEquipments")
    private ProduceEquipment produceEquipment;

    @PlanningVariable(valueRangeProviderRefs = "taskTimeRange")
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime startDate;

    @PlanningVariable(valueRangeProviderRefs = "taskTimeRange")
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime endDate;

    @PlanningVariable(valueRangeProviderRefs = "allWorkCenters")
    private ProduceEmployee produceEmployee;
    
    private String taskName;

}
import org.apache.commons.lang3.StringUtils;
import org.jeecg.modules.produce.planning.domain.*;
import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore;
import org.optaplanner.core.api.score.stream.Constraint;
import org.optaplanner.core.api.score.stream.ConstraintCollectors;
import org.optaplanner.core.api.score.stream.ConstraintFactory;
import org.optaplanner.core.api.score.stream.ConstraintProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import static org.optaplanner.core.api.score.stream.Joiners.equal;
import static org.optaplanner.core.api.score.stream.Joiners.filtering;

@Component
public class ProduceConstraintProvider implements ConstraintProvider {

    @Override
    public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
        return new Constraint[]{
                mismatchEquipmentSkill(constraintFactory),
                taskStartTimeNotBeforeEndTime(constraintFactory)
        };
    }

    /**
     * A produce task can only execute on a produce equipment whose name contains this task name.
     *
     * @param constraintFactory ConstraintFactory instance
     * @return A Constraint that penalize a solution when a produce task executes on a produce equipment whose name does NOT contains this task name.
     */
    protected Constraint mismatchEquipmentSkill(ConstraintFactory constraintFactory) {
        return constraintFactory
                .from(ProduceOrderTask.class)
                .join(ProduceEquipment.class, equal(ProduceOrderTask::getEquipId, ProduceEquipment::getId))
                .filter((task, equipment) -> !equipment.getSkill().contains(task.getTaskName()))
                .penalize("mismatchEquipmentSkill", HardSoftScore.ofHard(10));
    }

    /**
     * A produce task's start date must be before its end date.
     *
     * @param constraintFactory ConstraintFactory instance
     * @return A Constraint that penalize a solution when a produce task's start date is NOT before its end date.
     */
    protected Constraint taskStartTimeNotBeforeEndTime(ConstraintFactory constraintFactory) {
        return constraintFactory
                .from(ProduceOrderTask.class)
                .filter(task ->
                        task.getStartDate() != null
                        && task.getEndDate() != null
                        && !task.getStartDate().isBefore(task.getEndDate()))
                .penalize("taskStartTimeNotBeforeEndTime", HardSoftScore.ofHard(1000));
    }
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.domain.valuerange.CountableValueRange;
import org.optaplanner.core.api.domain.valuerange.ValueRangeFactory;
import org.optaplanner.core.api.domain.valuerange.ValueRangeProvider;
import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore;
import org.optaplanner.core.api.solver.SolverStatus;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;

@Slf4j
@EqualsAndHashCode(of = {"id"})
@NoArgsConstructor
@AllArgsConstructor
@PlanningSolution
@Data
public class ProducePlanningSolution implements Serializable {

    private static final long serialVersionUID = 3257608858882617194L;

    private String id;

    private LocalDateTime producePlanningStartTime;

    private LocalDateTime producePlanningEndTime;

    @ValueRangeProvider(id = "allEquipments")
    @ProblemFactCollectionProperty
    private List<ProduceEquipment> produceEquipments;

    @ValueRangeProvider(id = "taskTimeRange")
    public CountableValueRange<LocalDateTime> getTaskTimeRange() {
        return ValueRangeFactory.createLocalDateTimeValueRange(
                LocalDateTime.parse(""2023-12-01T08:00:00), LocalDateTime.parse("2023-12-31T23:59:59"), 1, ChronoUnit.Hours);
    }

    @PlanningEntityCollectionProperty
    private List<ProduceOrderTask> produceOrderTasks;

    @PlanningScore
    private HardSoftScore hardSoftScore;
}

But I failed. After the optaplanner having finished, in the best solution I found there are 7 produce tasks whose start date is after end date. I am confused and who can help me ?

I have tried to change optaplanner.solver.termination.spent-limit and optaplanner.solver.termination.spent-limit.unimproved-spent-limit to get more better solution, but it is unuseful. I wonder why these 7 produce tasks' start date and end date are always wrong, and I wonder how can get a correct solution?


Solution

    1. Do you constraints do what you think they do? Write a unit test for each one. See how in the timefold-quickstarts repo.
    2. Is there indeed a better solution? Ask it to run brute force on a very small dataset. If there is, it will find it.
    3. Is there any score corruption? Run once temporarly with environmentMode FULL_ASSERT (this will slow down solving immensity, do not commit it).