Skip to content

Commit

Permalink
chore: Allow parameterizing employee scheduling demo data (#483)
Browse files Browse the repository at this point in the history
- Change Quarkus text in Python School Timetabling to Python
- Add demo data set selector to employee scheduling UI
- Change Python employee scheduling feasibility test to use REST API
- Allow parameterizing employee scheduling demo data

---------

Co-authored-by: Lukáš Petrovický <[email protected]>
  • Loading branch information
Christopher-Chianelli and triceo authored Jun 20, 2024
1 parent e9127c3 commit 2c74ba3
Show file tree
Hide file tree
Showing 27 changed files with 323 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
<meta content="width=device-width, initial-scale=1" name="viewport">
<title>Bed Allocation Scheduling - Timefold Quarkus</title>
<title>Bed Allocation Scheduling - Timefold Solver on Quarkus</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css"/>
<link rel="stylesheet" href="/webjars/font-awesome/css/all.css"/>
<link rel="stylesheet" href="/webjars/timefold/css/timefold-webui.css"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
<meta content="width=device-width, initial-scale=1" name="viewport">
<title>Conference Scheduling - Timefold Quarkus</title>
<title>Conference Scheduling - Timefold Solver on Quarkus</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css"/>
<link rel="stylesheet" href="/webjars/font-awesome/css/all.css"/>
<link rel="stylesheet" href="/webjars/timefold/css/timefold-webui.css"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,78 @@

@ApplicationScoped
public class DemoDataGenerator {

public enum DemoData {
SMALL,
LARGE
SMALL(new DemoDataParameters(
List.of("Ambulatory care", "Critical care", "Pediatric care"),
List.of("Doctor", "Nurse"),
List.of("Anaesthetics", "Cardiology"),
14,
15,
List.of(new CountDistribution(1, 3),
new CountDistribution(2, 1)
),
List.of(new CountDistribution(1, 0.9),
new CountDistribution(2, 0.1)
),
List.of(new CountDistribution(1, 4),
new CountDistribution(2, 3),
new CountDistribution(3, 2),
new CountDistribution(4, 1)
),
0
)),
LARGE(new DemoDataParameters(
List.of("Ambulatory care",
"Neurology",
"Critical care",
"Pediatric care",
"Surgery",
"Radiology",
"Outpatient"),
List.of("Doctor", "Nurse"),
List.of("Anaesthetics", "Cardiology", "Radiology"),
28,
50,
List.of(new CountDistribution(1, 3),
new CountDistribution(2, 1)
),
List.of(new CountDistribution(1, 0.5),
new CountDistribution(2, 0.3),
new CountDistribution(3, 0.2)
),
List.of(new CountDistribution(5, 4),
new CountDistribution(10, 3),
new CountDistribution(15, 2),
new CountDistribution(20, 1)
),
0
));

private final DemoDataParameters parameters;

DemoData(DemoDataParameters parameters) {
this.parameters = parameters;
}

public DemoDataParameters getParameters() {
return parameters;
}
}

public record CountDistribution(int count, double weight) {}

public record DemoDataParameters(List<String> locations,
List<String> requiredSkills,
List<String> optionalSkills,
int daysInSchedule,
int employeeCount,
List<CountDistribution> optionalSkillDistribution,
List<CountDistribution> shiftCountDistribution,
List<CountDistribution> availabilityCountDistribution,
int randomSeed) {}

private static final String[] FIRST_NAMES = { "Amy", "Beth", "Chad", "Dan", "Elsa", "Flo", "Gus", "Hugo", "Ivy", "Jay" };
private static final String[] LAST_NAMES = { "Cole", "Fox", "Green", "Jones", "King", "Li", "Poe", "Rye", "Smith", "Watt" };
private static final String[] REQUIRED_SKILLS = { "Doctor", "Nurse" };
private static final String[] OPTIONAL_SKILLS = { "Anaesthetics", "Cardiology" };
private static final String[] LOCATIONS = { "Ambulatory care", "Critical care", "Pediatric care" };
private static final Duration SHIFT_LENGTH = Duration.ofHours(8);
private static final LocalTime MORNING_SHIFT_START_TIME = LocalTime.of(6, 0);
private static final LocalTime DAY_SHIFT_START_TIME = LocalTime.of(9, 0);
Expand All @@ -51,16 +112,19 @@ public enum DemoData {

Map<String, List<LocalTime>> locationToShiftStartTimeListMap = new HashMap<>();

public EmployeeSchedule generateDemoData() {
public EmployeeSchedule generateDemoData(DemoData demoData) {
return generateDemoData(demoData.getParameters());
}

public EmployeeSchedule generateDemoData(DemoDataParameters parameters) {
EmployeeSchedule employeeSchedule = new EmployeeSchedule();

int initialRosterLengthInDays = 14;
LocalDate startDate = LocalDate.now().with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));

Random random = new Random(0);
Random random = new Random(parameters.randomSeed);

int shiftTemplateIndex = 0;
for (String location : LOCATIONS) {
for (String location : parameters.locations) {
locationToShiftStartTimeListMap.put(location, List.of(SHIFT_START_TIMES_COMBOS[shiftTemplateIndex]));
shiftTemplateIndex = (shiftTemplateIndex + 1) % SHIFT_START_TIMES_COMBOS.length;
}
Expand All @@ -69,17 +133,18 @@ public EmployeeSchedule generateDemoData() {
Collections.shuffle(namePermutations, random);

List<Employee> employees = new ArrayList<>();
for (int i = 0; i < 15; i++) {
Set<String> skills = pickSubset(List.of(OPTIONAL_SKILLS), random, 3, 1);
skills.add(pickRandom(REQUIRED_SKILLS, random));
for (int i = 0; i < parameters.employeeCount; i++) {
Set<String> skills = pickSubset(parameters.optionalSkills, random, parameters.optionalSkillDistribution);
skills.add(pickRandom(parameters.requiredSkills, random));
Employee employee = new Employee(namePermutations.get(i), skills, new LinkedHashSet<>(), new LinkedHashSet<>(), new LinkedHashSet<>());
employees.add(employee);
}
employeeSchedule.setEmployees(employees);

List<Shift> shifts = new LinkedList<>();
for (int i = 0; i < initialRosterLengthInDays; i++) {
Set<Employee> employeesWithAvailabilitiesOnDay = pickSubset(employees, random, 4, 3, 2, 1);
for (int i = 0; i < parameters.daysInSchedule; i++) {
Set<Employee> employeesWithAvailabilitiesOnDay = pickSubset(employees, random,
parameters.availabilityCountDistribution);
LocalDate date = startDate.plusDays(i);
for (Employee employee : employeesWithAvailabilitiesOnDay) {
switch (random.nextInt(3)) {
Expand All @@ -88,7 +153,7 @@ public EmployeeSchedule generateDemoData() {
case 2 -> employee.getDesiredDates().add(date);
}
}
shifts.addAll(generateShiftsForDay(date, random));
shifts.addAll(generateShiftsForDay(parameters, date, random));
}
AtomicInteger countShift = new AtomicInteger();
shifts.forEach(s -> s.setId(Integer.toString(countShift.getAndIncrement())));
Expand All @@ -97,59 +162,60 @@ public EmployeeSchedule generateDemoData() {
return employeeSchedule;
}

private List<Shift> generateShiftsForDay(LocalDate date, Random random) {
private List<Shift> generateShiftsForDay(DemoDataParameters parameters, LocalDate date, Random random) {
List<Shift> shifts = new LinkedList<>();
for (String location : LOCATIONS) {
for (String location : parameters.locations) {
List<LocalTime> shiftStartTimes = locationToShiftStartTimeListMap.get(location);
for (LocalTime shiftStartTime : shiftStartTimes) {
LocalDateTime shiftStartDateTime = date.atTime(shiftStartTime);
LocalDateTime shiftEndDateTime = shiftStartDateTime.plus(SHIFT_LENGTH);
shifts.addAll(generateShiftForTimeslot(shiftStartDateTime, shiftEndDateTime, location, random));
shifts.addAll(generateShiftForTimeslot(parameters, shiftStartDateTime, shiftEndDateTime, location, random));
}
}
return shifts;
}

private List<Shift> generateShiftForTimeslot(LocalDateTime timeslotStart, LocalDateTime timeslotEnd, String location,
private List<Shift> generateShiftForTimeslot(DemoDataParameters parameters,
LocalDateTime timeslotStart, LocalDateTime timeslotEnd, String location,
Random random) {
int shiftCount = 1;

if (random.nextDouble() > 0.9) {
// generate an extra shift
shiftCount++;
}
var shiftCount = pickCount(random, parameters.shiftCountDistribution);

List<Shift> shifts = new LinkedList<>();
for (int i = 0; i < shiftCount; i++) {
String requiredSkill;
if (random.nextBoolean()) {
requiredSkill = pickRandom(REQUIRED_SKILLS, random);
requiredSkill = pickRandom(parameters.requiredSkills, random);
} else {
requiredSkill = pickRandom(OPTIONAL_SKILLS, random);
requiredSkill = pickRandom(parameters.optionalSkills, random);
}
shifts.add(new Shift(timeslotStart, timeslotEnd, location, requiredSkill));
}
return shifts;
}

private <T> T pickRandom(T[] source, Random random) {
return source[random.nextInt(source.length)];
private <T> T pickRandom(List<T> source, Random random) {
return source.get(random.nextInt(source.size()));
}

private <T> Set<T> pickSubset(List<T> sourceSet, Random random, int... distribution) {
int probabilitySum = 0;
for (int probability : distribution) {
probabilitySum += probability;
private int pickCount(Random random, List<CountDistribution> countDistribution) {
double probabilitySum = 0;
for (var possibility : countDistribution) {
probabilitySum += possibility.weight;
}
int choice = random.nextInt(probabilitySum);
var choice = random.nextDouble(probabilitySum);
int numOfItems = 0;
while (choice >= distribution[numOfItems]) {
choice -= distribution[numOfItems];
while (choice >= countDistribution.get(numOfItems).weight) {
choice -= countDistribution.get(numOfItems).weight;
numOfItems++;
}
return countDistribution.get(numOfItems).count;
}

private <T> Set<T> pickSubset(List<T> sourceSet, Random random, List<CountDistribution> countDistribution) {
var count = pickCount(random, countDistribution);
List<T> items = new ArrayList<>(sourceSet);
Collections.shuffle(items, random);
return new HashSet<>(items.subList(0, numOfItems + 1));
return new HashSet<>(items.subList(0, count));
}

private List<String> joinAllCombinations(String[]... partArrays) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,6 @@ public DemoData[] list() {
@GET
@Path("/{demoDataId}")
public Response generate(@PathParam("demoDataId") DemoData demoData) {
return Response.ok(dataGenerator.generateDemoData()).build();
return Response.ok(dataGenerator.generateDemoData(demoData)).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,18 @@ function setupAjax() {

function fetchDemoData() {
$.get("/demo-data", function (data) {
// load first data set
data.forEach(item => {
$("#testDataButton").append($('<a id="' + item + 'TestData" class="dropdown-item" href="#">' + item + '</a>'));
$("#" + item + "TestData").click(function () {
switchDataDropDownItemActive(item);
scheduleId = null;
demoDataId = item;

refreshSchedule();
});
});
demoDataId = data[0];
switchDataDropDownItemActive(demoDataId);
refreshSchedule();
}).fail(function (xhr, ajaxOptions, thrownError) {
// disable this page as there is no data
Expand All @@ -101,6 +111,11 @@ function fetchDemoData() {
});
}

function switchDataDropDownItemActive(newItem) {
activeCssClass = "active";
$("#testDataButton > a." + activeCssClass).removeClass(activeCssClass);
$("#" + newItem + "TestData").addClass(activeCssClass);
}

function getShiftColor(shift, employee) {
const shiftStart = JSJoda.LocalDateTime.parse(shift.start);
Expand Down Expand Up @@ -449,6 +464,14 @@ function replaceQuickstartTimefoldAutoHeaderFooter() {
</li>
</ul>
</div>
<div class="ms-auto">
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Data
</button>
<div id="testDataButton" class="dropdown-menu" aria-labelledby="dropdownMenuButton"></div>
</div>
</div>
</nav>
</div>`));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
<meta content="width=device-width, initial-scale=1" name="viewport">
<title>Employee scheduling - Timefold Quarkus</title>
<title>Employee scheduling - Timefold Solver on Quarkus</title>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/styles/vis-timeline-graph2d.min.css"
integrity="sha256-svzNasPg1yR5gvEaRei2jg+n4Pc3sVyMUWeS6xRAh6U=" crossorigin="anonymous">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Facility location - Timefold Quarkus</title>
<title>Facility location - Timefold Solver on Quarkus</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="/webjars/leaflet/leaflet.css">
<link rel="stylesheet" href="/webjars/font-awesome/css/all.min.css">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
<meta content="width=device-width, initial-scale=1" name="viewport">
<title>Flight Crew Scheduling - Timefold Quarkus</title>
<title>Flight Crew Scheduling - Timefold Solver on Quarkus</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css"/>
<link rel="stylesheet" href="/webjars/font-awesome/css/all.css"/>
<link rel="stylesheet" href="/webjars/timefold/css/timefold-webui.css"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
<meta content="width=device-width, initial-scale=1" name="viewport">
<title>Food packaging - Timefold Quarkus</title>
<title>Food packaging - Timefold Solver on Quarkus</title>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/styles/vis-timeline-graph2d.min.css"
integrity="sha256-svzNasPg1yR5gvEaRei2jg+n4Pc3sVyMUWeS6xRAh6U=" crossorigin="anonymous">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
<meta content="width=device-width, initial-scale=1" name="viewport">
<title>Maintenance scheduling - Timefold Quarkus</title>
<title>Maintenance scheduling - Timefold Solver on Quarkus</title>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/styles/vis-timeline-graph2d.min.css"
integrity="sha256-svzNasPg1yR5gvEaRei2jg+n4Pc3sVyMUWeS6xRAh6U=" crossorigin="anonymous">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
<meta content="width=device-width, initial-scale=1" name="viewport">
<title>Meeting Scheduling - Timefold Quarkus</title>
<title>Meeting Scheduling - Timefold Solver on Quarkus</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css"/>
<link rel="stylesheet" href="/webjars/font-awesome/css/all.css"/>
<link rel="stylesheet" href="/webjars/timefold/css/timefold-webui.css"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
<meta content="width=device-width, initial-scale=1" name="viewport">
<title>Order Picking - Timefold Quarkus</title>
<title>Order Picking - Timefold Solver on Quarkus</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css"/>
<link rel="stylesheet" href="/webjars/font-awesome/css/all.css"/>
<link rel="stylesheet" href="/webjars/timefold/css/timefold-webui.css" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
<meta content="width=device-width, initial-scale=1" name="viewport">
<title>Project Job Scheduling - Timefold Quarkus</title>
<title>Project Job Scheduling - Timefold Solver on Quarkus</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css"/>
<link rel="stylesheet" href="/webjars/font-awesome/css/all.css"/>
<link rel="stylesheet" href="/webjars/timefold/css/timefold-webui.css"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
<meta content="width=device-width, initial-scale=1" name="viewport">
<title>School timetabling - Timefold Quarkus</title>
<title>School timetabling - Timefold Solver on Quarkus</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css"/>
<link rel="stylesheet" href="/webjars/font-awesome/css/all.css"/>
<link rel="stylesheet" href="/webjars/timefold/css/timefold-webui.css"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
<meta content="width=device-width, initial-scale=1" name="viewport">
<title>Sports League Scheduling - Timefold Quarkus</title>
<title>Sports League Scheduling - Timefold Solver on Quarkus</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css"/>
<link rel="stylesheet" href="/webjars/font-awesome/css/all.css"/>
<link rel="stylesheet" href="/webjars/timefold/css/timefold-webui.css"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
<meta content="width=device-width, initial-scale=1" name="viewport">
<title>School timetabling - Timefold Spring Boot</title>
<title>School timetabling - Timefold Solver on Spring Boot</title>
<!-- Use versioned web jars since native mode do not support web jar locator -->
<link rel="stylesheet" href="/webjars/bootstrap/5.2.3/css/bootstrap.min.css" />
<link rel="stylesheet" href="/webjars/font-awesome/5.15.1/css/all.css" />
Expand Down
Loading

0 comments on commit 2c74ba3

Please sign in to comment.