UNPKG

unique-grouping

Version:

Groups list of individuals in K unique groupings of N groups using simulated annealing

177 lines (143 loc) 5.59 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var SimulatedAnnealing = _interopDefault(require('simulated-annealing')); var chunk = _interopDefault(require('chunk')); var Combinatorics = _interopDefault(require('js-combinatorics')); var empty = _interopDefault(require('is-empty')); var last = _interopDefault(require('array-last')); var isPositiveInteger = _interopDefault(require('validate.io-positive-integer')); var findLastIndex = _interopDefault(require('lodash.findlastindex')); var d3 = _interopDefault(require('d3-array')); var clone = _interopDefault(require('clone')); var equal = _interopDefault(require('fast-deep-equal')); var StringBuilder = _interopDefault(require('yassb')); var uniqueRandomArray = _interopDefault(require('unique-random-array')); var random = _interopDefault(require('random')); var randomArrayIndex = _interopDefault(require('random-array-index')); const UniqueGrouping = (people = [], history = [], forbiddenPairs = [], groupSize, options = { tempMax: 65, tempMin: 0.0007, coolingRate: 0.0002, alpha: 1, beta: 1 }) => { // functions const createRandomGroups = (people, groupSize) => chunk(d3.shuffle(people), groupSize); const getEnergy = v => v.reduce((acc, group) => { // functions const cost = (groupSize, decays) => beta * groupSize ** 2 + decays.reduce((acc, decay) => acc + decay * alpha, 0); const decay = (n, t, k) => n / (1 + (t / k) ** 2); // start of actual code const groupSize = group.length; if (groupSize === 0) { return acc; } else if (groupSize === 1) { return acc + cost(groupSize, []); } const cmb = Combinatorics.combination(group, 2); const decays = cmb.map(pair => { // functions const historyOf = (person1, person2) => { // start of actual code const collisions = history.map(grouping => grouping.filter(group => group.some(person => equal(person, person1)) && group.some(person => equal(person, person2)))); const largestIndex = collisions.length - 1; const lastSeen = empty(collisions) ? -100 : largestIndex - findLastIndex(collisions, grouping => !empty(grouping)); const timesSeen = collisions.filter(grouping => !empty(grouping)).length; return { lastSeen, timesSeen }; }; // start of actual code if (forbiddenPairs.some(forbiddenPair => equal(forbiddenPair, pair) || equal(forbiddenPair, pair.reverse()))) { return Infinity; } const [person1, person2] = pair; const { lastSeen, timesSeen } = historyOf(person1, person2); // 1 is added to not multiply or divide by 0 return decay(timesSeen + 1, lastSeen, history.length + 1); }); return acc + cost(groupSize, decays); }, 0); const newState = x => { // functions const swapPeople = (from, to) => { const person1 = randomArrayIndex(from); const person2 = randomArrayIndex(to); [from[person1], to[person2]] = [to[person2], from[person1]]; }; const movePerson = (from, to) => { const person = randomArrayIndex(from); to.push(...from.splice(person, 1)); }; // start of actual code const newState = clone(x); const randomGroup = uniqueRandomArray(newState.filter(group => !empty(group))); const from = randomGroup(); const to = randomGroup(); if (from.length > to.length && random.boolean()) { movePerson(from, to); } else { swapPeople(from, to); } return newState; }; const getTemp = prevTemp => prevTemp - coolingRate; // linear decreasing temperature // start of actual code const { tempMax, tempMin, coolingRate, alpha, beta } = options; if (!isPositiveInteger(groupSize)) { throw new Error("groupSize must be an integer greater than 0"); } if (empty(people)) { if (empty(history)) { throw new Error("Either history or people must be a non-empty array"); } else { people = last(history).flat(); } } return new Promise((resolve, _) => resolve(SimulatedAnnealing({ initialState: createRandomGroups(people, groupSize), tempMax, tempMin, newState, getTemp, getEnergy }))); }; const grade = (grouping, history, forbiddenPairs) => { const sb = new StringBuilder(); const totalTimesSeen = grouping.reduce((acc, group) => { const groupSize = group.length; if (groupSize < 2) { return acc; } const cmb = Combinatorics.combination(group, 2); const timesSeenOfGroup = cmb.reduce((acc, pair) => { // functions const historyOf = (person1, person2) => history.flat().filter(group => group.some(person => equal(person, person1)) && group.some(person => equal(person, person2))).length; // start of actual code if (forbiddenPairs.some(forbiddenPair => equal(forbiddenPair, pair) || equal(forbiddenPair, pair.reverse()))) { sb.addLine(`FORBIDDEN PAIR DETECTED: ${person1} is with ${person2}`); return Infinity; } const [person1, person2] = pair; const timesSeen = historyOf(person1, person2); sb.addLine(`${person1} has seen ${person2} ${timesSeen} time(s)`); return acc + timesSeen; }, 0); return acc + timesSeenOfGroup; }, 0); const log = sb.toString(); return { log, totalTimesSeen }; }; exports.default = UniqueGrouping; exports.grade = grade; //# sourceMappingURL=index.js.map