Skip to content

Commit

Permalink
refactor: minor tweaks and API documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Concision committed Mar 17, 2024
1 parent 42bb81e commit 447d8f0
Show file tree
Hide file tree
Showing 17 changed files with 178 additions and 116 deletions.
6 changes: 3 additions & 3 deletions src/genetic/api/EarlyStoppingEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ export abstract class EarlyStoppingEvaluator<I, TChildrenType extends GeneticOpe
export class EnsureGrowthEarlyStoppingEvaluator<I> extends EarlyStoppingEvaluator<I> {
private _maximumGrowthFailures: number;

private previousMaximumFitness: number | undefined;
private previousMaximumFitness?: number;
private _growthFailures: number = 0;

public constructor(maximumGrowthFailures: number);
public constructor(name: string, maximumGrowthFailures: number);
public constructor(nameOrMaximumGrowthFailures: string | number, maximumGrowthFailures?: number) {
super(typeof nameOrMaximumGrowthFailures === "string" ? nameOrMaximumGrowthFailures : EnsureGrowthEarlyStoppingEvaluator.name);
this._maximumGrowthFailures = typeof nameOrMaximumGrowthFailures === "string" ? maximumGrowthFailures! : nameOrMaximumGrowthFailures;
super(2 <= arguments.length ? nameOrMaximumGrowthFailures as string : EnsureGrowthEarlyStoppingEvaluator.name);
this._maximumGrowthFailures = arguments.length === 1 ? nameOrMaximumGrowthFailures as number : maximumGrowthFailures!;

this.validateIfConsumerInstantiation(EnsureGrowthEarlyStoppingEvaluator, arguments);
}
Expand Down
3 changes: 1 addition & 2 deletions src/genetic/api/FitnessFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ export interface IFitness<I> {
}


export abstract class FitnessFunction<I, TChildrenType = undefined>
extends GeneticOperator<I, TChildrenType> {
export abstract class FitnessFunction<I, TChildrenType = undefined> extends GeneticOperator<I, TChildrenType> {
public abstract evaluate(population: readonly I[]): readonly IFitness<I>[];
}

Expand Down
5 changes: 3 additions & 2 deletions src/genetic/api/GeneticOperator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {GeneticParameters} from "./GeneticParameters";

export abstract class GeneticOperator<I, TChildType = undefined> {
public readonly name: string;
private _parameters: GeneticParameters<TChildType, any> | undefined;

private _parameters?: GeneticParameters<TChildType, any>;

public constructor(name: string) {
this.name = name;
Expand Down Expand Up @@ -192,7 +193,7 @@ export abstract class GeneticOperator<I, TChildType = undefined> {
}

public replaceByPredicate<T extends TChildType>(predicate: (child: IGeneticOperatorChild) => boolean, replacement: TChildType, recursive: false): void;
public replaceByPredicate(predicate: (child: any, operator?: GeneticOperator<I, any>) => boolean, replacement: any, recursive: true): void;
public replaceByPredicate(predicate: (child: IGeneticOperatorChild) => boolean, replacement: any, recursive: true): void;
public replaceByPredicate(
predicate: (child: IGeneticOperatorChild) => boolean,
replacement: any,
Expand Down
8 changes: 4 additions & 4 deletions src/genetic/api/GeneticParameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ export class GeneticParameters<I, TMetricCollector> extends GeneticOperator<I, G
private _maximumGenerations: number;
private _maximumPopulationSize: number;

private _firstGeneration?: readonly I[] | undefined;
private _individualGenerator?: IndividualGenerator<I> | undefined;
private _firstGeneration?: readonly I[];
private _individualGenerator?: IndividualGenerator<I>;

private _individualMutator: IndividualMutator<I, any>;
private _fitnessFunction: FitnessFunction<I, any>;
private _populationSelector: PopulationSelector<I, any>;
private _earlyStopping?: EarlyStoppingEvaluator<I> | undefined;
private _earlyStopping?: EarlyStoppingEvaluator<I>;

private _metricCollector?: MetricCollector<I, TMetricCollector> | undefined;
private _metricCollector?: MetricCollector<I, TMetricCollector>;

public constructor(parameters: IGeneticParameters<I, TMetricCollector>) {
super(GeneticParameters.name);
Expand Down
1 change: 1 addition & 0 deletions src/genetic/api/MetricCollector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export class HallOfFameMetricCollector<I> extends MetricCollector<I, HallOfFameR
this._identityFunction = value;
}


public override update({population}: IGeneration<I>): void {
const uniqueIndividuals = Array.from(groupBy(
this.fittest.concat(population),
Expand Down
10 changes: 2 additions & 8 deletions src/genetic/api/PopulationSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,7 @@ export class RouletteWheelPopulationSelector<I> extends PopulationSelector<I> {
export class TournamentPopulationSelector<I> extends PopulationSelector<I> {
private _tournamentSize: number;

public constructor(
name: string,
tournamentSize: number,
) {
public constructor(name: string, tournamentSize: number) {
super(name);
this._tournamentSize = tournamentSize;

Expand Down Expand Up @@ -263,10 +260,7 @@ export class TournamentPopulationSelector<I> extends PopulationSelector<I> {
export class DeduplicatePopulationSelector<I> extends PopulationSelector<I> {
private _identityFunction: IndividualIdentityFunction<I>;

public constructor(
name: string,
identityFunction: IndividualIdentityFunction<I>,
) {
public constructor(name: string, identityFunction: IndividualIdentityFunction<I>) {
super(name);
this._identityFunction = identityFunction;

Expand Down
89 changes: 55 additions & 34 deletions src/matchmaking/TeamMatchmaking.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {geneticAlgorithm} from "../genetic/GeneticAlgorithm";
import {assignDefinedProperties, groupBy} from "../utilities/CollectionUtilities";
import {factorial} from "../utilities/MathUtil";
import {KeysOfType} from "../utilities/TypescriptTypes";
import {
IConfiguredMatchmakingOptions,
Expand All @@ -20,7 +21,7 @@ import {
translateTimeSlotToDate
} from "./TimeSlot";

const defaultPartitionKey = {};
const defaultPartitionKey = Object.freeze({});

export function matchmakeTeams<TTeam extends ITeam = ITeam, TPartitionKey = string>(
teams: readonly TTeam[],
Expand All @@ -32,30 +33,31 @@ export function matchmakeTeams<TTeam extends ITeam = ITeam, TPartitionKey = stri
options: IPartitionedMatchmakingOptions<TTeam, TPartitionKey>,
): Promise<IPartitionedMatchmakingResults<TTeam, TPartitionKey>>;

// TODO: update documentation
/**
* Performs the matchmaking algorithm on a set of teams, automatically partitioning them by region. This is a
* convenience method computing matchups for all regions in the league at once. If a specific region needs to be
* recomputed due to changes, use {@link matchmakeTeams} directly or invoke this with a subset of the teams.
* Performs the matchmaking algorithm on a set of teams, automatically partitioning them by
* {@link IMatchmakingOptions.partitionBy} if defined.
*
* @param teams An array of teams that are competing in the season's league. These teams will attempt to be paired up by the
* matchmaking algorithm, referred to as a "matchup".
* @param options The input options for the matchmaking algorithm. See {@link IMatchmakingOptions}.
* @template TTeam
* @template TPartitionKey
* @exception Error If {@link options.maximumGames} is not a positive integer, then an exception will be thrown.
* @param teams An array of teams that are competing in the season's league. These teams will attempt to be paired up
* by the matchmaking algorithm, referred to as a "matchup".
* @param options The input options for the matchmaking algorithm; see {@link IMatchmakingOptions}.
* @template TTeam A derived type of {@link ITeam}; this is useful for consumers to extend the base {@link ITeam} with
* additional properties that are specific to their domain.
* @template TPartitionKey The type of the partition key that is used to partition the teams into separate groups; e.g.
* a string "region".
*/
export async function matchmakeTeams<TTeam extends ITeam = ITeam, TPartitionKey = string>(
teams: readonly TTeam[],
options: IMatchmakingOptions<TTeam, TPartitionKey>,
): Promise<IMatchmakingResults<TTeam> | IPartitionedMatchmakingResults<TTeam, TPartitionKey>> {
const parameters: IConfiguredMatchmakingOptions<TTeam, TPartitionKey> = addDefaultsAndValidateParameters(options);

// partition all teams by the partitioning function
const partitioningFunction = parameters.partitionBy === undefined ? (() => <TPartitionKey>defaultPartitionKey)
: typeof parameters.partitionBy === 'function' ? parameters.partitionBy
: ((team: TTeam) => team[<keyof KeysOfType<ITeam, TPartitionKey>>parameters.partitionBy]);
const partitionedTeams: ReadonlyMap<TPartitionKey, readonly TTeam[]> = groupBy<TPartitionKey, TTeam>(teams, partitioningFunction);

// matchmake each partition of teams
type PartitionResult = { partitionKey: TPartitionKey; result: IMatchmakingResults<TTeam>; }
const promises: Promise<PartitionResult>[] = [];
for (const [partitionKey, partitionTeams] of partitionedTeams.entries()) {
Expand All @@ -66,17 +68,21 @@ export async function matchmakeTeams<TTeam extends ITeam = ITeam, TPartitionKey
}
const partitionedResults = groupBy(await Promise.all(promises), result => result.partitionKey);

// combine partitioned results into a single result structure
const unmatchedTeams = new Map<ITeam, MatchupFailureReason>();
const results = new Map<TPartitionKey, IMatchmakingResults<TTeam>>();
for (const [partitionKey, [{result}]] of partitionedResults.entries()) {
results.set(partitionKey, result);
for (const [team, reason] of result.unmatchedTeams.entries())
unmatchedTeams.set(team, reason);
}
return {results: results, unmatchedTeams};
const searchSpace = Array.from(results.values())
.filter(({searchSpace}) => 0 < searchSpace)
.reduce((sum, {searchSpace}) => sum * searchSpace, BigInt(10));
return {results, unmatchedTeams, searchSpace};
}

// TODO: implement worker multithreading; current async signature is a placeholder
// TODO: possibly implement worker multithreading; current async signature is a placeholder
async function matchmakeTeamPartition<TTeam extends ITeam = ITeam, TPartitionKey = string>(
teams: readonly TTeam[],
partitionKey: TPartitionKey,
Expand All @@ -91,22 +97,18 @@ async function matchmakeTeamPartition<TTeam extends ITeam = ITeam, TPartitionKey
teamsPartitionedByTimeSlot = filterTimeSlotsThatAlreadyOccurred(teamsPartitionedByTimeSlot);
const {teamsByTimeSlot, unavailableTeams} = teamsPartitionedByTimeSlot;

// if all teams are unavailable, no matchups can be scheduled
if (unavailableTeams.size === teams.length)
return Promise.resolve<IMatchmakingResults<TTeam>>({
teams,
scheduledMatchups: [],
unmatchedTeams: unavailableTeams,
teamAvailability: teamsByTimeSlot,
});

// prepare genetic algorithm constraints
const geneticParameters = options.config.configure({options, partitionKey, teamsByTimeSlot});

// run the genetic algorithm to compute matchmaking results
const results = geneticAlgorithm(geneticParameters);

const solution = options.config.selectSolution(results);
let solution: IMatchupSchedule<TTeam>;
if (Array.from(teamsByTimeSlot.values()).some(teams => 2 <= teams.length)) { // at least two teams are available
// prepare genetic algorithm constraints
const geneticParameters = options.config.configure({options, partitionKey, teamsByTimeSlot});

// run the genetic algorithm to compute matchmaking results
const results = geneticAlgorithm(geneticParameters);
solution = options.config.selectSolution(results);
} else { // no teams are available
solution = {matchups: [], unmatchedTeams: new Map()};
}

// translate matchups into a format that is more useful for the consumer
return convertToMatchmakingResults<TTeam>(teams, teamsByTimeSlot, unavailableTeams, solution);
Expand Down Expand Up @@ -134,9 +136,9 @@ function convertToMatchmakingResults<TTeam extends ITeam>(
teams: readonly TTeam[],
teamsByTimeSlot: ReadonlyMap<ITimeSlot, readonly TTeam[]>,
unavailableTeams: ReadonlyMap<TTeam, MatchupFailureReason>,
solution: IMatchupSchedule<TTeam>,
solution?: IMatchupSchedule<TTeam>,
): IMatchmakingResults<TTeam> {
const scheduledMatchups = solution.matchups
const scheduledMatchups = solution?.matchups
.map(({timeSlot, teams}) => <IScheduledMatchup<TTeam>>({
time: timeSlot,
teams: <[ITeamNotYetPlayed<TTeam>, ITeamNotYetPlayed<TTeam>]>teams.map(team => ({
Expand All @@ -146,15 +148,18 @@ function convertToMatchmakingResults<TTeam extends ITeam>(
})),
played: false,
}))
.toSorted(sortScheduledMatchupsByTime);
.toSorted(sortScheduledMatchupsByTime) ?? [];

const unmatchedTeamReasons = new Map<TTeam, MatchupFailureReason>(unavailableTeams);
const matchedTeams = new Set<TTeam>(solution.matchups.flatMap(matchup => matchup.teams));
const matchedTeams = new Set<TTeam>(solution?.matchups.flatMap(matchup => matchup.teams) ?? []);
const unmatchedTeams = Array.from(teams
.flatMap(teams => teams)
.filter(team => !matchedTeams.has(team) && !unmatchedTeamReasons.has(team))
.reduce((uniqueTeams, team) => !uniqueTeams.has(team.snowflake) ? uniqueTeams.set(team.snowflake, team) : uniqueTeams, new Map<string, TTeam>())
.values()
.reduce((uniqueTeams, team) => !uniqueTeams.has(team.snowflake)
? uniqueTeams.set(team.snowflake, team)
: uniqueTeams,
new Map<string, TTeam>()
).values()
);
for (const unmatchedTeam of unmatchedTeams)
unmatchedTeamReasons.set(unmatchedTeam, MatchupFailureReason.NO_IDEAL_MATCHUPS);
Expand All @@ -164,5 +169,21 @@ function convertToMatchmakingResults<TTeam extends ITeam>(
scheduledMatchups,
unmatchedTeams: unmatchedTeamReasons,
teamAvailability: teamsByTimeSlot,
searchSpace: computeSearchSpace(teamsByTimeSlot),
};
}

function computeSearchSpace<TTeam extends ITeam>(teamsByTimeSlot: ReadonlyMap<ITimeSlot, readonly TTeam[]>): bigint {
const solutions: bigint = Array.from(teamsByTimeSlot.values())
.filter(teams => 2 <= teams.length)
.map(teams => {
const teamFactorial = factorial(2 * Math.floor(teams.length / 2));
let sum = BigInt(1);
for (let t = 1; t <= Math.floor(teams.length / 2); t++)
// (|totalTeams| multi-choose 2, 2, ...) / |chosenTeamCount|
sum += teamFactorial / ((BigInt(2) ** BigInt(t)) * factorial(t));
return sum;
})
.reduce((sum, searchSpace) => sum * searchSpace, BigInt(1));
return solutions == BigInt(1) ? BigInt(0) : solutions;
}
3 changes: 1 addition & 2 deletions src/matchmaking/TimeSlot.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {Writeable} from "../utilities/TypescriptTypes";
import {TimeSlotToDateTranslator} from "./api/IMatchmakingOptions";
import {MatchupFailureReason} from "./api/IMatchmakingResults";
import {ITeam} from "./api/ITeam";
import {IScheduledMatchup} from "./api/ITeamMatchup";
import {Day, ITimeSlot} from "./api/ITimeSlot";
import {Day, ITimeSlot, TimeSlotToDateTranslator} from "./api/ITimeSlot";

export interface ITeamsPartitionedByTimeSlot<TTeam extends ITeam> {
teamsByTimeSlot: ReadonlyMap<ITimeSlot, readonly TTeam[]>;
Expand Down
Loading

0 comments on commit 447d8f0

Please sign in to comment.