[] },
- numParticipants: { type: Number, required: true },
+ participants: { type: Array, required: true },
anyDuplicateMembershipGroups: { type: Boolean, default: false },
valid: { type: Boolean, default: true },
},
@@ -42,7 +42,7 @@ export default {
if (this.value.length < 1) {
return false
}
- return this.value.every(({ split }) => validSplit(split, this.numParticipants))
+ return this.value.every(({ split }) => validSplit(split, this.participants.length))
},
},
watch: {
diff --git a/resources/js/components/participantGroups/ParticipantGroupGenerator.vue b/resources/js/components/participantGroups/ParticipantGroupGenerator.vue
index 46a06b26..e807c87f 100644
--- a/resources/js/components/participantGroups/ParticipantGroupGenerator.vue
+++ b/resources/js/components/participantGroups/ParticipantGroupGenerator.vue
@@ -3,7 +3,7 @@
+
+
p.id).indexOf(participant.id)
+ return this.selectedParticipants.map(p => p.id)
+ .indexOf(typeof participant === 'string' ? parseInt(participant) : participant.id)
},
indexToParticipant(index) {
return this.selectedParticipants[index]
@@ -163,8 +173,7 @@ export default {
id: (Math.random() + 1).toString(36).substring(2,7),
name: this.$t('t.views.admin.participant_group_generator.default_split_name'),
groups: String(Math.ceil(this.participants.length / Math.max(1, Math.min(this.participants.length, 4)))),
- discouragedPairings: [],
- forbiddenPairings: [],
+ forbiddenPairings: [[]],
forbidMembershipGroups: '0',
}
}
@@ -190,7 +199,7 @@ export default {
...split.split,
ofSize: Math.ceil(this.selectedParticipants.length / parseInt(split.split.groups)),
discouragedPairings: this.preparePairings([
- ...split.split.discouragedPairings,
+ ...this.discouragedPairings,
...this.discouragedExistingGroups,
...(this.discourageMembershipGroups === '1' ? this.membershipGroupPairings : [])
]),
diff --git a/resources/js/components/participantGroups/geneticGolferSolver.js b/resources/js/components/participantGroups/geneticGolferSolver.js
index 1b68d20b..eac3f19d 100644
--- a/resources/js/components/participantGroups/geneticGolferSolver.js
+++ b/resources/js/components/participantGroups/geneticGolferSolver.js
@@ -190,7 +190,7 @@ function geneticGolferSolver(numParticipants, roundSpecifications, onProgress) {
let bestSecondaryScore = null
let bestTertiaryScore = null
const numRounds = roundSpecifications.length
- const numPossiblePermutations = range(1, numRounds+1).reduce((factorial, i) => factorial*i)
+ const numPossiblePermutations = range(1, numRounds+1).reduce((factorial, i) => factorial*i, 1)
const numTriedPermutations = Math.min(MAX_PERMUTATIONS_TO_TRY, numPossiblePermutations)
for (let permutation of permute(range(numRounds))) {
// randomly sample whether to skip this permutation
@@ -203,9 +203,9 @@ function geneticGolferSolver(numParticipants, roundSpecifications, onProgress) {
// run the genetic algorithm
const result = optimizeOrderedRounds(reorderedRounds)
// record the best result (lower score is better)
- const totalScore = result.roundScores.reduce((sum, score) => sum + score)
+ const totalScore = result.roundScores.reduce((sum, score) => sum + score, 0)
const secondaryScore = -result.roundsWithoutViolations // with the number of rounds without violations, higher is better
- const tertiaryScore = result.roundScores.filter(score => score < Infinity).reduce((sum, score) => sum + score)
+ const tertiaryScore = result.roundScores.filter(score => score < Infinity).reduce((sum, score) => sum + score, 0)
if (bestScore === null ||
totalScore < bestScore ||
(totalScore === bestScore && secondaryScore < bestSecondaryScore) ||
From 32bde51e5de210db94187d99c0862bf37be92952 Mon Sep 17 00:00:00 2001
From: Carlo Beltrame
Date: Wed, 28 Dec 2022 15:45:31 +0100
Subject: [PATCH 058/114] Allow specifying encouraged groupings
---
lang/de/t.php | 1 +
.../participantGroups/InputGroupSplit.vue | 9 +++++++
.../ParticipantGroupGenerator.vue | 6 ++++-
.../participantGroups/geneticGolferSolver.js | 26 +++++++++++++++----
4 files changed, 36 insertions(+), 6 deletions(-)
diff --git a/lang/de/t.php b/lang/de/t.php
index bdfe095f..4c308547 100644
--- a/lang/de/t.php
+++ b/lang/de/t.php
@@ -379,6 +379,7 @@
"select_all" => "Alle auswählen",
"split" => array(
"conditions" => "Erweiterte Bedingungen",
+ "encouraged_pairings" => "Folgende TN wenn möglich zusammen in eine Gruppe einteilen",
"enter_number_of_groups" => "Bitte Gruppenanzahl eingeben",
"forbidden_pairings" => "Folgende TN trennen",
"forbid_membership_groups" => "Abteilungen unbedingt durchmischen",
diff --git a/resources/js/components/participantGroups/InputGroupSplit.vue b/resources/js/components/participantGroups/InputGroupSplit.vue
index 3aa542e3..ee240d8e 100644
--- a/resources/js/components/participantGroups/InputGroupSplit.vue
+++ b/resources/js/components/participantGroups/InputGroupSplit.vue
@@ -53,6 +53,15 @@
display-field="scout_name"
narrow-form
multiple>
+
+
diff --git a/resources/js/components/participantGroups/ParticipantGroupGenerator.vue b/resources/js/components/participantGroups/ParticipantGroupGenerator.vue
index e807c87f..4b6e39a1 100644
--- a/resources/js/components/participantGroups/ParticipantGroupGenerator.vue
+++ b/resources/js/components/participantGroups/ParticipantGroupGenerator.vue
@@ -173,8 +173,9 @@ export default {
id: (Math.random() + 1).toString(36).substring(2,7),
name: this.$t('t.views.admin.participant_group_generator.default_split_name'),
groups: String(Math.ceil(this.participants.length / Math.max(1, Math.min(this.participants.length, 4)))),
- forbiddenPairings: [[]],
forbidMembershipGroups: '0',
+ forbiddenPairings: [[]],
+ encouragedPairings: [[]],
}
}
},
@@ -207,6 +208,9 @@ export default {
...split.split.forbiddenPairings,
...(split.split.forbidMembershipGroups === '1' ? this.membershipGroupPairings : []),
]),
+ encouragedPairings: this.preparePairings([
+ ...split.split.encouragedPairings,
+ ])
})),
})
},
diff --git a/resources/js/components/participantGroups/geneticGolferSolver.js b/resources/js/components/participantGroups/geneticGolferSolver.js
index eac3f19d..9fe5672d 100644
--- a/resources/js/components/participantGroups/geneticGolferSolver.js
+++ b/resources/js/components/participantGroups/geneticGolferSolver.js
@@ -14,7 +14,7 @@ function geneticGolferSolver(numParticipants, roundSpecifications, onProgress) {
function score(round, weights) {
const groupScores = round.map(group => {
let groupCost = 0
- forEachPair(group, (a, b) => groupCost += Math.pow(weights[a][b], 2))
+ forEachPair(group, (a, b) => groupCost += Math.sign(weights[a][b]) * Math.pow(weights[a][b], 2))
return groupCost
})
return {
@@ -24,6 +24,14 @@ function geneticGolferSolver(numParticipants, roundSpecifications, onProgress) {
}
}
+ function potentialFor(group, weights) {
+ return -group.map(member => {
+ return weights[member]
+ .filter(index => !group.includes(index))
+ .reduce((sum, weight) => sum + Math.sign(weight) * Math.pow(weight, 2), 0)
+ }).reduce((sum, memberSum) => sum + memberSum, 0)
+ }
+
function generatePermutation({ groups, ofSize }) {
const people = shuffle(range(groups * ofSize))
return range(groups).map(i =>
@@ -37,8 +45,8 @@ function geneticGolferSolver(numParticipants, roundSpecifications, onProgress) {
const { ofSize } = roundSpecification
const mutations = []
candidates.forEach(candidate => {
- const scoredGroups = candidate.groups.map((g, i) => ({group: g, score: candidate.groupsScores[i]}))
- const sortedScoredGroups = sortBy(scoredGroups, sg => sg.score).reverse()
+ const scoredGroups = candidate.groups.map((g, i) => ({group: g, score: candidate.groupsScores[i], potential: potentialFor(g, weights)}))
+ const sortedScoredGroups = sortBy(scoredGroups, ['score', 'potential']).reverse()
const sorted = sortedScoredGroups.map(ssg => ssg.group)
// Always push the original candidate back onto the list
@@ -75,7 +83,7 @@ function geneticGolferSolver(numParticipants, roundSpecifications, onProgress) {
}
}
- function createWeights({ groups, ofSize, forbiddenPairings: forbiddenPairs, discouragedPairings: discouragedGroups }) {
+ function createWeights({ groups, ofSize, forbiddenPairings: forbiddenPairs, discouragedPairings: discouragedGroups, encouragedPairings: encouragedPairs }) {
const totalSize = groups * ofSize
const weights = range(totalSize).map(() => range(totalSize).fill(0))
@@ -101,6 +109,14 @@ function geneticGolferSolver(numParticipants, roundSpecifications, onProgress) {
})
})
+ // Encouraged pairings override all previous discouragement and even make it beneficial to pair the participants
+ encouragedPairs.forEach(group => {
+ forEachPair(group, (a, b) => {
+ if (a >= totalSize || b >= totalSize) return
+ weights[a][b] = weights[b][a] = -1
+ })
+ })
+
return weights
}
@@ -126,7 +142,7 @@ function geneticGolferSolver(numParticipants, roundSpecifications, onProgress) {
rounds.forEach(previousRound => updateWeights(previousRound, weights))
let topOptions = range(5).map(() => score(generatePermutation(roundSpecification), weights))
let generation = 0
- while (generation < GENERATIONS && topOptions[0].total > 0) {
+ while (generation < GENERATIONS) {
const candidates = generateMutations(topOptions, weights, roundSpecification)
let sorted = sortBy(candidates, c => c.total)
const bestScore = sorted[0].total
From a0e30ba4aea2a352642fdf2c786c48f4c60af948 Mon Sep 17 00:00:00 2001
From: Carlo Beltrame
Date: Wed, 28 Dec 2022 16:11:32 +0100
Subject: [PATCH 059/114] Order groups by the first participant, for better
comparability of multiple runs
---
.../js/components/participantGroups/geneticGolferSolver.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/resources/js/components/participantGroups/geneticGolferSolver.js b/resources/js/components/participantGroups/geneticGolferSolver.js
index 9fe5672d..c2f8e24c 100644
--- a/resources/js/components/participantGroups/geneticGolferSolver.js
+++ b/resources/js/components/participantGroups/geneticGolferSolver.js
@@ -126,8 +126,8 @@ function geneticGolferSolver(numParticipants, roundSpecifications, onProgress) {
.map(group => group.filter(participant => participant < numParticipants))
// Sort the participants inside the groups in their original ordering
.map(group => sortBy(group)),
- // Sort smaller groups to the back
- (group) => -group.length
+ // Sort smaller groups to the back, and then sort alphabetically by the first group member
+ [(group => -group.length), (group => group[0])]
)
}
From acfed5cc9a82e6a60b02ce536a5fb733811b9314 Mon Sep 17 00:00:00 2001
From: Carlo Beltrame
Date: Wed, 28 Dec 2022 16:11:53 +0100
Subject: [PATCH 060/114] Performance optimization in a simpler case
---
.../js/components/participantGroups/geneticGolferSolver.js | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/resources/js/components/participantGroups/geneticGolferSolver.js b/resources/js/components/participantGroups/geneticGolferSolver.js
index c2f8e24c..885e814d 100644
--- a/resources/js/components/participantGroups/geneticGolferSolver.js
+++ b/resources/js/components/participantGroups/geneticGolferSolver.js
@@ -142,7 +142,9 @@ function geneticGolferSolver(numParticipants, roundSpecifications, onProgress) {
rounds.forEach(previousRound => updateWeights(previousRound, weights))
let topOptions = range(5).map(() => score(generatePermutation(roundSpecification), weights))
let generation = 0
- while (generation < GENERATIONS) {
+ // We can exit early only if there are no encouraged pairings, because in that case, there can't be any
+ // negative weights
+ while (generation < GENERATIONS && (roundSpecification.encouragedPairings.length || topOptions[0].total > 0)) {
const candidates = generateMutations(topOptions, weights, roundSpecification)
let sorted = sortBy(candidates, c => c.total)
const bestScore = sorted[0].total
From de223e925ae5091877989e995905cb2eb69b0685 Mon Sep 17 00:00:00 2001
From: Carlo Beltrame
Date: Wed, 28 Dec 2022 18:09:09 +0100
Subject: [PATCH 061/114] Improve texts to help users understand the
encouraging / discouraging features better
---
lang/de/t.php | 8 +-
.../components/form/InputMultiMultiSelect.vue | 71 +++++++-----------
.../InputMultiMultiSelectEntry.vue | 75 +++++++++++++++++++
.../participantGroups/InputGroupSplit.vue | 4 +
resources/js/mixins/input.js | 2 +-
5 files changed, 110 insertions(+), 50 deletions(-)
create mode 100644 resources/js/components/form/inputMultiMultiSelect/InputMultiMultiSelectEntry.vue
diff --git a/lang/de/t.php b/lang/de/t.php
index 4c308547..86ab3c2a 100644
--- a/lang/de/t.php
+++ b/lang/de/t.php
@@ -366,7 +366,7 @@
"default_split_name" => "Arbeitsgruppe",
"discourage_existing_participant_groups" => "Überschneidungen mit bestehenden TN-Gruppen vermeiden",
"discourage_membership_groups" => "Abteilungs-durchmischte Gruppen generell bevorzugen",
- "discouraged_pairings" => "Folgende TN nach Möglichkeit trennen",
+ "discouraged_pairings" => "Folgende TN-Kombinationen nach Möglichkeit trennen",
"group_name" => "Gruppenname für :name nummer :number",
"group_splits" => "Gruppenaufteilungen",
"how_to_avoid_overlap" => array(
@@ -378,15 +378,17 @@
"participants" => "Zu gruppierende TN",
"select_all" => "Alle auswählen",
"split" => array(
+ "add_pairing" => "Weitere TN-Kombination hinzufügen",
"conditions" => "Erweiterte Bedingungen",
- "encouraged_pairings" => "Folgende TN wenn möglich zusammen in eine Gruppe einteilen",
+ "encouraged_pairings" => "Folgende TN-Kombinationen möglichst zusammen in eine Gruppe einteilen",
"enter_number_of_groups" => "Bitte Gruppenanzahl eingeben",
- "forbidden_pairings" => "Folgende TN trennen",
+ "forbidden_pairings" => "Folgende TN-Kombinationen trennen",
"forbid_membership_groups" => "Abteilungen unbedingt durchmischen",
"groups" => "Anzahl Gruppen",
"name" => "Bezeichnung",
"of_size" => "Gruppen mit je :size TN",
"of_size_between" => "Gruppen mit je :min-:max TN",
+ "select_multiple_participants" => "Wähle mehrere TN aus, sonst hat diese Kombination keine Wirkung."
),
),
"feedbacks" => array(
diff --git a/resources/js/components/form/InputMultiMultiSelect.vue b/resources/js/components/form/InputMultiMultiSelect.vue
index 23409fa2..e18e688f 100644
--- a/resources/js/components/form/InputMultiMultiSelect.vue
+++ b/resources/js/components/form/InputMultiMultiSelect.vue
@@ -3,72 +3,51 @@
-
- onInput(index, val)"
- v-bind="$attrs">
-
-
-
-
-
-
-
-
-
-
-
+ :name="`${name}${index}`"
+ :label="label"
+ :array-value.sync="currentValue[index]"
+ :class="{ 'is-invalid': errorMessage }"
+ :require-multiple="requireMultiple"
+ @remove="currentValue.splice(index, 1)"
+ v-bind="$attrs">
+
+
+
+
+
+
+
-
- error: {{ errorMessage }}
+
+ {{ errorMessage }}
{{ $t('t.global.add_more') }}
+ @click="currentValue.push([])"> {{ addMoreLabel }}
diff --git a/resources/js/components/form/inputMultiMultiSelect/InputMultiMultiSelectEntry.vue b/resources/js/components/form/inputMultiMultiSelect/InputMultiMultiSelectEntry.vue
new file mode 100644
index 00000000..32568fa9
--- /dev/null
+++ b/resources/js/components/form/inputMultiMultiSelect/InputMultiMultiSelectEntry.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
diff --git a/resources/js/components/participantGroups/InputGroupSplit.vue b/resources/js/components/participantGroups/InputGroupSplit.vue
index ee240d8e..ab142f2e 100644
--- a/resources/js/components/participantGroups/InputGroupSplit.vue
+++ b/resources/js/components/participantGroups/InputGroupSplit.vue
@@ -49,18 +49,22 @@
:name="`group-split-${value.id}-forbidden-pairings`"
v-model="value.forbiddenPairings"
:label="$t('t.views.admin.participant_group_generator.split.forbidden_pairings')"
+ :add-more-label="$t('t.views.admin.participant_group_generator.split.add_pairing')"
:options="participants"
display-field="scout_name"
narrow-form
+ :require-multiple="$t('t.views.admin.participant_group_generator.split.select_multiple_participants')"
multiple>
diff --git a/resources/js/mixins/input.js b/resources/js/mixins/input.js
index 0cb46c7b..98d5ac3d 100644
--- a/resources/js/mixins/input.js
+++ b/resources/js/mixins/input.js
@@ -14,7 +14,7 @@ export default {
computed: {
errorMessage() {
const errors = window.Laravel.errors[this.name]
- return errors && errors.length ? errors[0] : undefined;
+ return errors && errors.length ? errors[0] : undefined
},
labelClass() {
return this.narrowForm ? 'col-12' : 'col-md-3 text-md-right'
From 5754ff4744d409eda0728bf1719b383de3ef92db Mon Sep 17 00:00:00 2001
From: Carlo Beltrame