Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
package org.acme.foodpackaging.bootstrap;

import io.quarkus.runtime.StartupEvent;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.transaction.Transactional;
import org.acme.foodpackaging.domain.Job;
import org.acme.foodpackaging.domain.Line;
import org.acme.foodpackaging.domain.Operator;
import org.acme.foodpackaging.domain.PackagingSchedule;
import org.acme.foodpackaging.domain.Product;
import org.acme.foodpackaging.domain.WorkCalendar;
import org.acme.foodpackaging.persistence.PackagingScheduleRepository;
import org.eclipse.microprofile.config.inject.ConfigProperty;

import java.time.DayOfWeek;
import java.time.Duration;
import java.time.LocalDate;
Expand All @@ -10,77 +23,65 @@
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import io.quarkus.runtime.StartupEvent;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.acme.foodpackaging.domain.Job;
import org.acme.foodpackaging.domain.Line;
import org.acme.foodpackaging.domain.PackagingSchedule;
import org.acme.foodpackaging.domain.Product;
import org.acme.foodpackaging.domain.WorkCalendar;
import org.acme.foodpackaging.persistence.PackagingScheduleRepository;
import org.eclipse.microprofile.config.inject.ConfigProperty;

@ApplicationScoped
public class DemoDataGenerator {

@Inject
PackagingScheduleRepository repository;
private final PackagingScheduleRepository repository;

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

public DemoDataGenerator(PackagingScheduleRepository repository) {
this.repository = repository;
}

@Transactional
public void generateDemoData(@Observes StartupEvent startupEvent) {
int noCleaningMinutes = 10;
int cleaningMinutesMinimum = 30;
int cleaningMinutesMaximum = 60;
int jobDurationMinutesMinimum = 120;
int jobDurationMinutesMaximum = 300;
int averageCleaningAndJobDurationMinutes =
var noCleaningMinutes = 10;
var cleaningMinutesMinimum = 30;
var cleaningMinutesMaximum = 60;
var jobDurationMinutesMinimum = 120;
var jobDurationMinutesMaximum = 300;
var averageCleaningAndJobDurationMinutes =
(2 * noCleaningMinutes + cleaningMinutesMinimum + cleaningMinutesMaximum) / 4
+ (jobDurationMinutesMinimum + jobDurationMinutesMaximum) / 2;
+ (jobDurationMinutesMinimum + jobDurationMinutesMaximum) / 2;

final LocalDate START_DATE = LocalDate.now().with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));
final LocalDateTime START_DATE_TIME = LocalDateTime.of(START_DATE, LocalTime.MIDNIGHT);
final LocalDate END_DATE = START_DATE.plusWeeks(2);
final LocalDateTime END_DATE_TIME = LocalDateTime.of(END_DATE, LocalTime.MIDNIGHT);
final var START_DATE = LocalDate.now().with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));
final var START_DATE_TIME = LocalDateTime.of(START_DATE, LocalTime.MIDNIGHT);
final var END_DATE = START_DATE.plusWeeks(2);

Random random = new Random(37);
PackagingSchedule solution = new PackagingSchedule();
var random = new Random(37);
var solution = new PackagingSchedule();

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

Map<Product, Set<String>> ingredientMap = new HashMap<>(INGREDIENT_LIST.size() * PRODUCT_VARIATION_LIST.size() * 3);
long productId = 0;
for (int i = 0; i < INGREDIENT_LIST.size(); i++) {
String ingredient = INGREDIENT_LIST.get(i);
int r = random.nextInt(INGREDIENT_LIST.size() - 4);
String ingredientA = INGREDIENT_LIST.get((i + r + 1) % INGREDIENT_LIST.size());
String ingredientB = INGREDIENT_LIST.get((i + r + 2) % INGREDIENT_LIST.size());
String ingredientC = INGREDIENT_LIST.get((i + r + 3) % INGREDIENT_LIST.size());
for (String productVariation : PRODUCT_VARIATION_LIST) {
ingredientMap.put(new Product(Long.toString(productId++), ingredient + " " + productVariation), Set.of(ingredient));
var ingredientMap = new HashMap<Product, Set<String>>(INGREDIENT_LIST.size() * PRODUCT_VARIATION_LIST.size() * 3);
var productId = 0L;
for (var i = 0; i < INGREDIENT_LIST.size(); i++) {
var ingredient = INGREDIENT_LIST.get(i);
var r = random.nextInt(INGREDIENT_LIST.size() - 4);
var ingredientA = INGREDIENT_LIST.get((i + r + 1) % INGREDIENT_LIST.size());
var ingredientB = INGREDIENT_LIST.get((i + r + 2) % INGREDIENT_LIST.size());
var ingredientC = INGREDIENT_LIST.get((i + r + 3) % INGREDIENT_LIST.size());
for (var productVariation : PRODUCT_VARIATION_LIST) {
ingredientMap.put(new Product(Long.toString(productId++), ingredient + " " + productVariation), java.util.Set.of(ingredient));
}
ingredientMap.put(new Product(Long.toString(productId++), ingredient + " and " + ingredientA + " " + PRODUCT_VARIATION_LIST.get(1)), Set.of(ingredient, ingredientA));
ingredientMap.put(new Product(Long.toString(productId++), ingredient + " and " + ingredientB + " " + PRODUCT_VARIATION_LIST.get(2)), Set.of(ingredient, ingredientB));
ingredientMap.put(new Product(Long.toString(productId++), ingredient + ", " + ingredientA + " and " + ingredientC + " " + PRODUCT_VARIATION_LIST.get(1)), Set.of(ingredient, ingredientA, ingredientC));
ingredientMap.put(new Product(Long.toString(productId++), ingredient + " and " + ingredientA + " " + PRODUCT_VARIATION_LIST.get(1)), java.util.Set.of(ingredient, ingredientA));
ingredientMap.put(new Product(Long.toString(productId++), ingredient + " and " + ingredientB + " " + PRODUCT_VARIATION_LIST.get(2)), java.util.Set.of(ingredient, ingredientB));
ingredientMap.put(new Product(Long.toString(productId++), ingredient + ", " + ingredientA + " and " + ingredientC + " " + PRODUCT_VARIATION_LIST.get(1)), java.util.Set.of(ingredient, ingredientA, ingredientC));
}
List<Product> products = new ArrayList<>(ingredientMap.keySet());
for (Product product : products) {
Map<Product, Duration> cleaningDurationMap = new HashMap<>(products.size());
Set<String> ingredients = ingredientMap.get(product);
for (Product previousProduct : products) {
boolean noCleaning = ingredients.containsAll(ingredientMap.get(previousProduct));
Duration cleaningDuration = Duration.ofMinutes(product == previousProduct ? 0
var products = new ArrayList<>(ingredientMap.keySet().stream().sorted(Comparator.comparing(Product::getId)).toList());
for (var product : products) {
var cleaningDurationMap = new HashMap<Product, Duration>(products.size());
var ingredients = ingredientMap.get(product);
for (var previousProduct : products) {
var noCleaning = ingredients.containsAll(ingredientMap.get(previousProduct));
var cleaningDuration = Duration.ofMinutes(product == previousProduct ? 0
: noCleaning ? noCleaningMinutes
: cleaningMinutesMinimum + random.nextInt(cleaningMinutesMaximum - cleaningMinutesMinimum));
cleaningDurationMap.put(previousProduct, cleaningDuration);
Expand All @@ -89,24 +90,25 @@ public void generateDemoData(@Observes StartupEvent startupEvent) {
}
solution.setProducts(products);

List<Line> lines = new ArrayList<>(lineCount);
for (int i = 0; i < lineCount; i++) {
String name = "Line " + (i + 1);
String operator = "Operator " + ((char) ('A' + (i / 2)));
lines.add(new Line(Integer.toString(i), name, operator, START_DATE_TIME));
var lines = new ArrayList<Line>(lineCount);
var operators = new ArrayList<Operator>(lineCount);
for (var i = 0; i < lineCount; i++) {
lines.add(new Line(Integer.toString(i), "Line " + (i + 1), START_DATE_TIME));
operators.add(new Operator("Operator " + (i + 1)));
}
solution.setLines(lines);
solution.setOperators(operators);

List<Job> jobs = new ArrayList<>(jobCount);
for (int i = 0; i < jobCount; i++) {
var jobs = new ArrayList<Job>(jobCount);
for (var i = 0; i < jobCount; i++) {
Product product = products.get(random.nextInt(products.size()));
String name = product.getName();
Duration duration = Duration.ofMinutes(jobDurationMinutesMinimum
+ random.nextInt(jobDurationMinutesMaximum - jobDurationMinutesMinimum));
+ random.nextLong((long) jobDurationMinutesMaximum - jobDurationMinutesMinimum));
int targetDayIndex = (i / lineCount) * averageCleaningAndJobDurationMinutes / (24 * 60);
LocalDateTime minStartTime = START_DATE.plusDays(random.nextInt(Math.max(1, targetDayIndex - 2))).atTime(LocalTime.MIDNIGHT);
LocalDateTime idealEndTime = START_DATE.plusDays(targetDayIndex + random.nextInt(3)).atTime(16, 0);
LocalDateTime maxEndTime = idealEndTime.plusDays(1 + random.nextInt(3));
LocalDateTime idealEndTime = START_DATE.plusDays(targetDayIndex + random.nextLong(3)).atTime(16, 0);
LocalDateTime maxEndTime = idealEndTime.plusDays(1 + random.nextLong(3));
jobs.add(new Job(Integer.toString(i), name, product, duration, minStartTime, idealEndTime, maxEndTime, 1, false));
}
jobs.sort(Comparator.comparing(Job::getName));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
package org.acme.foodpackaging.domain;

import java.time.Duration;
import java.time.LocalDateTime;

import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
import ai.timefold.solver.core.api.domain.entity.PlanningPin;
import ai.timefold.solver.core.api.domain.lookup.PlanningId;
import ai.timefold.solver.core.api.domain.variable.CascadingUpdateShadowVariable;
import ai.timefold.solver.core.api.domain.variable.InverseRelationShadowVariable;
import ai.timefold.solver.core.api.domain.variable.NextElementShadowVariable;
import ai.timefold.solver.core.api.domain.variable.PreviousElementShadowVariable;

import ai.timefold.solver.core.api.domain.variable.ShadowVariable;
import com.fasterxml.jackson.annotation.JsonIgnore;

import java.time.Duration;
import java.time.LocalDateTime;

@PlanningEntity
public class Job {

Expand All @@ -34,6 +34,14 @@ public class Job {

@InverseRelationShadowVariable(sourceVariableName = "jobs")
private Line line;
@ShadowVariable(
variableListenerClass = LineOperatorUpdatingVariableListener.class,
sourceEntityClass = Line.class,
sourceVariableName = "operator")
@ShadowVariable(
variableListenerClass = JobOperatorUpdatingVariableListener.class,
sourceVariableName = "line")
private Operator lineOperator;
Comment thread
triceo marked this conversation as resolved.
@JsonIgnore
@PreviousElementShadowVariable(sourceVariableName = "jobs")
private Job previousJob;
Expand Down Expand Up @@ -129,6 +137,14 @@ public void setLine(Line line) {
this.line = line;
}

public Operator getLineOperator() {
return lineOperator;
}

public void setLineOperator(Operator lineOperator) {
this.lineOperator = lineOperator;
}

public Job getPreviousJob() {
return previousJob;
}
Expand Down Expand Up @@ -198,5 +214,4 @@ private void updateStartCleaningDateTime() {
var endTime = startProduction == null ? null : startProduction.plus(getDuration());
setEndDateTime(endTime);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.acme.foodpackaging.domain;

import ai.timefold.solver.core.api.domain.variable.VariableListener;
import ai.timefold.solver.core.api.score.director.ScoreDirector;
import org.jspecify.annotations.NonNull;

import java.util.Objects;

public class JobOperatorUpdatingVariableListener implements VariableListener<PackagingSchedule, Job> {

private static final String LINE_OPERATOR_FIELD = "lineOperator";

@Override
public void beforeVariableChanged(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Job job) {
// No need to do anything.
}

@Override
public void afterVariableChanged(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Job job) {
var line = job.getLine();
var operator = line != null ? line.getOperator() : null;
var lineOperator = job.getLineOperator();
if (line == null && lineOperator != null) {
scoreDirector.beforeVariableChanged(job, LINE_OPERATOR_FIELD);
job.setLineOperator(null);
scoreDirector.afterVariableChanged(job, LINE_OPERATOR_FIELD);
} else if (!Objects.equals(operator, lineOperator)) {
scoreDirector.beforeVariableChanged(job, LINE_OPERATOR_FIELD);
job.setLineOperator(operator);
scoreDirector.afterVariableChanged(job, LINE_OPERATOR_FIELD);
}
}

@Override
public void beforeEntityAdded(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Job job) {
// No need to do anything.
}

@Override
public void afterEntityAdded(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Job job) {
// No need to do anything.
}

@Override
public void beforeEntityRemoved(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Job job) {
// No need to do anything.
}

@Override
public void afterEntityRemoved(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Job job) {
// No need to do anything.
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
package org.acme.foodpackaging.domain;

import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
import ai.timefold.solver.core.api.domain.lookup.PlanningId;
import ai.timefold.solver.core.api.domain.variable.PlanningListVariable;
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.JsonIdentityReference;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

import ai.timefold.solver.core.api.domain.lookup.PlanningId;
import com.fasterxml.jackson.annotation.JsonIgnore;
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
import ai.timefold.solver.core.api.domain.variable.PlanningListVariable;

@JsonIdentityInfo(scope = Line.class, generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
@PlanningEntity
public class Line {

@PlanningId
private String id;
private String name;
private String operator;
private LocalDateTime startDateTime;

@JsonIdentityReference(alwaysAsId = true)
@PlanningVariable
private Operator operator;

@JsonIgnore
@PlanningListVariable
private List<Job> jobs;
Expand All @@ -26,7 +34,11 @@ public class Line {
public Line() {
}

public Line(String id, String name, String operator, LocalDateTime startDateTime) {
public Line(String id, String name, LocalDateTime startDateTime) {
this(id, name, null, startDateTime);
}

public Line(String id, String name, Operator operator, LocalDateTime startDateTime) {
this.id = id;
this.name = name;
this.operator = operator;
Expand All @@ -51,7 +63,11 @@ public String getName() {
return name;
}

public String getOperator() {
public void setOperator(Operator operator) {
this.operator = operator;
}

public Operator getOperator() {
return operator;
}

Expand All @@ -62,5 +78,4 @@ public LocalDateTime getStartDateTime() {
public List<Job> getJobs() {
return jobs;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.acme.foodpackaging.domain;

import ai.timefold.solver.core.api.domain.variable.VariableListener;
import ai.timefold.solver.core.api.score.director.ScoreDirector;
import org.jspecify.annotations.NonNull;

import java.util.Objects;

public class LineOperatorUpdatingVariableListener implements VariableListener<PackagingSchedule, Line> {
@Override
public void beforeVariableChanged(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Line line) {
// No need to do anything.
}

@Override
public void afterVariableChanged(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Line line) {
for (var job : line.getJobs()) {
if (!Objects.equals(job.getLineOperator(), line.getOperator())) {
scoreDirector.beforeVariableChanged(job, "lineOperator");
job.setLineOperator(line.getOperator());
scoreDirector.afterVariableChanged(job, "lineOperator");
}
}
}

@Override
public void beforeEntityAdded(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Line line) {
// No need to do anything.
}

@Override
public void afterEntityAdded(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Line line) {
// No need to do anything.
}

@Override
public void beforeEntityRemoved(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Line line) {
// No need to do anything.
}

@Override
public void afterEntityRemoved(@NonNull ScoreDirector<PackagingSchedule> scoreDirector, @NonNull Line line) {
// No need to do anything.
}
}
Loading
Loading