You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

OptaPlanner约束流Collector实现指导:同位素生产约束开发

问题:OptaPlanner约束流中自定义ConstraintCollector实现难题

我查阅OptaPlanner约束流相关文档后,仍无法理解ConstraintCollector的输入输出要求,无法写出可在constraint stream的groupBy()中使用的ConstraintCollector。

我的业务场景是将Employee分配至Shift:

  • Employee关联Tracer;
  • Tracer关联Isotope,且包含batchesPerProduction属性;
  • Isotope关联Contract,后者限制该同位素每日最大生产次数;
  • 每次同位素生产可产出多批次Tracer,每个Shift消耗1批次Tracer。

需要实现的约束逻辑:按日期统计各Tracer的批次总量,计算所需同位素生产次数(生产次数 = 当日Shift数量 / Tracer.getBatchesPerProduction(),向上取整),确保总生产次数不超过合同日上限。

我梳理的实现步骤:

  1. 遍历所有Tracer;
  2. 关联对应Employee的Shift;
  3. 按Tracer、日期分组,聚合计算同位素生产次数;
  4. 按Isotope、日期分组,聚合生产次数总和。

我尝试了BiConstraintCollector和TriConstraintCollector两种写法均未成功,也不清楚如何在groupBy()中调用。同时对Java和OptaPlanner的Stream接口、ConstraintCollectors.compose方法理解不足。

业务模型代码

@PlanningEntity
public class Shift {

    @PlanningVariable
    private Employee employee = null;

    private Isotope isotope;

    private OffsetDateTime startDateTime;
    private OffsetDateTime endDateTime;

}

public class Employee {

    private Tracer tracer;

}

public class Tracer {

    private Isotope isotope;

    private Long batchesPerProduction;

}


public class Isotope {

    private Contract contract;

}

public class Contract {

    private Integer maximumProductionsPerDay;

}

尝试的ConstraintCollector代码

public static <A,B> BiConstraintCollector<Tracer, LocalDate, ?, Long>
        batchesToProductions(ToLongFunction<A,B> groupValueMapping) {
            return compose(countLongBi(groupValueMapping), (tracer, day, countLongBi) -> {
                    return (long) Math.ceil(countLongBi / tracer.getBatchesPerProduction());
            });
    }
public static <A,B,C> TriConstraintCollector<Tracer, LocalDate, Long, ?, Long>
        batchesToProductions(ToLongFunction<A,B,C> groupValueMapping) {
            return compose((tracer, day, shiftsCount) -> {
                    return (long) Math.ceil(shiftsCount / tracer.getBatchesPerProduction());
            });
    }

尝试的约束流代码

Constraint dailyProductionsMustNotExceedContractMaximum(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(Tracer.class)
                .join(Shift.class,
                equal(Function.identity(), Shift::getTracerForEmployee))
                .groupBy(
                        (tracer,shift) -> tracer,
                        (tracer,shift) -> shift.getStartDateTime().toLocalDate(),
                        (tracer,shift) -> batchesToProductions(tracer,shift))

                .groupBy(
                        (tracer, day, productions) -> tracer.getIsotope(),
                        (tracer, day, productions) -> day,
                        (tracer, day, productions) -> sum(productions)
                )
                .filter((isotope, day, productions) -> productions > isotope.getContract().getMaximumShiftsPerDay())
                .penalizeConfigurableLong(CONSTRAINT_DAILY_PRODUCTIONS_MUST_NOT_EXCEED_CONTRACT_MAXIMUM,
                        (isotope, day, productions) -> productions
                                - isotope.getContract().getMaximumShiftsPerDay());
    }

解决方案

1. 核心思路:优先组合内置Collector

ConstraintCollector的本质是分组后的聚合逻辑,大部分场景无需自定义,通过组合OptaPlanner内置的Collector即可实现需求,避免复杂的类型参数处理。

2. 修正后的约束流实现

调整遍历起点为规划实体Shift,关联关系更直接,逻辑更清晰:

Constraint dailyProductionsMustNotExceedContractMaximum(ConstraintFactory constraintFactory) {
    return constraintFactory.forEach(Shift.class)
            // 关联到Employee,再关联到对应的Tracer
            .join(Employee.class, equal(Shift::getEmployee, Function.identity()))
            .join(Tracer.class, equal(Employee::getTracer, Function.identity()))
            // 第一步分组:按Tracer+日期统计当日Shift数量
            .groupBy(
                    (shift, employee, tracer) -> tracer,
                    (shift, employee, tracer) -> shift.getStartDateTime().toLocalDate(),
                    ConstraintCollectors.countLong()
            )
            // 计算当前Tracer当日所需的生产次数(注意转浮点避免整数除法精度丢失)
            .map((tracer, day, shiftCount) -> {
                double productionCount = Math.ceil((double) shiftCount / tracer.getBatchesPerProduction());
                return new TracerProduction(tracer.getIsotope(), day, (long) productionCount);
            })
            // 第二步分组:按Isotope+日期汇总生产次数
            .groupBy(
                    TracerProduction::getIsotope,
                    TracerProduction::getDay,
                    ConstraintCollectors.sumLong(TracerProduction::getProductionCount)
            )
            // 检查是否超出合同上限
            .filter((isotope, day, totalProductions) -> 
                    totalProductions > isotope.getContract().getMaximumProductionsPerDay())
            // 按超出量施加惩罚
            .penalizeConfigurableLong("每日生产次数不得超过合同上限",
                    (isotope, day, totalProductions) -> 
                            totalProductions - isotope.getContract().getMaximumProductionsPerDay());
}

// 辅助类:传递中间计算结果
private static class TracerProduction {
    private final Isotope isotope;
    private final LocalDate day;
    private final long productionCount;

    public TracerProduction(Isotope isotope, LocalDate day, long productionCount) {
        this.isotope = isotope;
        this.day = day;
        this.productionCount = productionCount;
    }

    public Isotope getIsotope() { return isotope; }
    public LocalDate getDay() { return day; }
    public long getProductionCount() { return productionCount; }
}

3. 自定义可复用ConstraintCollector(可选)

如果需要封装成通用Collector,可基于内置Collector组合实现:

public static ConstraintCollector<Shift, ?, Map<TracerDayKey, Long>> calculateProductionsPerTracerDay() {
    return ConstraintCollectors.compose(
            // 第一步:收集Shift对应的Tracer+日期,统计Shift数量
            ConstraintCollectors.mapping(
                    shift -> new TracerDayKey(shift.getEmployee().getTracer(), shift.getStartDateTime().toLocalDate()),
                    ConstraintCollectors.countLong()
            ),
            // 第二步:转换为生产次数
            countMap -> countMap.entrySet().stream()
                    .collect(Collectors.toMap(
                            Map.Entry::getKey,
                            entry -> (long) Math.ceil((double) entry.getValue() / entry.getKey().getTracer().getBatchesPerProduction())
                    ))
    );
}

// 分组键辅助类:必须重写equals和hashCode保证分组正确性
private static class TracerDayKey {
    private final Tracer tracer;
    private final LocalDate day;

    public TracerDayKey(Tracer tracer, LocalDate day) {
        this.tracer = tracer;
        this.day = day;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        TracerDayKey that = (TracerDayKey) o;
        return Objects.equals(tracer, that.tracer) && Objects.equals(day, that.day);
    }

    @Override
    public int hashCode() {
        return Objects.hash(tracer, day);
    }

    public Tracer getTracer() { return tracer; }
    public LocalDate getDay() { return day; }
}

关键注意事项

  • 整数除法必须转换为浮点类型后计算,否则Math.ceil会失效(例如5/2整数除法结果为2,转浮点后5.0/2=2.5,向上取整为3);
  • 自定义分组键必须重写equals和hashCode,否则分组结果会出现错误;
  • 优先使用内置Collector组合,比自定义Collector更简洁、可靠;
  • 约束流起点尽量选择规划实体(如Shift),关联关系更清晰。

内容的提问来源于stack exchange,提问作者nogretz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 11:31:01