Skip to content

Commit

Permalink
fix problem with dispatch
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesPHoughton committed Aug 29, 2024
1 parent ad09906 commit 3a39175
Show file tree
Hide file tree
Showing 4 changed files with 281 additions and 96 deletions.
188 changes: 102 additions & 86 deletions server/src/preFlight/dispatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,85 @@ import { compare } from "../utils/comparison";
import { getReference } from "../utils/reference";
import { shuffle, leftovers } from "../utils/math";

function knockdown(currentPayoffs, index, knockdowns, knockdownType) {
// Apply knockdowns to payoffs
// prevPayoffs: the previous array of payoffs for each treatment
// index: the index the knockdowns to apply
// returns: the new array of payoffs
//
// Use this after we select a treatment to compute the new payoffs for all treatments
// given that this treatment has been selected.

// no knockdowns
if (knockdownType === "none") {
return currentPayoffs;
}

// single value for knockdown_factors
// only knock down the payoff for the treatment used
if (knockdownType === "single") {
return currentPayoffs.map((p, i) => (i === index ? p * knockdowns : p));
}

// array of values for knockdown_factors of length n
// only knock down the payoff for the treatment used
if (knockdownType === "array") {
return currentPayoffs.map((p, i) =>
i === index ? p * knockdowns[index] : p
);
}

// matrix of values for knockdown_factors of size n x n
// knock down the payoff for all treatments when a particular treatment is used
if (knockdownType === "matrix") {
return currentPayoffs.map((p, i) => p * knockdowns[index][i]);
}

throw new Error("Invalid knockdown type");
}

function getUnconstrainedMaxPayoff(
payoffs,
nPlayers,
treatments,
knockdowns,
knockdownType
) {
// console.log(
// "getUnconstrainedMaxPayoff",
// payoffs,
// nPlayers,
// treatments.length
// );
// The theoretical maximum payoff assuming we can assign any participant to any slot
let updatedPayoffs = [...payoffs];

let playersLeft = nPlayers;
let maxPayoff = 0;
const leftover = leftovers(
nPlayers,
treatments.map((t) => t.playerCount)
);
// don't assign the leftovers, they would artificially inflate the max payoff
while (playersLeft > leftover) {
const bestTreatmentIndex = updatedPayoffs.indexOf(
Math.max(...updatedPayoffs)
);
const bestTreatmentPayoff = updatedPayoffs[bestTreatmentIndex];
maxPayoff +=
bestTreatmentPayoff * treatments[bestTreatmentIndex].playerCount;
updatedPayoffs = knockdown(
updatedPayoffs,
bestTreatmentIndex,
knockdowns,
knockdownType
);
playersLeft -= treatments[bestTreatmentIndex].playerCount;
}

return maxPayoff;
}

export function makeDispatcher({
treatments,
payoffs: payoffsArg,
Expand Down Expand Up @@ -107,93 +186,15 @@ export function makeDispatcher({
player: candidate,
});
if (!compare(reference, condition.comparator, condition.value)) {
// console.log(
// "check player:",
// playerId,
// reference,
// condition.comparator,
// condition.value,
// "false"
// );
isEligibleCache.set(cacheKey, false);
return false;
}
// console.log(
// "check player:",
// playerId,
// reference,
// condition.comparator,
// condition.value,
// "true"
// );
}

isEligibleCache.set(cacheKey, true);
return true;
}

function knockdown(currentPayoffs, index) {
// Apply knockdowns to payoffs
// prevPayoffs: the previous array of payoffs for each treatment
// index: the index the knockdowns to apply
// returns: the new array of payoffs
//
// Use this after we select a treatment to compute the new payoffs for all treatments
// given that this treatment has been selected.

// no knockdowns
if (knockdownType === "none") {
return currentPayoffs;
}

// single value for knockdown_factors
// only knock down the payoff for the treatment used
if (knockdownType === "single") {
return currentPayoffs.map((p, i) => (i === index ? p * knockdowns : p));
}

// array of values for knockdown_factors of length n
// only knock down the payoff for the treatment used
if (knockdownType === "array") {
return currentPayoffs.map((p, i) =>
i === index ? p * knockdowns[index] : p
);
}

// matrix of values for knockdown_factors of size n x n
// knock down the payoff for all treatments when a particular treatment is used
if (knockdownType === "matrix") {
return currentPayoffs.map((p, i) => p * knockdowns[index][i]);
}

throw new Error("Invalid knockdown type");
}

function getUnconstrainedMaxPayoff(payoffs, nPlayers) {
// The theoretical maximum payoff assuming we can assign any participant to any slot
let updatedPayoffs = [...payoffs];

let playersLeft = nPlayers;
let maxPayoff = 0;
const leftover = leftovers(
nPlayers,
treatments.map((t) => t.playerCount)
);
// don't assign the leftovers, they would artificially inflate the max payoff
while (playersLeft > leftover) {
const bestTreatmentIndex = updatedPayoffs.indexOf(
Math.max(...updatedPayoffs)
);
const bestTreatmentPayoff = updatedPayoffs[bestTreatmentIndex];
maxPayoff +=
bestTreatmentPayoff * treatments[bestTreatmentIndex].playerCount;
updatedPayoffs = knockdown(updatedPayoffs, bestTreatmentIndex);
playersLeft -= treatments[bestTreatmentIndex].playerCount;
}

return maxPayoff;
}

function dispatch(availablePlayers) {
// Dispatch participants to treatments
//
Expand Down Expand Up @@ -232,7 +233,10 @@ export function makeDispatcher({

const maxPayoff = getUnconstrainedMaxPayoff(
persistentPayoffs,
nPlayersAvailable
nPlayersAvailable,
treatments,
knockdowns,
knockdownType
);

const stoppingThreshold = maxPayoff * requiredFractionOfMaximumPayoff;
Expand Down Expand Up @@ -262,12 +266,19 @@ export function makeDispatcher({

// if this branch can never lead to a better solution than the current best, abandon it
// (this may turn out to be more expensive than the benefit, need to evaluate how aggressively this prunes the search tree)
if (
partialSolutionPayoff +
getUnconstrainedMaxPayoff(payoffs, unassignedPlayerIds.length) <
currentBestPayoff
)
return;
// if (
// partialSolutionPayoff +
// getUnconstrainedMaxPayoff(
// // need to recompute the max payoff as it changes with knockdowns
// payoffs,
// unassignedPlayerIds.length,
// treatments,
// knockdowns,
// knockdownType
// ) <
// currentBestPayoff
// )
// return;

// We have found a solution.
if (unassignedPlayerIds.length === 0) {
Expand Down Expand Up @@ -365,7 +376,12 @@ export function makeDispatcher({
if (
isEligible(availablePlayers, playerId, treatmentIndex, position)
) {
const newPayoffs = knockdown(payoffs, treatmentIndex);
const newPayoffs = knockdown(
payoffs,
treatmentIndex,
knockdowns,
knockdownType
);

recurse({
unassignedPlayerIds: unassignedPlayerIds.slice(1),
Expand Down
73 changes: 72 additions & 1 deletion server/src/preFlight/dispatch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -386,4 +386,75 @@ test("one-person games", () => {
expect(assignments.filter((x) => x.treatment.name === "A").length).toBe(7);
});

// Todo: test the unconstrained max payoff works properly
// helper function to get a random integer
function getRandomInt(max) {
return Math.floor(Math.random() * max);
}

const test_large_dispatch = () => {
const treatments = [];
for (let i = 0; i < 300; i++) {
const playerCount = getRandomInt(4) + 2;
const groupComposition = [];
for (let j = 0; j < playerCount; j++) {
groupComposition.push({
position: j,
conditions: [
{
reference: "prompt.alpha",
comparator: "equals",
value: `${getRandomInt(15)}`,
},
],
});
}

treatments.push({
name: `treatment${i}`,
playerCount,
groupComposition,
});
}

const players = [];
for (let i = 0; i < 800; i++) {
players.push(
new MockPlayer(`p_${i}`, { prompt_alpha: `${getRandomInt(15)}` })
);
}

const dispatch = makeDispatcher({
treatments,
payoffs: "equal",
knockdowns: "none",
});

const startTime = Date.now();
const assignments = dispatch(players);
const endTime = Date.now();
const timeTaken = (endTime - startTime) / 1000;

return timeTaken;
};

// test("large dispatch", () => {
// const timeTaken = test_large_dispatch();
// console.log("Time taken", timeTaken);

// expect(timeTaken).toBeLessThan(1);
// });

const average = (array) => array.reduce((a, b) => a + b) / array.length;

test("profile large dispatch", () => {
const timeTaken = [];
for (let i = 0; i < 20; i++) {
timeTaken.push(test_large_dispatch());
}
const averageTime = average(timeTaken);

console.log("Time taken", timeTaken);
console.log("Average time", averageTime);

expect(averageTime).toBeLessThan(1);
});
33 changes: 24 additions & 9 deletions server/src/utils/math.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,38 @@ export function shuffle(arr) {
return shuffled;
}

export function unique(arr) {
// return a new array with only unique elements
return [...new Set(arr)];
}

export function leftovers(target, factors) {
// Given an integer `target` and a list of integers `factors`,
// returns the smallest number needed to add to an arbitrary number of factors
// to sum to `target`.
// We use this to figure out how many participants we will not be able to assign
// to games of sizes in `factors`, even if we have optimum and unconstrained assignment.
if (target === 0) return 0;

const uniqueFactors = unique(factors)
.filter((f) => f <= target) // remove factors that are too large
.sort((a, b) => b - a); // sort descending

if (uniqueFactors.length === 0) return target;

let closest = target;
for (const factor of factors) {
if (factor === target) return 0;
}
for (const factor of factors) {
if (factor < target) {
const leftover = leftovers(target - factor, factors);
if (leftover < closest) {
closest = leftover;
}
for (const factor of uniqueFactors) {
const modulo = target % factor;
if (modulo === 0) return 0;
if (uniqueFactors.includes(modulo)) return 0;

const leftover = leftovers(target - factor, factors);
if (leftover === 0) return 0;

if (leftover < closest) {
closest = leftover;
}
}

return closest;
}
Loading

0 comments on commit 3a39175

Please sign in to comment.