group-planner 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1599 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/constants.ts
9
+ var EQUIPMENT_CLASSES = [
10
+ "HB",
11
+ // Heavy Bow
12
+ "LB",
13
+ // Light Bow
14
+ "TR",
15
+ // Traditional Recurve
16
+ "BHR",
17
+ // Barebow Heavy Recurve
18
+ "BBR",
19
+ // Barebow Basic Recurve
20
+ "FSR",
21
+ // Freestyle Recurve
22
+ "BBC",
23
+ // Barebow Compound
24
+ "BL",
25
+ // Barebow Longbow
26
+ "BU",
27
+ // Barebow Unlimited
28
+ "FSC",
29
+ // Freestyle Compound
30
+ "FU"
31
+ // Freestyle Unlimited
32
+ ];
33
+ var AGE_GROUPS = [
34
+ "C",
35
+ // Children
36
+ "J",
37
+ // Juniors
38
+ "YA",
39
+ // Young Adults
40
+ "A",
41
+ // Adults
42
+ "V",
43
+ // Veterans
44
+ "S"
45
+ // Seniors
46
+ ];
47
+ var GENDERS = [
48
+ "F",
49
+ // Female
50
+ "M"
51
+ // Male
52
+ ];
53
+ var EQUIPMENT_CLASS_DESCRIPTIONS = {
54
+ "HB": "Heavy Bow",
55
+ "LB": "Light Bow",
56
+ "TR": "Traditional Recurve",
57
+ "BHR": "Barebow Heavy Recurve",
58
+ "BBR": "Barebow Basic Recurve",
59
+ "FSR": "Freestyle Recurve",
60
+ "BBC": "Barebow Compound",
61
+ "BL": "Barebow Longbow",
62
+ "BU": "Barebow Unlimited",
63
+ "FSC": "Freestyle Compound",
64
+ "FU": "Freestyle Unlimited"
65
+ };
66
+ var AGE_GROUP_DESCRIPTIONS = {
67
+ "C": "Children",
68
+ "J": "Juniors",
69
+ "YA": "Young Adults",
70
+ "A": "Adults",
71
+ "V": "Veterans",
72
+ "S": "Seniors"
73
+ };
74
+ var GENDER_DESCRIPTIONS = {
75
+ "F": "Female",
76
+ "M": "Male"
77
+ };
78
+ function isValidEquipmentClass(value, allowed) {
79
+ return (allowed ?? EQUIPMENT_CLASSES).includes(value);
80
+ }
81
+ function isValidAgeGroup(value, allowed) {
82
+ return (allowed ?? AGE_GROUPS).includes(value);
83
+ }
84
+ function isValidGender(value, allowed) {
85
+ return (allowed ?? GENDERS).includes(value);
86
+ }
87
+ function getAgeGroupOrder(ageGroup) {
88
+ return AGE_GROUPS.indexOf(ageGroup);
89
+ }
90
+ function compareAgeGroups(a, b) {
91
+ return getAgeGroupOrder(a) - getAgeGroupOrder(b);
92
+ }
93
+
94
+ // src/subsetSize.ts
95
+ var MINIMUM_SUBSET_SIZE = 3;
96
+ function smallestAllowedSubset(requestedMin) {
97
+ return Math.max(MINIMUM_SUBSET_SIZE, requestedMin);
98
+ }
99
+ function isLinked(competitor, group) {
100
+ if (competitor.guardianId && group.some((member) => member.id === competitor.guardianId)) {
101
+ return true;
102
+ }
103
+ return group.some((member) => member.guardianId === competitor.id);
104
+ }
105
+ function takeUnlinked(group) {
106
+ for (let index = group.length - 1; index >= 0; index--) {
107
+ const competitor = group[index];
108
+ if (!isLinked(competitor, group)) {
109
+ group.splice(index, 1);
110
+ return competitor;
111
+ }
112
+ }
113
+ return void 0;
114
+ }
115
+ function undoMoves(short, moved) {
116
+ for (const move of moved.reverse()) {
117
+ const index = short.indexOf(move.person);
118
+ if (index >= 0) {
119
+ short.splice(index, 1);
120
+ }
121
+ move.donor.push(move.person);
122
+ }
123
+ }
124
+ function fillFromDonors(groups, shortIndex, min, max) {
125
+ const short = groups[shortIndex];
126
+ const need = min - short.length;
127
+ if (need <= 0 || short.length + need > max) {
128
+ return false;
129
+ }
130
+ const moved = [];
131
+ let remaining = need;
132
+ for (const group of groups) {
133
+ remaining = takeFromDonor(group, short, remaining, min, moved);
134
+ }
135
+ if (remaining === 0) {
136
+ return true;
137
+ }
138
+ undoMoves(short, moved);
139
+ return false;
140
+ }
141
+ function takeFromDonor(group, short, remaining, min, moved) {
142
+ let left = remaining;
143
+ while (left > 0 && group !== short && group.length > min) {
144
+ const person = takeUnlinked(group);
145
+ if (!person) {
146
+ return left;
147
+ }
148
+ short.push(person);
149
+ moved.push({ person, donor: group });
150
+ left -= 1;
151
+ }
152
+ return left;
153
+ }
154
+ function mergeIntoHost(groups, shortIndex, min, max) {
155
+ const short = groups[shortIndex];
156
+ const hostIndex = groups.findIndex((group, index) => {
157
+ if (index === shortIndex) {
158
+ return false;
159
+ }
160
+ const combined = group.length + short.length;
161
+ return combined >= min && combined <= max;
162
+ });
163
+ if (hostIndex < 0) {
164
+ return false;
165
+ }
166
+ groups[hostIndex].push(...short);
167
+ groups.splice(shortIndex, 1);
168
+ return true;
169
+ }
170
+ function absorbShortGroup(groups, min, max) {
171
+ const shortIndex = groups.findIndex((group) => group.length < min);
172
+ if (shortIndex < 0) {
173
+ return false;
174
+ }
175
+ if (fillFromDonors(groups, shortIndex, min, max)) {
176
+ return true;
177
+ }
178
+ if (mergeIntoHost(groups, shortIndex, min, max)) {
179
+ return true;
180
+ }
181
+ groups.splice(shortIndex, 1);
182
+ return true;
183
+ }
184
+ function retainLegalSubsets(subsets, min, max) {
185
+ const floor = smallestAllowedSubset(min);
186
+ const groups = subsets.map((subset) => [...subset]).filter((subset) => subset.length > 0);
187
+ let progressed = true;
188
+ let steps = 0;
189
+ const stepLimit = groups.length * groups.length + groups.length + 1;
190
+ while (progressed && steps < stepLimit) {
191
+ steps += 1;
192
+ progressed = absorbShortGroup(groups, floor, max);
193
+ }
194
+ return groups.filter((group) => group.length >= floor);
195
+ }
196
+
197
+ // src/utils.ts
198
+ var DEFAULT_OPTIONS = {
199
+ algorithm: "auto",
200
+ groupByEquipmentClass: true,
201
+ maxSubsets: 28,
202
+ minSubsetSize: 3,
203
+ maxSubsetSize: 6,
204
+ preferredSubsetSize: 4,
205
+ genderWeight: 1,
206
+ ageCategoryWeight: 1,
207
+ equipmentClassWeight: 1,
208
+ closedGroupWeight: 0
209
+ };
210
+ function validatePartition(competitors, subsets, options = {}) {
211
+ const opts = { ...DEFAULT_OPTIONS, ...options };
212
+ const errors = [];
213
+ const assignedCompetitors = /* @__PURE__ */ new Set();
214
+ for (const subset of subsets) {
215
+ for (const competitor of subset) {
216
+ if (assignedCompetitors.has(competitor.id)) {
217
+ errors.push(`Competitor ${competitor.id} assigned to multiple subsets`);
218
+ }
219
+ assignedCompetitors.add(competitor.id);
220
+ }
221
+ }
222
+ if (assignedCompetitors.size !== competitors.length) {
223
+ errors.push(`Not all competitors assigned: ${assignedCompetitors.size}/${competitors.length}`);
224
+ }
225
+ for (let i = 0; i < subsets.length; i++) {
226
+ const size = subsets[i].length;
227
+ const minimum = smallestAllowedSubset(opts.minSubsetSize);
228
+ if (size < minimum) {
229
+ errors.push(`Subset ${i} too small: ${size} < ${minimum}`);
230
+ }
231
+ if (size > opts.maxSubsetSize) {
232
+ errors.push(`Subset ${i} too large: ${size} > ${opts.maxSubsetSize}`);
233
+ }
234
+ }
235
+ if (subsets.length > opts.maxSubsets) {
236
+ errors.push(`Too many subsets: ${subsets.length} > ${opts.maxSubsets}`);
237
+ }
238
+ const guardianValidation = validateGuardianConstraints(competitors, subsets);
239
+ errors.push(...guardianValidation.errors);
240
+ return { valid: errors.length === 0, errors };
241
+ }
242
+ function mean(total, count) {
243
+ return count === 0 ? 0 : total / count;
244
+ }
245
+ function countLabels(subset, label) {
246
+ const counts = /* @__PURE__ */ new Map();
247
+ for (const competitor of subset) {
248
+ const key = label(competitor);
249
+ counts.set(key, (counts.get(key) ?? 0) + 1);
250
+ }
251
+ return counts;
252
+ }
253
+ function largestShare(counts, size) {
254
+ return Math.max(...counts.values()) / size;
255
+ }
256
+ function evenShare(counts, size) {
257
+ let even = 0;
258
+ for (const count of counts.values()) {
259
+ if (count % 2 === 0) {
260
+ even += count;
261
+ }
262
+ }
263
+ return even / size;
264
+ }
265
+ function equipmentPoints(subset, options) {
266
+ const counts = countLabels(subset, (competitor) => competitor.equipmentClass);
267
+ const size = subset.length;
268
+ if (options.groupByEquipmentClass) {
269
+ return largestShare(counts, size) * 10 * options.equipmentClassWeight;
270
+ }
271
+ return counts.size / Math.min(size, 3) * 10 * options.equipmentClassWeight;
272
+ }
273
+ function scoreSubset(subset, options = {}) {
274
+ if (subset.length === 0) {
275
+ return { size: 0, closedGroup: 0, gender: 0, ageCategory: 0, equipmentClass: 0, total: 0 };
276
+ }
277
+ const opts = { ...DEFAULT_OPTIONS, ...options };
278
+ const size = subset.length;
279
+ const sizeScore = Math.max(0, 10 - Math.abs(size - opts.preferredSubsetSize) * 2);
280
+ const closedGroup = size === opts.preferredSubsetSize ? 10 * opts.closedGroupWeight : 0;
281
+ const gender = evenShare(countLabels(subset, (competitor) => competitor.gender), size) * 10 * opts.genderWeight;
282
+ const ageCategory = largestShare(countLabels(subset, (competitor) => competitor.ageCategory), size) * 10 * opts.ageCategoryWeight;
283
+ const equipmentClass = equipmentPoints(subset, opts);
284
+ return {
285
+ size: sizeScore,
286
+ closedGroup,
287
+ gender,
288
+ ageCategory,
289
+ equipmentClass,
290
+ total: sizeScore + closedGroup + gender + ageCategory + equipmentClass
291
+ };
292
+ }
293
+ function calculateScore(subsets, options = {}) {
294
+ const totals = { size: 0, closedGroup: 0, gender: 0, ageCategory: 0, equipmentClass: 0 };
295
+ for (const subset of subsets) {
296
+ const parts = scoreSubset(subset, options);
297
+ totals.size += parts.size;
298
+ totals.closedGroup += parts.closedGroup;
299
+ totals.gender += parts.gender;
300
+ totals.ageCategory += parts.ageCategory;
301
+ totals.equipmentClass += parts.equipmentClass;
302
+ }
303
+ const breakdown = {
304
+ sizeScore: mean(totals.size, subsets.length),
305
+ closedGroupScore: mean(totals.closedGroup, subsets.length),
306
+ genderScore: mean(totals.gender, subsets.length),
307
+ ageCategoryScore: mean(totals.ageCategory, subsets.length),
308
+ equipmentClassScore: mean(totals.equipmentClass, subsets.length)
309
+ };
310
+ const score = breakdown.sizeScore + breakdown.closedGroupScore + breakdown.genderScore + breakdown.ageCategoryScore + breakdown.equipmentClassScore;
311
+ return { score, breakdown };
312
+ }
313
+ function createPartitionResult(subsets, options = {}) {
314
+ const totalCompetitors = subsets.reduce((sum, subset) => sum + subset.length, 0);
315
+ const { score, breakdown } = calculateScore(subsets, options);
316
+ return {
317
+ subsets,
318
+ score,
319
+ metadata: {
320
+ totalCompetitors,
321
+ totalSubsets: subsets.length,
322
+ averageSubsetSize: subsets.length === 0 ? 0 : totalCompetitors / subsets.length,
323
+ scoreBreakdown: breakdown
324
+ }
325
+ };
326
+ }
327
+ function getGuardianGroups(competitors) {
328
+ const guardianGroups = /* @__PURE__ */ new Map();
329
+ for (const competitor of competitors) {
330
+ if (competitor.guardianId) {
331
+ const key = competitor.guardianId;
332
+ if (!guardianGroups.has(key)) {
333
+ guardianGroups.set(key, []);
334
+ }
335
+ guardianGroups.get(key).push(competitor);
336
+ const guardian = competitors.find((c) => c.id === competitor.guardianId);
337
+ if (guardian && !guardianGroups.get(key).includes(guardian)) {
338
+ guardianGroups.get(key).push(guardian);
339
+ }
340
+ }
341
+ }
342
+ return guardianGroups;
343
+ }
344
+ function createGuardianFamilies(competitors) {
345
+ const families = [];
346
+ const processed = /* @__PURE__ */ new Set();
347
+ const guardianToDependents = /* @__PURE__ */ new Map();
348
+ const dependentToGuardian = /* @__PURE__ */ new Map();
349
+ for (const competitor of competitors) {
350
+ if (competitor.guardianId) {
351
+ if (!guardianToDependents.has(competitor.guardianId)) {
352
+ guardianToDependents.set(competitor.guardianId, []);
353
+ }
354
+ guardianToDependents.get(competitor.guardianId).push(competitor);
355
+ dependentToGuardian.set(competitor.id, competitor.guardianId);
356
+ }
357
+ }
358
+ for (const competitor of competitors) {
359
+ if (processed.has(competitor.id)) {
360
+ continue;
361
+ }
362
+ if (guardianToDependents.has(competitor.id)) {
363
+ const family = [competitor, ...guardianToDependents.get(competitor.id)];
364
+ families.push(family);
365
+ for (const member of family) {
366
+ processed.add(member.id);
367
+ }
368
+ } else if (dependentToGuardian.has(competitor.id)) {
369
+ continue;
370
+ } else {
371
+ families.push([competitor]);
372
+ processed.add(competitor.id);
373
+ }
374
+ }
375
+ return families;
376
+ }
377
+ function validateGuardianConstraints(competitors, subsets) {
378
+ const errors = [];
379
+ const competitorToSubset = /* @__PURE__ */ new Map();
380
+ for (let subsetIndex = 0; subsetIndex < subsets.length; subsetIndex++) {
381
+ for (const competitor of subsets[subsetIndex]) {
382
+ competitorToSubset.set(competitor.id, subsetIndex);
383
+ }
384
+ }
385
+ for (const competitor of competitors) {
386
+ if (competitor.guardianId) {
387
+ const competitorSubset = competitorToSubset.get(competitor.id);
388
+ const guardianSubset = competitorToSubset.get(competitor.guardianId);
389
+ if (competitorSubset === void 0) {
390
+ errors.push(`Competitor ${competitor.id} not assigned to any subset`);
391
+ continue;
392
+ }
393
+ if (guardianSubset === void 0) {
394
+ errors.push(`Guardian ${competitor.guardianId} for competitor ${competitor.id} not found in any subset`);
395
+ continue;
396
+ }
397
+ if (competitorSubset !== guardianSubset) {
398
+ errors.push(`Competitor ${competitor.id} and their guardian ${competitor.guardianId} are in different subsets (${competitorSubset} vs ${guardianSubset})`);
399
+ }
400
+ }
401
+ }
402
+ return { valid: errors.length === 0, errors };
403
+ }
404
+ function shuffle(array) {
405
+ const result = [...array];
406
+ for (let i = result.length - 1; i > 0; i--) {
407
+ const j = Math.floor(Math.random() * (i + 1));
408
+ [result[i], result[j]] = [result[j], result[i]];
409
+ }
410
+ return result;
411
+ }
412
+ function isRecord(value) {
413
+ return typeof value === "object" && value !== null;
414
+ }
415
+ function isValidCompetitor(competitor, space) {
416
+ if (!isRecord(competitor)) {
417
+ return false;
418
+ }
419
+ const { id, equipmentClass, ageCategory, gender, guardianId } = competitor;
420
+ if (typeof id !== "string" || id.trim().length === 0) {
421
+ return false;
422
+ }
423
+ if (typeof equipmentClass !== "string" || !isValidEquipmentClass(equipmentClass, space?.equipmentClasses)) {
424
+ return false;
425
+ }
426
+ if (typeof ageCategory !== "string" || !isValidAgeGroup(ageCategory, space?.ageCategories)) {
427
+ return false;
428
+ }
429
+ if (typeof gender !== "string" || !isValidGender(gender, space?.genders)) {
430
+ return false;
431
+ }
432
+ if (guardianId !== void 0 && (typeof guardianId !== "string" || guardianId.trim().length === 0)) {
433
+ return false;
434
+ }
435
+ return true;
436
+ }
437
+ function validateCompetitors(competitors, space) {
438
+ const errors = [];
439
+ if (!Array.isArray(competitors)) {
440
+ errors.push("Competitors must be an array");
441
+ return { valid: false, errors };
442
+ }
443
+ if (competitors.length === 0) {
444
+ errors.push("At least one competitor is required");
445
+ return { valid: false, errors };
446
+ }
447
+ if (competitors.length > 168) {
448
+ errors.push("Maximum 168 competitors allowed");
449
+ }
450
+ const competitorIds = /* @__PURE__ */ new Set();
451
+ competitors.forEach((competitor, index) => {
452
+ if (!isValidCompetitor(competitor, space)) {
453
+ errors.push(`Competitor at index ${index} is invalid`);
454
+ return;
455
+ }
456
+ if (competitorIds.has(competitor.id)) {
457
+ errors.push(`Duplicate competitor ID: ${competitor.id}`);
458
+ } else {
459
+ competitorIds.add(competitor.id);
460
+ }
461
+ });
462
+ competitors.forEach((competitor) => {
463
+ if (competitor.guardianId && !competitors.some((c) => c.id === competitor.guardianId)) {
464
+ errors.push(`Guardian ${competitor.guardianId} for competitor ${competitor.id} not found`);
465
+ }
466
+ });
467
+ const visited = /* @__PURE__ */ new Set();
468
+ const recursionStack = /* @__PURE__ */ new Set();
469
+ const hasCircularReference = (competitorId) => {
470
+ if (recursionStack.has(competitorId)) {
471
+ return true;
472
+ }
473
+ if (visited.has(competitorId)) {
474
+ return false;
475
+ }
476
+ visited.add(competitorId);
477
+ recursionStack.add(competitorId);
478
+ const competitor = competitors.find((c) => c.id === competitorId);
479
+ if (competitor?.guardianId) {
480
+ if (hasCircularReference(competitor.guardianId)) {
481
+ return true;
482
+ }
483
+ }
484
+ recursionStack.delete(competitorId);
485
+ return false;
486
+ };
487
+ for (const competitor of competitors) {
488
+ if (hasCircularReference(competitor.id)) {
489
+ errors.push(`Circular guardian reference detected involving competitor ${competitor.id}`);
490
+ break;
491
+ }
492
+ }
493
+ const guardianFamilySizes = /* @__PURE__ */ new Map();
494
+ competitors.forEach((competitor) => {
495
+ if (competitor.guardianId) {
496
+ guardianFamilySizes.set(competitor.guardianId, (guardianFamilySizes.get(competitor.guardianId) || 1) + 1);
497
+ } else {
498
+ if (!guardianFamilySizes.has(competitor.id)) {
499
+ guardianFamilySizes.set(competitor.id, 1);
500
+ }
501
+ }
502
+ });
503
+ guardianFamilySizes.forEach((familySize, guardianId) => {
504
+ if (familySize > 3) {
505
+ errors.push(`Family with guardian ${guardianId} has ${familySize} members, maximum allowed is 3 (1 guardian + 2 dependents)`);
506
+ }
507
+ });
508
+ return { valid: errors.length === 0, errors };
509
+ }
510
+
511
+ // src/common/options-validator.ts
512
+ function validateAndSanitizeOptions(options, competitorCount) {
513
+ const explicitMax = options.maxSubsetSize !== void 0 && options.maxSubsetSize >= MINIMUM_SUBSET_SIZE;
514
+ const sanitizedOptions = { ...DEFAULT_OPTIONS, ...options };
515
+ swapInvertedSizes(sanitizedOptions);
516
+ applySizeFloors(sanitizedOptions);
517
+ applyDefaultFieldCap(sanitizedOptions, explicitMax, competitorCount);
518
+ clampPreferredSize(sanitizedOptions);
519
+ if (sanitizedOptions.closedGroupWeight < 0) {
520
+ sanitizedOptions.closedGroupWeight = 0;
521
+ }
522
+ return sanitizedOptions;
523
+ }
524
+ function swapInvertedSizes(options) {
525
+ if (options.minSubsetSize <= options.maxSubsetSize) {
526
+ return;
527
+ }
528
+ const previousMin = options.minSubsetSize;
529
+ options.minSubsetSize = options.maxSubsetSize;
530
+ options.maxSubsetSize = previousMin;
531
+ }
532
+ function applySizeFloors(options) {
533
+ if (options.minSubsetSize < MINIMUM_SUBSET_SIZE) {
534
+ options.minSubsetSize = MINIMUM_SUBSET_SIZE;
535
+ }
536
+ if (options.maxSubsetSize < options.minSubsetSize) {
537
+ options.maxSubsetSize = options.minSubsetSize;
538
+ }
539
+ }
540
+ function applyDefaultFieldCap(options, explicitMax, competitorCount) {
541
+ const useFieldCap = !explicitMax && competitorCount > 0 && competitorCount <= 112;
542
+ if (!useFieldCap) {
543
+ return;
544
+ }
545
+ options.maxSubsetSize = Math.min(options.maxSubsetSize, 4);
546
+ if (options.maxSubsetSize < options.minSubsetSize) {
547
+ options.maxSubsetSize = options.minSubsetSize;
548
+ }
549
+ }
550
+ function clampPreferredSize(options) {
551
+ if (options.preferredSubsetSize < options.minSubsetSize) {
552
+ options.preferredSubsetSize = options.minSubsetSize;
553
+ }
554
+ if (options.preferredSubsetSize > options.maxSubsetSize) {
555
+ options.preferredSubsetSize = options.maxSubsetSize;
556
+ }
557
+ }
558
+
559
+ // src/common/algorithm-base.ts
560
+ function handleEmptyDataset(options, algorithmName) {
561
+ const validatedOptions = validateAndSanitizeOptions(options, 0);
562
+ return {
563
+ result: createPartitionResult([], validatedOptions),
564
+ executionTime: 0,
565
+ algorithmUsed: algorithmName
566
+ };
567
+ }
568
+ function createAlgorithmResult(competitors, subsets, options, algorithmName, startTime) {
569
+ const executionTime = performance.now() - startTime;
570
+ const legalSubsets = retainLegalSubsets(subsets, options.minSubsetSize, options.maxSubsetSize);
571
+ return {
572
+ result: createPartitionResult(legalSubsets, options),
573
+ executionTime,
574
+ algorithmUsed: algorithmName
575
+ };
576
+ }
577
+
578
+ // src/algorithms/mip-solver.ts
579
+ function openSubsetFor(subsets, familySize, currentSubset, options, maxSubsets) {
580
+ const started = Math.min(currentSubset, subsets.length);
581
+ let bestIndex = -1;
582
+ let bestScore = -1;
583
+ for (let index = 0; index < started; index++) {
584
+ const available = options.maxSubsetSize - subsets[index].length;
585
+ if (available < familySize) {
586
+ continue;
587
+ }
588
+ const newSize = subsets[index].length + familySize;
589
+ const totalScore = 1e3 - Math.abs(newSize - options.preferredSubsetSize) + available;
590
+ if (totalScore > bestScore) {
591
+ bestScore = totalScore;
592
+ bestIndex = index;
593
+ }
594
+ }
595
+ if (bestIndex >= 0) {
596
+ return { index: bestIndex, nextSubset: currentSubset };
597
+ }
598
+ if (currentSubset >= maxSubsets) {
599
+ return null;
600
+ }
601
+ return { index: currentSubset, nextSubset: currentSubset + 1 };
602
+ }
603
+ var SimpleMIPSolver = class {
604
+ constructor(competitors, options) {
605
+ this.bestSolution = null;
606
+ this.bestObjective = -Infinity;
607
+ this.competitors = competitors;
608
+ this.options = validateAndSanitizeOptions(options, competitors.length);
609
+ this.numCompetitors = competitors.length;
610
+ this.maxSubsets = Math.min(this.options.maxSubsets, Math.ceil(competitors.length / this.options.minSubsetSize));
611
+ this.variables = [];
612
+ this.constraints = [];
613
+ this.initializeVariables();
614
+ this.initializeConstraints();
615
+ }
616
+ initializeVariables() {
617
+ for (let i = 0; i < this.numCompetitors; i++) {
618
+ for (let j = 0; j < this.maxSubsets; j++) {
619
+ this.variables.push({
620
+ competitorIndex: i,
621
+ subsetIndex: j,
622
+ value: 0
623
+ });
624
+ }
625
+ }
626
+ }
627
+ initializeConstraints() {
628
+ for (let i = 0; i < this.numCompetitors; i++) {
629
+ const constraint = {
630
+ variables: [],
631
+ operator: "=",
632
+ rhs: 1,
633
+ name: `competitor_${i}_assignment`
634
+ };
635
+ for (let j = 0; j < this.maxSubsets; j++) {
636
+ constraint.variables.push({
637
+ competitorIndex: i,
638
+ subsetIndex: j,
639
+ coefficient: 1
640
+ });
641
+ }
642
+ this.constraints.push(constraint);
643
+ }
644
+ for (let j = 0; j < this.maxSubsets; j++) {
645
+ const minConstraint = {
646
+ variables: [],
647
+ operator: ">=",
648
+ rhs: 0,
649
+ // Will be adjusted based on subset usage
650
+ name: `subset_${j}_min_size`
651
+ };
652
+ const maxConstraint = {
653
+ variables: [],
654
+ operator: "<=",
655
+ rhs: this.options.maxSubsetSize,
656
+ name: `subset_${j}_max_size`
657
+ };
658
+ for (let i = 0; i < this.numCompetitors; i++) {
659
+ minConstraint.variables.push({
660
+ competitorIndex: i,
661
+ subsetIndex: j,
662
+ coefficient: 1
663
+ });
664
+ maxConstraint.variables.push({
665
+ competitorIndex: i,
666
+ subsetIndex: j,
667
+ coefficient: 1
668
+ });
669
+ }
670
+ this.constraints.push(maxConstraint);
671
+ }
672
+ }
673
+ calculateObjectiveValue(solution) {
674
+ const subsets = Array(this.maxSubsets).fill(null).map(() => []);
675
+ for (const variable of solution) {
676
+ if (variable.value === 1) {
677
+ subsets[variable.subsetIndex].push(this.competitors[variable.competitorIndex]);
678
+ }
679
+ }
680
+ const nonEmptySubsets = subsets.filter((subset) => subset.length > 0);
681
+ const validation = validatePartition(this.competitors, nonEmptySubsets, this.options);
682
+ if (!validation.valid) {
683
+ return -Infinity;
684
+ }
685
+ const result = createPartitionResult(nonEmptySubsets, this.options);
686
+ return result.score;
687
+ }
688
+ isSolutionValid(solution) {
689
+ const competitorAssignments = new Array(this.numCompetitors).fill(0);
690
+ for (const variable of solution) {
691
+ if (variable.value === 1) {
692
+ competitorAssignments[variable.competitorIndex]++;
693
+ }
694
+ }
695
+ return competitorAssignments.every((count) => count === 1);
696
+ }
697
+ solveGreedy() {
698
+ const solution = this.variables.map((v) => ({ ...v, value: 0 }));
699
+ const subsets = Array(this.maxSubsets).fill(null).map(() => []);
700
+ const families = createGuardianFamilies(this.competitors);
701
+ const unassignedFamilies = [...families];
702
+ let currentSubset = 0;
703
+ while (unassignedFamilies.length > 0 && currentSubset < this.maxSubsets) {
704
+ const subset = subsets[currentSubset];
705
+ let canAddMore = true;
706
+ while (canAddMore && unassignedFamilies.length > 0) {
707
+ let bestFamilyIndex = -1;
708
+ let bestScore = -Infinity;
709
+ for (let i = 0; i < unassignedFamilies.length; i++) {
710
+ const family = unassignedFamilies[i];
711
+ if (subset.length + family.length <= this.options.maxSubsetSize) {
712
+ const testSubset = [...subset, ...family];
713
+ const testScore = this.calculateSubsetScore(testSubset);
714
+ if (testScore > bestScore) {
715
+ bestScore = testScore;
716
+ bestFamilyIndex = i;
717
+ }
718
+ }
719
+ }
720
+ if (bestFamilyIndex >= 0) {
721
+ const family = unassignedFamilies.splice(bestFamilyIndex, 1)[0];
722
+ for (const competitor of family) {
723
+ subset.push(competitor);
724
+ const competitorIndex = this.competitors.findIndex((c) => c.id === competitor.id);
725
+ const variableIndex = competitorIndex * this.maxSubsets + currentSubset;
726
+ solution[variableIndex].value = 1;
727
+ }
728
+ } else {
729
+ canAddMore = false;
730
+ }
731
+ }
732
+ if (subset.length >= this.options.minSubsetSize || !canAddMore) {
733
+ currentSubset++;
734
+ } else if (unassignedFamilies.length === 0) {
735
+ break;
736
+ }
737
+ }
738
+ while (unassignedFamilies.length > 0) {
739
+ const family = unassignedFamilies[0];
740
+ const placement = openSubsetFor(subsets, family.length, currentSubset, this.options, this.maxSubsets);
741
+ if (!placement) {
742
+ break;
743
+ }
744
+ unassignedFamilies.shift();
745
+ currentSubset = placement.nextSubset;
746
+ for (const competitor of family) {
747
+ subsets[placement.index].push(competitor);
748
+ const competitorIndex = this.competitors.findIndex((c) => c.id === competitor.id);
749
+ const variableIndex = competitorIndex * this.maxSubsets + placement.index;
750
+ solution[variableIndex].value = 1;
751
+ }
752
+ }
753
+ return solution;
754
+ }
755
+ calculateSubsetScore(subset) {
756
+ return scoreSubset(subset, this.options).total;
757
+ }
758
+ solve() {
759
+ const greedySolution = this.solveGreedy();
760
+ if (this.isSolutionValid(greedySolution)) {
761
+ const objective = this.calculateObjectiveValue(greedySolution);
762
+ if (objective > this.bestObjective) {
763
+ this.bestObjective = objective;
764
+ this.bestSolution = greedySolution;
765
+ }
766
+ }
767
+ return this.bestSolution || greedySolution;
768
+ }
769
+ getBestSolution() {
770
+ return {
771
+ solution: this.bestSolution || [],
772
+ objective: this.bestObjective
773
+ };
774
+ }
775
+ };
776
+ function solveMIP(competitors, options = {}) {
777
+ const startTime = performance.now();
778
+ if (competitors.length === 0) {
779
+ return handleEmptyDataset(options, "Mixed Integer Programming");
780
+ }
781
+ const opts = validateAndSanitizeOptions(options, competitors.length);
782
+ const solver = new SimpleMIPSolver(competitors, opts);
783
+ const solution = solver.solve();
784
+ const subsets = Array(opts.maxSubsets).fill(null).map(() => []);
785
+ for (const variable of solution) {
786
+ if (variable.value === 1) {
787
+ subsets[variable.subsetIndex].push(competitors[variable.competitorIndex]);
788
+ }
789
+ }
790
+ const nonEmptySubsets = subsets.filter((subset) => subset.length > 0);
791
+ return createAlgorithmResult(competitors, nonEmptySubsets, opts, "Mixed Integer Programming", startTime);
792
+ }
793
+
794
+ // src/algorithms/mip-solver-lp.ts
795
+ function loadHighs() {
796
+ const highs = __require("highs");
797
+ return highs();
798
+ }
799
+ var HiGHSMIPSolver = class {
800
+ constructor(competitors, options) {
801
+ this.competitors = competitors;
802
+ this.families = createGuardianFamilies(competitors);
803
+ this.options = options;
804
+ this.maxSubsets = Math.min(this.options.maxSubsets, Math.ceil(competitors.length / this.options.minSubsetSize));
805
+ }
806
+ /**
807
+ * Creates a MIP model for the HiGHS solver
808
+ */
809
+ createMIPModel() {
810
+ const model = {
811
+ sense: "maximize",
812
+ variables: [],
813
+ constraints: []
814
+ };
815
+ const variableMap = /* @__PURE__ */ new Map();
816
+ let varIndex = 0;
817
+ for (let i = 0; i < this.families.length; i++) {
818
+ for (let j = 0; j < this.maxSubsets; j++) {
819
+ const varName = `x_${i}_${j}`;
820
+ variableMap.set(varName, varIndex++);
821
+ const cost = this.calculateObjectiveCoefficient(i, j);
822
+ model.variables.push({
823
+ name: varName,
824
+ lower: 0,
825
+ upper: 1,
826
+ type: "binary",
827
+ cost
828
+ });
829
+ }
830
+ }
831
+ for (let i = 0; i < this.families.length; i++) {
832
+ const entries = [];
833
+ for (let j = 0; j < this.maxSubsets; j++) {
834
+ const varName = `x_${i}_${j}`;
835
+ entries.push({ variable: varName, coefficient: 1 });
836
+ }
837
+ model.constraints.push({
838
+ name: `assign_family_${i}`,
839
+ lower: 1,
840
+ upper: 1,
841
+ entries
842
+ });
843
+ }
844
+ for (let j = 0; j < this.maxSubsets; j++) {
845
+ const entries = [];
846
+ for (let i = 0; i < this.families.length; i++) {
847
+ const varName = `x_${i}_${j}`;
848
+ const familySize = this.families[i].length;
849
+ entries.push({ variable: varName, coefficient: familySize });
850
+ }
851
+ model.constraints.push({
852
+ name: `max_size_subset_${j}`,
853
+ lower: 0,
854
+ upper: this.options.maxSubsetSize,
855
+ entries
856
+ });
857
+ }
858
+ for (let j = 0; j < this.maxSubsets; j++) {
859
+ const indicatorVarName = `y_${j}`;
860
+ variableMap.set(indicatorVarName, varIndex++);
861
+ model.variables.push({
862
+ name: indicatorVarName,
863
+ lower: 0,
864
+ upper: 1,
865
+ type: "binary",
866
+ cost: -0.1
867
+ // Small penalty for using more subsets
868
+ });
869
+ const linkEntries = [];
870
+ linkEntries.push({ variable: indicatorVarName, coefficient: -this.options.maxSubsetSize });
871
+ for (let i = 0; i < this.families.length; i++) {
872
+ const varName = `x_${i}_${j}`;
873
+ linkEntries.push({ variable: varName, coefficient: 1 });
874
+ }
875
+ model.constraints.push({
876
+ name: `link_subset_${j}`,
877
+ lower: -Infinity,
878
+ upper: 0,
879
+ entries: linkEntries
880
+ });
881
+ const minSizeEntries = [];
882
+ minSizeEntries.push({ variable: indicatorVarName, coefficient: -this.options.minSubsetSize });
883
+ for (let i = 0; i < this.families.length; i++) {
884
+ const varName = `x_${i}_${j}`;
885
+ const familySize = this.families[i].length;
886
+ minSizeEntries.push({ variable: varName, coefficient: familySize });
887
+ }
888
+ model.constraints.push({
889
+ name: `min_size_subset_${j}`,
890
+ lower: 0,
891
+ upper: Infinity,
892
+ entries: minSizeEntries
893
+ });
894
+ }
895
+ return model;
896
+ }
897
+ /**
898
+ * Calculates the objective coefficient for a family-subset assignment
899
+ */
900
+ calculateObjectiveCoefficient(familyIndex, subsetIndex) {
901
+ const family = this.families[familyIndex];
902
+ const familySize = family.length;
903
+ let coefficient = 10;
904
+ if (familySize === this.options.preferredSubsetSize) {
905
+ coefficient += 5 + 10 * this.options.closedGroupWeight;
906
+ } else if (familySize <= this.options.preferredSubsetSize) {
907
+ coefficient += 3;
908
+ }
909
+ const hasMultipleGenders = new Set(family.map((c) => c.gender)).size > 1;
910
+ if (hasMultipleGenders) {
911
+ coefficient += 2 * this.options.genderWeight;
912
+ }
913
+ const uniqueAges = new Set(family.map((c) => c.ageCategory)).size;
914
+ if (uniqueAges === 1) {
915
+ coefficient += 2 * this.options.ageCategoryWeight;
916
+ }
917
+ const uniqueEquipment = new Set(family.map((c) => c.equipmentClass)).size;
918
+ if (this.options.groupByEquipmentClass && uniqueEquipment === 1) {
919
+ coefficient += 2 * this.options.equipmentClassWeight;
920
+ } else if (!this.options.groupByEquipmentClass && uniqueEquipment > 1) {
921
+ coefficient += 1 * this.options.equipmentClassWeight;
922
+ }
923
+ return coefficient;
924
+ }
925
+ /**
926
+ * Solves the MIP model using HiGHS and returns the solution
927
+ */
928
+ async solve() {
929
+ const model = this.createMIPModel();
930
+ if (this.families.length <= 5) {
931
+ console.log(`HiGHS model: ${model.variables.length} variables, ${model.constraints.length} constraints`);
932
+ }
933
+ const lpModel = this.convertToLPFormat(model);
934
+ const solver = await loadHighs();
935
+ const solution = solver.solve(lpModel);
936
+ if (this.families.length <= 5) {
937
+ console.log("HiGHS solution status:", solution.Status);
938
+ }
939
+ if (!solution) {
940
+ throw new Error("HiGHS solver returned no solution");
941
+ }
942
+ if (solution.Status !== "Optimal") {
943
+ throw new Error(`HiGHS solver failed with status: ${solution.Status}`);
944
+ }
945
+ const subsets = Array(this.maxSubsets).fill(null).map(() => []);
946
+ for (let i = 0; i < this.families.length; i++) {
947
+ for (let j = 0; j < this.maxSubsets; j++) {
948
+ const varName = `x_${i}_${j}`;
949
+ const column = solution.Columns[varName];
950
+ if (column && column.Primal > 0.5) {
951
+ subsets[j].push(...this.families[i]);
952
+ break;
953
+ }
954
+ }
955
+ }
956
+ const nonEmptySubsets = subsets.filter((subset) => subset.length > 0);
957
+ if (!this.validateSolution(nonEmptySubsets)) {
958
+ throw new Error("HiGHS solution validation failed - constraints not met");
959
+ }
960
+ return nonEmptySubsets;
961
+ }
962
+ /**
963
+ * Converts our HiGHS model to LP format string that HiGHS can understand
964
+ */
965
+ convertToLPFormat(model) {
966
+ let lp = "";
967
+ lp += model.sense === "maximize" ? "Maximize\n" : "Minimize\n";
968
+ lp += "obj: ";
969
+ const objTerms = [];
970
+ for (const variable of model.variables) {
971
+ if (variable.cost !== 0) {
972
+ const sign = variable.cost >= 0 ? "+" : "";
973
+ objTerms.push(`${sign}${variable.cost} ${variable.name}`);
974
+ }
975
+ }
976
+ lp += objTerms.join(" ") + "\n\n";
977
+ lp += "Subject To\n";
978
+ for (const constraint of model.constraints) {
979
+ let constraintStr = `${constraint.name}: `;
980
+ const terms = [];
981
+ for (const entry of constraint.entries) {
982
+ const sign = entry.coefficient >= 0 ? "+" : "";
983
+ terms.push(`${sign}${entry.coefficient} ${entry.variable}`);
984
+ }
985
+ constraintStr += terms.join(" ");
986
+ if (constraint.lower === constraint.upper) {
987
+ constraintStr += ` = ${constraint.lower}`;
988
+ } else {
989
+ if (constraint.lower > -Infinity) {
990
+ constraintStr += ` >= ${constraint.lower}`;
991
+ }
992
+ if (constraint.upper < Infinity) {
993
+ if (constraint.lower > -Infinity) {
994
+ lp += constraintStr + "\n";
995
+ constraintStr = `${constraint.name}_upper: ${terms.join(" ")} <= ${constraint.upper}`;
996
+ } else {
997
+ constraintStr += ` <= ${constraint.upper}`;
998
+ }
999
+ }
1000
+ }
1001
+ lp += constraintStr + "\n";
1002
+ }
1003
+ lp += "\nBounds\n";
1004
+ const binaryVars = [];
1005
+ for (const variable of model.variables) {
1006
+ if (variable.type === "binary") {
1007
+ binaryVars.push(variable.name);
1008
+ } else {
1009
+ if (variable.lower !== 0 || variable.upper !== Infinity) {
1010
+ lp += `${variable.lower} <= ${variable.name} <= ${variable.upper}
1011
+ `;
1012
+ }
1013
+ }
1014
+ }
1015
+ if (binaryVars.length > 0) {
1016
+ lp += "\nBinary\n";
1017
+ lp += binaryVars.join(" ") + "\n";
1018
+ }
1019
+ lp += "\nEnd\n";
1020
+ if (this.families.length <= 3) {
1021
+ console.log("Generated LP model:");
1022
+ console.log(lp);
1023
+ }
1024
+ return lp;
1025
+ }
1026
+ /**
1027
+ * Validates that the solution meets all constraints
1028
+ */
1029
+ validateSolution(subsets) {
1030
+ const assignedIds = /* @__PURE__ */ new Set();
1031
+ for (const subset of subsets) {
1032
+ for (const competitor of subset) {
1033
+ if (assignedIds.has(competitor.id)) {
1034
+ return false;
1035
+ }
1036
+ assignedIds.add(competitor.id);
1037
+ }
1038
+ }
1039
+ if (assignedIds.size !== this.competitors.length) {
1040
+ return false;
1041
+ }
1042
+ for (const subset of subsets) {
1043
+ if (subset.length < this.options.minSubsetSize || subset.length > this.options.maxSubsetSize) {
1044
+ return false;
1045
+ }
1046
+ }
1047
+ for (const subset of subsets) {
1048
+ const subsetIds = new Set(subset.map((c) => c.id));
1049
+ for (const competitor of subset) {
1050
+ if (competitor.guardianId && !subsetIds.has(competitor.guardianId)) {
1051
+ return false;
1052
+ }
1053
+ }
1054
+ }
1055
+ return true;
1056
+ }
1057
+ };
1058
+ async function solveMIPWithLP(competitors, options = {}) {
1059
+ const startTime = performance.now();
1060
+ const opts = validateAndSanitizeOptions(options, competitors.length);
1061
+ if (competitors.length === 0) {
1062
+ return {
1063
+ result: createPartitionResult([], opts),
1064
+ executionTime: 0,
1065
+ algorithmUsed: "Mixed Integer Programming (HiGHS)"
1066
+ };
1067
+ }
1068
+ const solver = new HiGHSMIPSolver(competitors, opts);
1069
+ const subsets = await solver.solve();
1070
+ const legalSubsets = retainLegalSubsets(subsets, opts.minSubsetSize, opts.maxSubsetSize);
1071
+ const result = createPartitionResult(legalSubsets, opts);
1072
+ const executionTime = performance.now() - startTime;
1073
+ return {
1074
+ result,
1075
+ executionTime,
1076
+ algorithmUsed: "Mixed Integer Programming (HiGHS)"
1077
+ };
1078
+ }
1079
+
1080
+ // src/algorithms/clustering.ts
1081
+ function encodeCompetitor(competitor, allCompetitors) {
1082
+ const uniqueGenders = [...new Set(allCompetitors.map((c) => c.gender))];
1083
+ const uniqueAgeCategories = [...new Set(allCompetitors.map((c) => c.ageCategory))];
1084
+ const uniqueEquipmentClasses = [...new Set(allCompetitors.map((c) => c.equipmentClass))];
1085
+ const features = [];
1086
+ for (const gender of uniqueGenders) {
1087
+ features.push(competitor.gender === gender ? 1 : 0);
1088
+ }
1089
+ for (const ageCategory of uniqueAgeCategories) {
1090
+ features.push(competitor.ageCategory === ageCategory ? 1 : 0);
1091
+ }
1092
+ for (const equipmentClass of uniqueEquipmentClasses) {
1093
+ features.push(competitor.equipmentClass === equipmentClass ? 1 : 0);
1094
+ }
1095
+ features.push(competitor.guardianId ? 1 : 0);
1096
+ return features;
1097
+ }
1098
+ function calculateDistance(features1, features2) {
1099
+ let sum = 0;
1100
+ for (let i = 0; i < features1.length; i++) {
1101
+ const diff = features1[i] - features2[i];
1102
+ sum += diff * diff;
1103
+ }
1104
+ return Math.sqrt(sum);
1105
+ }
1106
+ function calculateWeightedDistance(features1, features2, options, featureWeights) {
1107
+ let sum = 0;
1108
+ for (let i = 0; i < features1.length; i++) {
1109
+ const diff = features1[i] - features2[i];
1110
+ let weight = 1;
1111
+ if (i >= featureWeights.genderStart && i < featureWeights.ageStart) {
1112
+ weight = options.genderWeight;
1113
+ } else if (i >= featureWeights.ageStart && i < featureWeights.equipmentStart) {
1114
+ weight = options.ageCategoryWeight;
1115
+ } else if (i >= featureWeights.equipmentStart) {
1116
+ weight = options.equipmentClassWeight;
1117
+ }
1118
+ sum += weight * diff * diff;
1119
+ }
1120
+ return Math.sqrt(sum);
1121
+ }
1122
+ function initializeClusterCenters(familyFeatures, numClusters, options) {
1123
+ const centers = [];
1124
+ const families = [...familyFeatures];
1125
+ const firstCenter = families[Math.floor(Math.random() * families.length)];
1126
+ centers.push({
1127
+ id: 0,
1128
+ features: [...firstCenter.features],
1129
+ members: []
1130
+ });
1131
+ for (let i = 1; i < numClusters; i++) {
1132
+ const distances = families.map((family) => {
1133
+ const minDist = Math.min(...centers.map(
1134
+ (center) => calculateDistance(family.features, center.features)
1135
+ ));
1136
+ return minDist * minDist;
1137
+ });
1138
+ const totalDistance = distances.reduce((sum, dist) => sum + dist, 0);
1139
+ const randomValue = Math.random() * totalDistance;
1140
+ let cumulativeDistance = 0;
1141
+ let selectedIndex = 0;
1142
+ for (let j = 0; j < distances.length; j++) {
1143
+ cumulativeDistance += distances[j];
1144
+ if (cumulativeDistance >= randomValue) {
1145
+ selectedIndex = j;
1146
+ break;
1147
+ }
1148
+ }
1149
+ centers.push({
1150
+ id: i,
1151
+ features: [...families[selectedIndex].features],
1152
+ members: []
1153
+ });
1154
+ }
1155
+ return centers;
1156
+ }
1157
+ function createFamilyFeatures(families, allCompetitors) {
1158
+ return families.map((family, familyIndex) => {
1159
+ const familyMemberFeatures = family.map(
1160
+ (competitor) => encodeCompetitor(competitor, allCompetitors)
1161
+ );
1162
+ const avgFeatures = new Array(familyMemberFeatures[0].length).fill(0);
1163
+ for (const memberFeatures of familyMemberFeatures) {
1164
+ for (let i = 0; i < memberFeatures.length; i++) {
1165
+ avgFeatures[i] += memberFeatures[i];
1166
+ }
1167
+ }
1168
+ for (let i = 0; i < avgFeatures.length; i++) {
1169
+ avgFeatures[i] /= familyMemberFeatures.length;
1170
+ }
1171
+ return {
1172
+ family,
1173
+ features: avgFeatures,
1174
+ clusterId: -1
1175
+ };
1176
+ });
1177
+ }
1178
+ function assignToClusters(familyFeatures, centers, options, featureWeights) {
1179
+ centers.forEach((center) => center.members = []);
1180
+ for (const family of familyFeatures) {
1181
+ let minDistance = Infinity;
1182
+ let bestCluster = 0;
1183
+ for (let i = 0; i < centers.length; i++) {
1184
+ const distance = calculateWeightedDistance(
1185
+ family.features,
1186
+ centers[i].features,
1187
+ options,
1188
+ featureWeights
1189
+ );
1190
+ if (distance < minDistance) {
1191
+ minDistance = distance;
1192
+ bestCluster = i;
1193
+ }
1194
+ }
1195
+ family.clusterId = bestCluster;
1196
+ centers[bestCluster].members.push(family);
1197
+ }
1198
+ }
1199
+ function updateClusterCenters(centers) {
1200
+ let changed = false;
1201
+ for (const center of centers) {
1202
+ if (center.members.length === 0) continue;
1203
+ const newFeatures = new Array(center.features.length).fill(0);
1204
+ for (const member of center.members) {
1205
+ for (let i = 0; i < member.features.length; i++) {
1206
+ newFeatures[i] += member.features[i];
1207
+ }
1208
+ }
1209
+ for (let i = 0; i < newFeatures.length; i++) {
1210
+ newFeatures[i] /= center.members.length;
1211
+ }
1212
+ for (let i = 0; i < center.features.length; i++) {
1213
+ if (Math.abs(center.features[i] - newFeatures[i]) > 1e-6) {
1214
+ changed = true;
1215
+ break;
1216
+ }
1217
+ }
1218
+ center.features = newFeatures;
1219
+ }
1220
+ return changed;
1221
+ }
1222
+ function adjustClusterSizes(centers, options, maxAllowedClusters) {
1223
+ const effectiveMaxSubsets = maxAllowedClusters || options.maxSubsets;
1224
+ const validCenters = [];
1225
+ const redistributeFamilies = [];
1226
+ for (const center of centers) {
1227
+ if (center.members.length === 0) {
1228
+ continue;
1229
+ }
1230
+ const totalCompetitors = center.members.reduce((sum, family) => sum + family.family.length, 0);
1231
+ if (totalCompetitors <= options.maxSubsetSize && totalCompetitors >= options.minSubsetSize) {
1232
+ validCenters.push(center);
1233
+ } else if (totalCompetitors > options.maxSubsetSize) {
1234
+ const shuffledFamilies = shuffle([...center.members]);
1235
+ let currentCluster = [];
1236
+ let currentSize = 0;
1237
+ for (const family of shuffledFamilies) {
1238
+ const familySize = family.family.length;
1239
+ if (currentSize + familySize > options.maxSubsetSize) {
1240
+ if (currentSize >= options.minSubsetSize) {
1241
+ validCenters.push({
1242
+ id: validCenters.length,
1243
+ features: center.features,
1244
+ members: [...currentCluster]
1245
+ });
1246
+ } else {
1247
+ redistributeFamilies.push(...currentCluster);
1248
+ }
1249
+ currentCluster = [family];
1250
+ currentSize = familySize;
1251
+ } else {
1252
+ currentCluster.push(family);
1253
+ currentSize += familySize;
1254
+ }
1255
+ }
1256
+ if (currentCluster.length > 0) {
1257
+ if (currentSize >= options.minSubsetSize) {
1258
+ validCenters.push({
1259
+ id: validCenters.length,
1260
+ features: center.features,
1261
+ members: currentCluster
1262
+ });
1263
+ } else {
1264
+ redistributeFamilies.push(...currentCluster);
1265
+ }
1266
+ }
1267
+ } else {
1268
+ redistributeFamilies.push(...center.members);
1269
+ }
1270
+ }
1271
+ while (redistributeFamilies.length > 0) {
1272
+ let placed = false;
1273
+ for (const center of validCenters) {
1274
+ if (redistributeFamilies.length === 0) break;
1275
+ const currentSize = center.members.reduce((sum, family) => sum + family.family.length, 0);
1276
+ for (let i = redistributeFamilies.length - 1; i >= 0; i--) {
1277
+ const family = redistributeFamilies[i];
1278
+ if (currentSize + family.family.length <= options.maxSubsetSize) {
1279
+ center.members.push(family);
1280
+ redistributeFamilies.splice(i, 1);
1281
+ placed = true;
1282
+ break;
1283
+ }
1284
+ }
1285
+ }
1286
+ if (!placed && redistributeFamilies.length > 0 && validCenters.length < effectiveMaxSubsets) {
1287
+ const newCluster = [];
1288
+ let newClusterSize = 0;
1289
+ while (redistributeFamilies.length > 0 && newClusterSize < options.preferredSubsetSize) {
1290
+ const family = redistributeFamilies.shift();
1291
+ if (newClusterSize + family.family.length <= options.maxSubsetSize) {
1292
+ newCluster.push(family);
1293
+ newClusterSize += family.family.length;
1294
+ } else {
1295
+ redistributeFamilies.unshift(family);
1296
+ break;
1297
+ }
1298
+ }
1299
+ if (newCluster.length > 0) {
1300
+ const newFeatures = new Array(newCluster[0].features.length).fill(0);
1301
+ for (const family of newCluster) {
1302
+ for (let i = 0; i < family.features.length; i++) {
1303
+ newFeatures[i] += family.features[i];
1304
+ }
1305
+ }
1306
+ for (let i = 0; i < newFeatures.length; i++) {
1307
+ newFeatures[i] /= newCluster.length;
1308
+ }
1309
+ validCenters.push({
1310
+ id: validCenters.length,
1311
+ features: newFeatures,
1312
+ members: newCluster
1313
+ });
1314
+ }
1315
+ if (redistributeFamilies.length > 0 && (newCluster.length === 0 || validCenters.length >= effectiveMaxSubsets)) {
1316
+ while (redistributeFamilies.length > 0) {
1317
+ let smallestCenter = validCenters[0];
1318
+ let smallestSize = smallestCenter.members.reduce((sum, family2) => sum + family2.family.length, 0);
1319
+ for (const center of validCenters) {
1320
+ const size = center.members.reduce((sum, family2) => sum + family2.family.length, 0);
1321
+ if (size < smallestSize) {
1322
+ smallestCenter = center;
1323
+ smallestSize = size;
1324
+ }
1325
+ }
1326
+ const family = redistributeFamilies.shift();
1327
+ smallestCenter.members.push(family);
1328
+ }
1329
+ }
1330
+ }
1331
+ if (!placed && redistributeFamilies.length > 0) {
1332
+ const family = redistributeFamilies.shift();
1333
+ placeFamilyInSmallestCenter(validCenters, family);
1334
+ }
1335
+ }
1336
+ return validCenters;
1337
+ }
1338
+ function placeFamilyInSmallestCenter(validCenters, family) {
1339
+ if (validCenters.length === 0) {
1340
+ validCenters.push({ id: 0, features: [...family.features], members: [family] });
1341
+ return;
1342
+ }
1343
+ let smallestCenter = validCenters[0];
1344
+ let smallestSize = centerSize(smallestCenter);
1345
+ for (const center of validCenters) {
1346
+ const size = centerSize(center);
1347
+ if (size < smallestSize) {
1348
+ smallestCenter = center;
1349
+ smallestSize = size;
1350
+ }
1351
+ }
1352
+ smallestCenter.members.push(family);
1353
+ }
1354
+ function centerSize(center) {
1355
+ return center.members.reduce((sum, family) => sum + family.family.length, 0);
1356
+ }
1357
+ function solveClustering(competitors, options = {}) {
1358
+ const startTime = performance.now();
1359
+ if (competitors.length === 0) {
1360
+ return handleEmptyDataset(options, "K-means Clustering");
1361
+ }
1362
+ const opts = validateAndSanitizeOptions(options, competitors.length);
1363
+ const families = createGuardianFamilies(competitors);
1364
+ const familyFeatures = createFamilyFeatures(families, competitors);
1365
+ const uniqueGenders = [...new Set(competitors.map((c) => c.gender))];
1366
+ const uniqueAgeCategories = [...new Set(competitors.map((c) => c.ageCategory))];
1367
+ const featureWeights = {
1368
+ genderStart: 0,
1369
+ ageStart: uniqueGenders.length,
1370
+ equipmentStart: uniqueGenders.length + uniqueAgeCategories.length
1371
+ };
1372
+ const totalCompetitors = competitors.length;
1373
+ const minClusters = Math.ceil(totalCompetitors / opts.maxSubsetSize);
1374
+ const maxClusters = Math.min(opts.maxSubsets, Math.floor(totalCompetitors / opts.minSubsetSize));
1375
+ const preferredClusters = Math.round(totalCompetitors / opts.preferredSubsetSize);
1376
+ const numClusters = Math.min(
1377
+ maxClusters,
1378
+ Math.max(minClusters, Math.min(preferredClusters, families.length))
1379
+ );
1380
+ let centers = initializeClusterCenters(familyFeatures, numClusters, opts);
1381
+ const maxIterations = 100;
1382
+ for (let iteration = 0; iteration < maxIterations; iteration++) {
1383
+ assignToClusters(familyFeatures, centers, opts, featureWeights);
1384
+ const centersChanged = updateClusterCenters(centers);
1385
+ if (!centersChanged) {
1386
+ break;
1387
+ }
1388
+ }
1389
+ const adjustedCenters = adjustClusterSizes(centers, opts, numClusters);
1390
+ let subsets = adjustedCenters.map((center) => {
1391
+ const subset = [];
1392
+ for (const family of center.members) {
1393
+ subset.push(...family.family);
1394
+ }
1395
+ return subset;
1396
+ });
1397
+ while (subsets.length > opts.maxSubsets) {
1398
+ let bestMerge = null;
1399
+ let bestScore = -1;
1400
+ for (let i = 0; i < subsets.length - 1; i++) {
1401
+ for (let j = i + 1; j < subsets.length; j++) {
1402
+ const combinedSize = subsets[i].length + subsets[j].length;
1403
+ let score = 0;
1404
+ if (combinedSize <= opts.maxSubsetSize) {
1405
+ score = 1e3 - combinedSize;
1406
+ } else if (combinedSize <= opts.maxSubsetSize + 1) {
1407
+ score = 50 - combinedSize;
1408
+ } else {
1409
+ score = 10 - combinedSize;
1410
+ }
1411
+ if (score > bestScore) {
1412
+ bestScore = score;
1413
+ bestMerge = { i, j, combinedSize };
1414
+ }
1415
+ }
1416
+ }
1417
+ if (bestMerge) {
1418
+ subsets[bestMerge.i].push(...subsets[bestMerge.j]);
1419
+ subsets.splice(bestMerge.j, 1);
1420
+ } else {
1421
+ let smallestIdx1 = 0, smallestIdx2 = 1;
1422
+ for (let i = 0; i < subsets.length; i++) {
1423
+ if (subsets[i].length < subsets[smallestIdx1].length) {
1424
+ smallestIdx2 = smallestIdx1;
1425
+ smallestIdx1 = i;
1426
+ } else if (i !== smallestIdx1 && subsets[i].length < subsets[smallestIdx2].length) {
1427
+ smallestIdx2 = i;
1428
+ }
1429
+ }
1430
+ subsets[smallestIdx1].push(...subsets[smallestIdx2]);
1431
+ subsets.splice(smallestIdx2, 1);
1432
+ }
1433
+ }
1434
+ if (subsets.length === opts.maxSubsets) {
1435
+ const totalCompetitors2 = subsets.reduce((sum, subset) => sum + subset.length, 0);
1436
+ const targetSize = Math.floor(totalCompetitors2 / opts.maxSubsets);
1437
+ const remainder = totalCompetitors2 % opts.maxSubsets;
1438
+ const oversized = subsets.filter((s) => s.length > opts.maxSubsetSize);
1439
+ const undersized = subsets.filter((s) => s.length < opts.minSubsetSize);
1440
+ if (oversized.length > 0 || undersized.length > 0) {
1441
+ const familiesInSubsets = families.map((family) => {
1442
+ const familyIds = new Set(family.map((c) => c.id));
1443
+ for (let i = 0; i < subsets.length; i++) {
1444
+ const subsetIds = new Set(subsets[i].map((c) => c.id));
1445
+ if (family.every((c) => subsetIds.has(c.id))) {
1446
+ return { family, subsetIndex: i };
1447
+ }
1448
+ }
1449
+ return { family, subsetIndex: -1 };
1450
+ });
1451
+ subsets.length = 0;
1452
+ const sortedFamilies = familiesInSubsets.sort((a, b) => b.family.length - a.family.length);
1453
+ for (const { family } of sortedFamilies) {
1454
+ let bestSubsetIndex = -1;
1455
+ let bestScore = -1;
1456
+ for (let i = 0; i < Math.min(subsets.length, opts.maxSubsets); i++) {
1457
+ const newSize = subsets[i].length + family.length;
1458
+ if (newSize <= opts.maxSubsetSize) {
1459
+ const score = 1e3 - Math.abs(newSize - targetSize);
1460
+ if (score > bestScore) {
1461
+ bestScore = score;
1462
+ bestSubsetIndex = i;
1463
+ }
1464
+ }
1465
+ }
1466
+ if (bestSubsetIndex === -1 && subsets.length < opts.maxSubsets) {
1467
+ subsets.push([]);
1468
+ bestSubsetIndex = subsets.length - 1;
1469
+ }
1470
+ if (bestSubsetIndex !== -1) {
1471
+ subsets[bestSubsetIndex].push(...family);
1472
+ } else {
1473
+ let smallestIndex = 0;
1474
+ for (let i = 1; i < subsets.length; i++) {
1475
+ if (subsets[i].length < subsets[smallestIndex].length) {
1476
+ smallestIndex = i;
1477
+ }
1478
+ }
1479
+ subsets[smallestIndex].push(...family);
1480
+ }
1481
+ }
1482
+ }
1483
+ }
1484
+ let finalUndersized = subsets.filter((s) => s.length < opts.minSubsetSize);
1485
+ let maxCleanupIterations = subsets.length;
1486
+ let cleanupIterations = 0;
1487
+ while (finalUndersized.length > 0 && cleanupIterations < maxCleanupIterations) {
1488
+ cleanupIterations++;
1489
+ let smallestUndersized = finalUndersized[0];
1490
+ let smallestIdx = subsets.indexOf(smallestUndersized);
1491
+ for (const undersized of finalUndersized) {
1492
+ if (undersized.length < smallestUndersized.length) {
1493
+ smallestUndersized = undersized;
1494
+ smallestIdx = subsets.indexOf(undersized);
1495
+ }
1496
+ }
1497
+ let bestMergeIdx = -1;
1498
+ let bestMergeSize = Infinity;
1499
+ for (let i = 0; i < subsets.length; i++) {
1500
+ if (i === smallestIdx) continue;
1501
+ const combinedSize = subsets[i].length + smallestUndersized.length;
1502
+ if (combinedSize <= opts.maxSubsetSize && subsets[i].length < bestMergeSize) {
1503
+ bestMergeSize = subsets[i].length;
1504
+ bestMergeIdx = i;
1505
+ }
1506
+ }
1507
+ if (bestMergeIdx === -1) {
1508
+ bestMergeIdx = 0;
1509
+ for (let i = 1; i < subsets.length; i++) {
1510
+ if (i !== smallestIdx && subsets[i].length < subsets[bestMergeIdx].length) {
1511
+ bestMergeIdx = i;
1512
+ }
1513
+ }
1514
+ }
1515
+ if (bestMergeIdx !== -1 && bestMergeIdx !== smallestIdx) {
1516
+ subsets[bestMergeIdx].push(...smallestUndersized);
1517
+ subsets.splice(smallestIdx, 1);
1518
+ finalUndersized = subsets.filter((s) => s.length < opts.minSubsetSize);
1519
+ } else {
1520
+ break;
1521
+ }
1522
+ }
1523
+ return createAlgorithmResult(competitors, subsets, opts, "K-means Clustering", startTime);
1524
+ }
1525
+
1526
+ // src/index.ts
1527
+ async function solve(competitors, options = {}) {
1528
+ const algorithm = options.algorithm || "auto";
1529
+ switch (algorithm) {
1530
+ case "mip":
1531
+ return solveMIP(competitors, options);
1532
+ case "mip-lp":
1533
+ return await solveMIPWithLP(competitors, options);
1534
+ case "clustering":
1535
+ return solveClustering(competitors, options);
1536
+ case "auto":
1537
+ default:
1538
+ const results = [
1539
+ solveMIP(competitors, options),
1540
+ await solveMIPWithLP(competitors, options),
1541
+ solveClustering(competitors, options)
1542
+ ];
1543
+ const bestResult = results.reduce(
1544
+ (best, current) => current.result.score > best.result.score ? current : best
1545
+ );
1546
+ return {
1547
+ ...bestResult,
1548
+ algorithmUsed: `${bestResult.algorithmUsed} (auto-selected)`
1549
+ };
1550
+ }
1551
+ }
1552
+ async function compareAlgorithms(competitors, options = {}) {
1553
+ const { algorithm, ...algorithmOptions } = options;
1554
+ const results = [
1555
+ solveMIP(competitors, algorithmOptions),
1556
+ await solveMIPWithLP(competitors, algorithmOptions),
1557
+ solveClustering(competitors, algorithmOptions)
1558
+ ];
1559
+ const bestResult = results.reduce(
1560
+ (best, current) => current.result.score > best.result.score ? current : best
1561
+ );
1562
+ return {
1563
+ bestResult,
1564
+ allResults: results
1565
+ };
1566
+ }
1567
+ var version = "0.1.0";
1568
+ export {
1569
+ AGE_GROUPS,
1570
+ AGE_GROUP_DESCRIPTIONS,
1571
+ DEFAULT_OPTIONS,
1572
+ EQUIPMENT_CLASSES,
1573
+ EQUIPMENT_CLASS_DESCRIPTIONS,
1574
+ GENDERS,
1575
+ GENDER_DESCRIPTIONS,
1576
+ MINIMUM_SUBSET_SIZE,
1577
+ calculateScore,
1578
+ compareAgeGroups,
1579
+ compareAlgorithms,
1580
+ createGuardianFamilies,
1581
+ createPartitionResult,
1582
+ getAgeGroupOrder,
1583
+ getGuardianGroups,
1584
+ isValidAgeGroup,
1585
+ isValidCompetitor,
1586
+ isValidEquipmentClass,
1587
+ isValidGender,
1588
+ retainLegalSubsets,
1589
+ smallestAllowedSubset,
1590
+ solve,
1591
+ solveClustering,
1592
+ solveMIP,
1593
+ solveMIPWithLP,
1594
+ validateCompetitors,
1595
+ validateGuardianConstraints,
1596
+ validatePartition,
1597
+ version
1598
+ };
1599
+ //# sourceMappingURL=index.mjs.map