Skip to content

Commit

Permalink
replaces Workshop with TopicTimeslot and introduces a BiMap between W…
Browse files Browse the repository at this point in the history
…orkshopId and TopicTimeslot.

Therefore, the WorkshopSeats are now a separate attribute to workshops
  • Loading branch information
carsten-langer committed Dec 26, 2023
1 parent 321691e commit 0477618
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 30 deletions.
22 changes: 12 additions & 10 deletions src/main/scala/hcd/algorithm/Algorithm.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ object Algorithm {
selectedTopics
.toMap // transform back from BiMap to Map, so that several workshops can have the same selection priority
.flatMap { case (topicId, selectionPriority) =>
workshops.collect { case (workshopId, Workshop(`topicId`, _, _)) =>
workshops.collect { case (workshopId, TopicTimeslot(`topicId`, _)) =>
workshopId -> selectionPriority
}
}
Expand All @@ -33,9 +33,9 @@ object Algorithm {

private def areDistinct[A](it: Iterable[A]): Boolean = it.groupBy(identity).values.forall(_.size == 1)

def hasDistinctTopicIds: WorkshopComboCandidate => Boolean = extract(_.workshop.topicId).andThen(areDistinct)
def hasDistinctTopicIds: WorkshopComboCandidate => Boolean = extract(_.topicId).andThen(areDistinct)

def hasDistinctTimeslots: WorkshopComboCandidate => Boolean = extract(_.workshop.timeSlot).andThen(areDistinct)
def hasDistinctTimeslots: WorkshopComboCandidate => Boolean = extract(_.timeSlot).andThen(areDistinct)

// Students are not allowed to get assigned a combo with all workshops of category nutrition,
// nor a combo with all workshops of category relaxation.
Expand All @@ -51,7 +51,7 @@ object Algorithm {
def hasSufficientSelectionPriority: WorkshopComboCandidate => Boolean = workshopComboCandidate =>
workshopComboCandidate
.values
.map { case WorkshopCandidate(_, _, SelectionPriority(prio)) => prio }
.map { case WorkshopCandidate(_, _, _, SelectionPriority(prio)) => prio }
.min <= 3

/**
Expand All @@ -67,9 +67,9 @@ object Algorithm {
extraPredicates.foldLeft(true) { case (result, predicate) => result && predicate(workshopComboCandidate) }
matchingWorkshops
.map { case (workshopId, selectionPriority) =>
val workshop = workshops(workshopId)
val category = topics(workshop.topicId)
workshopId -> WorkshopCandidate(workshop, category, selectionPriority)
val TopicTimeslot(topicId, timeSlot) = workshops(workshopId)
val category = topics(topicId)
workshopId -> WorkshopCandidate(topicId, timeSlot, category, selectionPriority)
}
.toSeq
.combinations(comboSize)
Expand All @@ -79,7 +79,7 @@ object Algorithm {
.filter(hasDistinctTimeslots)
.filter(extraPredicate)
.map(workshopComboCandidate =>
workshopComboCandidate.map { case (workshopId, WorkshopCandidate(_, category, selectionPriority)) =>
workshopComboCandidate.map { case (workshopId, WorkshopCandidate(_, _, category, selectionPriority)) =>
workshopId -> PossibleWorkshop(category, selectionPriority)
}
)
Expand All @@ -91,7 +91,7 @@ object Algorithm {
.mapValues(generateWorkshopCombos(workshops, topics, comboSize, hasVaryingCategories, hasSufficientSelectionPriority))
.toMap

def distributeStudentsToWorkshops(workshops: Workshops, topics: Topics, comboSize: Int)(studentsSelectedTopics: StudentsSelectedTopics): (WorkshopAssignments, Metric) = {
def distributeStudentsToWorkshops(workshops: Workshops, topics: Topics, workshopSeats: WorkshopSeats, comboSize: Int)(studentsSelectedTopics: StudentsSelectedTopics): (WorkshopAssignments, Metric) = {
val studentsWorkshopCombos = generateStudentsWorkshopCombos(workshops, topics, comboSize)(studentsSelectedTopics)
val orderedStudentsWorkshopCombos = studentsWorkshopCombos
.view
Expand All @@ -110,7 +110,9 @@ object Algorithm {

case class FilledWorkshop(freeSeats: Int, students: Set[StudentId])
type FilledWorkshops = Map[WorkshopId, FilledWorkshop]
val initialFilledWorkshops = workshops.view.mapValues(workshop => FilledWorkshop(workshop.seats, Set.empty)).toMap
val initialFilledWorkshops = workshops.toMap.map { case (workshopId, _) =>
workshopId -> FilledWorkshop(workshopSeats(workshopId).n, Set.empty)
}

var currentN = 0
val startTime = System.currentTimeMillis()
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/hcd/algorithm/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ package object algorithm {
// Aggregate types

// Collects temporarily all attributes of a workshop candidate which, if possible, could then be part of a workshop combo.
protected[algorithm] final case class WorkshopCandidate(workshop: Workshop, category: Category, selectionPriority: SelectionPriority)
protected[algorithm] final case class WorkshopCandidate(topicId: TopicId, timeSlot: TimeSlot, category: Category, selectionPriority: SelectionPriority)

// Only the attributes of a possible workshop which are relevant to the algorithm to find the perfect distribution.
protected[algorithm] final case class PossibleWorkshop(category: Category, selectionPriority: SelectionPriority)
Expand Down
17 changes: 12 additions & 5 deletions src/main/scala/hcd/models/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ package object models {
/** ID of a concrete workshop, e.g. 1 to 150. */
final case class WorkshopId(id: Int) extends AnyVal

/** Number of workshop seats, e.g. 20. */
final case class Seats(n: Int) extends AnyVal

/** Metric of a combo or distribution. */
final case class Metric(metric: Int) extends AnyVal

Expand All @@ -44,8 +47,8 @@ package object models {

// Aggregate types

/** All attributes to a concrete workshop. */
final case class Workshop(topicId: TopicId, timeSlot: TimeSlot, seats: Int)
/** A workshop is determined by a topic and a timeslot. This combination determines a concrete workshop. */
final case class TopicTimeslot(topicId: TopicId, timeSlot: TimeSlot)

// Mappings

Expand All @@ -63,10 +66,14 @@ package object models {

/**
* All the concrete workshops.
* BiMap guarantees both workshop id and workshop are unique.
* This works as a workshop is also unique by its combination of topicId and timeSlot.
* BiMap guarantees both workshop id and combination of topic id and timeslot is unique.
* That is: each topic can only exist once per timeslot, and such topic/timeslot combination is a unique concrete
* workshop.
*/
type Workshops = BiMap[WorkshopId, Workshop]
type Workshops = BiMap[WorkshopId, TopicTimeslot]

/** The seats that each workshop has. */
type WorkshopSeats = Map[WorkshopId, Seats]

/** The assignments of students to a workshop. */
type WorkshopAssignments = Map[WorkshopId, Set[StudentId]]
Expand Down
31 changes: 17 additions & 14 deletions src/test/scala/hcd/algorithm/AlgorithmSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,17 @@ class AlgorithmSpec extends AnyWordSpec with ScalaCheckDrivenPropertyChecks with

def workshops: Workshops

def workshopSeats: WorkshopSeats

// create a WorkshopComboCandidate with random SelectionPriority
def expectedWorkshopComboCandidate(wsIds: Set[Int]): WorkshopComboCandidate =
BiMap.from(
wsIds
.map(WorkshopId)
.map { workshopId =>
val workshop = workshops(workshopId)
val category = topics(workshop.topicId)
workshopId -> WorkshopCandidate(workshop, category, SelectionPriority(Random.nextInt()))
val TopicTimeslot(topicId, timeSlot) = workshops(workshopId)
val category = topics(topicId)
workshopId -> WorkshopCandidate(topicId, timeSlot, category, SelectionPriority(Random.nextInt()))
}
)

Expand All @@ -37,9 +39,9 @@ class AlgorithmSpec extends AnyWordSpec with ScalaCheckDrivenPropertyChecks with
wsIdSelPrios
.map { case (wsId, selPrio) =>
val workshopId = WorkshopId(wsId)
val workshop = workshops(workshopId)
val category = topics(workshop.topicId)
workshopId -> WorkshopCandidate(workshop, category, SelectionPriority(selPrio))
val TopicTimeslot(topicId, timeSlot) = workshops(workshopId)
val category = topics(topicId)
workshopId -> WorkshopCandidate(topicId, timeSlot, category, SelectionPriority(selPrio))
}

// create workshop combos for workshops ids, taking the selection priority from matching workshops
Expand Down Expand Up @@ -75,12 +77,12 @@ class AlgorithmSpec extends AnyWordSpec with ScalaCheckDrivenPropertyChecks with
// timeslots alter f,s,t, f,s,t, f,s,t, f,s,t, ...
override val topics: Topics = topicIds.map(topicId => topicId -> categories(topicId.id % categories.size)).toMap
override val workshops: Workshops = BiMap.from(workshopIds.map(workshopId =>
workshopId -> Workshop(
workshopId -> TopicTimeslot(
TopicId(workshopId.id / timeSlots.size),
timeSlots(workshopId.id % timeSlots.size),
noSeats
timeSlots(workshopId.id % timeSlots.size)
)
))
override val workshopSeats: WorkshopSeats = workshopIds.map(_ -> Seats(noSeats)).toMap
}

def fixtureSymmetricWorkshops(noTopics: Int): FixtureWorkshops =
Expand All @@ -96,6 +98,7 @@ class AlgorithmSpec extends AnyWordSpec with ScalaCheckDrivenPropertyChecks with
private val underlyingWorkshops = fixtureSymmetricWorkshopsFor(noTopics, noSeats)
override val topics: Topics = underlyingWorkshops.topics
override val workshops: Workshops = underlyingWorkshops.workshops
override val workshopSeats: WorkshopSeats = underlyingWorkshops.workshopSeats
lazy val studentIds: Set[StudentId] = Range(0, noStudents).toSet.map(StudentId)
lazy val selectionPriorities: Set[SelectionPriority] = Range.inclusive(1, noSelectionsPerStudent).toSet.map(SelectionPriority)

Expand All @@ -114,9 +117,9 @@ class AlgorithmSpec extends AnyWordSpec with ScalaCheckDrivenPropertyChecks with
f.topics(TopicId(0)) shouldEqual Nutrition
f.topics(TopicId(1)) shouldEqual Relaxation
f.topics(TopicId(2)) shouldEqual Sports
f.workshops(WorkshopId(0)) shouldEqual Workshop(TopicId(0), FirstTimeSlot, 20)
f.workshops(WorkshopId(4)) shouldEqual Workshop(TopicId(1), SecondTimeSlot, 20)
f.workshops(WorkshopId(8)) shouldEqual Workshop(TopicId(2), ThirdTimeSlot, 20)
f.workshops(WorkshopId(0)) shouldEqual TopicTimeslot(TopicId(0), FirstTimeSlot)
f.workshops(WorkshopId(4)) shouldEqual TopicTimeslot(TopicId(1), SecondTimeSlot)
f.workshops(WorkshopId(8)) shouldEqual TopicTimeslot(TopicId(2), ThirdTimeSlot)

// print workshops ordered by id
//f.workshops.toSeq.sortBy(_._1.id).foreach(println)
Expand All @@ -135,7 +138,7 @@ class AlgorithmSpec extends AnyWordSpec with ScalaCheckDrivenPropertyChecks with
//println(studentsWorkshopCombos.view.filterKeys(_.id < 2).toMap)

// print distributeStudentsToWorkshops for full model
lazy val (workshopAssignments, metric) = distributeStudentsToWorkshops(f.workshops, f.topics, comboSize = 3)(f.studentsSelectedTopics)
lazy val (workshopAssignments, metric) = distributeStudentsToWorkshops(f.workshops, f.topics, f.workshopSeats, comboSize = 3)(f.studentsSelectedTopics)
//println(workshopAssignments, metric)
}

Expand Down Expand Up @@ -496,7 +499,7 @@ class AlgorithmSpec extends AnyWordSpec with ScalaCheckDrivenPropertyChecks with
val studentsSelectedTopics: StudentsSelectedTopics = Map.empty
val expectedDistribution = (f.workshops.view.mapValues(_ => Set.empty).toMap, Metric(0))

distributeStudentsToWorkshops(f.workshops, f.topics, comboSize)(studentsSelectedTopics) shouldEqual expectedDistribution
distributeStudentsToWorkshops(f.workshops, f.topics, f.workshopSeats, comboSize)(studentsSelectedTopics) shouldEqual expectedDistribution
}

// "yields a valid distribution for a single student" in {
Expand Down

0 comments on commit 0477618

Please sign in to comment.