Skip to content

Commit 2cb22ec

Browse files
authored
chore: add mixed model (#823)
This PR updates the food packaging model to utilize the mixed model. The proposed changes introduce a new planning entity, `operator`, and update the entity, `line`, to use it. The current implementation already defines an operator field, which consists of static input data, and it has been converted into a basic variable.
1 parent 98ccd30 commit 2cb22ec

10 files changed

Lines changed: 320 additions & 108 deletions

File tree

java/food-packaging/src/main/java/org/acme/foodpackaging/bootstrap/DemoDataGenerator.java

Lines changed: 61 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
package org.acme.foodpackaging.bootstrap;
22

3+
import io.quarkus.runtime.StartupEvent;
4+
import jakarta.enterprise.context.ApplicationScoped;
5+
import jakarta.enterprise.event.Observes;
6+
import jakarta.transaction.Transactional;
7+
import org.acme.foodpackaging.domain.Job;
8+
import org.acme.foodpackaging.domain.Line;
9+
import org.acme.foodpackaging.domain.Operator;
10+
import org.acme.foodpackaging.domain.PackagingSchedule;
11+
import org.acme.foodpackaging.domain.Product;
12+
import org.acme.foodpackaging.domain.WorkCalendar;
13+
import org.acme.foodpackaging.persistence.PackagingScheduleRepository;
14+
import org.eclipse.microprofile.config.inject.ConfigProperty;
15+
316
import java.time.DayOfWeek;
417
import java.time.Duration;
518
import java.time.LocalDate;
@@ -10,77 +23,65 @@
1023
import java.util.Comparator;
1124
import java.util.HashMap;
1225
import java.util.List;
13-
import java.util.Map;
1426
import java.util.Random;
1527
import java.util.Set;
1628

17-
import io.quarkus.runtime.StartupEvent;
18-
import jakarta.enterprise.context.ApplicationScoped;
19-
import jakarta.enterprise.event.Observes;
20-
import jakarta.inject.Inject;
21-
import jakarta.transaction.Transactional;
22-
import org.acme.foodpackaging.domain.Job;
23-
import org.acme.foodpackaging.domain.Line;
24-
import org.acme.foodpackaging.domain.PackagingSchedule;
25-
import org.acme.foodpackaging.domain.Product;
26-
import org.acme.foodpackaging.domain.WorkCalendar;
27-
import org.acme.foodpackaging.persistence.PackagingScheduleRepository;
28-
import org.eclipse.microprofile.config.inject.ConfigProperty;
29-
3029
@ApplicationScoped
3130
public class DemoDataGenerator {
3231

33-
@Inject
34-
PackagingScheduleRepository repository;
32+
private final PackagingScheduleRepository repository;
3533

3634
@ConfigProperty(name = "demo-data.line-count", defaultValue = "5")
3735
int lineCount;
3836
@ConfigProperty(name = "demo-data.job-count", defaultValue = "100")
3937
int jobCount;
4038

39+
public DemoDataGenerator(PackagingScheduleRepository repository) {
40+
this.repository = repository;
41+
}
42+
4143
@Transactional
4244
public void generateDemoData(@Observes StartupEvent startupEvent) {
43-
int noCleaningMinutes = 10;
44-
int cleaningMinutesMinimum = 30;
45-
int cleaningMinutesMaximum = 60;
46-
int jobDurationMinutesMinimum = 120;
47-
int jobDurationMinutesMaximum = 300;
48-
int averageCleaningAndJobDurationMinutes =
45+
var noCleaningMinutes = 10;
46+
var cleaningMinutesMinimum = 30;
47+
var cleaningMinutesMaximum = 60;
48+
var jobDurationMinutesMinimum = 120;
49+
var jobDurationMinutesMaximum = 300;
50+
var averageCleaningAndJobDurationMinutes =
4951
(2 * noCleaningMinutes + cleaningMinutesMinimum + cleaningMinutesMaximum) / 4
50-
+ (jobDurationMinutesMinimum + jobDurationMinutesMaximum) / 2;
52+
+ (jobDurationMinutesMinimum + jobDurationMinutesMaximum) / 2;
5153

52-
final LocalDate START_DATE = LocalDate.now().with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));
53-
final LocalDateTime START_DATE_TIME = LocalDateTime.of(START_DATE, LocalTime.MIDNIGHT);
54-
final LocalDate END_DATE = START_DATE.plusWeeks(2);
55-
final LocalDateTime END_DATE_TIME = LocalDateTime.of(END_DATE, LocalTime.MIDNIGHT);
54+
final var START_DATE = LocalDate.now().with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));
55+
final var START_DATE_TIME = LocalDateTime.of(START_DATE, LocalTime.MIDNIGHT);
56+
final var END_DATE = START_DATE.plusWeeks(2);
5657

57-
Random random = new Random(37);
58-
PackagingSchedule solution = new PackagingSchedule();
58+
var random = new Random(37);
59+
var solution = new PackagingSchedule();
5960

6061
solution.setWorkCalendar(new WorkCalendar(START_DATE, END_DATE));
6162

62-
Map<Product, Set<String>> ingredientMap = new HashMap<>(INGREDIENT_LIST.size() * PRODUCT_VARIATION_LIST.size() * 3);
63-
long productId = 0;
64-
for (int i = 0; i < INGREDIENT_LIST.size(); i++) {
65-
String ingredient = INGREDIENT_LIST.get(i);
66-
int r = random.nextInt(INGREDIENT_LIST.size() - 4);
67-
String ingredientA = INGREDIENT_LIST.get((i + r + 1) % INGREDIENT_LIST.size());
68-
String ingredientB = INGREDIENT_LIST.get((i + r + 2) % INGREDIENT_LIST.size());
69-
String ingredientC = INGREDIENT_LIST.get((i + r + 3) % INGREDIENT_LIST.size());
70-
for (String productVariation : PRODUCT_VARIATION_LIST) {
71-
ingredientMap.put(new Product(Long.toString(productId++), ingredient + " " + productVariation), Set.of(ingredient));
63+
var ingredientMap = new HashMap<Product, Set<String>>(INGREDIENT_LIST.size() * PRODUCT_VARIATION_LIST.size() * 3);
64+
var productId = 0L;
65+
for (var i = 0; i < INGREDIENT_LIST.size(); i++) {
66+
var ingredient = INGREDIENT_LIST.get(i);
67+
var r = random.nextInt(INGREDIENT_LIST.size() - 4);
68+
var ingredientA = INGREDIENT_LIST.get((i + r + 1) % INGREDIENT_LIST.size());
69+
var ingredientB = INGREDIENT_LIST.get((i + r + 2) % INGREDIENT_LIST.size());
70+
var ingredientC = INGREDIENT_LIST.get((i + r + 3) % INGREDIENT_LIST.size());
71+
for (var productVariation : PRODUCT_VARIATION_LIST) {
72+
ingredientMap.put(new Product(Long.toString(productId++), ingredient + " " + productVariation), java.util.Set.of(ingredient));
7273
}
73-
ingredientMap.put(new Product(Long.toString(productId++), ingredient + " and " + ingredientA + " " + PRODUCT_VARIATION_LIST.get(1)), Set.of(ingredient, ingredientA));
74-
ingredientMap.put(new Product(Long.toString(productId++), ingredient + " and " + ingredientB + " " + PRODUCT_VARIATION_LIST.get(2)), Set.of(ingredient, ingredientB));
75-
ingredientMap.put(new Product(Long.toString(productId++), ingredient + ", " + ingredientA + " and " + ingredientC + " " + PRODUCT_VARIATION_LIST.get(1)), Set.of(ingredient, ingredientA, ingredientC));
74+
ingredientMap.put(new Product(Long.toString(productId++), ingredient + " and " + ingredientA + " " + PRODUCT_VARIATION_LIST.get(1)), java.util.Set.of(ingredient, ingredientA));
75+
ingredientMap.put(new Product(Long.toString(productId++), ingredient + " and " + ingredientB + " " + PRODUCT_VARIATION_LIST.get(2)), java.util.Set.of(ingredient, ingredientB));
76+
ingredientMap.put(new Product(Long.toString(productId++), ingredient + ", " + ingredientA + " and " + ingredientC + " " + PRODUCT_VARIATION_LIST.get(1)), java.util.Set.of(ingredient, ingredientA, ingredientC));
7677
}
77-
List<Product> products = new ArrayList<>(ingredientMap.keySet());
78-
for (Product product : products) {
79-
Map<Product, Duration> cleaningDurationMap = new HashMap<>(products.size());
80-
Set<String> ingredients = ingredientMap.get(product);
81-
for (Product previousProduct : products) {
82-
boolean noCleaning = ingredients.containsAll(ingredientMap.get(previousProduct));
83-
Duration cleaningDuration = Duration.ofMinutes(product == previousProduct ? 0
78+
var products = new ArrayList<>(ingredientMap.keySet().stream().sorted(Comparator.comparing(Product::getId)).toList());
79+
for (var product : products) {
80+
var cleaningDurationMap = new HashMap<Product, Duration>(products.size());
81+
var ingredients = ingredientMap.get(product);
82+
for (var previousProduct : products) {
83+
var noCleaning = ingredients.containsAll(ingredientMap.get(previousProduct));
84+
var cleaningDuration = Duration.ofMinutes(product == previousProduct ? 0
8485
: noCleaning ? noCleaningMinutes
8586
: cleaningMinutesMinimum + random.nextInt(cleaningMinutesMaximum - cleaningMinutesMinimum));
8687
cleaningDurationMap.put(previousProduct, cleaningDuration);
@@ -89,24 +90,25 @@ public void generateDemoData(@Observes StartupEvent startupEvent) {
8990
}
9091
solution.setProducts(products);
9192

92-
List<Line> lines = new ArrayList<>(lineCount);
93-
for (int i = 0; i < lineCount; i++) {
94-
String name = "Line " + (i + 1);
95-
String operator = "Operator " + ((char) ('A' + (i / 2)));
96-
lines.add(new Line(Integer.toString(i), name, operator, START_DATE_TIME));
93+
var lines = new ArrayList<Line>(lineCount);
94+
var operators = new ArrayList<Operator>(lineCount);
95+
for (var i = 0; i < lineCount; i++) {
96+
lines.add(new Line(Integer.toString(i), "Line " + (i + 1), START_DATE_TIME));
97+
operators.add(new Operator("Operator " + (i + 1)));
9798
}
9899
solution.setLines(lines);
100+
solution.setOperators(operators);
99101

100-
List<Job> jobs = new ArrayList<>(jobCount);
101-
for (int i = 0; i < jobCount; i++) {
102+
var jobs = new ArrayList<Job>(jobCount);
103+
for (var i = 0; i < jobCount; i++) {
102104
Product product = products.get(random.nextInt(products.size()));
103105
String name = product.getName();
104106
Duration duration = Duration.ofMinutes(jobDurationMinutesMinimum
105-
+ random.nextInt(jobDurationMinutesMaximum - jobDurationMinutesMinimum));
107+
+ random.nextLong((long) jobDurationMinutesMaximum - jobDurationMinutesMinimum));
106108
int targetDayIndex = (i / lineCount) * averageCleaningAndJobDurationMinutes / (24 * 60);
107109
LocalDateTime minStartTime = START_DATE.plusDays(random.nextInt(Math.max(1, targetDayIndex - 2))).atTime(LocalTime.MIDNIGHT);
108-
LocalDateTime idealEndTime = START_DATE.plusDays(targetDayIndex + random.nextInt(3)).atTime(16, 0);
109-
LocalDateTime maxEndTime = idealEndTime.plusDays(1 + random.nextInt(3));
110+
LocalDateTime idealEndTime = START_DATE.plusDays(targetDayIndex + random.nextLong(3)).atTime(16, 0);
111+
LocalDateTime maxEndTime = idealEndTime.plusDays(1 + random.nextLong(3));
110112
jobs.add(new Job(Integer.toString(i), name, product, duration, minStartTime, idealEndTime, maxEndTime, 1, false));
111113
}
112114
jobs.sort(Comparator.comparing(Job::getName));

java/food-packaging/src/main/java/org/acme/foodpackaging/domain/Job.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
package org.acme.foodpackaging.domain;
22

3-
import java.time.Duration;
4-
import java.time.LocalDateTime;
5-
63
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
74
import ai.timefold.solver.core.api.domain.entity.PlanningPin;
85
import ai.timefold.solver.core.api.domain.lookup.PlanningId;
96
import ai.timefold.solver.core.api.domain.variable.CascadingUpdateShadowVariable;
107
import ai.timefold.solver.core.api.domain.variable.InverseRelationShadowVariable;
118
import ai.timefold.solver.core.api.domain.variable.NextElementShadowVariable;
129
import ai.timefold.solver.core.api.domain.variable.PreviousElementShadowVariable;
13-
10+
import ai.timefold.solver.core.api.domain.variable.ShadowVariable;
1411
import com.fasterxml.jackson.annotation.JsonIgnore;
1512

13+
import java.time.Duration;
14+
import java.time.LocalDateTime;
15+
1616
@PlanningEntity
1717
public class Job {
1818

@@ -34,6 +34,14 @@ public class Job {
3434

3535
@InverseRelationShadowVariable(sourceVariableName = "jobs")
3636
private Line line;
37+
@ShadowVariable(
38+
variableListenerClass = LineOperatorUpdatingVariableListener.class,
39+
sourceEntityClass = Line.class,
40+
sourceVariableName = "operator")
41+
@ShadowVariable(
42+
variableListenerClass = JobOperatorUpdatingVariableListener.class,
43+
sourceVariableName = "line")
44+
private Operator lineOperator;
3745
@JsonIgnore
3846
@PreviousElementShadowVariable(sourceVariableName = "jobs")
3947
private Job previousJob;
@@ -129,6 +137,14 @@ public void setLine(Line line) {
129137
this.line = line;
130138
}
131139

140+
public Operator getLineOperator() {
141+
return lineOperator;
142+
}
143+
144+
public void setLineOperator(Operator lineOperator) {
145+
this.lineOperator = lineOperator;
146+
}
147+
132148
public Job getPreviousJob() {
133149
return previousJob;
134150
}
@@ -198,5 +214,4 @@ private void updateStartCleaningDateTime() {
198214
var endTime = startProduction == null ? null : startProduction.plus(getDuration());
199215
setEndDateTime(endTime);
200216
}
201-
202217
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package org.acme.foodpackaging.domain;
2+
3+
import ai.timefold.solver.core.api.domain.variable.VariableListener;
4+
import ai.timefold.solver.core.api.score.director.ScoreDirector;
5+
import org.jspecify.annotations.NonNull;
6+
7+
import java.util.Objects;
8+
9+
public class JobOperatorUpdatingVariableListener implements VariableListener<PackagingSchedule, Job> {
10+
11+
private static final String LINE_OPERATOR_FIELD = "lineOperator";
12+
13+
@Override
14+
public void beforeVariableChanged(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Job job) {
15+
// No need to do anything.
16+
}
17+
18+
@Override
19+
public void afterVariableChanged(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Job job) {
20+
var line = job.getLine();
21+
var operator = line != null ? line.getOperator() : null;
22+
var lineOperator = job.getLineOperator();
23+
if (line == null && lineOperator != null) {
24+
scoreDirector.beforeVariableChanged(job, LINE_OPERATOR_FIELD);
25+
job.setLineOperator(null);
26+
scoreDirector.afterVariableChanged(job, LINE_OPERATOR_FIELD);
27+
} else if (!Objects.equals(operator, lineOperator)) {
28+
scoreDirector.beforeVariableChanged(job, LINE_OPERATOR_FIELD);
29+
job.setLineOperator(operator);
30+
scoreDirector.afterVariableChanged(job, LINE_OPERATOR_FIELD);
31+
}
32+
}
33+
34+
@Override
35+
public void beforeEntityAdded(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Job job) {
36+
// No need to do anything.
37+
}
38+
39+
@Override
40+
public void afterEntityAdded(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Job job) {
41+
// No need to do anything.
42+
}
43+
44+
@Override
45+
public void beforeEntityRemoved(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Job job) {
46+
// No need to do anything.
47+
}
48+
49+
@Override
50+
public void afterEntityRemoved(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Job job) {
51+
// No need to do anything.
52+
}
53+
}

java/food-packaging/src/main/java/org/acme/foodpackaging/domain/Line.java

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
11
package org.acme.foodpackaging.domain;
22

3+
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
4+
import ai.timefold.solver.core.api.domain.lookup.PlanningId;
5+
import ai.timefold.solver.core.api.domain.variable.PlanningListVariable;
6+
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
7+
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
8+
import com.fasterxml.jackson.annotation.JsonIdentityReference;
9+
import com.fasterxml.jackson.annotation.JsonIgnore;
10+
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
11+
312
import java.time.LocalDateTime;
413
import java.util.ArrayList;
514
import java.util.List;
615

7-
import ai.timefold.solver.core.api.domain.lookup.PlanningId;
8-
import com.fasterxml.jackson.annotation.JsonIgnore;
9-
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
10-
import ai.timefold.solver.core.api.domain.variable.PlanningListVariable;
11-
16+
@JsonIdentityInfo(scope = Line.class, generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
1217
@PlanningEntity
1318
public class Line {
1419

1520
@PlanningId
1621
private String id;
1722
private String name;
18-
private String operator;
1923
private LocalDateTime startDateTime;
2024

25+
@JsonIdentityReference(alwaysAsId = true)
26+
@PlanningVariable
27+
private Operator operator;
28+
2129
@JsonIgnore
2230
@PlanningListVariable
2331
private List<Job> jobs;
@@ -26,7 +34,11 @@ public class Line {
2634
public Line() {
2735
}
2836

29-
public Line(String id, String name, String operator, LocalDateTime startDateTime) {
37+
public Line(String id, String name, LocalDateTime startDateTime) {
38+
this(id, name, null, startDateTime);
39+
}
40+
41+
public Line(String id, String name, Operator operator, LocalDateTime startDateTime) {
3042
this.id = id;
3143
this.name = name;
3244
this.operator = operator;
@@ -51,7 +63,11 @@ public String getName() {
5163
return name;
5264
}
5365

54-
public String getOperator() {
66+
public void setOperator(Operator operator) {
67+
this.operator = operator;
68+
}
69+
70+
public Operator getOperator() {
5571
return operator;
5672
}
5773

@@ -62,5 +78,4 @@ public LocalDateTime getStartDateTime() {
6278
public List<Job> getJobs() {
6379
return jobs;
6480
}
65-
6681
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.acme.foodpackaging.domain;
2+
3+
import ai.timefold.solver.core.api.domain.variable.VariableListener;
4+
import ai.timefold.solver.core.api.score.director.ScoreDirector;
5+
import org.jspecify.annotations.NonNull;
6+
7+
import java.util.Objects;
8+
9+
public class LineOperatorUpdatingVariableListener implements VariableListener<PackagingSchedule, Line> {
10+
@Override
11+
public void beforeVariableChanged(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Line line) {
12+
// No need to do anything.
13+
}
14+
15+
@Override
16+
public void afterVariableChanged(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Line line) {
17+
for (var job : line.getJobs()) {
18+
if (!Objects.equals(job.getLineOperator(), line.getOperator())) {
19+
scoreDirector.beforeVariableChanged(job, "lineOperator");
20+
job.setLineOperator(line.getOperator());
21+
scoreDirector.afterVariableChanged(job, "lineOperator");
22+
}
23+
}
24+
}
25+
26+
@Override
27+
public void beforeEntityAdded(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Line line) {
28+
// No need to do anything.
29+
}
30+
31+
@Override
32+
public void afterEntityAdded(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Line line) {
33+
// No need to do anything.
34+
}
35+
36+
@Override
37+
public void beforeEntityRemoved(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Line line) {
38+
// No need to do anything.
39+
}
40+
41+
@Override
42+
public void afterEntityRemoved(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Line line) {
43+
// No need to do anything.
44+
}
45+
}

0 commit comments

Comments
 (0)