genetic-search
Version:
Multiprocessing genetic algorithm implementation library
435 lines • 13.5 kB
JavaScript
import { zip } from "./itertools";
/**
* Generates unique identifiers for genomes.
*
* @template TGenome The type of genome objects in the population.
*
* @category Utils
*/
export class IdGenerator {
constructor() {
this.id = 1;
}
nextId() {
return this.id++;
}
reset(population) {
this.id = population.reduce((max, genome) => Math.max(max, genome.id), 0) + 1;
}
}
/**
* Manages an array of `T` objects.
*
* @template T The type of the objects to manage.
*/
export class ArrayManager {
/**
* Creates a new `ArrayManager` from an array of `T` objects.
*
* @param data The array of `T` objects to manage.
*/
constructor(data) {
this._data = data;
}
/**
* Updates elements of the managed array that match the given filter.
*
* @param filter A function that returns true if the element should be updated.
* @param update A function that updates the element.
*
* @returns The updated items.
*/
update(filter, update) {
let updated = [];
for (const genome of this._data) {
if (filter(genome)) {
update(genome);
updated.push(genome);
}
}
return updated;
}
/**
* Removes elements of the managed array that match the given filter and optionally sorts the rest of the array.
*
* @param filter A function that returns true if the element should be removed.
* @param maxCount The maximum number of elements to remove.
* @param order The order to sort the remaining elements.
*
* @returns The removed items.
*/
remove(filter, maxCount = Infinity, order = 'asc') {
const buf = [...this._data];
if (order === 'desc') {
buf.reverse();
}
this._data.length = 0;
let removed = [];
for (const genome of buf) {
if (filter(genome) && removed.length < maxCount) {
removed.push(genome);
}
else {
this._data.push(genome);
}
}
if (order === 'desc') {
this._data.reverse();
}
return removed;
}
}
/**
* Creates a deep copy of an object.
*
* @template T The type of the object to copy.
* @param obj The object to copy.
* @returns A deep copy of the object.
*
* @category Utils
*/
export const fullCopyObject = (obj) => JSON.parse(JSON.stringify(obj));
/**
* Rounds a number to a given precision.
*
* @param value The number to round.
* @param precision The precision to round to.
* @returns The rounded number.
*
* @category Utils
*/
export function round(value, precision) {
return Number(value.toFixed(precision));
}
/**
* Generates an array of a given length, filled with a specified value.
*
* @template T The type of the values in the array.
* @param length The length of the array to generate.
* @param value The value to fill the array with.
* @returns An array of the specified length, filled with the specified value.
*
* @category Utils
*/
export function createFilledArray(length, value) {
return Array.from({ length }, () => value);
}
/**
* Calculates the sum of an array of numbers.
*
* @param input The array of numbers to sum.
* @returns The sum of the input array.
*
* @category Utils
*/
export function arraySum(input) {
return input.reduce((acc, val) => acc + val, 0);
}
/**
* Calculates the mean of an array of numbers.
*
* @param input The array of numbers to calculate the mean of.
* @returns The mean of the input array.
*
* @category Utils
*/
export function arrayMean(input) {
return arraySum(input) / input.length;
}
/**
* Calculates the median of a sorted array of numbers.
*
* @param sortedInput The sorted array of numbers to find the median of.
* @returns The median value of the input array.
*
* @category Utils
*/
export function arrayMedian(sortedInput) {
const middleIndex = Math.floor(sortedInput.length / 2);
if (sortedInput.length % 2 !== 0) {
return sortedInput[middleIndex];
}
return (sortedInput[middleIndex - 1] + sortedInput[middleIndex]) / 2;
}
/**
* Applies a binary operator to two arrays of the same length.
*
* @template T The type of the values in the arrays.
* @param lhs The left-hand side array.
* @param rhs The right-hand side array.
* @param operator The binary operator to apply.
* @returns A new array with the result of applying the operator to each pair of values.
*
* @category Utils
*/
export function arrayBinaryOperation(lhs, rhs, operator) {
const result = [];
const len = Math.min(lhs.length, rhs.length);
for (let i = 0; i < len; ++i) {
result.push(operator(lhs[i], rhs[i]));
}
return result;
}
/**
* Returns a random element from the input array.
*
* @template T The type of the values in the array.
* @param input The array to select a random element from.
* @returns A random element from the input array.
*
* @category Utils
*/
export function getRandomArrayItem(input) {
return input[Math.floor(Math.random() * input.length)];
}
/**
* Normalizes an array of numbers to the range from -1 to 1, where the `reference` value is mapped to 0.
*
* @param input The array of numbers to normalize.
* @param reference The reference value to map to 0.
* @returns The normalized array of numbers.
*
* @category Utils
*/
export function normalizePhenomeRow(input, reference) {
// Find the minimum and maximum values in the array
const minVal = Math.min(...input);
const maxVal = Math.max(...input);
// Calculate the maximum distance relative to the reference
const maxDistance = Math.max(Math.abs(maxVal - reference), Math.abs(minVal - reference));
const denominator = maxDistance || 1;
return input.map(num => {
// Normalize each number to the range from -1 to 1, where reference = 0
return (num - reference) / denominator;
});
}
/**
* Normalizes the columns of a matrix of phenome to the range from -1 to 1, where the `reference` value is mapped to 0.
*
* @param input The matrix of phenome to normalize.
* @param reference The reference value to map to 0.
* @returns The normalized matrix of phenome.
*
* @category Utils
*/
export function normalizePhenomeMatrixColumns(input, reference) {
const result = fullCopyObject(input);
if (result.length === 0) {
return result;
}
for (let i = 0; i < result[0].length; i++) {
const columnNormalized = normalizePhenomeRow(result.map((row) => row[i]), reference[i]);
for (let j = 0; j < result.length; j++) {
result[j][i] = columnNormalized[j];
}
}
return result;
}
/**
* Normalizes the columns of a matrix of phenome to the range from -1 to 1, where the `reference` value is mapped to 0.
*
* @param matrix The matrix of phenome to normalize.
* @param reference The reference value to map to 0.
* @param abs Whether to take the absolute value of the normalized values.
* @returns The normalized matrix of phenome.
*
* @category Utils
*/
export function normalizePhenomeMatrix(matrix, reference, abs = true) {
const result = normalizePhenomeMatrixColumns(matrix, reference);
if (abs) {
return result.map((row) => row.map((x) => Math.abs(x)));
}
return result;
}
/**
* Creates an empty `StatSummary` object.
*
* A `StatSummary` object contains the count of genomes in the population,
* as well as the best, second best, mean, median, and worst values.
* This function initializes a `StatSummary` with all values set to zero.
*
* @returns An initialized `StatSummary` object with all fields set to zero.
*
* @category Utils
*/
export function createEmptyStatSummary() {
return {
count: 0,
best: 0,
second: 0,
mean: 0,
median: 0,
worst: 0,
};
}
/**
* Creates an empty `GroupedStatSummary` object.
*
* A `GroupedStatSummary` object contains a summary of the statistics of a population of genomes,
* grouped by origin into three categories: initial, crossover, and mutation.
* This function initializes a `GroupedStatSummary` with all values set to zero.
*
* @returns An initialized `GroupedStatSummary` object with all fields set to zero.
*
* @category Utils
*/
export function createEmptyGroupedStatSummary() {
return {
initial: createEmptyStatSummary(),
crossover: createEmptyStatSummary(),
mutation: createEmptyStatSummary(),
};
}
/**
* Creates an empty `RangeStatSummary` object.
*
* A `RangeStatSummary` object contains the minimum, mean, and maximum values
* for a set of numerical data.
* This function initializes a `RangeStatSummary` with all values set to zero.
*
* @returns An initialized `RangeStatSummary` object with all fields set to zero.
*
* @category Utils
*/
export function createEmptyRangeStatSummary() {
return {
min: 0,
mean: 0,
max: 0,
};
}
/**
* Calculates a summary of the statistics for a sorted array of numbers.
*
* The summary includes the count of numbers in the array,
* as well as the best, second best, mean, median, and worst values.
*
* @param sortedSource The sorted array of numbers.
* @returns A summary of the statistics for the array of numbers.
*
* @category Utils
*/
export function calcStatSummary(sortedSource) {
var _a;
if (sortedSource.length === 0) {
return createEmptyStatSummary();
}
return {
count: sortedSource.length,
best: sortedSource[0],
second: (_a = sortedSource[1]) !== null && _a !== void 0 ? _a : 0,
mean: arrayMean(sortedSource),
median: arrayMedian(sortedSource),
worst: sortedSource[sortedSource.length - 1],
};
}
/**
* Calculates a summary of the statistics for a sorted array of numbers.
*
* The summary includes the minimum, mean, and maximum values
* for a set of numerical data.
*
* @param source The array of numbers.
* @returns A summary of the statistics for the array of numbers.
*
* @category Utils
*/
export function calcRangeStatSummary(source) {
if (source.length === 0) {
return createEmptyRangeStatSummary();
}
return {
min: Math.min(...source),
mean: arrayMean(source),
max: Math.max(...source),
};
}
/**
* Rounds the fields of a StatSummary object to a given precision.
*
* @param summary The StatSummary object to round.
* @param precision The number of decimal places to round to.
* @returns A new StatSummary object with rounded fields.
*
* @category Utils
*/
export function roundStatSummary(summary, precision) {
return {
count: summary.count,
best: round(summary.best, precision),
second: round(summary.second, precision),
mean: round(summary.mean, precision),
median: round(summary.median, precision),
worst: round(summary.worst, precision),
};
}
/**
* Rounds the fields of a GroupedStatSummary object to a given precision.
*
* This function rounds the statistics in each category of the GroupedStatSummary
* (initial, crossover, mutation) to the specified number of decimal places.
*
* @param summary The GroupedStatSummary object to round.
* @param precision The number of decimal places to round to.
* @returns A new GroupedStatSummary object with rounded fields.
*
* @category Utils
*/
export function roundGroupedStatSummary(summary, precision) {
return {
initial: roundStatSummary(summary.initial, precision),
crossover: roundStatSummary(summary.crossover, precision),
mutation: roundStatSummary(summary.mutation, precision),
};
}
/**
* Rounds the fields of a RangeStatSummary object to a given precision.
*
* This function rounds the minimum, mean, and maximum values of the RangeStatSummary
* to the specified number of decimal places.
*
* @param summary The RangeStatSummary object to round.
* @param precision The number of decimal places to round to.
* @returns A new RangeStatSummary object with rounded fields.
*
* @category Utils
*/
export function roundRangeStatSummary(summary, precision) {
return {
min: round(summary.min, precision),
mean: round(summary.mean, precision),
max: round(summary.max, precision),
};
}
/**
* Creates an array of `EvaluatedGenome` objects from a population and its fitness and phenome.
*
* @param population The population of genomes.
* @param fitnessColumn The fitness column of the population.
* @param phenomeMatrix The phenome matrix of the population.
* @returns An array of EvaluatedGenome objects.
*
* @category Utils
*/
export function createEvaluatedPopulation(population, fitnessColumn, phenomeMatrix) {
const zipped = [...zip(population, fitnessColumn, phenomeMatrix)];
return zipped.map(([genome, fitness, phenome]) => ({ genome, fitness, phenome }));
}
/**
* Extracts the population, fitness column, and phenome matrix from an array of
* [[EvaluatedGenome]] objects.
*
* @param input The array of [[EvaluatedGenome]] objects.
* @returns An array of three elements: the population, fitness column, and phenome matrix.
*
* @category Utils
*/
export function extractEvaluatedPopulation(input) {
return [
input.map((x) => x.genome),
input.map((x) => x.fitness),
input.map((x) => x.phenome),
];
}
//# sourceMappingURL=utils.js.map